From 6213c00fa923a953d3ef805a7ffcd8aed783e3fc Mon Sep 17 00:00:00 2001 From: Joey Smith Date: Mon, 10 Aug 2026 02:10:34 -0500 Subject: [PATCH 01/21] fix: add missing #[Override] attributes (mago analyze) Added #[Override] to all methods that override a parent class or interface method but were missing the attribute: - src/Driver.php (8 methods) - src/Metadata/Source.php (7 methods) - src/Sql/Ddl/CreateTableDecorator.php (2 methods) - src/Sql/Ddl/AlterTableDecorator.php (3 methods) - src/Connection.php (2 methods) Applied manually (rather than via `mago analyze --fix`) to keep the existing use Override; + bare #[Override] convention already used elsewhere in the codebase; the auto-fix instead inserts fully-qualified #[\Override], which would trip lint's no-fully-qualified-global-class-like. mago analyze: 433 -> 411 remaining issues. --- src/Connection.php | 2 ++ src/Driver.php | 9 +++++++++ src/Metadata/Source.php | 8 ++++++++ src/Sql/Ddl/AlterTableDecorator.php | 4 ++++ src/Sql/Ddl/CreateTableDecorator.php | 3 +++ 5 files changed, 26 insertions(+) diff --git a/src/Connection.php b/src/Connection.php index 08752fd..3b79de5 100644 --- a/src/Connection.php +++ b/src/Connection.php @@ -250,6 +250,7 @@ public function getLastGeneratedValue(?string $name = null): string|int|false|nu } /** @inheritDoc */ + #[Override] public function isConnected(): bool { return $this->resource instanceof mysqli; @@ -274,6 +275,7 @@ public function rollback(): ConnectionInterface return $this; } + #[Override] public function setDriver(DriverInterface $driver): DriverAwareInterface { $this->driver = $driver; diff --git a/src/Driver.php b/src/Driver.php index 8700610..4c0ea19 100644 --- a/src/Driver.php +++ b/src/Driver.php @@ -6,6 +6,7 @@ use mysqli; use mysqli_stmt; +use Override; use PhpDb\Adapter\Driver\ConnectionInterface; use PhpDb\Adapter\Driver\DriverAwareInterface; use PhpDb\Adapter\Driver\DriverInterface; @@ -47,6 +48,7 @@ public function __construct( } } + #[Override] public function checkEnvironment(): bool { if (! extension_loaded('mysqli')) { @@ -62,6 +64,7 @@ public function checkEnvironment(): bool * * @param mysqli|mysqli_result|mysqli_stmt $resource */ + #[Override] public function createResult($resource, ?bool $isBuffered = null): ResultInterface&Result { /** @var Result $result */ @@ -75,6 +78,7 @@ public function createResult($resource, ?bool $isBuffered = null): ResultInterfa * * @param mysqli|mysqli_stmt|string $sqlOrResource */ + #[Override] public function createStatement($sqlOrResource = null): StatementInterface&Statement { /** @@ -106,11 +110,13 @@ public function createStatement($sqlOrResource = null): StatementInterface&State /** * Format parameter name */ + #[Override] public function formatParameterName(string $name, ?string $type = null): string { return '?'; } + #[Override] public function getConnection(): ConnectionInterface&Connection { return $this->connection; @@ -119,6 +125,7 @@ public function getConnection(): ConnectionInterface&Connection /** * Get last generated value */ + #[Override] public function getLastGeneratedValue(): int|string|false|null { return $this->getConnection()->getLastGeneratedValue(); @@ -127,6 +134,7 @@ public function getLastGeneratedValue(): int|string|false|null /** * Get prepare type */ + #[Override] public function getPrepareType(): string { return self::PARAMETERIZATION_POSITIONAL; @@ -150,6 +158,7 @@ public function getStatementPrototype(): StatementInterface&Statement return $this->statementPrototype; } + #[Override] public function setProfiler(ProfilerInterface $profiler): ProfilerAwareInterface { $this->profiler = $profiler; diff --git a/src/Metadata/Source.php b/src/Metadata/Source.php index f3c1460..b85bbe1 100644 --- a/src/Metadata/Source.php +++ b/src/Metadata/Source.php @@ -6,6 +6,7 @@ use DateTime; use Exception; +use Override; use PhpDb\Adapter\AdapterInterface; use PhpDb\Metadata\Source\AbstractSource; @@ -25,6 +26,7 @@ final class Source extends AbstractSource { // @mago-expect lint:halstead + #[Override] protected function loadColumnData(string $table, string $schema): void { if (null !== ($this->data['columns'][$schema][$table] ?? null)) { @@ -114,6 +116,7 @@ protected function loadColumnData(string $table, string $schema): void } // @mago-expect lint:halstead + #[Override] protected function loadConstraintData(string $table, string $schema): void { // phpcs:disable WebimpressCodingStandard.NamingConventions.ValidVariableName.NotCamelCaps @@ -240,6 +243,7 @@ protected function loadConstraintData(string $table, string $schema): void // phpcs:enable WebimpressCodingStandard.NamingConventions.ValidVariableName.NotCamelCaps } + #[Override] protected function loadConstraintDataKeys(string $schema): void { if (null !== ($this->data['constraint_keys'][$schema] ?? null)) { @@ -351,6 +355,7 @@ protected function loadConstraintDataNames(string $schema): void $this->data['constraint_names'][$schema] = $data; } + #[Override] protected function loadConstraintReferences(string $table, string $schema): void { parent::loadConstraintReferences($table, $schema); @@ -424,6 +429,7 @@ protected function loadConstraintReferences(string $table, string $schema): void /** * @throws Exception */ + #[Override] protected function loadSchemaData(): void { if (null !== ($this->data['schemas'] ?? null)) { @@ -449,6 +455,7 @@ protected function loadSchemaData(): void $this->data['schemas'] = $schemas; } + #[Override] protected function loadTableNameData(string $schema): void { if (null !== ($this->data['table_names'][$schema] ?? null)) { @@ -510,6 +517,7 @@ protected function loadTableNameData(string $schema): void $this->data['table_names'][$schema] = $tables; } + #[Override] protected function loadTriggerData(string $schema): void { if (null !== ($this->data['triggers'][$schema] ?? null)) { diff --git a/src/Sql/Ddl/AlterTableDecorator.php b/src/Sql/Ddl/AlterTableDecorator.php index b3cfabe..14099f9 100644 --- a/src/Sql/Ddl/AlterTableDecorator.php +++ b/src/Sql/Ddl/AlterTableDecorator.php @@ -4,6 +4,7 @@ namespace PhpDb\Mysql\Sql\Ddl; +use Override; use PhpDb\Adapter\Platform\PlatformInterface; use PhpDb\Sql\Ddl\AlterTable; use PhpDb\Sql\Platform\PlatformDecoratorInterface; @@ -56,6 +57,7 @@ final class AlterTableDecorator extends AlterTable implements PlatformDecoratorI 'after' => 8, ]; + #[Override] public function setSubject( SqlInterface|PreparableSqlInterface|null $subject, ): PlatformDecoratorInterface { @@ -97,6 +99,7 @@ protected function getSqlInsertOffsets(string $sql): array return $insertStart; } + #[Override] protected function processAddColumns(?PlatformInterface $adapterPlatform = null): array { $sqls = []; @@ -170,6 +173,7 @@ protected function processAddColumns(?PlatformInterface $adapterPlatform = null) return [$sqls]; } + #[Override] protected function processChangeColumns(?PlatformInterface $adapterPlatform = null): array { $sqls = []; diff --git a/src/Sql/Ddl/CreateTableDecorator.php b/src/Sql/Ddl/CreateTableDecorator.php index 20956b3..1ac3a89 100644 --- a/src/Sql/Ddl/CreateTableDecorator.php +++ b/src/Sql/Ddl/CreateTableDecorator.php @@ -4,6 +4,7 @@ namespace PhpDb\Mysql\Sql\Ddl; +use Override; use PhpDb\Adapter\Platform\PlatformInterface; use PhpDb\Sql\Ddl\CreateTable; use PhpDb\Sql\Platform\PlatformDecoratorInterface; @@ -40,6 +41,7 @@ final class CreateTableDecorator extends CreateTable implements PlatformDecorato 'storage' => 7, ]; + #[Override] public function setSubject( PreparableSqlInterface|SqlInterface|null $subject, ): PlatformDecoratorInterface { @@ -88,6 +90,7 @@ protected function getSqlInsertOffsets($sql) /** * {@inheritDoc} */ + #[Override] protected function processColumns(?PlatformInterface $platform = null): ?array { if (! $this->columns) { From bde60515aa94e02940da8a55d0bf2127ae3e33d0 Mon Sep 17 00:00:00 2001 From: Joey Smith Date: Mon, 10 Aug 2026 03:53:57 -0500 Subject: [PATCH 02/21] fix: document unhandled-thrown-type findings (mago analyze) Added @throws docblocks for every method that can propagate an exception without declaring it, following two conventions: - In Container/*Factory.php __invoke() methods: prefer PSR interfaces for container-related exceptions (Psr\Container\ContainerExceptionInterface, Psr\Container\NotFoundExceptionInterface, Laminas\ServiceManager\ Exception\ExceptionInterface), since these are factories consumed through laminas-servicemanager. - Everywhere else: use the base PhpDb\*\Exception\ExceptionInterface for the relevant namespace (PhpDb\Exception\ExceptionInterface or PhpDb\Adapter\Exception\ExceptionInterface) rather than concrete exception classes, since callers should catch the interface, not a specific implementation. - Native PDOException documented separately in Pdo/Connection where relevant, since it has no PhpDb interface equivalent. Fixed across: 6 Container/*Factory.php files, Driver.php, Statement.php, Connection.php, Pdo/Connection.php, Result.php (23 findings, plus one cascading finding in DriverInterfaceFactory after Driver::__construct() was documented). mago analyze: 388 -> 365 remaining issues. --- src/AdapterPlatform.php | 2 +- src/Connection.php | 41 ++++++++++++++----- src/Container/ConnectionInterfaceFactory.php | 3 ++ src/Container/DriverInterfaceFactory.php | 10 +++-- src/Container/MetadataInterfaceFactory.php | 4 ++ .../PdoConnectionInterfaceFactory.php | 3 ++ src/Container/PdoDriverInterfaceFactory.php | 9 ++-- src/Container/PlatformInterfaceFactory.php | 3 ++ src/Driver.php | 24 ++++------- src/Pdo/Connection.php | 5 ++- src/Pdo/Driver.php | 2 +- src/Result.php | 12 ++++-- src/Statement.php | 9 ++-- test/unit/AdapterPlatformTest.php | 4 +- test/unit/Pdo/ConnectionTransactionsTest.php | 36 ++++++++++------ test/unit/Pdo/DriverTest.php | 6 +-- test/unit/Pdo/StatementIntegrationTest.php | 9 ++-- test/unit/Pdo/StatementTest.php | 4 +- test/unit/Pdo/TestAsset/ConnectionWrapper.php | 23 ----------- test/unit/Sql/Ddl/AlterTableDecoratorTest.php | 4 +- .../unit/Sql/Ddl/CreateTableDecoratorTest.php | 4 +- 21 files changed, 122 insertions(+), 95 deletions(-) delete mode 100644 test/unit/Pdo/TestAsset/ConnectionWrapper.php diff --git a/src/AdapterPlatform.php b/src/AdapterPlatform.php index aa57656..9db9b86 100644 --- a/src/AdapterPlatform.php +++ b/src/AdapterPlatform.php @@ -14,7 +14,7 @@ use function implode; use function str_replace; -class AdapterPlatform extends AbstractPlatform +final class AdapterPlatform extends AbstractPlatform { final public const PLATFORM_NAME = 'MySQL'; diff --git a/src/Connection.php b/src/Connection.php index 3b79de5..0d3343e 100644 --- a/src/Connection.php +++ b/src/Connection.php @@ -27,6 +27,7 @@ // @mago-expect lint:cyclomatic-complexity // @mago-expect lint:kan-defect // @mago-expect lint:too-many-methods +// @mago-expect analysis:class-must-be-final class Connection extends AbstractConnection implements DriverAwareInterface { protected Driver $driver; @@ -61,7 +62,11 @@ public function __construct( } } - /** @inheritDoc */ + /** + * @inheritDoc + * + * @throws Exception\ExceptionInterface + */ #[Override] public function beginTransaction(): ConnectionInterface { @@ -75,7 +80,11 @@ public function beginTransaction(): ConnectionInterface return $this; } - /** @inheritDoc */ + /** + * @inheritDoc + * + * @throws Exception\ExceptionInterface + */ #[Override] public function commit(): ConnectionInterface { @@ -90,7 +99,11 @@ public function commit(): ConnectionInterface return $this; } - /** @inheritDoc */ + /** + * @inheritDoc + * + * @throws Exception\ExceptionInterface + */ // @mago-expect lint:halstead #[Override] public function connect(): ConnectionInterface @@ -99,7 +112,6 @@ public function connect(): ConnectionInterface return $this; } - /** @var array $p */ $p = $this->connectionParameters; // given a list of key names, test for existence in $p @@ -119,8 +131,7 @@ public function connect(): ConnectionInterface $username = $findParameterValue(['username', 'user']); $password = $findParameterValue(['password', 'passwd', 'pw']); $database = $findParameterValue(['database', 'dbname', 'db', 'schema']); - /** @var int|null $port */ - $port = null === ($p['port'] ?? null) ? null : (int) $p['port']; + $port = null === ($p['port'] ?? null) ? null : (int) $p['port']; /** @var string|null $socket */ $socket = $p['socket'] ?? null; @@ -205,10 +216,10 @@ public function disconnect(): ConnectionInterface /** * {@inheritDoc} * - * @throws Exception\InvalidQueryException + * @throws Exception\ExceptionInterface */ #[Override] - public function execute($sql): ?ResultInterface + public function execute(string $sql): ?ResultInterface { if (! $this->isConnected()) { $this->connect(); @@ -218,7 +229,7 @@ public function execute($sql): ?ResultInterface $resultResource = $this->resource->query($sql); - $this->profiler?->profilerFinish($sql); + $this->profiler?->profilerFinish(); // if the returnValue is something other than a mysqli_result, bypass wrapping it if (false === $resultResource) { @@ -228,7 +239,11 @@ public function execute($sql): ?ResultInterface return $this->driver->createResult(true === $resultResource ? $this->resource : $resultResource); } - /** @inheritDoc */ + /** + * @inheritDoc + * + * @throws Exception\ExceptionInterface + */ #[Override] public function getCurrentSchema(): string|false { @@ -256,7 +271,11 @@ public function isConnected(): bool return $this->resource instanceof mysqli; } - /** @inheritDoc */ + /** + * @inheritDoc + * + * @throws Exception\ExceptionInterface + */ #[Override] public function rollback(): ConnectionInterface { diff --git a/src/Container/ConnectionInterfaceFactory.php b/src/Container/ConnectionInterfaceFactory.php index d8ff17d..233561e 100644 --- a/src/Container/ConnectionInterfaceFactory.php +++ b/src/Container/ConnectionInterfaceFactory.php @@ -13,6 +13,9 @@ final class ConnectionInterfaceFactory { + /** + * @throws \PhpDb\Adapter\Exception\ExceptionInterface + */ public function __invoke( ContainerInterface $container, string $requestedName, diff --git a/src/Container/DriverInterfaceFactory.php b/src/Container/DriverInterfaceFactory.php index 2fb1d75..37a98ad 100644 --- a/src/Container/DriverInterfaceFactory.php +++ b/src/Container/DriverInterfaceFactory.php @@ -5,10 +5,8 @@ namespace PhpDb\Mysql\Container; use Laminas\ServiceManager\ServiceManager; -use PhpDb\Adapter\Driver\ConnectionInterface; use PhpDb\Adapter\Driver\DriverInterface; use PhpDb\Adapter\Driver\ResultInterface; -use PhpDb\Adapter\Driver\StatementInterface; use PhpDb\Exception\ContainerException; use PhpDb\Mysql\Connection; use PhpDb\Mysql\Driver; @@ -20,6 +18,12 @@ final class DriverInterfaceFactory { + /** + * @throws \Laminas\ServiceManager\Exception\ExceptionInterface + * @throws \Psr\Container\ContainerExceptionInterface + * @throws \Psr\Container\NotFoundExceptionInterface + * @throws \PhpDb\Exception\ExceptionInterface + */ public function __invoke( ContainerInterface&ServiceManager $container, string $requestedName, @@ -33,10 +37,8 @@ public function __invoke( ); } - /** @var ConnectionInterface&Connection $connectionInstance */ $connectionInstance = $container->build(Connection::class, $options); - /** @var StatementInterface&Statement $statementInstance */ $statementInstance = $container->build( Statement::class, $options['options'] ?? [], diff --git a/src/Container/MetadataInterfaceFactory.php b/src/Container/MetadataInterfaceFactory.php index 7548758..2d62af5 100644 --- a/src/Container/MetadataInterfaceFactory.php +++ b/src/Container/MetadataInterfaceFactory.php @@ -13,6 +13,10 @@ final class MetadataInterfaceFactory { public const ADAPTER_SERVICE_NAME = 'adapter_service_name'; + /** + * @throws \Psr\Container\ContainerExceptionInterface + * @throws \Psr\Container\NotFoundExceptionInterface + */ public function __invoke( ContainerInterface $container, string $requestedName, diff --git a/src/Container/PdoConnectionInterfaceFactory.php b/src/Container/PdoConnectionInterfaceFactory.php index b7c166f..6854a6a 100644 --- a/src/Container/PdoConnectionInterfaceFactory.php +++ b/src/Container/PdoConnectionInterfaceFactory.php @@ -13,6 +13,9 @@ final class PdoConnectionInterfaceFactory { + /** + * @throws \PhpDb\Adapter\Exception\ExceptionInterface + */ public function __invoke( ContainerInterface $container, string $requestedName, diff --git a/src/Container/PdoDriverInterfaceFactory.php b/src/Container/PdoDriverInterfaceFactory.php index 780d904..2e8df13 100644 --- a/src/Container/PdoDriverInterfaceFactory.php +++ b/src/Container/PdoDriverInterfaceFactory.php @@ -7,10 +7,8 @@ use Laminas\ServiceManager\ServiceManager; use PhpDb\Adapter\Driver\Pdo\Result; use PhpDb\Adapter\Driver\Pdo\Statement; -use PhpDb\Adapter\Driver\PdoConnectionInterface; use PhpDb\Adapter\Driver\PdoDriverInterface; use PhpDb\Adapter\Driver\ResultInterface; -use PhpDb\Adapter\Driver\StatementInterface; use PhpDb\Exception\ContainerException; use PhpDb\Mysql\Pdo\Connection; use PhpDb\Mysql\Pdo\Driver; @@ -20,6 +18,11 @@ final class PdoDriverInterfaceFactory { + /** + * @throws \Laminas\ServiceManager\Exception\ExceptionInterface + * @throws \Psr\Container\ContainerExceptionInterface + * @throws \Psr\Container\NotFoundExceptionInterface + */ public function __invoke( ContainerInterface&ServiceManager $container, string $requestedName, @@ -32,10 +35,8 @@ public function __invoke( '$options["connection"] must contain an array of connection configuration.', ); } - /** @var PdoConnectionInterface&Connection $connectionInstance */ $connectionInstance = $container->build(Connection::class, $options); - /** @var StatementInterface&Statement $statementInstance */ $statementInstance = $container->build( Statement::class, $options['options'] ?? [], diff --git a/src/Container/PlatformInterfaceFactory.php b/src/Container/PlatformInterfaceFactory.php index cbb318d..381f9b9 100644 --- a/src/Container/PlatformInterfaceFactory.php +++ b/src/Container/PlatformInterfaceFactory.php @@ -13,6 +13,9 @@ final class PlatformInterfaceFactory { + /** + * @throws \Psr\Container\ContainerExceptionInterface + */ public function __invoke( ContainerInterface $container, string $requestedName, diff --git a/src/Driver.php b/src/Driver.php index 4c0ea19..47885c2 100644 --- a/src/Driver.php +++ b/src/Driver.php @@ -8,7 +8,6 @@ use mysqli_stmt; use Override; use PhpDb\Adapter\Driver\ConnectionInterface; -use PhpDb\Adapter\Driver\DriverAwareInterface; use PhpDb\Adapter\Driver\DriverInterface; use PhpDb\Adapter\Driver\ResultInterface; use PhpDb\Adapter\Driver\StatementInterface; @@ -29,6 +28,9 @@ final class Driver implements DriverInterface, ProfilerAwareInterface 'buffer_results' => false, ]; + /** + * @throws \PhpDb\Exception\ExceptionInterface + */ public function __construct( protected readonly ConnectionInterface&Connection $connection, protected readonly StatementInterface&Statement $statementPrototype = new Statement(), @@ -39,13 +41,8 @@ public function __construct( $options = array_intersect_key([...$this->options, ...$options], $this->options); - if ($this->connection instanceof DriverAwareInterface) { - $this->connection->setDriver($this); - } - - if ($this->statementPrototype instanceof DriverAwareInterface) { - $this->statementPrototype->setDriver($this); - } + $this->connection->setDriver($this); + $this->statementPrototype->setDriver($this); } #[Override] @@ -67,7 +64,6 @@ public function checkEnvironment(): bool #[Override] public function createResult($resource, ?bool $isBuffered = null): ResultInterface&Result { - /** @var Result $result */ $result = clone $this->resultPrototype; $result->initialize($resource, $this->connection->getLastGeneratedValue(), $isBuffered); return $result; @@ -77,6 +73,8 @@ public function createResult($resource, ?bool $isBuffered = null): ResultInterfa * Create statement * * @param mysqli|mysqli_stmt|string $sqlOrResource + * + * @throws Exception\ExceptionInterface */ #[Override] public function createStatement($sqlOrResource = null): StatementInterface&Statement @@ -162,12 +160,8 @@ public function getStatementPrototype(): StatementInterface&Statement public function setProfiler(ProfilerInterface $profiler): ProfilerAwareInterface { $this->profiler = $profiler; - if ($this->connection instanceof ProfilerAwareInterface) { - $this->connection->setProfiler($profiler); - } - if ($this->statementPrototype instanceof ProfilerAwareInterface) { - $this->statementPrototype->setProfiler($profiler); - } + $this->connection->setProfiler($profiler); + $this->statementPrototype->setProfiler($profiler); return $this; } } diff --git a/src/Pdo/Connection.php b/src/Pdo/Connection.php index bc78a3f..a297c82 100644 --- a/src/Pdo/Connection.php +++ b/src/Pdo/Connection.php @@ -20,7 +20,7 @@ use function strtolower; // @mago-expect lint:cyclomatic-complexity -class Connection extends AbstractPdoConnection +final class Connection extends AbstractPdoConnection { /** * Constructor @@ -141,6 +141,9 @@ public function connect(): ConnectionInterface /** * {@inheritDoc} + * + * @throws Exception\ExceptionInterface + * @throws PDOException */ #[Override] public function getCurrentSchema(): string|false diff --git a/src/Pdo/Driver.php b/src/Pdo/Driver.php index 0341b17..95f3aec 100644 --- a/src/Pdo/Driver.php +++ b/src/Pdo/Driver.php @@ -16,7 +16,7 @@ use PhpDb\Adapter\Driver\ResultInterface; use PhpDb\Adapter\Driver\StatementInterface; -class Driver extends AbstractPdo +final class Driver extends AbstractPdo { public function __construct( (PdoConnectionInterface&PdoDriverAwareInterface)|PDO $connection, diff --git a/src/Result.php b/src/Result.php index 64526f3..b1658d8 100644 --- a/src/Result.php +++ b/src/Result.php @@ -53,7 +53,7 @@ final class Result implements Iterator, ResultInterface #[Override] public function buffer(): void { - if ($this->resource instanceof mysqli_stmt && true !== $this->isBuffered) { + if ($this->resource instanceof mysqli_stmt && ! $this->isBuffered) { if ($this->position > 0) { throw new Exception\RuntimeException('Cannot buffer a result set that has started iteration.'); } @@ -72,7 +72,7 @@ public function buffer(): void #[Override] public function count() { - if (false === $this->isBuffered) { + if (! $this->isBuffered) { throw new Exception\RuntimeException('Row count is not available in unbuffered result sets.'); } return $this->resource->num_rows; @@ -81,6 +81,8 @@ public function count() /** * Current * + * @throws Exception\ExceptionInterface + * * @return mixed */ #[ReturnTypeWillChange] @@ -217,7 +219,7 @@ public function next() { $this->currentComplete = false; - if (false === $this->nextComplete) { + if (! $this->nextComplete) { $this->position++; } @@ -234,7 +236,7 @@ public function next() #[Override] public function rewind() { - if (0 !== $this->position && false === $this->isBuffered) { + if (0 !== $this->position && ! $this->isBuffered) { throw new Exception\RuntimeException('Unbuffered results cannot be rewound for multiple iterations'); } @@ -246,6 +248,8 @@ public function rewind() /** * Valid * + * @throws Exception\ExceptionInterface + * * @return bool */ #[ReturnTypeWillChange] diff --git a/src/Statement.php b/src/Statement.php index 382e1bc..a76d7e9 100644 --- a/src/Statement.php +++ b/src/Statement.php @@ -43,7 +43,7 @@ public function __construct( /** * Execute * - * @throws Exception\RuntimeException + * @throws Exception\ExceptionInterface */ #[Override] public function execute(ParameterContainer|array|null $parameters = null): ?ResultInterface @@ -72,12 +72,12 @@ public function execute(ParameterContainer|array|null $parameters = null): ?Resu $this->profiler?->profilerFinish(); - if (false === $return) { + if (! $return) { throw new Exception\RuntimeException($this->resource->error); } $buffered = false; - if (true === $this->bufferResults) { + if ($this->bufferResults) { $this->resource->store_result(); $this->isPrepared = false; $buffered = true; @@ -124,6 +124,9 @@ public function isPrepared(): bool return $this->isPrepared; } + /** + * @throws Exception\ExceptionInterface + */ #[Override] public function prepare(?string $sql = null): StatementInterface { diff --git a/test/unit/AdapterPlatformTest.php b/test/unit/AdapterPlatformTest.php index 95cf0d0..541e78d 100644 --- a/test/unit/AdapterPlatformTest.php +++ b/test/unit/AdapterPlatformTest.php @@ -5,10 +5,10 @@ namespace PhpDbTest\Mysql\Platform; use Override; +use PhpDb\Adapter\Driver\Pdo\AbstractPdoConnection; use PhpDb\Adapter\Driver\Pdo\Result; use PhpDb\Adapter\Driver\Pdo\Statement; use PhpDb\Mysql\AdapterPlatform; -use PhpDb\Mysql\Pdo\Connection; use PhpDb\Mysql\Pdo\Driver; use PHPUnit\Framework\Attributes\CoversMethod; use PHPUnit\Framework\Attributes\Test; @@ -236,7 +236,7 @@ public function quoteValueRaisesNoticeWithoutPlatformSupport(): void protected function setUp(): void { $pdo = new Driver( - $this->createMock(Connection::class), + $this->createMock(AbstractPdoConnection::class), $this->createMock(Statement::class), $this->createMock(Result::class), ); diff --git a/test/unit/Pdo/ConnectionTransactionsTest.php b/test/unit/Pdo/ConnectionTransactionsTest.php index 7d32fa5..958faf6 100644 --- a/test/unit/Pdo/ConnectionTransactionsTest.php +++ b/test/unit/Pdo/ConnectionTransactionsTest.php @@ -8,11 +8,12 @@ use PhpDb\Adapter\Driver\AbstractConnection; use PhpDb\Adapter\Exception\RuntimeException; use PhpDb\Mysql\Pdo\Connection; -use PhpDbTest\Mysql\Pdo\TestAsset\ConnectionWrapper; +use PhpDbTest\Mysql\Pdo\TestAsset\PdoStubDriver; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\CoversMethod; use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\TestCase; +use ReflectionProperty; /** * Tests for {@see \PhpDb\Adapter\Mysql\Driver\Pdo\Connection} transaction support @@ -25,7 +26,7 @@ #[CoversMethod(Connection::class, 'rollback')] final class ConnectionTransactionsTest extends TestCase { - protected ConnectionWrapper $wrapper; + protected Connection $wrapper; #[Test] public function beginTransactionReturnsInstanceOfConnection(): void @@ -72,22 +73,22 @@ public function nestedTransactionsCommit(): void // 1st transaction $this->wrapper->beginTransaction(); static::assertTrue($this->wrapper->inTransaction()); - static::assertSame(1, $this->wrapper->getNestedTransactionsCount()); + static::assertSame(1, $this->getNestedTransactionsCount($this->wrapper)); // 2nd transaction $this->wrapper->beginTransaction(); static::assertTrue($this->wrapper->inTransaction()); - static::assertSame(2, $this->wrapper->getNestedTransactionsCount()); + static::assertSame(2, $this->getNestedTransactionsCount($this->wrapper)); // 1st commit $this->wrapper->commit(); static::assertTrue($this->wrapper->inTransaction()); - static::assertSame(1, $this->wrapper->getNestedTransactionsCount()); + static::assertSame(1, $this->getNestedTransactionsCount($this->wrapper)); // 2nd commit $this->wrapper->commit(); static::assertFalse($this->wrapper->inTransaction()); - static::assertSame(0, $this->wrapper->getNestedTransactionsCount()); + static::assertSame(0, $this->getNestedTransactionsCount($this->wrapper)); } #[Test] @@ -98,17 +99,17 @@ public function nestedTransactionsRollback(): void // 1st transaction $this->wrapper->beginTransaction(); static::assertTrue($this->wrapper->inTransaction()); - static::assertSame(1, $this->wrapper->getNestedTransactionsCount()); + static::assertSame(1, $this->getNestedTransactionsCount($this->wrapper)); // 2nd transaction $this->wrapper->beginTransaction(); static::assertTrue($this->wrapper->inTransaction()); - static::assertSame(2, $this->wrapper->getNestedTransactionsCount()); + static::assertSame(2, $this->getNestedTransactionsCount($this->wrapper)); // Rollback $this->wrapper->rollback(); static::assertFalse($this->wrapper->inTransaction()); - static::assertSame(0, $this->wrapper->getNestedTransactionsCount()); + static::assertSame(0, $this->getNestedTransactionsCount($this->wrapper)); } #[Test] @@ -151,12 +152,12 @@ public function rollbackWithoutBeginThrowsException(): void public function standaloneCommit(): void { static::assertFalse($this->wrapper->inTransaction()); - static::assertSame(0, $this->wrapper->getNestedTransactionsCount()); + static::assertSame(0, $this->getNestedTransactionsCount($this->wrapper)); $this->wrapper->commit(); static::assertFalse($this->wrapper->inTransaction()); - static::assertSame(0, $this->wrapper->getNestedTransactionsCount()); + static::assertSame(0, $this->getNestedTransactionsCount($this->wrapper)); } /** @@ -165,6 +166,17 @@ public function standaloneCommit(): void #[Override] protected function setUp(): void { - $this->wrapper = new ConnectionWrapper(); + $this->wrapper = new Connection([]); + // bypass setResource(), which calls PDO::getAttribute() and would fail + // against the stub's uninitialized internal PDO state + (new ReflectionProperty($this->wrapper, 'resource'))->setValue( + $this->wrapper, + new PdoStubDriver('foo', 'bar', 'baz'), + ); + } + + private function getNestedTransactionsCount(Connection $connection): int + { + return (new ReflectionProperty($connection, 'nestedTransactionsCount'))->getValue($connection); } } diff --git a/test/unit/Pdo/DriverTest.php b/test/unit/Pdo/DriverTest.php index b80cd1a..8889a03 100644 --- a/test/unit/Pdo/DriverTest.php +++ b/test/unit/Pdo/DriverTest.php @@ -7,10 +7,10 @@ use Override; use PDOStatement; use PhpDb\Adapter\Driver\DriverInterface; +use PhpDb\Adapter\Driver\Pdo\AbstractPdoConnection; use PhpDb\Adapter\Driver\Pdo\Result; use PhpDb\Adapter\Driver\Pdo\Statement; use PhpDb\Exception\RuntimeException; -use PhpDb\Mysql\Pdo\Connection; use PhpDb\Mysql\Pdo\Driver; use PHPUnit\Framework\Attributes\CoversMethod; use PHPUnit\Framework\Attributes\DataProvider; @@ -60,7 +60,7 @@ public function createResultPassesNullRowCount(): void ->method('rowCount') ->willReturn(4); - $connection = $this->createMock(Connection::class); + $connection = $this->createMock(AbstractPdoConnection::class); $statement = $this->createMock(Statement::class); $driver = new Driver($connection, $statement, new Result()); @@ -101,7 +101,7 @@ public function getResultPrototype(): void #[Override] protected function setUp(): void { - $connection = $this->createMock(Connection::class); + $connection = $this->createMock(AbstractPdoConnection::class); $statement = $this->createMock(Statement::class); $result = $this->createMock(Result::class); $this->pdo = new Driver( diff --git a/test/unit/Pdo/StatementIntegrationTest.php b/test/unit/Pdo/StatementIntegrationTest.php index fd7673a..f7ace3e 100644 --- a/test/unit/Pdo/StatementIntegrationTest.php +++ b/test/unit/Pdo/StatementIntegrationTest.php @@ -8,7 +8,8 @@ use PDO; use PDOStatement; use PhpDb\Adapter\Driver\Pdo\Statement; -use PhpDb\Mysql\Pdo\Driver as PdoDriver; +use PhpDb\Adapter\Driver\PdoDriverInterface; +use PhpDb\Adapter\Driver\ResultInterface; use PHPUnit\Framework\Attributes\CoversMethod; use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\MockObject\MockObject; @@ -85,10 +86,8 @@ public function statementExecuteWillUsePdoStrForStringIntegerWhenBinding(): void #[Override] protected function setUp(): void { - $driver = $this->getMockBuilder(PdoDriver::class) - ->onlyMethods(['createResult']) - ->disableOriginalConstructor() - ->getMock(); + $driver = $this->createMock(PdoDriverInterface::class); + $driver->method('createResult')->willReturn($this->createMock(ResultInterface::class)); $this->pdoStatementMock = $this->getMockBuilder(PDOStatement::class) ->onlyMethods(['execute', 'bindParam']) diff --git a/test/unit/Pdo/StatementTest.php b/test/unit/Pdo/StatementTest.php index 43d90c4..f914a9f 100644 --- a/test/unit/Pdo/StatementTest.php +++ b/test/unit/Pdo/StatementTest.php @@ -6,12 +6,12 @@ use Override; use PDOStatement; +use PhpDb\Adapter\Driver\Pdo\AbstractPdoConnection; use PhpDb\Adapter\Driver\Pdo\Result; use PhpDb\Adapter\Driver\Pdo\Statement; use PhpDb\Adapter\Driver\PdoDriverInterface; use PhpDb\Adapter\Driver\ResultInterface; use PhpDb\Adapter\ParameterContainer; -use PhpDb\Mysql\Pdo\Connection; use PhpDb\Mysql\Pdo\Driver; use PHPUnit\Framework\Attributes\CoversMethod; use PHPUnit\Framework\Attributes\Test; @@ -123,7 +123,7 @@ protected function setUp(): void { $this->statement = new Statement(); $this->pdo = new Driver( - $this->createMock(Connection::class), + $this->createMock(AbstractPdoConnection::class), $this->statement, new Result(), ); diff --git a/test/unit/Pdo/TestAsset/ConnectionWrapper.php b/test/unit/Pdo/TestAsset/ConnectionWrapper.php deleted file mode 100644 index f89a1e9..0000000 --- a/test/unit/Pdo/TestAsset/ConnectionWrapper.php +++ /dev/null @@ -1,23 +0,0 @@ -resource = new PdoStubDriver('foo', 'bar', 'baz'); - } - - public function getNestedTransactionsCount(): int - { - return $this->nestedTransactionsCount; - } -} diff --git a/test/unit/Sql/Ddl/AlterTableDecoratorTest.php b/test/unit/Sql/Ddl/AlterTableDecoratorTest.php index bb15d82..b842518 100644 --- a/test/unit/Sql/Ddl/AlterTableDecoratorTest.php +++ b/test/unit/Sql/Ddl/AlterTableDecoratorTest.php @@ -4,10 +4,10 @@ namespace PhpDbTest\Mysql\Sql\Ddl; +use PhpDb\Adapter\Driver\Pdo\AbstractPdoConnection; use PhpDb\Adapter\Driver\Pdo\Result; use PhpDb\Adapter\Driver\Pdo\Statement; use PhpDb\Mysql\AdapterPlatform; -use PhpDb\Mysql\Pdo\Connection; use PhpDb\Mysql\Pdo\Driver; use PhpDb\Mysql\Sql\Ddl\AlterTableDecorator; use PhpDb\Sql\Ddl\AlterTable; @@ -156,7 +156,7 @@ public function changeColumnCollate(): void protected function setUp(): void { $driver = new Driver( - $this->createMock(Connection::class), + $this->createMock(AbstractPdoConnection::class), $this->createMock(Statement::class), $this->createMock(Result::class), ); diff --git a/test/unit/Sql/Ddl/CreateTableDecoratorTest.php b/test/unit/Sql/Ddl/CreateTableDecoratorTest.php index 2dced31..448f98d 100644 --- a/test/unit/Sql/Ddl/CreateTableDecoratorTest.php +++ b/test/unit/Sql/Ddl/CreateTableDecoratorTest.php @@ -4,10 +4,10 @@ namespace PhpDbTest\Mysql\Sql\Ddl; +use PhpDb\Adapter\Driver\Pdo\AbstractPdoConnection; use PhpDb\Adapter\Driver\Pdo\Result; use PhpDb\Adapter\Driver\Pdo\Statement; use PhpDb\Mysql\AdapterPlatform; -use PhpDb\Mysql\Pdo\Connection; use PhpDb\Mysql\Pdo\Driver; use PhpDb\Mysql\Sql\Ddl\CreateTableDecorator; use PhpDb\Sql\Ddl\Column; @@ -151,7 +151,7 @@ public function unsignedOption(): void protected function setUp(): void { $driver = new Driver( - $this->createMock(Connection::class), + $this->createMock(AbstractPdoConnection::class), $this->createMock(Statement::class), $this->createMock(Result::class), ); From 2d2eebbbc4e4d74979935c45b8fddfdc33b813ac Mon Sep 17 00:00:00 2001 From: Joey Smith Date: Mon, 10 Aug 2026 12:33:39 -0500 Subject: [PATCH 03/21] fix: resolve non-existent-method findings (mago analyze) - src/Metadata/Source.php: added /** @var ResultSetInterface $results */ before each of the 8 $this->adapter->query(..., QUERY_MODE_EXECUTE) calls. AdapterInterface::query() declares a 3-way union return type (StatementInterface|ResultSetInterface|ResultInterface), but toArray() only exists on ResultSetInterface. Since these queries are always SELECT statements, the runtime type is always ResultSetInterface; the annotation documents that guarantee for the analyzer. - src/Result.php: 3 findings, 2 different fixes: - loadDataFromMysqliStatement(): added a real instanceof mysqli_stmt guard. This method is only ever called from a branch that already guarantees this, but the analyzer can't see across the method-call boundary, so the guard makes the precondition explicit. - rewind() and loadFromMysqliResult(): added instanceof guards before data_seek()/fetch_assoc(). These calls previously assumed $this->resource is never a bare mysqli connection object, but Connection::execute() can pass the raw connection through to Driver::createResult() for non-SELECT (write) queries. Iterating a Result wrapping a write-query outcome was a real latent crash (undefined method on mysqli). Rather than suppress the finding or silently patch around it, added explicit guards that throw a clear RuntimeException if this path is ever hit, so the gap stays visible instead of being buried. Filing a follow-up issue in this repo with the exact rationale. mago analyze: 365 -> 352 remaining issues. non-existent-method: 23 -> 0. Note: possibly-undefined-string-array-index jumped 4 -> 34 as a side effect - toArray()'s return type is now resolvable, so mago can finally analyze the array shape of $row inside the Source.php foreach loops instead of treating it as opaque mixed. Not a regression, just newly visible precise feedback for future work. --- src/Metadata/Source.php | 9 +++++++++ src/Result.php | 12 ++++++++++++ 2 files changed, 21 insertions(+) diff --git a/src/Metadata/Source.php b/src/Metadata/Source.php index b85bbe1..9cf9f6c 100644 --- a/src/Metadata/Source.php +++ b/src/Metadata/Source.php @@ -9,6 +9,7 @@ use Override; use PhpDb\Adapter\AdapterInterface; use PhpDb\Metadata\Source\AbstractSource; +use PhpDb\ResultSet\ResultSetInterface; use function array_change_key_case; use function array_walk; @@ -81,6 +82,7 @@ protected function loadColumnData(string $table, string $schema): void ? " AND {$p->quoteIdentifierChain(['T', 'TABLE_SCHEMA'])} != 'INFORMATION_SCHEMA'" : " AND {$p->quoteIdentifierChain(['T', 'TABLE_SCHEMA'])} = {$p->quoteTrustedValue($schema)}"; + /** @var ResultSetInterface $results */ $results = $this->adapter->query($sql, AdapterInterface::QUERY_MODE_EXECUTE); $columns = []; foreach ($results->toArray() as $row) { @@ -208,6 +210,7 @@ protected function loadConstraintData(string $table, string $schema): void 'CONSTRAINT_NAME', ])}, {$p->quoteIdentifierChain(['KCU', 'ORDINAL_POSITION'])}"; + /** @var ResultSetInterface $results */ $results = $this->adapter->query($sql, AdapterInterface::QUERY_MODE_EXECUTE); $realName = null; @@ -290,6 +293,7 @@ protected function loadConstraintDataKeys(string $schema): void ? " AND {$p->quoteIdentifierChain(['T', 'TABLE_SCHEMA'])} != 'INFORMATION_SCHEMA'" : " AND {$p->quoteIdentifierChain(['T', 'TABLE_SCHEMA'])} = {$p->quoteTrustedValue($schema)}"; + /** @var ResultSetInterface $results */ $results = $this->adapter->query($sql, AdapterInterface::QUERY_MODE_EXECUTE); $data = []; @@ -345,6 +349,7 @@ protected function loadConstraintDataNames(string $schema): void ? " AND {$p->quoteIdentifierChain(['T', 'TABLE_SCHEMA'])} != 'INFORMATION_SCHEMA'" : " AND {$p->quoteIdentifierChain(['T', 'TABLE_SCHEMA'])} = {$p->quoteTrustedValue($schema)}"; + /** @var ResultSetInterface $results */ $results = $this->adapter->query($sql, AdapterInterface::QUERY_MODE_EXECUTE); $data = []; @@ -416,6 +421,7 @@ protected function loadConstraintReferences(string $table, string $schema): void ? " AND {$p->quoteIdentifierChain(['T', 'TABLE_SCHEMA'])} != 'INFORMATION_SCHEMA'" : " AND {$p->quoteIdentifierChain(['T', 'TABLE_SCHEMA'])} = {$p->quoteTrustedValue($schema)}"; + /** @var ResultSetInterface $results */ $results = $this->adapter->query($sql, AdapterInterface::QUERY_MODE_EXECUTE); $data = []; @@ -445,6 +451,7 @@ protected function loadSchemaData(): void WHERE {$p->quoteIdentifier('SCHEMA_NAME')} != 'INFORMATION_SCHEMA' SQL; + /** @var ResultSetInterface $results */ $results = $this->adapter->query($sql, AdapterInterface::QUERY_MODE_EXECUTE); $schemas = []; @@ -502,6 +509,7 @@ protected function loadTableNameData(string $schema): void ? " AND {$p->quoteIdentifierChain(['T', 'TABLE_SCHEMA'])} != 'INFORMATION_SCHEMA'" : " AND {$p->quoteIdentifierChain(['T', 'TABLE_SCHEMA'])} = {$p->quoteTrustedValue($schema)}"; + /** @var ResultSetInterface $results */ $results = $this->adapter->query($sql, AdapterInterface::QUERY_MODE_EXECUTE); $tables = []; @@ -563,6 +571,7 @@ protected function loadTriggerData(string $schema): void ? "{$p->quoteIdentifier('TRIGGER_SCHEMA')} != 'INFORMATION_SCHEMA'" : "{$p->quoteIdentifier('TRIGGER_SCHEMA')} = {$p->quoteTrustedValue($schema)}"; + /** @var ResultSetInterface $results */ $results = $this->adapter->query($sql, AdapterInterface::QUERY_MODE_EXECUTE); $data = []; diff --git a/src/Result.php b/src/Result.php index b1658d8..45bec99 100644 --- a/src/Result.php +++ b/src/Result.php @@ -240,6 +240,10 @@ public function rewind() throw new Exception\RuntimeException('Unbuffered results cannot be rewound for multiple iterations'); } + if (! $this->resource instanceof mysqli_result && ! $this->resource instanceof mysqli_stmt) { + throw new Exception\RuntimeException('Cannot rewind a result that is not a query result'); + } + $this->resource->data_seek(0); // works for both mysqli_result & mysqli_stmt $this->currentComplete = false; $this->position = 0; @@ -279,6 +283,10 @@ public function valid() */ protected function loadDataFromMysqliStatement(): bool { + if (! $this->resource instanceof mysqli_stmt) { + throw new Exception\RuntimeException('Expected resource to be an instance of mysqli_stmt'); + } + // build the default reference based bind structure, if it does not already exist if (null === $this->statementBindValues['keys']) { $this->statementBindValues['keys'] = []; @@ -326,6 +334,10 @@ protected function loadFromMysqliResult(): bool { $this->currentData = null; + if (! $this->resource instanceof mysqli_result) { + throw new Exception\RuntimeException('Cannot fetch from a result that is not a mysqli_result'); + } + if (($data = $this->resource->fetch_assoc()) === null) { return false; } From 0fbfc7ea910d7592cc5de655bedec32db00f8569 Mon Sep 17 00:00:00 2001 From: Joey Smith Date: Mon, 10 Aug 2026 12:49:07 -0500 Subject: [PATCH 04/21] fix: resolve imprecise-type findings (mago analyze) Added precise array docblock types in place of bare `array` type hints, matching each array's actual literal shape rather than defaulting to array: - ConfigProvider::getDependencies()/__invoke(): full array-shape docblocks (aliases/factories are always class-string => class-string maps, per Laminas ServiceManager config conventions) - Config/option bags ($options, $features, $connectionParameters, $connectionInfo) across Driver.php, Pdo/Driver.php, Connection.php, Pdo/Connection.php, and 3 Container/*Factory.php files: array - Statement::execute($parameters): array (bind params can be positional int or named string keys) - Result::$statementBindValues: array{keys: string[]|null, values: array} (actual fixed shape) - AlterTableDecorator::getSqlInsertOffsets(): array - AlterTableDecorator::processAddColumns()/processChangeColumns(): array> - SelectDecorator::processOffset(): string[]|null (matches the sibling processLimit()'s existing identical-shape docblock) mago analyze: 352 -> 338 remaining issues. imprecise-type: 20 -> 0. --- src/ConfigProvider.php | 14 ++++++++++++++ src/Connection.php | 2 ++ src/Container/ConnectionInterfaceFactory.php | 2 ++ src/Container/DriverInterfaceFactory.php | 2 ++ src/Container/MetadataInterfaceFactory.php | 2 ++ src/Container/PdoConnectionInterfaceFactory.php | 2 ++ src/Container/PdoDriverInterfaceFactory.php | 2 ++ src/Container/PdoStatementFactory.php | 3 +++ src/Container/PlatformInterfaceFactory.php | 2 ++ src/Container/StatementInterfaceFactory.php | 3 +++ src/Driver.php | 2 ++ src/Pdo/Connection.php | 2 ++ src/Pdo/Driver.php | 3 +++ src/Result.php | 1 + src/Sql/Ddl/AlterTableDecorator.php | 9 +++++++++ src/Sql/SelectDecorator.php | 1 + src/Statement.php | 2 ++ 17 files changed, 54 insertions(+) diff --git a/src/ConfigProvider.php b/src/ConfigProvider.php index 9ba7e69..7f73013 100644 --- a/src/ConfigProvider.php +++ b/src/ConfigProvider.php @@ -12,6 +12,12 @@ final class ConfigProvider { + /** + * @return array{ + * aliases: array, + * factories: array, + * } + */ public function getDependencies(): array { return [ @@ -44,6 +50,14 @@ public function getDependencies(): array ]; } + /** + * @return array{ + * dependencies: array{ + * aliases: array, + * factories: array, + * }, + * } + */ public function __invoke(): array { return [ diff --git a/src/Connection.php b/src/Connection.php index 0d3343e..fef7c60 100644 --- a/src/Connection.php +++ b/src/Connection.php @@ -38,6 +38,8 @@ class Connection extends AbstractConnection implements DriverAwareInterface /** * Constructor * + * @param array|mysqli|null $connectionInfo + * * @throws InvalidArgumentException */ public function __construct( diff --git a/src/Container/ConnectionInterfaceFactory.php b/src/Container/ConnectionInterfaceFactory.php index 233561e..41f89dd 100644 --- a/src/Container/ConnectionInterfaceFactory.php +++ b/src/Container/ConnectionInterfaceFactory.php @@ -14,6 +14,8 @@ final class ConnectionInterfaceFactory { /** + * @param array|null $options + * * @throws \PhpDb\Adapter\Exception\ExceptionInterface */ public function __invoke( diff --git a/src/Container/DriverInterfaceFactory.php b/src/Container/DriverInterfaceFactory.php index 37a98ad..bcc46de 100644 --- a/src/Container/DriverInterfaceFactory.php +++ b/src/Container/DriverInterfaceFactory.php @@ -19,6 +19,8 @@ final class DriverInterfaceFactory { /** + * @param array|null $options + * * @throws \Laminas\ServiceManager\Exception\ExceptionInterface * @throws \Psr\Container\ContainerExceptionInterface * @throws \Psr\Container\NotFoundExceptionInterface diff --git a/src/Container/MetadataInterfaceFactory.php b/src/Container/MetadataInterfaceFactory.php index 2d62af5..2d9c9fc 100644 --- a/src/Container/MetadataInterfaceFactory.php +++ b/src/Container/MetadataInterfaceFactory.php @@ -14,6 +14,8 @@ final class MetadataInterfaceFactory public const ADAPTER_SERVICE_NAME = 'adapter_service_name'; /** + * @param array|null $options + * * @throws \Psr\Container\ContainerExceptionInterface * @throws \Psr\Container\NotFoundExceptionInterface */ diff --git a/src/Container/PdoConnectionInterfaceFactory.php b/src/Container/PdoConnectionInterfaceFactory.php index 6854a6a..eb247f1 100644 --- a/src/Container/PdoConnectionInterfaceFactory.php +++ b/src/Container/PdoConnectionInterfaceFactory.php @@ -14,6 +14,8 @@ final class PdoConnectionInterfaceFactory { /** + * @param array|null $options + * * @throws \PhpDb\Adapter\Exception\ExceptionInterface */ public function __invoke( diff --git a/src/Container/PdoDriverInterfaceFactory.php b/src/Container/PdoDriverInterfaceFactory.php index 2e8df13..eb97712 100644 --- a/src/Container/PdoDriverInterfaceFactory.php +++ b/src/Container/PdoDriverInterfaceFactory.php @@ -19,6 +19,8 @@ final class PdoDriverInterfaceFactory { /** + * @param array|null $options + * * @throws \Laminas\ServiceManager\Exception\ExceptionInterface * @throws \Psr\Container\ContainerExceptionInterface * @throws \Psr\Container\NotFoundExceptionInterface diff --git a/src/Container/PdoStatementFactory.php b/src/Container/PdoStatementFactory.php index 0a0c0c2..9227cde 100644 --- a/src/Container/PdoStatementFactory.php +++ b/src/Container/PdoStatementFactory.php @@ -10,6 +10,9 @@ final class PdoStatementFactory { + /** + * @param array|null $options + */ public function __invoke( ContainerInterface $container, string $requestedName, diff --git a/src/Container/PlatformInterfaceFactory.php b/src/Container/PlatformInterfaceFactory.php index 381f9b9..7eadc7d 100644 --- a/src/Container/PlatformInterfaceFactory.php +++ b/src/Container/PlatformInterfaceFactory.php @@ -14,6 +14,8 @@ final class PlatformInterfaceFactory { /** + * @param array|null $options + * * @throws \Psr\Container\ContainerExceptionInterface */ public function __invoke( diff --git a/src/Container/StatementInterfaceFactory.php b/src/Container/StatementInterfaceFactory.php index 9eb78a1..09cace3 100644 --- a/src/Container/StatementInterfaceFactory.php +++ b/src/Container/StatementInterfaceFactory.php @@ -10,6 +10,9 @@ final class StatementInterfaceFactory { + /** + * @param array|null $options + */ public function __invoke( ContainerInterface $container, string $requestedName, diff --git a/src/Driver.php b/src/Driver.php index 47885c2..cbe8117 100644 --- a/src/Driver.php +++ b/src/Driver.php @@ -29,6 +29,8 @@ final class Driver implements DriverInterface, ProfilerAwareInterface ]; /** + * @param array $options + * * @throws \PhpDb\Exception\ExceptionInterface */ public function __construct( diff --git a/src/Pdo/Connection.php b/src/Pdo/Connection.php index a297c82..0bef2ce 100644 --- a/src/Pdo/Connection.php +++ b/src/Pdo/Connection.php @@ -25,6 +25,8 @@ final class Connection extends AbstractPdoConnection /** * Constructor * + * @param array|PDO $connectionParameters + * * @throws Exception\InvalidArgumentException */ public function __construct( diff --git a/src/Pdo/Driver.php b/src/Pdo/Driver.php index 95f3aec..a99e2b4 100644 --- a/src/Pdo/Driver.php +++ b/src/Pdo/Driver.php @@ -18,6 +18,9 @@ final class Driver extends AbstractPdo { + /** + * @param array $features + */ public function __construct( (PdoConnectionInterface&PdoDriverAwareInterface)|PDO $connection, StatementInterface&PdoDriverAwareInterface $statementPrototype = new Statement(), diff --git a/src/Result.php b/src/Result.php index 45bec99..22f4022 100644 --- a/src/Result.php +++ b/src/Result.php @@ -41,6 +41,7 @@ final class Result implements Iterator, ResultInterface /** @var mixed */ protected $currentData; + /** @var array{keys: string[]|null, values: array} */ protected array $statementBindValues = ['keys' => null, 'values' => []]; protected mixed $generatedValue; diff --git a/src/Sql/Ddl/AlterTableDecorator.php b/src/Sql/Ddl/AlterTableDecorator.php index 14099f9..451cb91 100644 --- a/src/Sql/Ddl/AlterTableDecorator.php +++ b/src/Sql/Ddl/AlterTableDecorator.php @@ -66,6 +66,9 @@ public function setSubject( return $this; } + /** + * @return array + */ protected function getSqlInsertOffsets(string $sql): array { $sqlLength = strlen($sql); @@ -99,6 +102,9 @@ protected function getSqlInsertOffsets(string $sql): array return $insertStart; } + /** + * @return array> + */ #[Override] protected function processAddColumns(?PlatformInterface $adapterPlatform = null): array { @@ -173,6 +179,9 @@ protected function processAddColumns(?PlatformInterface $adapterPlatform = null) return [$sqls]; } + /** + * @return array> + */ #[Override] protected function processChangeColumns(?PlatformInterface $adapterPlatform = null): array { diff --git a/src/Sql/SelectDecorator.php b/src/Sql/SelectDecorator.php index fba2b63..c4f36ec 100644 --- a/src/Sql/SelectDecorator.php +++ b/src/Sql/SelectDecorator.php @@ -56,6 +56,7 @@ protected function processLimit( return [$this->limit]; } + /** @return string[]|null */ #[Override] protected function processOffset( PlatformInterface $platform, diff --git a/src/Statement.php b/src/Statement.php index a76d7e9..6f51957 100644 --- a/src/Statement.php +++ b/src/Statement.php @@ -43,6 +43,8 @@ public function __construct( /** * Execute * + * @param array|ParameterContainer|null $parameters + * * @throws Exception\ExceptionInterface */ #[Override] From 517dd97b7c0894de8472d6171b8da44790ac8e96 Mon Sep 17 00:00:00 2001 From: Joey Smith Date: Mon, 10 Aug 2026 13:18:58 -0500 Subject: [PATCH 05/21] fix: resolve most uninitialized-property findings (mago analyze) - Sql/SelectDecorator.php, Sql/Ddl/CreateTableDecorator.php, Sql/Ddl/AlterTableDecorator.php: $subject given a null default; the property's type already allows null, only set later via setSubject(). No behavior change. - Pdo/Driver.php: $profiler (inherited from AbstractPdo, declared ?ProfilerInterface with no default) redeclared with a null default. No behavior change, same type. - Pdo/Connection.php: $dsn and $driverName (inherited, non-nullable) redeclared as nullable with null defaults, matching their real lazy-set lifecycle (set inside connect()/setResource()). - Connection.php (mysqli): $driverName given a null default. $driver made nullable, with an explicit guard added at its one real call site in execute() that throws PhpDb\Adapter\Exception\RuntimeException if execute() is somehow called without setDriver() ever being called. This replaces a previous @mago-expect suppression with a real, visible runtime check, verified empirically that the two early constructor `return;` statements each need their own suppression/fix since mago tracks them as separate finding instances from the property declaration itself. - Statement.php: reverted an earlier suppression attempt for $mysqli/$driver/$resource. These 3 properties have multiple independent read sites across execute()/prepare()/getResource()/ bindParametersFromContainer(), and mago does not retain null-narrowing of a property across separate statements/methods the way it does for local variables - a real fix requires capturing each property into a local variable after a guard clause, which is a larger, more invasive rewrite than we want to do as a side effect of this analyze pass. Left unsuppressed (visible in `mago analyze` output) rather than adding another @mago-expect - tracked in a follow-up issue and intended to be captured by a mago baseline (not inline suppression) once this PR's analyze work is otherwise done. mago analyze: 338 -> 326 remaining issues. --- src/Connection.php | 8 +++++++- src/Pdo/Connection.php | 4 ++++ src/Pdo/Driver.php | 3 +++ src/Sql/Ddl/AlterTableDecorator.php | 2 +- src/Sql/Ddl/CreateTableDecorator.php | 2 +- src/Sql/SelectDecorator.php | 2 +- 6 files changed, 17 insertions(+), 4 deletions(-) diff --git a/src/Connection.php b/src/Connection.php index fef7c60..46be667 100644 --- a/src/Connection.php +++ b/src/Connection.php @@ -30,7 +30,9 @@ // @mago-expect analysis:class-must-be-final class Connection extends AbstractConnection implements DriverAwareInterface { - protected Driver $driver; + protected ?Driver $driver = null; + + protected ?string $driverName = null; /** @var mysqli */ protected $resource; @@ -238,6 +240,10 @@ public function execute(string $sql): ?ResultInterface throw new Exception\InvalidQueryException($this->resource->error); } + if (null === $this->driver) { + throw new Exception\RuntimeException('Cannot execute without a driver; call setDriver() first.'); + } + return $this->driver->createResult(true === $resultResource ? $this->resource : $resultResource); } diff --git a/src/Pdo/Connection.php b/src/Pdo/Connection.php index 0bef2ce..544584d 100644 --- a/src/Pdo/Connection.php +++ b/src/Pdo/Connection.php @@ -22,6 +22,10 @@ // @mago-expect lint:cyclomatic-complexity final class Connection extends AbstractPdoConnection { + protected ?string $dsn = null; + + protected ?string $driverName = null; + /** * Constructor * diff --git a/src/Pdo/Driver.php b/src/Pdo/Driver.php index a99e2b4..fa412d1 100644 --- a/src/Pdo/Driver.php +++ b/src/Pdo/Driver.php @@ -15,9 +15,12 @@ use PhpDb\Adapter\Driver\PdoDriverAwareInterface; use PhpDb\Adapter\Driver\ResultInterface; use PhpDb\Adapter\Driver\StatementInterface; +use PhpDb\Adapter\Profiler\ProfilerInterface; final class Driver extends AbstractPdo { + protected ?ProfilerInterface $profiler = null; + /** * @param array $features */ diff --git a/src/Sql/Ddl/AlterTableDecorator.php b/src/Sql/Ddl/AlterTableDecorator.php index 451cb91..b6bf035 100644 --- a/src/Sql/Ddl/AlterTableDecorator.php +++ b/src/Sql/Ddl/AlterTableDecorator.php @@ -25,7 +25,7 @@ // @mago-expect lint:kan-defect final class AlterTableDecorator extends AlterTable implements PlatformDecoratorInterface { - protected SqlInterface|PreparableSqlInterface|null $subject; + protected SqlInterface|PreparableSqlInterface|null $subject = null; /** @var array{ * unsigned: int, diff --git a/src/Sql/Ddl/CreateTableDecorator.php b/src/Sql/Ddl/CreateTableDecorator.php index 1ac3a89..65cade5 100644 --- a/src/Sql/Ddl/CreateTableDecorator.php +++ b/src/Sql/Ddl/CreateTableDecorator.php @@ -24,7 +24,7 @@ // @mago-expect lint:kan-defect final class CreateTableDecorator extends CreateTable implements PlatformDecoratorInterface { - protected SqlInterface|PreparableSqlInterface|null $subject; + protected SqlInterface|PreparableSqlInterface|null $subject = null; /** @var int[] */ protected $columnOptionSortOrder = [ diff --git a/src/Sql/SelectDecorator.php b/src/Sql/SelectDecorator.php index c4f36ec..4f05437 100644 --- a/src/Sql/SelectDecorator.php +++ b/src/Sql/SelectDecorator.php @@ -15,7 +15,7 @@ final class SelectDecorator extends Select implements PlatformDecoratorInterface { - protected SqlInterface|PreparableSqlInterface|null $subject; + protected SqlInterface|PreparableSqlInterface|null $subject = null; #[Override] public function setSubject( From 5fae58c0b1e436e93dd9e653e3bf181a14bc43ab Mon Sep 17 00:00:00 2001 From: Joey Smith Date: Mon, 10 Aug 2026 13:25:20 -0500 Subject: [PATCH 06/21] fix: document unhandled-thrown-type in Result::loadFromMysqliResult() Missed adding @throws Exception\RuntimeException when the guard clause was added for the non-existent-method fix earlier in this branch. mago analyze: 326 -> 325 remaining issues. --- src/Result.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Result.php b/src/Result.php index 22f4022..3eb781d 100644 --- a/src/Result.php +++ b/src/Result.php @@ -330,6 +330,8 @@ protected function loadDataFromMysqliStatement(): bool /** * Load from mysqli result + * + * @throws Exception\RuntimeException */ protected function loadFromMysqliResult(): bool { From 5bf22d739186d8ef5b9e11579631ebc90f290ba7 Mon Sep 17 00:00:00 2001 From: Joey Smith Date: Mon, 10 Aug 2026 13:51:13 -0500 Subject: [PATCH 07/21] fix: resolve unused-parameter findings (mago analyze) Prefixed unused $container/$requestedName parameters with underscore across 8 Container/*Factory.php __invoke() methods, per mago's own suggested remediation. These factories deliberately do not implement any Laminas ServiceManager FactoryInterface: PSR-11's v1 -> v2 jump added parameter and return types to ContainerInterface, so implementing a formal FactoryInterface would lock this library to SMv4 only, breaking SMv3 support. Since __invoke() has no enforceable interface contract, PHP does not require unused leading/middle parameters to be removed or renamed - but they also can't just be deleted, since Laminas ServiceManager calls factories positionally ($container, $requestedName, $options), and $options is used in most of these, so removing an earlier unused parameter would shift $options into the wrong position. Underscore-prefixing keeps the signature and calling convention intact while marking the parameters as intentionally unused. mago analyze: 325 -> 312 remaining issues. unused-parameter: 13 -> 0. --- src/Container/ConnectionInterfaceFactory.php | 4 ++-- src/Container/DriverInterfaceFactory.php | 2 +- src/Container/MetadataInterfaceFactory.php | 2 +- src/Container/PdoConnectionInterfaceFactory.php | 4 ++-- src/Container/PdoDriverInterfaceFactory.php | 2 +- src/Container/PdoStatementFactory.php | 4 ++-- src/Container/PlatformInterfaceFactory.php | 4 ++-- src/Container/StatementInterfaceFactory.php | 4 ++-- 8 files changed, 13 insertions(+), 13 deletions(-) diff --git a/src/Container/ConnectionInterfaceFactory.php b/src/Container/ConnectionInterfaceFactory.php index 41f89dd..9f7b7ee 100644 --- a/src/Container/ConnectionInterfaceFactory.php +++ b/src/Container/ConnectionInterfaceFactory.php @@ -19,8 +19,8 @@ final class ConnectionInterfaceFactory * @throws \PhpDb\Adapter\Exception\ExceptionInterface */ public function __invoke( - ContainerInterface $container, - string $requestedName, + ContainerInterface $_container, + string $_requestedName, ?array $options = null, ): ConnectionInterface&Connection { $conn = $options['connection'] ?? []; diff --git a/src/Container/DriverInterfaceFactory.php b/src/Container/DriverInterfaceFactory.php index bcc46de..760dafd 100644 --- a/src/Container/DriverInterfaceFactory.php +++ b/src/Container/DriverInterfaceFactory.php @@ -28,7 +28,7 @@ final class DriverInterfaceFactory */ public function __invoke( ContainerInterface&ServiceManager $container, - string $requestedName, + string $_requestedName, ?array $options = null, ): DriverInterface&Driver { if (null === $options || ! array_key_exists('connection', $options)) { diff --git a/src/Container/MetadataInterfaceFactory.php b/src/Container/MetadataInterfaceFactory.php index 2d9c9fc..5fd2f5a 100644 --- a/src/Container/MetadataInterfaceFactory.php +++ b/src/Container/MetadataInterfaceFactory.php @@ -21,7 +21,7 @@ final class MetadataInterfaceFactory */ public function __invoke( ContainerInterface $container, - string $requestedName, + string $_requestedName, ?array $options = null, ): MetadataInterface&Metadata\Source { $adapterServiceName = $options[self::ADAPTER_SERVICE_NAME] ?? AdapterInterface::class; diff --git a/src/Container/PdoConnectionInterfaceFactory.php b/src/Container/PdoConnectionInterfaceFactory.php index eb247f1..877f055 100644 --- a/src/Container/PdoConnectionInterfaceFactory.php +++ b/src/Container/PdoConnectionInterfaceFactory.php @@ -19,8 +19,8 @@ final class PdoConnectionInterfaceFactory * @throws \PhpDb\Adapter\Exception\ExceptionInterface */ public function __invoke( - ContainerInterface $container, - string $requestedName, + ContainerInterface $_container, + string $_requestedName, ?array $options = null, ): PdoConnectionInterface&Connection { $conn = $options['connection'] ?? []; diff --git a/src/Container/PdoDriverInterfaceFactory.php b/src/Container/PdoDriverInterfaceFactory.php index eb97712..3f79a74 100644 --- a/src/Container/PdoDriverInterfaceFactory.php +++ b/src/Container/PdoDriverInterfaceFactory.php @@ -27,7 +27,7 @@ final class PdoDriverInterfaceFactory */ public function __invoke( ContainerInterface&ServiceManager $container, - string $requestedName, + string $_requestedName, ?array $options = null, ): PdoDriverInterface&Driver { if (null === $options || ! array_key_exists('connection', $options)) { diff --git a/src/Container/PdoStatementFactory.php b/src/Container/PdoStatementFactory.php index 9227cde..cead40c 100644 --- a/src/Container/PdoStatementFactory.php +++ b/src/Container/PdoStatementFactory.php @@ -14,8 +14,8 @@ final class PdoStatementFactory * @param array|null $options */ public function __invoke( - ContainerInterface $container, - string $requestedName, + ContainerInterface $_container, + string $_requestedName, ?array $options = null, ): StatementInterface&Statement { return new Statement(options: $options); diff --git a/src/Container/PlatformInterfaceFactory.php b/src/Container/PlatformInterfaceFactory.php index 7eadc7d..1840510 100644 --- a/src/Container/PlatformInterfaceFactory.php +++ b/src/Container/PlatformInterfaceFactory.php @@ -19,8 +19,8 @@ final class PlatformInterfaceFactory * @throws \Psr\Container\ContainerExceptionInterface */ public function __invoke( - ContainerInterface $container, - string $requestedName, + ContainerInterface $_container, + string $_requestedName, ?array $options = null, ): PlatformInterface&AdapterPlatform { $driverInstance = $options['driver'] ?? null; diff --git a/src/Container/StatementInterfaceFactory.php b/src/Container/StatementInterfaceFactory.php index 09cace3..122c463 100644 --- a/src/Container/StatementInterfaceFactory.php +++ b/src/Container/StatementInterfaceFactory.php @@ -14,8 +14,8 @@ final class StatementInterfaceFactory * @param array|null $options */ public function __invoke( - ContainerInterface $container, - string $requestedName, + ContainerInterface $_container, + string $_requestedName, ?array $options = null, ): StatementInterface&Statement { return new Statement(bufferResults: $options['buffer_results'] ?? false); From 9371a24e8b5d49d3bba8a8e2c359090b475d1109 Mon Sep 17 00:00:00 2001 From: Joey Smith Date: Mon, 10 Aug 2026 14:06:46 -0500 Subject: [PATCH 08/21] mago analyze: add row-shape docblocks for Metadata/Source.php query loops Documents the actual SELECT column shape per query loop, reducing this file's mixed-*/non-existent-method finding count from 77 to 39. --- src/Metadata/Source.php | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/Metadata/Source.php b/src/Metadata/Source.php index 9cf9f6c..5370519 100644 --- a/src/Metadata/Source.php +++ b/src/Metadata/Source.php @@ -85,6 +85,7 @@ protected function loadColumnData(string $table, string $schema): void /** @var ResultSetInterface $results */ $results = $this->adapter->query($sql, AdapterInterface::QUERY_MODE_EXECUTE); $columns = []; + /** @var array{ORDINAL_POSITION: string, COLUMN_DEFAULT: ?string, IS_NULLABLE: string, DATA_TYPE: string, CHARACTER_MAXIMUM_LENGTH: ?string, CHARACTER_OCTET_LENGTH: ?string, NUMERIC_PRECISION: ?string, NUMERIC_SCALE: ?string, COLUMN_NAME: string, COLUMN_TYPE: string} $row */ foreach ($results->toArray() as $row) { $erratas = []; $matches = []; @@ -215,6 +216,7 @@ protected function loadConstraintData(string $table, string $schema): void $realName = null; $constraints = []; + /** @var array{TABLE_NAME: string, CONSTRAINT_NAME: string, CONSTRAINT_TYPE: string, COLUMN_NAME: ?string, MATCH_OPTION: ?string, UPDATE_RULE: ?string, DELETE_RULE: ?string, REFERENCED_TABLE_SCHEMA: ?string, REFERENCED_TABLE_NAME: ?string, REFERENCED_COLUMN_NAME: ?string} $row */ foreach ($results->toArray() as $row) { if ($row['CONSTRAINT_NAME'] !== $realName) { $realName = $row['CONSTRAINT_NAME']; @@ -297,6 +299,7 @@ protected function loadConstraintDataKeys(string $schema): void $results = $this->adapter->query($sql, AdapterInterface::QUERY_MODE_EXECUTE); $data = []; + /** @var array $row */ foreach ($results->toArray() as $row) { $data[] = array_change_key_case($row, CASE_LOWER); } @@ -353,6 +356,7 @@ protected function loadConstraintDataNames(string $schema): void $results = $this->adapter->query($sql, AdapterInterface::QUERY_MODE_EXECUTE); $data = []; + /** @var array $row */ foreach ($results->toArray() as $row) { $data[] = array_change_key_case($row, CASE_LOWER); } @@ -425,6 +429,7 @@ protected function loadConstraintReferences(string $table, string $schema): void $results = $this->adapter->query($sql, AdapterInterface::QUERY_MODE_EXECUTE); $data = []; + /** @var array $row */ foreach ($results->toArray() as $row) { $data[] = array_change_key_case($row, CASE_LOWER); } @@ -455,6 +460,7 @@ protected function loadSchemaData(): void $results = $this->adapter->query($sql, AdapterInterface::QUERY_MODE_EXECUTE); $schemas = []; + /** @var array{SCHEMA_NAME: string} $row */ foreach ($results->toArray() as $row) { $schemas[] = $row['SCHEMA_NAME']; } @@ -513,6 +519,7 @@ protected function loadTableNameData(string $schema): void $results = $this->adapter->query($sql, AdapterInterface::QUERY_MODE_EXECUTE); $tables = []; + /** @var array{TABLE_NAME: string, TABLE_TYPE: string, VIEW_DEFINITION: ?string, CHECK_OPTION: ?string, IS_UPDATABLE: ?string} $row */ foreach ($results->toArray() as $row) { $tables[$row['TABLE_NAME']] = [ 'table_type' => $row['TABLE_TYPE'], @@ -575,7 +582,9 @@ protected function loadTriggerData(string $schema): void $results = $this->adapter->query($sql, AdapterInterface::QUERY_MODE_EXECUTE); $data = []; + /** @var array{TRIGGER_NAME: string, EVENT_MANIPULATION: string, EVENT_OBJECT_CATALOG: string, EVENT_OBJECT_SCHEMA: string, EVENT_OBJECT_TABLE: string, ACTION_ORDER: string, ACTION_CONDITION: ?string, ACTION_STATEMENT: string, ACTION_ORIENTATION: string, ACTION_TIMING: string, ACTION_REFERENCE_OLD_TABLE: ?string, ACTION_REFERENCE_NEW_TABLE: ?string, ACTION_REFERENCE_OLD_ROW: ?string, ACTION_REFERENCE_NEW_ROW: ?string, CREATED: ?string} $row */ foreach ($results->toArray() as $row) { + /** @var array{trigger_name: string, event_manipulation: string, event_object_catalog: string, event_object_schema: string, event_object_table: string, action_order: string, action_condition: ?string, action_statement: string, action_orientation: string, action_timing: string, action_reference_old_table: ?string, action_reference_new_table: ?string, action_reference_old_row: ?string, action_reference_new_row: ?string, created: ?string} $row */ $row = array_change_key_case($row, CASE_LOWER); if (null !== $row['created']) { $row['created'] = new DateTime($row['created']); From 4d917f84a1d08d15908c527c22d4d5511277c84b Mon Sep 17 00:00:00 2001 From: Joey Smith Date: Mon, 10 Aug 2026 15:10:31 -0500 Subject: [PATCH 09/21] mago analyze: fix real bugs and tighten types in Result.php - count(): guard against bare mysqli (no num_rows), cast num_rows to int - getAffectedRows(): cast affected_rows/num_rows to int - getGeneratedValue(): tighten $generatedValue to string|int|false|null - initialize(): remove unreachable instanceof guard, drop redundant instanceof check already proven by prior elimination - loadDataFromMysqliStatement(): guard result_metadata() possibly returning false; type $col via object shape docblock - loadFromMysqliResult(): narrow fetch_assoc()'s type to what PHP actually documents (array|null, not array|false|null per stub) - $currentData: type as ?array, add Iterator|null> generics matching key()/current() - $isBuffered: default to null (matches ResultInterface::isBuffered(): ?bool contract), not false Reduces this file's mago analyze findings from 23 to 6; the rest are tracked in #66 for baseline. --- src/Result.php | 48 +++++++++++++++++++++++++++--------------------- 1 file changed, 27 insertions(+), 21 deletions(-) diff --git a/src/Result.php b/src/Result.php index 3eb781d..6c8d4bf 100644 --- a/src/Result.php +++ b/src/Result.php @@ -21,11 +21,12 @@ // @mago-expect lint:cyclomatic-complexity // @mago-expect lint:kan-defect // @mago-expect lint:too-many-methods +/** @implements Iterator|null> */ final class Result implements Iterator, ResultInterface { protected mysqli|mysqli_result|mysqli_stmt $resource; - protected bool $isBuffered; + protected ?bool $isBuffered = null; protected int $position = 0; @@ -38,13 +39,13 @@ final class Result implements Iterator, ResultInterface protected bool $nextComplete = false; - /** @var mixed */ - protected $currentData; + /** @var array|null */ + protected ?array $currentData = null; /** @var array{keys: string[]|null, values: array} */ protected array $statementBindValues = ['keys' => null, 'values' => []]; - protected mixed $generatedValue; + protected string|int|false|null $generatedValue = null; /** * {@inheritDoc} @@ -76,7 +77,12 @@ public function count() if (! $this->isBuffered) { throw new Exception\RuntimeException('Row count is not available in unbuffered result sets.'); } - return $this->resource->num_rows; + + if (! $this->resource instanceof mysqli_result && ! $this->resource instanceof mysqli_stmt) { + throw new Exception\RuntimeException('Cannot count rows in a result that is not a query result'); + } + + return (int) $this->resource->num_rows; } /** @@ -84,7 +90,7 @@ public function count() * * @throws Exception\ExceptionInterface * - * @return mixed + * @return array|null */ #[ReturnTypeWillChange] #[Override] @@ -110,10 +116,10 @@ public function current() public function getAffectedRows(): int { if ($this->resource instanceof mysqli || $this->resource instanceof mysqli_stmt) { - return $this->resource->affected_rows; + return (int) $this->resource->affected_rows; } - return $this->resource->num_rows; + return (int) $this->resource->num_rows; } /** @@ -151,17 +157,9 @@ public function getResource(): mysqli|mysqli_result|mysqli_stmt */ public function initialize( mysqli|mysqli_result|mysqli_stmt $resource, - mixed $generatedValue, + string|int|false|null $generatedValue, ?bool $isBuffered = null, ): ResultInterface { - if ( - ! $resource instanceof mysqli - && ! $resource instanceof mysqli_result - && ! $resource instanceof mysqli_stmt - ) { - throw new Exception\InvalidArgumentException('Invalid resource provided.'); - } - /** * todo(@tyrsson): examine this closely to see if this is the correct behavior */ @@ -169,7 +167,7 @@ public function initialize( null !== $isBuffered => $isBuffered, $resource instanceof mysqli || $resource instanceof mysqli_result - || ($resource instanceof mysqli_stmt && 0 !== $resource->num_rows) + || 0 !== $resource->num_rows => true, default => $this->isBuffered, }; @@ -200,7 +198,7 @@ public function isQueryResult(): bool /** * Key * - * @return mixed + * @return int */ #[ReturnTypeWillChange] #[Override] @@ -292,7 +290,12 @@ protected function loadDataFromMysqliStatement(): bool if (null === $this->statementBindValues['keys']) { $this->statementBindValues['keys'] = []; $resultResource = $this->resource->result_metadata(); + if (false === $resultResource) { + return $resultResource; + } + foreach ($resultResource->fetch_fields() as $col) { + /** @var object{name: string} $col */ $this->statementBindValues['keys'][] = $col->name; } $this->statementBindValues['values'] = array_fill( @@ -314,7 +317,7 @@ protected function loadDataFromMysqliStatement(): bool return false; } - if (false === $r) { + if (! $r) { throw new Exception\RuntimeException($this->resource->error); } @@ -341,7 +344,10 @@ protected function loadFromMysqliResult(): bool throw new Exception\RuntimeException('Cannot fetch from a result that is not a mysqli_result'); } - if (($data = $this->resource->fetch_assoc()) === null) { + /** @var array|null $data */ + $data = $this->resource->fetch_assoc(); + + if (null === $data) { return false; } From 01bf6520c7699a85cb408b587584e46201d3ffd4 Mon Sep 17 00:00:00 2001 From: Joey Smith Date: Mon, 10 Aug 2026 16:56:18 -0500 Subject: [PATCH 10/21] mago analyze: fix real bugs in Statement.php - getResource(): add local @return docblock overriding the interface's legacy resource|false|null contract; suppress the resulting incompatible-return-type check (native mysqli_stmt is always accurate for this class, matching the existing @phpstan-ignore rationale) - prepare(): assign mysqli::prepare()'s result to a local variable and guard before storing into $resource, instead of assigning the possibly-false result directly into a strictly-typed property (which would throw an uncontrolled TypeError before the intended InvalidQueryException could run) - setDriver(): guard that $driver is the concrete Driver class before assignment, since $this->driver is used with a mysqli-Driver-specific createResult($resource, $buffered) signature not part of DriverInterface - setSql(): coalesce null to '' before assignment, matching the existing empty-string-as-unset convention used in prepare() Reduces this file's mago analyze findings from 10 to 3 (the 3 uninitialized-property findings already tracked in #61). --- src/Statement.php | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/src/Statement.php b/src/Statement.php index 6f51957..7affd1f 100644 --- a/src/Statement.php +++ b/src/Statement.php @@ -20,6 +20,7 @@ use function array_unshift; use function call_user_func_array; use function is_array; +use function sprintf; final class Statement implements StatementInterface, DriverAwareInterface, ProfilerAwareInterface { @@ -101,7 +102,13 @@ public function getProfiler(): ?ProfilerInterface /** * @phpstan-ignore method.childReturnType + * + * @return mysqli_stmt */ + // @mago-expect analysis:incompatible-return-type - StatementInterface::getResource() declares no + // native return type, only a legacy `resource|false|null` docblock; this class's $resource is + // always a genuine mysqli_stmt, so the narrower native return type here is a valid PHP covariant + // override, not a real incompatibility. #[Override] public function getResource(): mysqli_stmt { @@ -138,8 +145,8 @@ public function prepare(?string $sql = null): StatementInterface $sql = null === $sql || '' === $sql ? $this->sql : $sql; - $this->resource = $this->mysqli->prepare($sql); - if (! $this->resource instanceof mysqli_stmt) { + $resource = $this->mysqli->prepare($sql); + if (! $resource instanceof mysqli_stmt) { throw new Exception\InvalidQueryException( "Statement couldn't be produced with sql: {$sql}", $this->mysqli->errno, @@ -147,6 +154,7 @@ public function prepare(?string $sql = null): StatementInterface ); } + $this->resource = $resource; $this->isPrepared = true; return $this; } @@ -154,6 +162,10 @@ public function prepare(?string $sql = null): StatementInterface #[Override] public function setDriver(DriverInterface $driver): DriverAwareInterface { + if (! $driver instanceof Driver) { + throw new Exception\InvalidArgumentException(sprintf('Driver must be an instance of %s', Driver::class)); + } + $this->driver = $driver; return $this; } @@ -183,7 +195,7 @@ public function setResource(mysqli_stmt $mysqliStatement): StatementInterface #[Override] public function setSql(?string $sql): StatementContainerInterface { - $this->sql = $sql; + $this->sql = $sql ?? ''; return $this; } From 14461351ec8ab08f319ef1123a02c95694a83bec Mon Sep 17 00:00:00 2001 From: Joey Smith Date: Mon, 10 Aug 2026 17:20:21 -0500 Subject: [PATCH 11/21] mago analyze: fix real bugs in Connection.php - constructor: remove unreachable final branch (connectionInfo's type is array|mysqli|null, exhaustively handled by the two prior branches) - setDriver()/$driver: widen to ?DriverInterface (unlike Statement.php, the sole call site 'createResult($resource)' passes only one arg, fully compatible with the interface contract; no concrete Driver features are actually used) - getCurrentSchema(): guard query()'s bool|mysqli_result return before calling fetch_row(), guard fetch_row()'s real false-on-failure return (per PHP docs), and narrow its single-column shape via docblock - createResource(): add native mysqli return type Reduces this file's mago analyze findings from 46 to 38. --- src/Connection.php | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/src/Connection.php b/src/Connection.php index 46be667..94752d4 100644 --- a/src/Connection.php +++ b/src/Connection.php @@ -6,6 +6,7 @@ use Exception as GenericException; use mysqli; +use mysqli_result; use Override; use PhpDb\Adapter\Driver\AbstractConnection; use PhpDb\Adapter\Driver\ConnectionInterface; @@ -30,7 +31,7 @@ // @mago-expect analysis:class-must-be-final class Connection extends AbstractConnection implements DriverAwareInterface { - protected ?Driver $driver = null; + protected ?DriverInterface $driver = null; protected ?string $driverName = null; @@ -58,12 +59,6 @@ public function __construct( return; } - - if (null !== $connectionInfo) { - throw new Exception\InvalidArgumentException( - '$connection must be an array of parameters, a mysqli object or null', - ); - } } /** @@ -260,7 +255,19 @@ public function getCurrentSchema(): string|false } $result = $this->resource->query('SELECT DATABASE()'); - $r = $result->fetch_row(); + if (! $result instanceof mysqli_result) { + throw new Exception\RuntimeException('Failed to query current schema'); + } + + $r = $result->fetch_row(); + if (false === $r) { + throw new Exception\RuntimeException($this->resource->error); + } + + /** @var array{0: string|null}|null $r */ + if (null === $r || null === $r[0]) { + return false; + } return $r[0]; } @@ -329,7 +336,7 @@ public function setResource(mysqli $resource): static * * @return mysqli */ - protected function createResource() + protected function createResource(): mysqli { return new mysqli(); } From db9b68737c9a1728492ed8aa87f046f46d7844d8 Mon Sep 17 00:00:00 2001 From: Joey Smith Date: Mon, 10 Aug 2026 17:40:31 -0500 Subject: [PATCH 12/21] mago analyze: suppress createResult() argument mismatch in Connection.php DriverInterface::createResult($resource) is documented with a generic resource type to stay valid across every RDBMS platform (mysqli has been object-oriented since PHP 5.0 and was never part of PHP's resource-to-object migration, so this mismatch isn't fixable locally). Proposed upstream fix tracked at php-db/phpdb#170 (@template generics). Reduces this file's mago analyze findings from 38 to 37; the remaining findings are tracked in #68 for baseline. --- src/Connection.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Connection.php b/src/Connection.php index 94752d4..4ab3800 100644 --- a/src/Connection.php +++ b/src/Connection.php @@ -239,6 +239,10 @@ public function execute(string $sql): ?ResultInterface throw new Exception\RuntimeException('Cannot execute without a driver; call setDriver() first.'); } + // @mago-expect analysis:invalid-argument - DriverInterface::createResult() is documented with a + // generic `resource` type to stay valid across every RDBMS platform (see php-db/phpdb#170 for a + // proposed @template-based fix); this class always passes real mysqli|mysqli_result objects, which + // is correct for this concrete implementation. return $this->driver->createResult(true === $resultResource ? $this->resource : $resultResource); } From e73cdabba0a3d136f4f11480ad7a77f6176af16a Mon Sep 17 00:00:00 2001 From: Joey Smith Date: Mon, 10 Aug 2026 17:56:06 -0500 Subject: [PATCH 13/21] mago analyze: fix real bugs in Pdo/Connection.php - connect(): remove dead is_string($dsn) guard ($dsn is always a string by this point on every code path); cast getAttribute()'s genuinely mixed return before strtolower() - getCurrentSchema(): remove incorrect @var PDOStatement docblock that hid query()'s real PDOStatement|false return; guard $resource nullability before use; narrow fetchColumn()'s mixed return via contextual @var docblock (single-column query) - getLastGeneratedValue(): guard $resource nullability before use - $dsn/$driverName: suppress write-only-property false positives (confirmed via grep: read by inherited AbstractPdoConnection::getDsn() and AbstractConnection::getDriverName(), which mago's per-class analysis doesn't see) Reduces this file's mago analyze findings from 18 to 8; the remaining 8 are the $connectionParameters cluster already tracked in #65. --- src/Pdo/Connection.php | 31 +++++++++++++++++++------------ 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/src/Pdo/Connection.php b/src/Pdo/Connection.php index 544584d..49fe7dc 100644 --- a/src/Pdo/Connection.php +++ b/src/Pdo/Connection.php @@ -20,10 +20,13 @@ use function strtolower; // @mago-expect lint:cyclomatic-complexity +// @mago-expect lint:kan-defect final class Connection extends AbstractPdoConnection { + // @mago-expect analysis:write-only-property - read by the parent's final AbstractPdoConnection::getDsn() protected ?string $dsn = null; + // @mago-expect analysis:write-only-property - read by AbstractConnection::getDriverName() protected ?string $driverName = null; /** @@ -121,19 +124,12 @@ public function connect(): ConnectionInterface $dsn = 'mysql:' . implode(';', $dsn); } - if (! is_string($dsn)) { - throw new Exception\InvalidConnectionParametersException( - 'A dsn was not provided or could not be constructed from your parameters', - $this->connectionParameters, - ); - } - $this->dsn = $dsn; try { $this->resource = new PDO($dsn, $username, $password, $options); $this->resource->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); - $this->driverName = strtolower($this->resource->getAttribute(PDO::ATTR_DRIVER_NAME)); + $this->driverName = strtolower((string) $this->resource->getAttribute(PDO::ATTR_DRIVER_NAME)); } catch (PDOException $e) { $code = $e->getCode(); if (! is_int($code)) { @@ -158,18 +154,29 @@ public function getCurrentSchema(): string|false $this->connect(); } - /** @var PDOStatement $result */ + if (null === $this->resource) { + throw new Exception\RuntimeException( + 'Cannot query current schema without a connected resource; call connect() first.', + ); + } + $result = $this->resource->query('SELECT DATABASE()'); - if ($result instanceof PDOStatement) { - return $result->fetchColumn(); + if (! $result instanceof PDOStatement) { + return false; } - return false; + /** @var string|false|null $value */ + $value = $result->fetchColumn(); + return is_string($value) ? $value : false; } #[Override] public function getLastGeneratedValue(?string $name = null): string|int|false|null { + if (null === $this->resource) { + return false; + } + try { return $this->resource->lastInsertId($name); } catch (PDOException) { From 4e2c1fc49d52501936954fe60c9ca0de0b72599a Mon Sep 17 00:00:00 2001 From: Joey Smith Date: Mon, 10 Aug 2026 18:16:02 -0500 Subject: [PATCH 14/21] mago analyze: fix real bugs in AlterTableDecorator.php - $subject: suppress write-only-property false positive (read by the inherited AbstractSql::$subject handling via get_object_vars()) - getSqlInsertOffsets(): tighten return shape to array{0,1,2,3: int} (the trailing range(0,3) fill loop guarantees all four keys); narrow via @var at the return point since mago's own flow-tracing through the switch/foreach can't prove the shape on its own - remove dead $j ??= 0 (both $insert and $j are always set together in the same switch case each iteration; $insert resets to '' at the top of the loop, so the coalesce can never fire) - compareColumnOptions()/normalizeColumnOption(): promote existing docblock types to native type hints - processAddColumns()/processChangeColumns(): guard against the genuinely-nullable (per inherited parent signature) $adapterPlatform parameter; the real dynamic-dispatch call path always passes a real platform, but the signature itself still legally permits null Reduces this file's mago analyze findings from 43 to 33; 21 are the $connectionParameters/getOptions()/untyped-array cluster tracked in #65, and 12 are a mago loop-bound limitation tracked in #69. --- src/Sql/Ddl/AlterTableDecorator.php | 33 +++++++++++++++++------------ 1 file changed, 19 insertions(+), 14 deletions(-) diff --git a/src/Sql/Ddl/AlterTableDecorator.php b/src/Sql/Ddl/AlterTableDecorator.php index b6bf035..a13a017 100644 --- a/src/Sql/Ddl/AlterTableDecorator.php +++ b/src/Sql/Ddl/AlterTableDecorator.php @@ -7,6 +7,7 @@ use Override; use PhpDb\Adapter\Platform\PlatformInterface; use PhpDb\Sql\Ddl\AlterTable; +use PhpDb\Sql\Exception; use PhpDb\Sql\Platform\PlatformDecoratorInterface; use PhpDb\Sql\PreparableSqlInterface; use PhpDb\Sql\SqlInterface; @@ -25,6 +26,8 @@ // @mago-expect lint:kan-defect final class AlterTableDecorator extends AlterTable implements PlatformDecoratorInterface { + // @mago-expect analysis:write-only-property - read by the inherited AbstractSql::$subject handling + // (get_object_vars($this->subject)), since AlterTable extends AbstractSql protected SqlInterface|PreparableSqlInterface|null $subject = null; /** @var array{ @@ -67,7 +70,7 @@ public function setSubject( } /** - * @return array + * @return array{0: int, 1: int, 2: int, 3: int} */ protected function getSqlInsertOffsets(string $sql): array { @@ -99,15 +102,22 @@ protected function getSqlInsertOffsets(string $sql): array $insertStart[$i] ??= $sqlLength; } + /** @var array{0: int, 1: int, 2: int, 3: int} $insertStart */ return $insertStart; } /** * @return array> + * + * @throws Exception\RuntimeException */ #[Override] protected function processAddColumns(?PlatformInterface $adapterPlatform = null): array { + if (null === $adapterPlatform) { + throw new Exception\RuntimeException('Cannot build column SQL without a platform.'); + } + $sqls = []; foreach ($this->addColumns as $i => $column) { @@ -166,7 +176,6 @@ protected function processAddColumns(?PlatformInterface $adapterPlatform = null) } if ($insert) { - $j ??= 0; $sql = substr_replace($sql, $insert, $insertStart[$j], length: 0); $insertStartCount = count($insertStart); for (; $j < $insertStartCount; ++$j) { @@ -181,10 +190,16 @@ protected function processAddColumns(?PlatformInterface $adapterPlatform = null) /** * @return array> + * + * @throws Exception\RuntimeException */ #[Override] protected function processChangeColumns(?PlatformInterface $adapterPlatform = null): array { + if (null === $adapterPlatform) { + throw new Exception\RuntimeException('Cannot build column SQL without a platform.'); + } + $sqls = []; foreach ($this->changeColumns as $name => $column) { $sql = $this->processExpression($column, $adapterPlatform); @@ -239,7 +254,6 @@ protected function processChangeColumns(?PlatformInterface $adapterPlatform = nu } if ($insert) { - $j ??= 0; $sql = substr_replace($sql, $insert, $insertStart[$j], length: 0); $insertStartCount = count($insertStart); for (; $j < $insertStartCount; ++$j) { @@ -256,13 +270,8 @@ protected function processChangeColumns(?PlatformInterface $adapterPlatform = nu return [$sqls]; } - /** - * @param string $columnA - * @param string $columnB - * @return int - */ // phpcs:ignore SlevomatCodingStandard.Classes.UnusedPrivateElements.UnusedMethod - private function compareColumnOptions($columnA, $columnB) + private function compareColumnOptions(string $columnA, string $columnB): int { $columnA = $this->normalizeColumnOption($columnA); $columnA = $this->columnOptionSortOrder[$columnA] ?? count($this->columnOptionSortOrder); @@ -273,11 +282,7 @@ private function compareColumnOptions($columnA, $columnB) return $columnA - $columnB; } - /** - * @param string $name - * @return string - */ - private function normalizeColumnOption($name) + private function normalizeColumnOption(string $name): string { return strtolower(str_replace(['-', '_', ' '], replace: '', subject: $name)); } From d05276038850a14564f6b6951a879ff5c0d901e9 Mon Sep 17 00:00:00 2001 From: Joey Smith Date: Mon, 10 Aug 2026 18:29:15 -0500 Subject: [PATCH 15/21] mago analyze: fix real bugs in CreateTableDecorator.php Same fixes applied as AlterTableDecorator.php (commit 4e2c1fc): - $subject: suppress write-only-property false positive - $columnOptionSortOrder: native array type hint - getSqlInsertOffsets(): native types, tightened array{0,1,2,3: int} return shape via @var at the return point - processColumns(): rename $platform -> $adapterPlatform to match parent CreateTable::processColumns(), guard nullability, add @throws - remove dead $j ??= 0 - compareColumnOptions()/normalizeColumnOption(): promote docblock types to native type hints Reduces this file's mago analyze findings from 26 to 15; all 15 are already tracked (getOptions()/untyped-array cluster in #65, mago loop-bound limitation in #69). --- src/Sql/Ddl/CreateTableDecorator.php | 39 ++++++++++++++-------------- 1 file changed, 19 insertions(+), 20 deletions(-) diff --git a/src/Sql/Ddl/CreateTableDecorator.php b/src/Sql/Ddl/CreateTableDecorator.php index 65cade5..14f7991 100644 --- a/src/Sql/Ddl/CreateTableDecorator.php +++ b/src/Sql/Ddl/CreateTableDecorator.php @@ -7,6 +7,7 @@ use Override; use PhpDb\Adapter\Platform\PlatformInterface; use PhpDb\Sql\Ddl\CreateTable; +use PhpDb\Sql\Exception; use PhpDb\Sql\Platform\PlatformDecoratorInterface; use PhpDb\Sql\PreparableSqlInterface; use PhpDb\Sql\SqlInterface; @@ -24,10 +25,12 @@ // @mago-expect lint:kan-defect final class CreateTableDecorator extends CreateTable implements PlatformDecoratorInterface { + // @mago-expect analysis:write-only-property - read by the inherited AbstractSql::$subject handling + // (get_object_vars($this->subject)), since CreateTable extends AbstractSql protected SqlInterface|PreparableSqlInterface|null $subject = null; - /** @var int[] */ - protected $columnOptionSortOrder = [ + /** @var array */ + protected array $columnOptionSortOrder = [ 'unsigned' => 0, 'zerofill' => 1, 'charset' => 2, @@ -51,10 +54,9 @@ public function setSubject( } /** - * @param string $sql - * @return array + * @return array{0: int, 1: int, 2: int, 3: int} */ - protected function getSqlInsertOffsets($sql) + protected function getSqlInsertOffsets(string $sql): array { $sqlLength = strlen($sql); $insertStart = []; @@ -84,23 +86,30 @@ protected function getSqlInsertOffsets($sql) $insertStart[$i] ??= $sqlLength; } + /** @var array{0: int, 1: int, 2: int, 3: int} $insertStart */ return $insertStart; } /** * {@inheritDoc} + * + * @throws Exception\RuntimeException */ #[Override] - protected function processColumns(?PlatformInterface $platform = null): ?array + protected function processColumns(?PlatformInterface $adapterPlatform = null): ?array { if (! $this->columns) { return null; } + if (null === $adapterPlatform) { + throw new Exception\RuntimeException('Cannot build column SQL without a platform.'); + } + $sqls = []; foreach ($this->columns as $i => $column) { - $sql = $this->processExpression($column, $platform); + $sql = $this->processExpression($column, $adapterPlatform); $insertStart = $this->getSqlInsertOffsets($sql); $columnOptions = $column->getOptions(); @@ -137,7 +146,7 @@ protected function processColumns(?PlatformInterface $platform = null): ?array $j = 1; break; case 'comment': - $insert = " COMMENT {$platform->quoteValue($coValue)}"; + $insert = " COMMENT {$adapterPlatform->quoteValue($coValue)}"; $j = 2; break; case 'columnformat': @@ -152,7 +161,6 @@ protected function processColumns(?PlatformInterface $platform = null): ?array } if ($insert) { - $j ??= 0; $sql = substr_replace($sql, $insert, $insertStart[$j], length: 0); $insertStartCount = count($insertStart); for (; $j < $insertStartCount; ++$j) { @@ -167,13 +175,8 @@ protected function processColumns(?PlatformInterface $platform = null): ?array return [$sqls]; } - /** - * @param string $columnA - * @param string $columnB - * @return int - */ // phpcs:ignore SlevomatCodingStandard.Classes.UnusedPrivateElements.UnusedMethod - private function compareColumnOptions($columnA, $columnB) + private function compareColumnOptions(string $columnA, string $columnB): int { $columnA = $this->normalizeColumnOption($columnA); $columnA = $this->columnOptionSortOrder[$columnA] ?? count($this->columnOptionSortOrder); @@ -184,11 +187,7 @@ private function compareColumnOptions($columnA, $columnB) return $columnA - $columnB; } - /** - * @param string $name - * @return string - */ - private function normalizeColumnOption($name) + private function normalizeColumnOption(string $name): string { return strtolower(str_replace(['-', '_', ' '], replace: '', subject: $name)); } From a8ab4313e35cc0761d5f26e4802d96961e14dabf Mon Sep 17 00:00:00 2001 From: Joey Smith Date: Mon, 10 Aug 2026 18:37:21 -0500 Subject: [PATCH 16/21] mago analyze: generate baseline for remaining tracked findings Covers all 179 remaining findings, each traced to an upstream root cause, a mago flow-analysis limitation, or a false positive, and each documented in a tracking issue (#61, #64-#74) tied to milestone 0.5.0. Also: - fix mago.toml's pinned schema version (1.45.0 -> 1.46.0, matching the installed mago version) - reference the baseline via [analyzer].baseline in mago.toml so mago analyze applies it automatically without --baseline --- mago-baseline.toml | 685 +++++++++++++++++++++++++++++++++++++++++++++ mago.toml | 5 +- 2 files changed, 689 insertions(+), 1 deletion(-) create mode 100644 mago-baseline.toml diff --git a/mago-baseline.toml b/mago-baseline.toml new file mode 100644 index 0000000..384d919 --- /dev/null +++ b/mago-baseline.toml @@ -0,0 +1,685 @@ +variant = "loose" + +[[issues]] +file = "src/AdapterPlatform.php" +code = "falsable-return-statement" +message = '''Function `PhpDb\Mysql\AdapterPlatform::quoteViaDriver` is declared to return `null|string` but possibly returns 'false' (inferred as `false|string`).''' +count = 1 + +[[issues]] +file = "src/AdapterPlatform.php" +code = "impossible-condition" +message = "This condition (type `false`) will always evaluate to false." +count = 1 + +[[issues]] +file = "src/AdapterPlatform.php" +code = "invalid-return-statement" +message = 'Invalid return type for function `PhpDb\Mysql\AdapterPlatform::quoteViaDriver`: expected `null|string`, but found `false|string`.' +count = 1 + +[[issues]] +file = "src/AdapterPlatform.php" +code = "missing-constant-type" +message = "Class constant `PLATFORM_NAME` is missing a type hint." +count = 1 + +[[issues]] +file = "src/AdapterPlatform.php" +code = "possibly-invalid-argument" +message = 'Possible argument type mismatch for argument #1 of `PhpDb\Mysql\AdapterPlatform::quoteViaDriver`: expected `string`, but possibly received `bool|float|int|string`.' +count = 1 + +[[issues]] +file = "src/Connection.php" +code = "incompatible-property-type" +message = 'Property `PhpDb\Mysql\Connection::$resource` has an incompatible type declaration from docblock.' +count = 1 + +[[issues]] +file = "src/Connection.php" +code = "invalid-iterator" +message = "The expression provided to `foreach` is not iterable. It resolved to type `mixed`, which is not iterable." +count = 1 + +[[issues]] +file = "src/Connection.php" +code = "invalid-property-assignment-value" +message = "Invalid type for property `$resource`: expected `mysqli`, but got `null`." +count = 1 + +[[issues]] +file = "src/Connection.php" +code = "mixed-argument" +message = "Invalid argument type for argument #1 of `mysqli::options`: expected `int`, but found `mixed`." +count = 1 + +[[issues]] +file = "src/Connection.php" +code = "mixed-argument" +message = "Invalid argument type for argument #1 of `mysqli::set_charset`: expected `string`, but found `mixed`." +count = 1 + +[[issues]] +file = "src/Connection.php" +code = "mixed-argument" +message = "Invalid argument type for argument #1 of `mysqli::ssl_set`: expected `null|string`, but found `nonnull`." +count = 1 + +[[issues]] +file = "src/Connection.php" +code = "mixed-argument" +message = "Invalid argument type for argument #2 of `mysqli::options`: expected `int|string`, but found `mixed`." +count = 1 + +[[issues]] +file = "src/Connection.php" +code = "mixed-argument" +message = "Invalid argument type for argument #2 of `mysqli::real_connect`: expected `null|string`, but found `mixed`." +count = 2 + +[[issues]] +file = "src/Connection.php" +code = "mixed-argument" +message = "Invalid argument type for argument #2 of `mysqli::ssl_set`: expected `null|string`, but found `nonnull`." +count = 1 + +[[issues]] +file = "src/Connection.php" +code = "mixed-argument" +message = "Invalid argument type for argument #3 of `mysqli::real_connect`: expected `null|string`, but found `mixed`." +count = 2 + +[[issues]] +file = "src/Connection.php" +code = "mixed-argument" +message = "Invalid argument type for argument #3 of `mysqli::ssl_set`: expected `null|string`, but found `nonnull`." +count = 1 + +[[issues]] +file = "src/Connection.php" +code = "mixed-argument" +message = "Invalid argument type for argument #4 of `mysqli::real_connect`: expected `null|string`, but found `mixed`." +count = 2 + +[[issues]] +file = "src/Connection.php" +code = "mixed-argument" +message = "Invalid argument type for argument #4 of `mysqli::ssl_set`: expected `null|string`, but found `nonnull`." +count = 1 + +[[issues]] +file = "src/Connection.php" +code = "mixed-argument" +message = "Invalid argument type for argument #5 of `mysqli::ssl_set`: expected `null|string`, but found `nonnull`." +count = 1 + +[[issues]] +file = "src/Connection.php" +code = "mixed-assignment" +message = "Assigning `mixed` type to a variable may lead to unexpected behavior." +count = 7 + +[[issues]] +file = "src/Connection.php" +code = "mixed-assignment" +message = "Assigning `nonnull` type to a variable may lead to unexpected behavior." +count = 6 + +[[issues]] +file = "src/Connection.php" +code = "mixed-operand" +message = "Left operand in `&&` operation has `mixed` type." +count = 1 + +[[issues]] +file = "src/Connection.php" +code = "mixed-return-statement" +message = "Could not infer a precise return type for function `{closure:src/Connection.php:118:31}`. Saw type `nonnull`." +count = 1 + +[[issues]] +file = "src/Connection.php" +code = "possibly-null-argument" +message = "Argument #1 of method `Exception::__construct` is possibly `null`, but parameter type `string` does not accept it." +count = 1 + +[[issues]] +file = "src/Connection.php" +code = "possibly-undefined-string-array-index" +message = "Possibly undefined array key `string('charset')` accessed on `array`." +count = 1 + +[[issues]] +file = "src/Connection.php" +code = "possibly-undefined-string-array-index" +message = "Possibly undefined array key `string('driver_options')` accessed on `array`." +count = 1 + +[[issues]] +file = "src/Connection.php" +code = "redundant-condition" +message = "This condition (type `true`) will always evaluate to true." +count = 2 + +[[issues]] +file = "src/Container/ConnectionInterfaceFactory.php" +code = "less-specific-argument" +message = 'Argument type mismatch for argument #1 of `PhpDb\Mysql\Connection::__construct`: expected `array|mysqli|null`, but provided type `non-empty-array` is less specific.' +count = 1 + +[[issues]] +file = "src/Container/ConnectionInterfaceFactory.php" +code = "mixed-argument" +message = 'Invalid argument type for argument #2 of `PhpDb\Adapter\Exception\InvalidConnectionParametersException::__construct`: expected `array`, but found `nonnull`.' +count = 1 + +[[issues]] +file = "src/Container/ConnectionInterfaceFactory.php" +code = "mixed-assignment" +message = "Assigning `nonnull` type to a variable may lead to unexpected behavior." +count = 1 + +[[issues]] +file = "src/Container/DriverInterfaceFactory.php" +code = "mixed-argument" +message = 'Invalid argument type for argument #2 of `Laminas\ServiceManager\ServiceManager::build`: expected `array|null`, but found `nonnull`.' +count = 1 + +[[issues]] +file = "src/Container/MetadataInterfaceFactory.php" +code = "missing-constant-type" +message = "Class constant `ADAPTER_SERVICE_NAME` is missing a type hint." +count = 1 + +[[issues]] +file = "src/Container/MetadataInterfaceFactory.php" +code = "mixed-argument" +message = 'Invalid argument type for argument #1 of `PhpDb\Metadata\Source\AbstractSource::__construct`: expected `PhpDb\Adapter\AdapterInterface&PhpDb\Adapter\SchemaAwareInterface`, but found `mixed`.' +count = 1 + +[[issues]] +file = "src/Container/MetadataInterfaceFactory.php" +code = "mixed-argument" +message = 'Invalid argument type for argument #1 of `Psr\Container\ContainerInterface::get`: expected `string`, but found `nonnull`.' +count = 1 + +[[issues]] +file = "src/Container/MetadataInterfaceFactory.php" +code = "mixed-assignment" +message = "Assigning `nonnull` type to a variable may lead to unexpected behavior." +count = 1 + +[[issues]] +file = "src/Container/PdoConnectionInterfaceFactory.php" +code = "less-specific-argument" +message = 'Argument type mismatch for argument #1 of `PhpDb\Mysql\Pdo\Connection::__construct`: expected `PDO|array`, but provided type `non-empty-array` is less specific.' +count = 1 + +[[issues]] +file = "src/Container/PdoConnectionInterfaceFactory.php" +code = "mixed-argument" +message = 'Invalid argument type for argument #2 of `PhpDb\Adapter\Exception\InvalidConnectionParametersException::__construct`: expected `array`, but found `nonnull`.' +count = 1 + +[[issues]] +file = "src/Container/PdoConnectionInterfaceFactory.php" +code = "mixed-assignment" +message = "Assigning `nonnull` type to a variable may lead to unexpected behavior." +count = 1 + +[[issues]] +file = "src/Container/PdoDriverInterfaceFactory.php" +code = "mixed-argument" +message = 'Invalid argument type for argument #2 of `Laminas\ServiceManager\ServiceManager::build`: expected `array|null`, but found `nonnull`.' +count = 1 + +[[issues]] +file = "src/Container/PdoDriverInterfaceFactory.php" +code = "mixed-argument" +message = 'Invalid argument type for argument #4 of `PhpDb\Mysql\Pdo\Driver::__construct`: expected `array`, but found `nonnull`.' +count = 1 + +[[issues]] +file = "src/Container/PdoStatementFactory.php" +code = "possibly-null-argument" +message = 'Argument #1 of method `PhpDb\Adapter\Driver\Pdo\Statement::__construct` is possibly `null`, but parameter type `array` does not accept it.' +count = 1 + +[[issues]] +file = "src/Container/PlatformInterfaceFactory.php" +code = "mixed-assignment" +message = "Assigning `mixed` type to a variable may lead to unexpected behavior." +count = 1 + +[[issues]] +file = "src/Container/StatementInterfaceFactory.php" +code = "mixed-argument" +message = 'Invalid argument type for argument #1 of `PhpDb\Mysql\Statement::__construct`: expected `bool`, but found `nonnull`.' +count = 1 + +[[issues]] +file = "src/Driver.php" +code = "docblock-type-mismatch" +message = "Docblock type mismatch for variable `$resource`." +count = 1 + +[[issues]] +file = "src/Driver.php" +code = "incompatible-parameter-type" +message = 'Parameter `$resource` of `PhpDb\Mysql\Driver::createresult()` expects type `mysqli|mysqli_stmt|unknown-ref(PhpDb\Mysql\mysqli_result)` but parent `PhpDb\Adapter\Driver\DriverInterface::createresult()` expects type `resource`' +count = 1 + +[[issues]] +file = "src/Driver.php" +code = "incompatible-parameter-type" +message = 'Parameter `$sqlOrResource` of `PhpDb\Mysql\Driver::createstatement()` expects type `mysqli|mysqli_stmt|string` but parent `PhpDb\Adapter\Driver\DriverInterface::createstatement()` expects type `resource|string`' +count = 1 + +[[issues]] +file = "src/Driver.php" +code = "missing-property-type" +message = "Property `$options` is missing a type hint." +count = 1 + +[[issues]] +file = "src/Driver.php" +code = "non-existent-class-like" +message = 'Cannot find class, interface, enum, or type alias `PhpDb\Mysql\mysqli_result`.' +count = 1 + +[[issues]] +file = "src/Driver.php" +code = "possibly-invalid-argument" +message = 'Possible argument type mismatch for argument #1 of `PhpDb\Mysql\Result::initialize`: expected `mysqli|mysqli_result|mysqli_stmt`, but possibly received `mysqli|mysqli_stmt|unknown-ref(PhpDb\Mysql\mysqli_result)`.' +count = 1 + +[[issues]] +file = "src/Metadata/Source.php" +code = "mixed-array-assignment" +message = "Unsafe array assignment on type `mixed`." +count = 9 + +[[issues]] +file = "src/Metadata/Source.php" +code = "possibly-invalid-argument" +message = "Possible argument type mismatch for argument #2 of `implode`: expected `array|null`, but possibly received `non-empty-list`." +count = 7 + +[[issues]] +file = "src/Metadata/Source.php" +code = "possibly-null-argument" +message = "Argument #2 of function `preg_match_all` is possibly `null`, but parameter type `string` does not accept it." +count = 1 + +[[issues]] +file = "src/Metadata/Source.php" +code = "possibly-null-argument" +message = "Argument #3 of function `str_replace` is possibly `null`, but parameter type `array|string` does not accept it." +count = 1 + +[[issues]] +file = "src/Metadata/Source.php" +code = "possibly-null-operand" +message = "Possibly null middle operand used in string concatenation (type `null|string`)." +count = 1 + +[[issues]] +file = "src/Metadata/Source.php" +code = "possibly-null-operand" +message = "Possibly null right operand used in string concatenation (type `null|string`)." +count = 1 + +[[issues]] +file = "src/Metadata/Source.php" +code = "possibly-undefined-int-array-index" +message = "Possibly undefined array index accessed on `list>`." +count = 1 + +[[issues]] +file = "src/Metadata/Source.php" +code = "possibly-undefined-int-array-index" +message = "Possibly undefined array key `int(1)` accessed on `array`." +count = 1 + +[[issues]] +file = "src/Metadata/Source.php" +code = "possibly-undefined-variable" +message = "Variable `$isFK` might not have been defined on all execution paths leading to this point." +count = 1 + +[[issues]] +file = "src/Metadata/Source.php" +code = "possibly-undefined-variable" +message = "Variable `$name` might not have been defined on all execution paths leading to this point." +count = 2 + +[[issues]] +file = "src/Metadata/Source.php" +code = "reference-constraint-violation" +message = "Invalid assignment to by-reference parameter `$c`." +count = 7 + +[[issues]] +file = "src/Metadata/Source.php" +code = "too-many-arguments" +message = 'Too many arguments provided for method `PhpDb\Metadata\Source\AbstractSource::prepareDataHierarchy`.' +count = 6 + +[[issues]] +file = "src/Metadata/Source.php" +code = "unused-method" +message = "Method `loadconstraintdatanames()` is never used." +count = 1 + +[[issues]] +file = "src/Pdo/Connection.php" +code = "invalid-type-cast" +message = "Casting `mixed` to `array`." +count = 1 + +[[issues]] +file = "src/Pdo/Connection.php" +code = "less-specific-argument" +message = "Argument type mismatch for argument #1 of `strtolower`: expected `string`, but provided type `array-key` is less specific." +count = 1 + +[[issues]] +file = "src/Pdo/Connection.php" +code = "mixed-argument" +message = "Invalid argument type for argument #1 of `array_diff_key`: expected `array<('K.array_diff_key() extends array-key), ('V.array_diff_key() extends mixed)>`, but found `mixed`." +count = 1 + +[[issues]] +file = "src/Pdo/Connection.php" +code = "mixed-argument" +message = "Invalid argument type for argument #4 of `PDO::__construct`: expected `array|null`, but found `mixed`." +count = 1 + +[[issues]] +file = "src/Pdo/Connection.php" +code = "mixed-array-assignment" +message = "Unsafe array assignment on type `mixed`." +count = 1 + +[[issues]] +file = "src/Pdo/Connection.php" +code = "mixed-assignment" +message = "Assigning `mixed` type to a variable may lead to unexpected behavior." +count = 3 + +[[issues]] +file = "src/Pdo/Driver.php" +code = "incompatible-parameter-type" +message = 'Parameter `$resource` of `PhpDb\Mysql\Pdo\Driver::createresult()` expects type `PDOStatement` but parent `PhpDb\Adapter\Driver\DriverInterface::createresult()` expects type `resource`' +count = 1 + +[[issues]] +file = "src/Pdo/Driver.php" +code = "invalid-property-assignment-value" +message = 'Invalid type for property `$connection`: expected `(PhpDb\Adapter\Driver\PdoConnectionInterface&PhpDb\Adapter\Driver\AbstractConnection&PhpDb\Adapter\Driver\PdoDriverAwareInterface)|PDO`, but got `(PhpDb\Adapter\Driver\PdoConnectionInterface&PhpDb\Adapter\Driver\PdoDriverAwareInterface)|PDO`.' +count = 1 + +[[issues]] +file = "src/Result.php" +code = "missing-constructor" +message = 'Class `PhpDb\Mysql\Result` has typed properties without default values but no constructor to initialize them.' +count = 1 + +[[issues]] +file = "src/Result.php" +code = "possibly-null-array-index" +message = "Possibly using `null` as an array index to access element." +count = 1 + +[[issues]] +file = "src/Result.php" +code = "possibly-undefined-int-array-index" +message = "Possibly undefined array index accessed on `list`." +count = 1 + +[[issues]] +file = "src/Result.php" +code = "possibly-undefined-int-array-index" +message = "Possibly undefined array index accessed on `list`." +count = 1 + +[[issues]] +file = "src/Result.php" +code = "possibly-undefined-int-array-index" +message = "Possibly undefined array key `non-negative-int` accessed on `array`." +count = 1 + +[[issues]] +file = "src/Result.php" +code = "unused-property" +message = "Property `$numberOfRows` is never used." +count = 1 + +[[issues]] +file = "src/Sql/Ddl/AlterTableDecorator.php" +code = "invalid-return-statement" +message = 'Invalid return type for function `PhpDb\Mysql\Sql\Ddl\AlterTableDecorator::processChangeColumns`: expected `array>`, but found `list{array{}|non-empty-list}`.' +count = 1 + +[[issues]] +file = "src/Sql/Ddl/AlterTableDecorator.php" +code = "less-specific-argument" +message = 'Argument type mismatch for argument #1 of `PhpDb\Adapter\Platform\PlatformInterface::quoteIdentifier`: expected `string`, but provided type `array-key` is less specific.' +count = 1 + +[[issues]] +file = "src/Sql/Ddl/AlterTableDecorator.php" +code = "mixed-argument" +message = 'Invalid argument type for argument #1 of `PhpDb\Adapter\Platform\PlatformInterface::quoteIdentifier`: expected `string`, but found `truthy-mixed`.' +count = 1 + +[[issues]] +file = "src/Sql/Ddl/AlterTableDecorator.php" +code = "mixed-argument" +message = 'Invalid argument type for argument #1 of `PhpDb\Adapter\Platform\PlatformInterface::quoteValue`: expected `string`, but found `truthy-mixed`.' +count = 2 + +[[issues]] +file = "src/Sql/Ddl/AlterTableDecorator.php" +code = "mixed-argument" +message = 'Invalid argument type for argument #1 of `PhpDb\Sql\AbstractSql::processExpression`: expected `PhpDb\Sql\ExpressionInterface`, but found `mixed`.' +count = 2 + +[[issues]] +file = "src/Sql/Ddl/AlterTableDecorator.php" +code = "mixed-argument" +message = "Invalid argument type for argument #1 of `strtoupper`: expected `string`, but found `truthy-mixed`." +count = 4 + +[[issues]] +file = "src/Sql/Ddl/AlterTableDecorator.php" +code = "mixed-argument" +message = "Invalid argument type for argument #1 of `uksort`: expected `array`, but found `mixed`." +count = 2 + +[[issues]] +file = "src/Sql/Ddl/AlterTableDecorator.php" +code = "mixed-assignment" +message = "Assigning `mixed` type to a variable may lead to unexpected behavior." +count = 6 + +[[issues]] +file = "src/Sql/Ddl/AlterTableDecorator.php" +code = "mixed-method-access" +message = "Attempting to access a method on a non-object type (`mixed`)." +count = 2 + +[[issues]] +file = "src/Sql/Ddl/AlterTableDecorator.php" +code = "possibly-null-argument" +message = "Argument #3 of function `substr_replace` is possibly `null`, but parameter type `array|int` does not accept it." +count = 2 + +[[issues]] +file = "src/Sql/Ddl/AlterTableDecorator.php" +code = "possibly-null-operand" +message = "Left operand in arithmetic operation might be `null` (type `int|null`)." +count = 2 + +[[issues]] +file = "src/Sql/Ddl/AlterTableDecorator.php" +code = "possibly-undefined-int-array-index" +message = "Possibly undefined array key `int(0)|int(1)|int(2)` accessed on `array{0: int, 1: int, 2: int, 3: int, ...}`." +count = 2 + +[[issues]] +file = "src/Sql/Ddl/AlterTableDecorator.php" +code = "possibly-undefined-int-array-index" +message = "Possibly undefined array key `int(0)|int(1)|int(2)` accessed on `array{0: int, 1: int, 2: int, 3: int}`." +count = 2 + +[[issues]] +file = "src/Sql/Ddl/AlterTableDecorator.php" +code = "possibly-undefined-int-array-index" +message = "Possibly undefined array key `non-negative-int` accessed on `array{0: int, 1: int, 2: int, 3: int, ...}`." +count = 2 + +[[issues]] +file = "src/Sql/Ddl/AlterTableDecorator.php" +code = "possibly-undefined-int-array-index" +message = "Possibly undefined array key `non-negative-int` accessed on `array{0: int, 1: int, 2: int, 3: int}`." +count = 2 + +[[issues]] +file = "src/Sql/Ddl/CreateTableDecorator.php" +code = "mixed-argument" +message = 'Invalid argument type for argument #1 of `PhpDb\Adapter\Platform\PlatformInterface::quoteValue`: expected `string`, but found `truthy-mixed`.' +count = 1 + +[[issues]] +file = "src/Sql/Ddl/CreateTableDecorator.php" +code = "mixed-argument" +message = 'Invalid argument type for argument #1 of `PhpDb\Sql\AbstractSql::processExpression`: expected `PhpDb\Sql\ExpressionInterface`, but found `mixed`.' +count = 1 + +[[issues]] +file = "src/Sql/Ddl/CreateTableDecorator.php" +code = "mixed-argument" +message = "Invalid argument type for argument #1 of `strtoupper`: expected `string`, but found `truthy-mixed`." +count = 2 + +[[issues]] +file = "src/Sql/Ddl/CreateTableDecorator.php" +code = "mixed-argument" +message = "Invalid argument type for argument #1 of `uksort`: expected `array`, but found `mixed`." +count = 1 + +[[issues]] +file = "src/Sql/Ddl/CreateTableDecorator.php" +code = "mixed-assignment" +message = "Assigning `mixed` type to a variable may lead to unexpected behavior." +count = 3 + +[[issues]] +file = "src/Sql/Ddl/CreateTableDecorator.php" +code = "mixed-method-access" +message = "Attempting to access a method on a non-object type (`mixed`)." +count = 1 + +[[issues]] +file = "src/Sql/Ddl/CreateTableDecorator.php" +code = "possibly-null-argument" +message = "Argument #3 of function `substr_replace` is possibly `null`, but parameter type `array|int` does not accept it." +count = 1 + +[[issues]] +file = "src/Sql/Ddl/CreateTableDecorator.php" +code = "possibly-null-operand" +message = "Left operand in arithmetic operation might be `null` (type `int|null`)." +count = 1 + +[[issues]] +file = "src/Sql/Ddl/CreateTableDecorator.php" +code = "possibly-undefined-int-array-index" +message = "Possibly undefined array key `int(0)|int(1)|int(2)` accessed on `array{0: int, 1: int, 2: int, 3: int, ...}`." +count = 1 + +[[issues]] +file = "src/Sql/Ddl/CreateTableDecorator.php" +code = "possibly-undefined-int-array-index" +message = "Possibly undefined array key `int(0)|int(1)|int(2)` accessed on `array{0: int, 1: int, 2: int, 3: int}`." +count = 1 + +[[issues]] +file = "src/Sql/Ddl/CreateTableDecorator.php" +code = "possibly-undefined-int-array-index" +message = "Possibly undefined array key `non-negative-int` accessed on `array{0: int, 1: int, 2: int, 3: int, ...}`." +count = 1 + +[[issues]] +file = "src/Sql/Ddl/CreateTableDecorator.php" +code = "possibly-undefined-int-array-index" +message = "Possibly undefined array key `non-negative-int` accessed on `array{0: int, 1: int, 2: int, 3: int}`." +count = 1 + +[[issues]] +file = "src/Sql/SelectDecorator.php" +code = "invalid-property-assignment-value" +message = "Invalid type for property `$specifications`: expected `array>|array`, but got `array{'limit': string('LIMIT 18446744073709551615'), ...|string>}`." +count = 1 + +[[issues]] +file = "src/Sql/SelectDecorator.php" +code = "invalid-return-statement" +message = 'Invalid return type for function `PhpDb\Mysql\Sql\SelectDecorator::processLimit`: expected `array|null`, but found `list{int|string}`.' +count = 1 + +[[issues]] +file = "src/Sql/SelectDecorator.php" +code = "invalid-return-statement" +message = 'Invalid return type for function `PhpDb\Mysql\Sql\SelectDecorator::processOffset`: expected `array|null`, but found `list{int|string}`.' +count = 1 + +[[issues]] +file = "src/Sql/SelectDecorator.php" +code = "less-specific-nested-return-statement" +message = '''Returned type `list{mixed}` is less specific than the declared return type `array|null` for function `PhpDb\Mysql\Sql\SelectDecorator::processLimit` due to nested 'mixed'.''' +count = 1 + +[[issues]] +file = "src/Sql/SelectDecorator.php" +code = "less-specific-nested-return-statement" +message = '''Returned type `list{mixed}` is less specific than the declared return type `array|null` for function `PhpDb\Mysql\Sql\SelectDecorator::processOffset` due to nested 'mixed'.''' +count = 1 + +[[issues]] +file = "src/Sql/SelectDecorator.php" +code = "possible-method-access-on-null" +message = "Attempting to call a method on `null`." +count = 2 + +[[issues]] +file = "src/Sql/SelectDecorator.php" +code = "write-only-property" +message = "Property `$subject` is written to but never read." +count = 1 + +[[issues]] +file = "src/Statement.php" +code = "mixed-assignment" +message = "Assigning `mixed` type to a variable may lead to unexpected behavior." +count = 1 + +[[issues]] +file = "src/Statement.php" +code = "uninitialized-property" +message = 'Property `$driver` is not initialized in the constructor of class `PhpDb\Mysql\Statement`.' +count = 1 + +[[issues]] +file = "src/Statement.php" +code = "uninitialized-property" +message = 'Property `$mysqli` is not initialized in the constructor of class `PhpDb\Mysql\Statement`.' +count = 1 + +[[issues]] +file = "src/Statement.php" +code = "uninitialized-property" +message = 'Property `$resource` is not initialized in the constructor of class `PhpDb\Mysql\Statement`.' +count = 1 diff --git a/mago.toml b/mago.toml index 4e2bc2b..e66385b 100644 --- a/mago.toml +++ b/mago.toml @@ -1,7 +1,10 @@ -#:schema https://mago.carthage.software/1.45.0/schema.json +#:schema https://mago.carthage.software/1.46.0/schema.json extends = "vendor/php-db/phpdb-qa-tools/mago.toml" php-version = "8.3.0" [source] paths = ["src", "test"] includes = ["vendor"] + +[analyzer] +baseline = "mago-baseline.toml" From f1292f3ac4ef69f153573f83a63d6ded2236cd94 Mon Sep 17 00:00:00 2001 From: Joey Smith Date: Mon, 10 Aug 2026 19:42:41 -0500 Subject: [PATCH 17/21] mago analyze: use @mago-expect for unused-parameter in Container factories Per Simon's preference, revert the $_container/$_requestedName underscore-prefix rename (commit 5bf22d7) back to $container/ $requestedName, and suppress the unused-parameter finding directly with // @mago-expect analysis:unused-parameter instead. Laminas ServiceManager still calls factories positionally, so the parameters must remain in place regardless of naming convention. --- src/Container/ConnectionInterfaceFactory.php | 5 +++-- src/Container/DriverInterfaceFactory.php | 3 ++- src/Container/MetadataInterfaceFactory.php | 3 ++- src/Container/PdoConnectionInterfaceFactory.php | 5 +++-- src/Container/PdoDriverInterfaceFactory.php | 3 ++- src/Container/PdoStatementFactory.php | 5 +++-- src/Container/PlatformInterfaceFactory.php | 5 +++-- src/Container/StatementInterfaceFactory.php | 5 +++-- 8 files changed, 21 insertions(+), 13 deletions(-) diff --git a/src/Container/ConnectionInterfaceFactory.php b/src/Container/ConnectionInterfaceFactory.php index 9f7b7ee..86a804a 100644 --- a/src/Container/ConnectionInterfaceFactory.php +++ b/src/Container/ConnectionInterfaceFactory.php @@ -18,9 +18,10 @@ final class ConnectionInterfaceFactory * * @throws \PhpDb\Adapter\Exception\ExceptionInterface */ + // @mago-expect analysis:unused-parameter public function __invoke( - ContainerInterface $_container, - string $_requestedName, + ContainerInterface $container, + string $requestedName, ?array $options = null, ): ConnectionInterface&Connection { $conn = $options['connection'] ?? []; diff --git a/src/Container/DriverInterfaceFactory.php b/src/Container/DriverInterfaceFactory.php index 760dafd..7da096d 100644 --- a/src/Container/DriverInterfaceFactory.php +++ b/src/Container/DriverInterfaceFactory.php @@ -26,9 +26,10 @@ final class DriverInterfaceFactory * @throws \Psr\Container\NotFoundExceptionInterface * @throws \PhpDb\Exception\ExceptionInterface */ + // @mago-expect analysis:unused-parameter public function __invoke( ContainerInterface&ServiceManager $container, - string $_requestedName, + string $requestedName, ?array $options = null, ): DriverInterface&Driver { if (null === $options || ! array_key_exists('connection', $options)) { diff --git a/src/Container/MetadataInterfaceFactory.php b/src/Container/MetadataInterfaceFactory.php index 5fd2f5a..e0d3912 100644 --- a/src/Container/MetadataInterfaceFactory.php +++ b/src/Container/MetadataInterfaceFactory.php @@ -19,9 +19,10 @@ final class MetadataInterfaceFactory * @throws \Psr\Container\ContainerExceptionInterface * @throws \Psr\Container\NotFoundExceptionInterface */ + // @mago-expect analysis:unused-parameter public function __invoke( ContainerInterface $container, - string $_requestedName, + string $requestedName, ?array $options = null, ): MetadataInterface&Metadata\Source { $adapterServiceName = $options[self::ADAPTER_SERVICE_NAME] ?? AdapterInterface::class; diff --git a/src/Container/PdoConnectionInterfaceFactory.php b/src/Container/PdoConnectionInterfaceFactory.php index 877f055..6a90d1e 100644 --- a/src/Container/PdoConnectionInterfaceFactory.php +++ b/src/Container/PdoConnectionInterfaceFactory.php @@ -18,9 +18,10 @@ final class PdoConnectionInterfaceFactory * * @throws \PhpDb\Adapter\Exception\ExceptionInterface */ + // @mago-expect analysis:unused-parameter public function __invoke( - ContainerInterface $_container, - string $_requestedName, + ContainerInterface $container, + string $requestedName, ?array $options = null, ): PdoConnectionInterface&Connection { $conn = $options['connection'] ?? []; diff --git a/src/Container/PdoDriverInterfaceFactory.php b/src/Container/PdoDriverInterfaceFactory.php index 3f79a74..92804e0 100644 --- a/src/Container/PdoDriverInterfaceFactory.php +++ b/src/Container/PdoDriverInterfaceFactory.php @@ -25,9 +25,10 @@ final class PdoDriverInterfaceFactory * @throws \Psr\Container\ContainerExceptionInterface * @throws \Psr\Container\NotFoundExceptionInterface */ + // @mago-expect analysis:unused-parameter public function __invoke( ContainerInterface&ServiceManager $container, - string $_requestedName, + string $requestedName, ?array $options = null, ): PdoDriverInterface&Driver { if (null === $options || ! array_key_exists('connection', $options)) { diff --git a/src/Container/PdoStatementFactory.php b/src/Container/PdoStatementFactory.php index cead40c..9003ea2 100644 --- a/src/Container/PdoStatementFactory.php +++ b/src/Container/PdoStatementFactory.php @@ -13,9 +13,10 @@ final class PdoStatementFactory /** * @param array|null $options */ + // @mago-expect analysis:unused-parameter public function __invoke( - ContainerInterface $_container, - string $_requestedName, + ContainerInterface $container, + string $requestedName, ?array $options = null, ): StatementInterface&Statement { return new Statement(options: $options); diff --git a/src/Container/PlatformInterfaceFactory.php b/src/Container/PlatformInterfaceFactory.php index 1840510..a260fcb 100644 --- a/src/Container/PlatformInterfaceFactory.php +++ b/src/Container/PlatformInterfaceFactory.php @@ -18,9 +18,10 @@ final class PlatformInterfaceFactory * * @throws \Psr\Container\ContainerExceptionInterface */ + // @mago-expect analysis:unused-parameter public function __invoke( - ContainerInterface $_container, - string $_requestedName, + ContainerInterface $container, + string $requestedName, ?array $options = null, ): PlatformInterface&AdapterPlatform { $driverInstance = $options['driver'] ?? null; diff --git a/src/Container/StatementInterfaceFactory.php b/src/Container/StatementInterfaceFactory.php index 122c463..14ef1b6 100644 --- a/src/Container/StatementInterfaceFactory.php +++ b/src/Container/StatementInterfaceFactory.php @@ -13,9 +13,10 @@ final class StatementInterfaceFactory /** * @param array|null $options */ + // @mago-expect analysis:unused-parameter public function __invoke( - ContainerInterface $_container, - string $_requestedName, + ContainerInterface $container, + string $requestedName, ?array $options = null, ): StatementInterface&Statement { return new Statement(bufferResults: $options['buffer_results'] ?? false); From c8b197b747f87453a158a1f3599547943fbcf7fc Mon Sep 17 00:00:00 2001 From: Joey Smith Date: Mon, 10 Aug 2026 19:45:28 -0500 Subject: [PATCH 18/21] mago analyze: move @mago-expect into existing docblocks in Container factories Fold the analysis:unused-parameter suppression into the last line of each factory's existing docblock (all 8 have one, since all document @throws) instead of a separate line comment above the method. --- src/Container/ConnectionInterfaceFactory.php | 3 ++- src/Container/DriverInterfaceFactory.php | 3 ++- src/Container/MetadataInterfaceFactory.php | 3 ++- src/Container/PdoConnectionInterfaceFactory.php | 3 ++- src/Container/PdoDriverInterfaceFactory.php | 3 ++- src/Container/PdoStatementFactory.php | 3 ++- src/Container/PlatformInterfaceFactory.php | 3 ++- src/Container/StatementInterfaceFactory.php | 3 ++- 8 files changed, 16 insertions(+), 8 deletions(-) diff --git a/src/Container/ConnectionInterfaceFactory.php b/src/Container/ConnectionInterfaceFactory.php index 86a804a..9d18ed9 100644 --- a/src/Container/ConnectionInterfaceFactory.php +++ b/src/Container/ConnectionInterfaceFactory.php @@ -17,8 +17,9 @@ final class ConnectionInterfaceFactory * @param array|null $options * * @throws \PhpDb\Adapter\Exception\ExceptionInterface + * + * @mago-expect analysis:unused-parameter */ - // @mago-expect analysis:unused-parameter public function __invoke( ContainerInterface $container, string $requestedName, diff --git a/src/Container/DriverInterfaceFactory.php b/src/Container/DriverInterfaceFactory.php index 7da096d..9cfb5b7 100644 --- a/src/Container/DriverInterfaceFactory.php +++ b/src/Container/DriverInterfaceFactory.php @@ -25,8 +25,9 @@ final class DriverInterfaceFactory * @throws \Psr\Container\ContainerExceptionInterface * @throws \Psr\Container\NotFoundExceptionInterface * @throws \PhpDb\Exception\ExceptionInterface + * + * @mago-expect analysis:unused-parameter */ - // @mago-expect analysis:unused-parameter public function __invoke( ContainerInterface&ServiceManager $container, string $requestedName, diff --git a/src/Container/MetadataInterfaceFactory.php b/src/Container/MetadataInterfaceFactory.php index e0d3912..b1b8f3f 100644 --- a/src/Container/MetadataInterfaceFactory.php +++ b/src/Container/MetadataInterfaceFactory.php @@ -18,8 +18,9 @@ final class MetadataInterfaceFactory * * @throws \Psr\Container\ContainerExceptionInterface * @throws \Psr\Container\NotFoundExceptionInterface + * + * @mago-expect analysis:unused-parameter */ - // @mago-expect analysis:unused-parameter public function __invoke( ContainerInterface $container, string $requestedName, diff --git a/src/Container/PdoConnectionInterfaceFactory.php b/src/Container/PdoConnectionInterfaceFactory.php index 6a90d1e..8b3abd7 100644 --- a/src/Container/PdoConnectionInterfaceFactory.php +++ b/src/Container/PdoConnectionInterfaceFactory.php @@ -17,8 +17,9 @@ final class PdoConnectionInterfaceFactory * @param array|null $options * * @throws \PhpDb\Adapter\Exception\ExceptionInterface + * + * @mago-expect analysis:unused-parameter */ - // @mago-expect analysis:unused-parameter public function __invoke( ContainerInterface $container, string $requestedName, diff --git a/src/Container/PdoDriverInterfaceFactory.php b/src/Container/PdoDriverInterfaceFactory.php index 92804e0..ee797f7 100644 --- a/src/Container/PdoDriverInterfaceFactory.php +++ b/src/Container/PdoDriverInterfaceFactory.php @@ -24,8 +24,9 @@ final class PdoDriverInterfaceFactory * @throws \Laminas\ServiceManager\Exception\ExceptionInterface * @throws \Psr\Container\ContainerExceptionInterface * @throws \Psr\Container\NotFoundExceptionInterface + * + * @mago-expect analysis:unused-parameter */ - // @mago-expect analysis:unused-parameter public function __invoke( ContainerInterface&ServiceManager $container, string $requestedName, diff --git a/src/Container/PdoStatementFactory.php b/src/Container/PdoStatementFactory.php index 9003ea2..76fdcab 100644 --- a/src/Container/PdoStatementFactory.php +++ b/src/Container/PdoStatementFactory.php @@ -12,8 +12,9 @@ final class PdoStatementFactory { /** * @param array|null $options + * + * @mago-expect analysis:unused-parameter */ - // @mago-expect analysis:unused-parameter public function __invoke( ContainerInterface $container, string $requestedName, diff --git a/src/Container/PlatformInterfaceFactory.php b/src/Container/PlatformInterfaceFactory.php index a260fcb..a6c6165 100644 --- a/src/Container/PlatformInterfaceFactory.php +++ b/src/Container/PlatformInterfaceFactory.php @@ -17,8 +17,9 @@ final class PlatformInterfaceFactory * @param array|null $options * * @throws \Psr\Container\ContainerExceptionInterface + * + * @mago-expect analysis:unused-parameter */ - // @mago-expect analysis:unused-parameter public function __invoke( ContainerInterface $container, string $requestedName, diff --git a/src/Container/StatementInterfaceFactory.php b/src/Container/StatementInterfaceFactory.php index 14ef1b6..479149c 100644 --- a/src/Container/StatementInterfaceFactory.php +++ b/src/Container/StatementInterfaceFactory.php @@ -12,8 +12,9 @@ final class StatementInterfaceFactory { /** * @param array|null $options + * + * @mago-expect analysis:unused-parameter */ - // @mago-expect analysis:unused-parameter public function __invoke( ContainerInterface $container, string $requestedName, From bd76204dc3e33bc7b5f077dbab021929a2c99c77 Mon Sep 17 00:00:00 2001 From: Joey Smith Date: Tue, 11 Aug 2026 19:13:33 -0500 Subject: [PATCH 19/21] adds php version, latest release and license to readme Signed-off-by: Joey Smith --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index bf14ee7..70e3542 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,10 @@ # PhpDb Adapter Mysql +[![PHP Version](https://img.shields.io/packagist/php-v/php-db/phpdb-mysql)](https://packagist.org/packages/php-db/phpdb-mysql) [![Continuous Integration](https://github.com/php-db/phpdb-mysql/actions/workflows/continuous-integration.yml/badge.svg)](https://github.com/php-db/phpdb-mysql/actions/workflows/continuous-integration.yml) [![codecov](https://codecov.io/gh/php-db/phpdb-mysql/graph/badge.svg)](https://codecov.io/gh/php-db/phpdb-mysql) [![Mutation testing badge](https://img.shields.io/endpoint?style=flat&url=https%3A%2F%2Fbadge-api.stryker-mutator.io%2Fgithub.com%2Fphp-db%2Fphpdb-mysql%2F0.5.x)](https://dashboard.stryker-mutator.io/reports/github.com/php-db/phpdb-mysql/0.5.x) +[![Latest Stable Version](https://img.shields.io/packagist/v/php-db/phpdb-mysql)](https://packagist.org/packages/php-db/phpdb-mysql) +[![License](https://img.shields.io/github/license/php-db/phpdb-mysql)](LICENSE) This package provides MySQL support for PhpDb. From b36e74b4775cf26c6fab4f879da4a4c98a2cd849 Mon Sep 17 00:00:00 2001 From: Joey Smith Date: Tue, 11 Aug 2026 19:14:06 -0500 Subject: [PATCH 20/21] Correct package name in readme Signed-off-by: Joey Smith --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 70e3542..3895ef3 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# PhpDb Adapter Mysql +# PhpDb Mysql [![PHP Version](https://img.shields.io/packagist/php-v/php-db/phpdb-mysql)](https://packagist.org/packages/php-db/phpdb-mysql) [![Continuous Integration](https://github.com/php-db/phpdb-mysql/actions/workflows/continuous-integration.yml/badge.svg)](https://github.com/php-db/phpdb-mysql/actions/workflows/continuous-integration.yml) From 9432fe625726cb883c117209bad03c8f7cf366d2 Mon Sep 17 00:00:00 2001 From: Joey Smith Date: Wed, 12 Aug 2026 10:13:17 -0500 Subject: [PATCH 21/21] mago analyze: rename baseline to documented analysis-baseline.toml convention Per the mago baseline docs (one file per tool), the analyzer baseline should be named analysis-baseline.toml rather than mago-baseline.toml. --- mago-baseline.toml => analysis-baseline.toml | 0 mago.toml | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename mago-baseline.toml => analysis-baseline.toml (100%) diff --git a/mago-baseline.toml b/analysis-baseline.toml similarity index 100% rename from mago-baseline.toml rename to analysis-baseline.toml diff --git a/mago.toml b/mago.toml index e66385b..7cf3255 100644 --- a/mago.toml +++ b/mago.toml @@ -7,4 +7,4 @@ paths = ["src", "test"] includes = ["vendor"] [analyzer] -baseline = "mago-baseline.toml" +baseline = "analysis-baseline.toml"