diff --git a/.gitattributes b/.gitattributes
index 516c22d..cde4f1f 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -3,8 +3,5 @@ tests/ export-ignore
.editorconfig export-ignore
.gitattributes export-ignore
.gitignore export-ignore
-.scrutinizer.yml export-ignore
-.travis.yml export-ignore
CONTRIBUTING.md export-ignore
-phpspec.yml.dist export-ignore
phpunit.xml.dist export-ignore
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 0000000..f7b8104
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,13 @@
+version: 2
+updates:
+ - package-ecosystem: github-actions
+ directory: /
+ schedule:
+ interval: weekly
+ open-pull-requests-limit: 5
+ - package-ecosystem: composer
+ directory: /
+ schedule:
+ interval: monthly
+ open-pull-requests-limit: 5
+ versioning-strategy: widen
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
new file mode 100644
index 0000000..e34a488
--- /dev/null
+++ b/.github/workflows/test.yml
@@ -0,0 +1,61 @@
+name: Test
+
+on:
+ push:
+ branches: [ master ]
+ pull_request:
+ branches: [ master ]
+
+permissions:
+ contents: read
+
+jobs:
+ test:
+ strategy:
+ fail-fast: false
+ matrix:
+ php-versions: [ '8.2', '8.3', '8.4', '8.5' ]
+
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Set up PHP
+ uses: shivammathur/setup-php@v2
+ with:
+ php-version: ${{ matrix.php-versions }}
+ coverage: none
+ tools: composer:v2
+ extensions: pdo_sqlite
+
+ - name: Validate Composer metadata
+ run: |
+ if [ -f composer.lock ]; then
+ composer validate --strict --no-interaction
+ else
+ composer validate --strict --no-check-lock --no-interaction
+ fi
+
+ - name: Get Composer cache directory
+ id: composer-cache
+ run: echo "dir=$(composer config cache-files-dir)" >> "$GITHUB_OUTPUT"
+
+ - name: Cache Composer packages
+ uses: actions/cache@v4
+ with:
+ path: ${{ steps.composer-cache.outputs.dir }}
+ key: ${{ runner.os }}-composer-${{ matrix.php-versions }}-${{ hashFiles('**/composer.json', '**/composer.lock') }}
+ restore-keys: |
+ ${{ runner.os }}-composer-${{ matrix.php-versions }}-
+
+ - name: Install dependencies
+ run: |
+ if [ -f composer.lock ]; then
+ composer install --prefer-dist --no-progress --no-interaction
+ else
+ composer update --prefer-dist --no-progress --no-interaction
+ fi
+
+ - name: Run test suite
+ run: vendor/bin/phpunit
diff --git a/.scrutinizer.yml b/.scrutinizer.yml
deleted file mode 100644
index acbda73..0000000
--- a/.scrutinizer.yml
+++ /dev/null
@@ -1,7 +0,0 @@
-checks:
- php:
- code_rating: true
- duplication: true
-
-tools:
- external_code_coverage: true
diff --git a/.travis.yml b/.travis.yml
deleted file mode 100644
index 27005b6..0000000
--- a/.travis.yml
+++ /dev/null
@@ -1,27 +0,0 @@
-language: php
-
-php:
- - 5.6
- - 7.0
- - 7.1
- - hhvm
-
-matrix:
- allow_failures:
- - php: hhvm
- include:
- - php: 5.6
- env:
- - COMPOSER_FLAGS="--prefer-lowest --prefer-stable"
- - COVERAGE=true
- - PHPUNIT_FLAGS="--coverage-clover=coverage.clover"
-
-install:
- - travis_retry composer update ${COMPOSER_FLAGS} --prefer-source --no-interaction
-
-script:
- - vendor/bin/phpunit ${PHPUNIT_FLAGS}
-
-after_script:
- - if [[ "$COVERAGE" = true ]]; then wget https://scrutinizer-ci.com/ocular.phar; fi
- - if [[ "$COVERAGE" = true ]]; then php ocular.phar code-coverage:upload --format=php-clover coverage.clover; fi
diff --git a/README.md b/README.md
index a3833ba..6f4cc37 100644
--- a/README.md
+++ b/README.md
@@ -1,44 +1,27 @@
# Port DBAL adapter
[](https://github.com/portphp/dbal/releases)
-[](https://travis-ci.org/portphp/dbal)
-[](https://scrutinizer-ci.com/g/portphp/dbal/?branch=master)
-[](https://scrutinizer-ci.com/g/portphp/dbal/?branch=master)
+[](https://github.com/portphp/dbal/actions/workflows/test.yml)
+[](https://packagist.org/packages/portphp/dbal)
+
+**Requirements:** PHP ^8.2 (tested on 8.2–8.5). Requires `portphp/portphp` ^2.0 and `doctrine/dbal` ^3 or ^4.
Doctrine DBAL adapter for [Port](https://github.com/portphp).
## Installation
-Open a command console, enter your project directory and execute the
-following command to download the latest stable version of this package:
-
```bash
$ composer require portphp/dbal
```
-This command requires you to have Composer installed globally, as explained
-in the [installation chapter](https://getcomposer.org/doc/00-intro.md)
-of the Composer documentation.
-
## Documentation
-Documentation is available at https://portphp.readthedocs.io/.
+Documentation is available at https://portphp.readthedocs.io.
## Issues and feature requests
Please report issues and request features at https://github.com/portphp/portphp/issues.
-## Contributing
-
-Contributions are very welcome. Please see [CONTRIBUTING.md](CONTRIBUTING.md) for
-details. Thanks to [everyone who has contributed](https://github.com/portphp/dbal/graphs/contributors)
-already.
-
-## Security
-
-If you discover any security-related issues, please contact us at
-[security@portphp.org](mailto:security@portphp.org).
-
## License
This package is licensed under the [MIT license](LICENSE).
diff --git a/UPGRADE-2.0.md b/UPGRADE-2.0.md
new file mode 100644
index 0000000..638553e
--- /dev/null
+++ b/UPGRADE-2.0.md
@@ -0,0 +1,8 @@
+# Upgrade from 1.x to 2.0
+
+- Minimum PHP is **8.2** (`^8.2`).
+- Requires `portphp/portphp` **^2.0**.
+- Requires `doctrine/dbal` **^3.0 || ^4.0** (DBAL 2.x is no longer supported).
+- `DbalReader` uses the DBAL 3/4 Result API (`executeQuery` / `fetchAssociative`).
+- `DbalReaderFactory` no longer implements the file-based `ReaderFactory` interface (it is a SQL factory).
+- CI is GitHub Actions (Travis/Scrutinizer removed).
diff --git a/composer.json b/composer.json
index 07e7ee5..80855ac 100644
--- a/composer.json
+++ b/composer.json
@@ -14,17 +14,18 @@
},
{
"name": "Community contributors",
- "homepage": "https://github.com/portphp/dbal-adapter/graphs/contributors"
+ "homepage": "https://github.com/portphp/dbal/graphs/contributors"
}
],
"support": {
"issues": "https://github.com/portphp/portphp/issues",
- "source": "https://github.com/portphp/dbal-adapter",
- "docs": "https://docs.portphp.org"
+ "source": "https://github.com/portphp/dbal",
+ "docs": "https://portphp.readthedocs.io"
},
"require": {
- "portphp/portphp": "^1.0.0",
- "doctrine/dbal": "^2.4"
+ "php": "^8.2",
+ "portphp/portphp": "^2.0",
+ "doctrine/dbal": "^3.0 || ^4.0"
},
"autoload": {
"psr-4": {
@@ -32,9 +33,8 @@
}
},
"require-dev": {
- "ext-sqlite3": "*",
- "phpunit/phpunit": "^4.0",
- "phpspec/phpspec": "^2.1"
+ "ext-pdo_sqlite": "*",
+ "phpunit/phpunit": "^9.6"
},
"autoload-dev": {
"psr-4": {
@@ -43,7 +43,7 @@
},
"extra": {
"branch-alias": {
- "dev-master": "1.0.x-dev"
+ "dev-master": "2.0.x-dev"
}
}
}
diff --git a/phpspec.yml.dist b/phpspec.yml.dist
deleted file mode 100644
index e1fe828..0000000
--- a/phpspec.yml.dist
+++ /dev/null
@@ -1,5 +0,0 @@
-suites:
- library_suite:
- namespace: Port\Dbal
- psr4_prefix: Port\Dbal
-formatter.name: pretty
diff --git a/phpunit.xml.dist b/phpunit.xml.dist
index f2c9ad5..8ca7a88 100644
--- a/phpunit.xml.dist
+++ b/phpunit.xml.dist
@@ -1,20 +1,12 @@
-
-
+ beStrictAboutOutputDuringTests="true">
-
- ./tests/
+
+ tests
diff --git a/spec/DbalReaderFactorySpec.php b/spec/DbalReaderFactorySpec.php
deleted file mode 100644
index 01b163d..0000000
--- a/spec/DbalReaderFactorySpec.php
+++ /dev/null
@@ -1,24 +0,0 @@
-beConstructedWith($dbal);
- }
-
- function it_is_initializable()
- {
- $this->shouldHaveType('Port\Dbal\DbalReaderFactory');
- }
-
- function it_creates_a_reader()
- {
- $this->getReader('SQL', [])->shouldHaveType('Port\Dbal\DbalReader');
- }
-}
diff --git a/src/DbalReader.php b/src/DbalReader.php
index a113f0b..f6bbbb0 100644
--- a/src/DbalReader.php
+++ b/src/DbalReader.php
@@ -3,7 +3,7 @@
namespace Port\Dbal;
use Doctrine\DBAL\Connection;
-use Doctrine\DBAL\Statement;
+use Doctrine\DBAL\Result;
use Port\Reader\CountableReader;
/**
@@ -11,202 +11,108 @@
*/
class DbalReader implements CountableReader
{
- /**
- * @var Connection
- */
- private $connection;
-
- /**
- * @var array
- */
- private $data;
-
- /**
- * @var Statement
- */
- private $stmt;
-
- /**
- * @var string
- */
- private $sql;
-
- /**
- * @var array
- */
- private $params;
-
- /**
- * @var integer
- */
- private $rowCount;
-
- /**
- * @var boolean
- */
- private $rowCountCalculated = true;
-
- /**
- * @var string
- */
- private $key;
-
- /**
- * @param Connection $connection
- * @param string $sql
- * @param array $params
- */
- public function __construct(Connection $connection, $sql, array $params = [])
+ private Connection $connection;
+
+ private array|false|null $data = null;
+
+ private ?Result $result = null;
+
+ private string $sql = '';
+
+ private array $params = [];
+
+ private ?int $rowCount = null;
+
+ private bool $rowCountCalculated = true;
+
+ private int $key = 0;
+
+ public function __construct(Connection $connection, string $sql, array $params = [])
{
$this->connection = $connection;
-
$this->setSql($sql, $params);
}
- /**
- * Do calculate row count?
- *
- * @param boolean $calculate
- */
- public function setRowCountCalculated($calculate = true)
+ public function setRowCountCalculated(bool $calculate = true): void
{
- $this->rowCountCalculated = (bool) $calculate;
+ $this->rowCountCalculated = $calculate;
}
- /**
- * Is row count calculated?
- *
- * @return boolean
- */
- public function isRowCountCalculated()
+ public function isRowCountCalculated(): bool
{
return $this->rowCountCalculated;
}
- /**
- * Set Query string with Parameters
- *
- * @param string $sql
- * @param array $params
- */
- public function setSql($sql, array $params = [])
+ public function setSql(string $sql, array $params = []): void
{
- $this->sql = (string) $sql;
-
+ $this->sql = $sql;
$this->setSqlParameters($params);
}
- /**
- * Set SQL parameters
- *
- * @param array $params
- */
- public function setSqlParameters(array $params)
+ public function setSqlParameters(array $params): void
{
$this->params = $params;
-
- $this->stmt = null;
+ $this->result = null;
$this->rowCount = null;
+ $this->data = null;
+ $this->key = 0;
}
- /**
- * {@inheritdoc}
- */
- public function current()
+ public function current(): mixed
{
- if (null === $this->data) {
+ if (null === $this->data && null === $this->result) {
$this->rewind();
}
return $this->data;
}
- /**
- * {@inheritdoc}
- */
- public function next()
+ public function next(): void
{
$this->key++;
- $this->data = $this->stmt->fetch(\PDO::FETCH_ASSOC);
+ $this->data = $this->result?->fetchAssociative() ?: false;
}
- /**
- * {@inheritdoc}
- */
- public function key()
+ public function key(): mixed
{
return $this->key;
}
- /**
- * {@inheritdoc}
- */
- public function valid()
+ public function valid(): bool
{
- if (null === $this->data) {
+ if (null === $this->data && null === $this->result) {
$this->rewind();
}
- return (false !== $this->data);
+ return false !== $this->data;
}
- /**
- * {@inheritdoc}
- */
- public function rewind()
+ public function rewind(): void
{
- if (null === $this->stmt) {
- $this->stmt = $this->prepare($this->sql, $this->params);
- }
- if (0 !== $this->key) {
- $this->stmt->execute();
- $this->data = $this->stmt->fetch(\PDO::FETCH_ASSOC);
- $this->key = 0;
- }
+ $this->result = $this->connection->executeQuery($this->sql, $this->params);
+ $this->data = $this->result->fetchAssociative();
+ $this->key = 0;
}
- /**
- * {@inheritdoc}
- */
- public function count()
+ public function count(): int
{
if (null === $this->rowCount) {
if ($this->rowCountCalculated) {
$this->doCalcRowCount();
} else {
- if (null === $this->stmt) {
+ if (null === $this->result) {
$this->rewind();
}
- $this->rowCount = $this->stmt->rowCount();
+ $this->rowCount = $this->result?->rowCount() ?? 0;
}
}
return $this->rowCount;
}
- private function doCalcRowCount()
+ private function doCalcRowCount(): void
{
- $statement = $this->prepare(sprintf('SELECT COUNT(*) FROM (%s) AS port_cnt', $this->sql), $this->params);
- $statement->execute();
-
- $this->rowCount = (int) $statement->fetchColumn(0);
- }
-
- /**
- * Prepare given statement
- *
- * @param string $sql
- * @param array $params
- *
- * @return Statement
- */
- private function prepare($sql, array $params)
- {
- $statement = $this->connection->prepare($sql);
- foreach ($params as $key => $value) {
- $statement->bindValue($key, $value);
- }
-
- return $statement;
+ $countSql = sprintf('SELECT COUNT(*) FROM (%s) AS port_cnt', $this->sql);
+ $this->rowCount = (int) $this->connection->fetchOne($countSql, $this->params);
}
}
diff --git a/src/DbalReaderFactory.php b/src/DbalReaderFactory.php
index b69e906..76c1726 100644
--- a/src/DbalReaderFactory.php
+++ b/src/DbalReaderFactory.php
@@ -3,35 +3,18 @@
namespace Port\Dbal;
use Doctrine\DBAL\Connection;
-use Port\Reader\ReaderFactory;
/**
* Factory that creates DbalReaders
- *
- * @author David de Boer
*/
-class DbalReaderFactory implements ReaderFactory
+class DbalReaderFactory
{
- /**
- * @var Connection
- */
- protected $connection;
-
- /**
- * @param Connection $connection
- */
- public function __construct(Connection $connection)
- {
- $this->connection = $connection;
+ public function __construct(
+ private Connection $connection
+ ) {
}
- /**
- * @param string $sql
- * @param array $params
- *
- * @return DbalReader
- */
- public function getReader($sql, array $params = [])
+ public function getReader(string $sql, array $params = []): DbalReader
{
return new DbalReader($this->connection, $sql, $params);
}
diff --git a/tests/DbalReaderTest.php b/tests/DbalReaderTest.php
new file mode 100644
index 0000000..902c2be
--- /dev/null
+++ b/tests/DbalReaderTest.php
@@ -0,0 +1,117 @@
+assertSame(10, $this->getReader()->count());
+ }
+
+ public function testCountInhibited(): void
+ {
+ $reader = $this->getReader();
+ $reader->setRowCountCalculated(false);
+
+ // SQLite SELECT rowCount is often 0; ensure method is callable
+ $this->assertIsInt($reader->count());
+ }
+
+ public function testIterate(): void
+ {
+ $i = 31;
+ foreach ($this->getReader() as $key => $row) {
+ $this->assertIsArray($row);
+ $this->assertSame('user-'.$i, $row['username']);
+ $this->assertSame($i - 31, $key);
+ $i++;
+ }
+
+ $this->assertSame(41, $i);
+ }
+
+ public function testReaderRewindWorksCorrectly(): void
+ {
+ $reader = $this->getReader();
+ foreach ($reader as $row) {
+ if (($row['username'] ?? null) === 'user-35') {
+ break;
+ }
+ }
+
+ $reader->rewind();
+
+ $this->assertSame([
+ 'id' => 31,
+ 'username' => 'user-31',
+ 'name' => 'name 4',
+ ], $reader->current());
+ }
+
+ public function testCallingCurrentTwiceShouldNotAdvance(): void
+ {
+ $reader = $this->getReader();
+ $first = $reader->current();
+ $second = $reader->current();
+ $this->assertSame($first, $second);
+ }
+
+ private function getConnection()
+ {
+ $connection = DriverManager::getConnection([
+ 'driver' => 'pdo_sqlite',
+ 'memory' => true,
+ ]);
+
+ $schema = new Schema();
+ $table = $schema->createTable('groups');
+ $table->addColumn('id', 'integer', ['autoincrement' => true]);
+ $table->addColumn('name', 'string', ['length' => 45]);
+ $table->setPrimaryKey(['id']);
+
+ $user = $schema->createTable('user');
+ $user->addColumn('id', 'integer', ['autoincrement' => true]);
+ $user->addColumn('username', 'string', ['length' => 32]);
+ $user->addColumn('group_id', 'integer');
+ $user->setPrimaryKey(['id']);
+
+ $platform = $connection->getDatabasePlatform();
+ foreach ($schema->toSql($platform) as $query) {
+ $connection->executeStatement($query);
+ }
+
+ $counter = 1;
+ for ($i = 1; $i <= 10; $i++) {
+ $connection->insert('groups', ['name' => "name {$i}"]);
+ $id = $connection->lastInsertId();
+ for ($j = 1; $j <= 10; $j++) {
+ $connection->insert('user', [
+ 'username' => "user-{$counter}",
+ 'group_id' => $id,
+ ]);
+ $counter++;
+ }
+ }
+
+ return $connection;
+ }
+
+ private function getReader(): DbalReader
+ {
+ $connection = $this->getConnection();
+
+ return new DbalReader($connection, implode(' ', [
+ 'SELECT u.id, u.username, g.name',
+ 'FROM user u INNER JOIN groups g ON u.group_id = g.id',
+ 'WHERE g.name LIKE :name',
+ ]), [
+ 'name' => 'name 4',
+ ]);
+ }
+}
diff --git a/tests/Reader/DbalReaderTest.php b/tests/Reader/DbalReaderTest.php
deleted file mode 100644
index 280d5dd..0000000
--- a/tests/Reader/DbalReaderTest.php
+++ /dev/null
@@ -1,193 +0,0 @@
-getReader();
-
- $reader->setRowCountCalculated();
- $this->assertTrue($reader->isRowCountCalculated());
-
- $reader->setRowCountCalculated(false);
- $this->assertFalse($reader->isRowCountCalculated());
-
- $reader->setRowCountCalculated(true);
- $this->assertTrue($reader->isRowCountCalculated());
- }
-
- public function testCount()
- {
- $this->assertEquals(10, $this->getReader()->count());
- }
-
- public function testCountInhibited()
- {
- $reader = $this->getReader();
- $reader->setRowCountCalculated(false);
-
- $this->assertEquals(null, $reader->count());
- }
-
- public function testSqlAndParamsAreMutable()
- {
- $reader = $this->getReader();
-
- $reader->setSql('SELECT * FROM groups WHERE id = :id', array('id' => 2));
- $this->assertAttributeEquals('SELECT * FROM groups WHERE id = :id', 'sql', $reader);
- $this->assertAttributeEquals(array('id' => 2), 'params', $reader);
- }
-
- public function testChangeSqlOrParamsClearsNumRowsAndStatement()
- {
- $reader = $this->getReader();
- $reader->count();
- $reader->current();
-
- $this->assertAttributeNotEmpty('rowCount', $reader);
- $this->assertAttributeNotEmpty('stmt', $reader);
-
- $reader->setSql('SELECT * FROM `user` WHERE id IN (:id)', array('id' => array()));
-
- $this->assertAttributeEmpty('rowCount', $reader);
- $this->assertAttributeEmpty('stmt', $reader);
- }
-
- public function testIterate()
- {
- $i=31;
- foreach ($this->getReader() as $key => $row) {
- $this->assertInternalType('array', $row);
- $this->assertEquals('user-'.$i, $row['username']);
- $this->assertEquals($i - 31, $key);
- $i++;
- }
-
- $this->assertEquals(41, $i);
- }
-
- public function testReaderRewindWorksCorrectly()
- {
- $reader = $this->getReader();
- foreach ($reader as $row) {
- if (!isset($row['username'])) {
- $this->fail('There should be a username');
- }
- if ($row['username'] == 'user-35') {
- break;
- }
- }
-
- $reader->rewind();
-
- $this->assertEquals(array(
- 'id' => 31,
- 'username' => 'user-31',
- 'name' => 'name 4',
- ), $reader->current());
- }
-
- public function testCallingCurrentTwiceShouldNotAdvance()
- {
- $reader = $this->getReader();
-
- $expected = array(
- 'id' => 31,
- 'username' => 'user-31',
- 'name' => 'name 4',
- );
- $this->assertEquals($expected, $reader->current());
- $this->assertEquals($expected, $reader->current());
- }
-
- public function testEmptyResultDoesNotThrowException()
- {
- $reader = $this->getReader();
-
- $reader->setSql(null, array('name' => 'unknown group'));
- $this->assertFalse($reader->current());
- }
-
- public function testCallValidRewindsIfNeeded()
- {
- $reader = $this->getReader();
-
- $this->assertTrue($reader->valid());
- $this->assertAttributeInternalType('array', 'data', $reader);
- }
-
- public function getConnection()
- {
- $params = array(
- 'driver' => 'pdo_sqlite',
- 'memory' => true,
- );
-
- $connection = DriverManager::getConnection($params, new Configuration());
-
- $schema = new Schema();
-
- $table = $schema->createTable('groups');
- $table->addColumn('id', 'integer', ['autoincrement' => true]);
- $table->addColumn('name', 'string', array('length' => 45));
- $table->setPrimaryKey(array('id'));
-
- $myTable = $schema->createTable('user');
- $myTable->addColumn('id', 'integer', array('unsigned' => true, 'autoincrement' => true));
- $myTable->addColumn('username', 'string', array('length' => 32));
- $myTable->addColumn('group_id', 'integer');
- $myTable->setPrimaryKey(array('id'));
- $myTable->addUniqueIndex(array('username'));
- $myTable->addForeignKeyConstraint($table, array('group_id'), array('id'));
-
- foreach ($schema->toSql(new SqlitePlatform()) as $query) {
- $connection->query($query);
- };
-
- return $connection;
- }
-
- protected function getReader()
- {
- $connection = $this->getConnection();
- $this->loadFixtures($connection);
-
- return new DbalReader($connection, implode(' ', array(
- 'SELECT u.id, u.username, g.name',
- 'FROM `user` u INNER JOIN groups g ON u.group_id = g.id',
- 'WHERE g.name LIKE :name',
- )), array(
- 'name' => 'name 4',
- ));
- }
-
- /**
- * @param Connection $connection
- */
- protected function loadFixtures($connection)
- {
- $counter = 1;
- for ($i = 1; $i <= 10; $i++) {
- $connection->insert('groups', array('name' => "name {$i}"));
- $id = $connection->lastInsertId();
-
- for ($j = 1; $j <= 10; $j++) {
- $connection->insert('user', array(
- 'username' => "user-{$counter}",
- 'group_id' => $id,
- ));
-
- $counter++;
- }
- }
- }
-}