diff --git a/.github/workflows/platform-matrix-test.yml b/.github/workflows/platform-matrix-test.yml
new file mode 100644
index 00000000..a74ec83c
--- /dev/null
+++ b/.github/workflows/platform-matrix-test.yml
@@ -0,0 +1,61 @@
+# https://help.github.com/en/categories/automating-your-workflow-with-github-actions
+
+name: "Platform matrix test"
+
+on:
+ pull_request:
+ push:
+ branches:
+ - "1.4.x"
+
+jobs:
+ tests:
+ name: "Platform matrix test"
+ runs-on: "ubuntu-latest"
+ env:
+ MYSQL_HOST: '127.0.0.1'
+ PGSQL_HOST: '127.0.0.1'
+
+ strategy:
+ fail-fast: false
+ matrix:
+ php-version:
+ - "8.0"
+ - "8.1"
+
+ steps:
+ - name: "Checkout"
+ uses: actions/checkout@v4
+
+ - name: "Install PHP"
+ uses: "shivammathur/setup-php@v2"
+ with:
+ coverage: "none"
+ php-version: "${{ matrix.php-version }}"
+ ini-file: development
+ extensions: pdo, mysqli, pgsql, pdo_mysql, pdo_pgsql, pdo_sqlite, mongodb
+
+ - name: "Install dependencies"
+ run: "composer install --no-interaction --no-progress"
+
+ - name: "Run platform matrix test"
+ run: vendor/bin/phpunit --group=platform
+
+ services:
+ postgres:
+ image: "postgres:latest"
+ env:
+ POSTGRES_PASSWORD: "secret"
+ POSTGRES_USER: root
+ POSTGRES_DB: foo
+ ports:
+ - "5432:5432"
+
+ mysql:
+ image: "mysql:latest"
+ env:
+ MYSQL_ALLOW_EMPTY_PASSWORD: yes
+ MYSQL_ROOT_PASSWORD: secret
+ MYSQL_DATABASE: foo
+ ports:
+ - "3306:3306"
diff --git a/.gitignore b/.gitignore
index 7de9f3c5..e3d740a7 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,4 +2,5 @@
/build-cs
/vendor
/composer.lock
+/.env
.phpunit.result.cache
diff --git a/phpstan.neon b/phpstan.neon
index b3c5d642..8dfe69fa 100644
--- a/phpstan.neon
+++ b/phpstan.neon
@@ -43,3 +43,9 @@ parameters:
message: '#^Call to function method_exists\(\) with ''Doctrine\\\\ORM\\\\EntityManager'' and ''create'' will always evaluate to true\.$#'
path: src/Doctrine/Mapping/ClassMetadataFactory.php
reportUnmatched: false
+ -
+ messages:
+ - '#^Call to function method_exists\(\) with Doctrine\\DBAL\\Connection and ''getNativeConnection'' will always evaluate to true\.$#'
+ - '#^Cannot call method getWrappedResourceHandle\(\) on class\-string\|object\.$#'
+ path: tests/Platform/QueryResultTypeWalkerFetchTypeMatrixTest.php
+ reportUnmatched: false
diff --git a/phpunit.xml b/phpunit.xml
index 6d69639a..f4beeb21 100644
--- a/phpunit.xml
+++ b/phpunit.xml
@@ -36,5 +36,11 @@
+
+
+ platform
+
+
+
diff --git a/src/Type/Doctrine/Descriptors/BooleanType.php b/src/Type/Doctrine/Descriptors/BooleanType.php
index 0e6980c0..955883a8 100644
--- a/src/Type/Doctrine/Descriptors/BooleanType.php
+++ b/src/Type/Doctrine/Descriptors/BooleanType.php
@@ -28,7 +28,8 @@ public function getDatabaseInternalType(): Type
{
return TypeCombinator::union(
new ConstantIntegerType(0),
- new ConstantIntegerType(1)
+ new ConstantIntegerType(1),
+ new \PHPStan\Type\BooleanType()
);
}
diff --git a/src/Type/Doctrine/Query/QueryResultTypeWalker.php b/src/Type/Doctrine/Query/QueryResultTypeWalker.php
index d68bcdb8..55375c5f 100644
--- a/src/Type/Doctrine/Query/QueryResultTypeWalker.php
+++ b/src/Type/Doctrine/Query/QueryResultTypeWalker.php
@@ -14,6 +14,7 @@
use Doctrine\ORM\Query\SqlWalker;
use PHPStan\ShouldNotHappenException;
use PHPStan\Type\BooleanType;
+use PHPStan\Type\Constant\ConstantBooleanType;
use PHPStan\Type\Constant\ConstantFloatType;
use PHPStan\Type\Constant\ConstantIntegerType;
use PHPStan\Type\Constant\ConstantStringType;
@@ -1113,8 +1114,11 @@ public function walkLiteral($literal): string
break;
case AST\Literal::BOOLEAN:
- $value = strtolower($literal->value) === 'true' ? 1 : 0;
- $type = new ConstantIntegerType($value);
+ $value = strtolower($literal->value) === 'true';
+ $type = TypeCombinator::union(
+ new ConstantIntegerType($value ? 1 : 0),
+ new ConstantBooleanType($value)
+ );
break;
case AST\Literal::NUMERIC:
diff --git a/tests/Platform/MatrixEntity/TestEntity.php b/tests/Platform/MatrixEntity/TestEntity.php
new file mode 100644
index 00000000..74f2ae57
--- /dev/null
+++ b/tests/Platform/MatrixEntity/TestEntity.php
@@ -0,0 +1,56 @@
+ $connectionParams
+ * @param array $expectedOnPhp80AndBelow
+ * @param array $expectedOnPhp81AndAbove
+ * @param array $connectionAttributes
+ *
+ * @dataProvider provideCases
+ */
+ public function testFetchedTypes(
+ array $connectionParams,
+ array $expectedOnPhp80AndBelow,
+ array $expectedOnPhp81AndAbove,
+ array $connectionAttributes
+ ): void
+ {
+ $phpVersion = PHP_MAJOR_VERSION * 10 + PHP_MINOR_VERSION;
+
+ try {
+ $connection = DriverManager::getConnection($connectionParams + [
+ 'user' => 'root',
+ 'password' => 'secret',
+ 'dbname' => 'foo',
+ ]);
+
+ $nativeConnection = $this->getNativeConnection($connection);
+ $this->setupAttributes($nativeConnection, $connectionAttributes);
+
+ $config = new Configuration();
+ $config->setProxyNamespace('PHPstan\Doctrine\OrmMatrixProxies');
+ $config->setProxyDir('/tmp/doctrine');
+ $config->setAutoGenerateProxyClasses(false);
+ $config->setSecondLevelCacheEnabled(false);
+ $config->setMetadataCache(new ArrayCachePool());
+ $config->setMetadataDriverImpl(new AnnotationDriver(new AnnotationReader(), [__DIR__ . '/MatrixEntity']));
+ $entityManager = new EntityManager($connection, $config);
+
+ } catch (DbalException $e) {
+ if (strpos($e->getMessage(), 'Doctrine currently supports only the following drivers') !== false) {
+ self::markTestSkipped($e->getMessage()); // older doctrine versions, needed for old PHP versions
+ }
+ throw $e;
+ }
+
+ $schemaTool = new SchemaTool($entityManager);
+ $classes = $entityManager->getMetadataFactory()->getAllMetadata();
+ $schemaTool->dropSchema($classes);
+ $schemaTool->createSchema($classes);
+
+ $entity = new TestEntity();
+ $entity->col_bool = true;
+ $entity->col_float = 0.125;
+ $entity->col_decimal = '0.1';
+ $entity->col_int = 9;
+ $entity->col_bigint = '2147483648';
+ $entity->col_string = 'foobar';
+
+ $entityManager->persist($entity);
+ $entityManager->flush();
+
+ $columnsQueryTemplate = 'SELECT %s FROM %s t GROUP BY t.col_int, t.col_float, t.col_decimal, t.col_bigint, t.col_bool, t.col_string';
+
+ $expected = $phpVersion >= 81
+ ? $expectedOnPhp81AndAbove
+ : $expectedOnPhp80AndBelow;
+
+ foreach ($expected as $select => $expectedType) {
+ if ($expectedType === null) {
+ continue; // e.g. no such function
+ }
+ $dql = sprintf($columnsQueryTemplate, $select, TestEntity::class);
+
+ $query = $entityManager->createQuery($dql);
+ $result = $query->getSingleResult();
+
+ $typeBuilder = new QueryResultTypeBuilder();
+ QueryResultTypeWalker::walk($query, $typeBuilder, self::getContainer()->getByType(DescriptorRegistry::class));
+
+ $inferredPhpStanType = $typeBuilder->getResultType();
+ $realRowPhpStanType = ConstantTypeHelper::getTypeFromValue($result);
+
+ $firstResult = reset($result);
+ $resultType = gettype($firstResult);
+ $resultExported = var_export($firstResult, true);
+
+ self::assertTrue(
+ $inferredPhpStanType->accepts($realRowPhpStanType, true)->yes(),
+ sprintf(
+ "Result of 'SELECT %s' for '%s' and PHP %s was inferred as %s, but the real result was %s",
+ $select,
+ $this->dataName(),
+ $phpVersion,
+ $inferredPhpStanType->describe(VerbosityLevel::precise()),
+ $realRowPhpStanType->describe(VerbosityLevel::precise())
+ )
+ );
+
+ self::assertThat(
+ $firstResult,
+ new IsType($expectedType),
+ sprintf(
+ "Result of 'SELECT %s' for '%s' and PHP %s is expected to be %s, but %s returned (%s).",
+ $select,
+ $this->dataName(),
+ $phpVersion,
+ $expectedType,
+ $resultType,
+ $resultExported
+ )
+ );
+ }
+ }
+
+ /**
+ * @return iterable
+ */
+ public function provideCases(): iterable
+ {
+ // Preserve space-driven formatting for better readability
+ // phpcs:disable Squiz.WhiteSpace.OperatorSpacing.SpacingBefore
+ // phpcs:disable Squiz.WhiteSpace.OperatorSpacing.SpacingAfter
+
+ // Notes:
+ // - Any direct column fetch uses the type declared in entity, but when passed to a function, the driver decides the type
+
+ $testData = [ // mysql, sqlite, pdo_pgsql, pgsql, stringified, stringifiedOldPostgre
+ // bool-ish
+ '(TRUE)' => ['int', 'int', 'bool', 'bool', 'string', 'bool'],
+ 't.col_bool' => ['bool', 'bool', 'bool', 'bool', 'bool', 'bool'],
+ 'COALESCE(t.col_bool, TRUE)' => ['int', 'int', 'bool', 'bool', 'string', 'bool'],
+
+ // float-ish
+ 't.col_float' => ['float', 'float', 'float', 'float', 'float', 'float'],
+ 'AVG(t.col_float)' => ['float', 'float', 'string', 'float', 'string', 'string'],
+ 'SUM(t.col_float)' => ['float', 'float', 'string', 'float', 'string', 'string'],
+ 'MIN(t.col_float)' => ['float', 'float', 'string', 'float', 'string', 'string'],
+ 'MAX(t.col_float)' => ['float', 'float', 'string', 'float', 'string', 'string'],
+ 'SQRT(t.col_float)' => ['float', 'float', 'string', 'float', 'string', 'string'],
+ 'ABS(t.col_float)' => ['float', 'float', 'string', 'float', 'string', 'string'],
+
+ // decimal-ish
+ 't.col_decimal' => ['string', 'string', 'string', 'string', 'string', 'string'],
+ '0.1' => ['string', 'float', 'string', 'string', 'string', 'string'],
+ '0.125e0' => ['float', 'float', 'string', 'string', 'string', 'string'],
+ 'AVG(t.col_decimal)' => ['string', 'float', 'string', 'string', 'string', 'string'],
+ 'AVG(t.col_int)' => ['string', 'float', 'string', 'string', 'string', 'string'],
+ 'AVG(t.col_bigint)' => ['string', 'float', 'string', 'string', 'string', 'string'],
+ 'SUM(t.col_decimal)' => ['string', 'float', 'string', 'string', 'string', 'string'],
+ 'MIN(t.col_decimal)' => ['string', 'float', 'string', 'string', 'string', 'string'],
+ 'MAX(t.col_decimal)' => ['string', 'float', 'string', 'string', 'string', 'string'],
+ 'SQRT(t.col_decimal)' => ['float', 'float', 'string', 'string', 'string', 'string'],
+ 'SQRT(t.col_int)' => ['float', 'float', 'string', 'float', 'string', 'string'],
+ 'SQRT(t.col_bigint)' => ['float', null, 'string', 'float', null, null], // sqlite3 returns float, but pdo_sqlite returns NULL
+ 'ABS(t.col_decimal)' => ['string', 'float', 'string', 'string', 'string', 'string'],
+
+ // int-ish
+ '1' => ['int', 'int', 'int', 'int', 'string', 'string'],
+ '2147483648' => ['int', 'int', 'int', 'int', 'string', 'string'],
+ 't.col_int' => ['int', 'int', 'int', 'int', 'int', 'int'],
+ 't.col_bigint' => ['string', 'string', 'string', 'string', 'string', 'string'],
+ 'SUM(t.col_int)' => ['string', 'int', 'int', 'int', 'string', 'string'],
+ 'SUM(t.col_bigint)' => ['string', 'int', 'string', 'string', 'string', 'string'],
+ "LENGTH('')" => ['int', 'int', 'int', 'int', 'int', 'int'],
+ 'COUNT(t)' => ['int', 'int', 'int', 'int', 'int', 'int'],
+ 'COUNT(1)' => ['int', 'int', 'int', 'int', 'int', 'int'],
+ 'COUNT(t.col_int)' => ['int', 'int', 'int', 'int', 'int', 'int'],
+ 'MIN(t.col_int)' => ['int', 'int', 'int', 'int', 'string', 'string'],
+ 'MIN(t.col_bigint)' => ['int', 'int', 'int', 'int', 'string', 'string'],
+ 'MAX(t.col_int)' => ['int', 'int', 'int', 'int', 'string', 'string'],
+ 'MAX(t.col_bigint)' => ['int', 'int', 'int', 'int', 'string', 'string'],
+ 'MOD(t.col_int, 2)' => ['int', 'int', 'int', 'int', 'string', 'string'],
+ 'MOD(t.col_bigint, 2)' => ['int', 'int', 'int', 'int', 'string', 'string'],
+ 'ABS(t.col_int)' => ['int', 'int', 'int', 'int', 'string', 'string'],
+ 'ABS(t.col_bigint)' => ['int', 'int', 'int', 'int', 'string', 'string'],
+
+ // string
+ 't.col_string' => ['string', 'string', 'string', 'string', 'string', 'string'],
+ 'LOWER(t.col_string)' => ['string', 'string', 'string', 'string', 'string', 'string'],
+ 'UPPER(t.col_string)' => ['string', 'string', 'string', 'string', 'string', 'string'],
+ 'TRIM(t.col_string)' => ['string', 'string', 'string', 'string', 'string', 'string'],
+ ];
+
+ $selects = array_keys($testData);
+
+ $nativeMysql = array_combine($selects, array_column($testData, 0));
+ $nativeSqlite = array_combine($selects, array_column($testData, 1));
+ $nativePdoPg = array_combine($selects, array_column($testData, 2));
+ $nativePg = array_combine($selects, array_column($testData, 3));
+
+ $stringified = array_combine($selects, array_column($testData, 4));
+ $stringifiedOldPostgre = array_combine($selects, array_column($testData, 5));
+
+ yield 'sqlite3' => [
+ 'connection' => ['driver' => 'sqlite3', 'memory' => true],
+ 'php80-' => $nativeSqlite,
+ 'php81+' => $nativeSqlite,
+ 'setup' => [],
+ ];
+
+ yield 'pdo_sqlite, no stringify' => [
+ 'connection' => ['driver' => 'pdo_sqlite', 'memory' => true],
+ 'php80-' => $stringified,
+ 'php81+' => $nativeSqlite,
+ 'setup' => [],
+ ];
+
+ yield 'pdo_sqlite, stringify' => [
+ 'connection' => ['driver' => 'pdo_sqlite', 'memory' => true],
+ 'php80-' => $stringified,
+ 'php81+' => $stringified,
+ 'setup' => [PDO::ATTR_STRINGIFY_FETCHES => true],
+ ];
+
+ yield 'mysqli, no native numbers' => [
+ 'connection' => ['driver' => 'mysqli', 'host' => getenv('MYSQL_HOST')],
+ 'php80-' => $nativeMysql,
+ 'php81+' => $nativeMysql,
+ 'setup' => [
+ // This has no effect when using prepared statements (which is what doctrine/dbal uses)
+ // - prepared statements => always native types
+ // - non-prepared statements => stringified by default, can be changed by MYSQLI_OPT_INT_AND_FLOAT_NATIVE = true
+ // documented here: https://www.php.net/manual/en/mysqli.quickstart.prepared-statements.php#example-4303
+ MYSQLI_OPT_INT_AND_FLOAT_NATIVE => false,
+ ],
+ ];
+
+ yield 'mysqli, native numbers' => [
+ 'connection' => ['driver' => 'mysqli', 'host' => getenv('MYSQL_HOST')],
+ 'php80-' => $nativeMysql,
+ 'php81+' => $nativeMysql,
+ 'setup' => [MYSQLI_OPT_INT_AND_FLOAT_NATIVE => true],
+ ];
+
+ yield 'pdo_mysql, stringify, no emulate' => [
+ 'connection' => ['driver' => 'pdo_mysql', 'host' => getenv('MYSQL_HOST')],
+ 'php80-' => $stringified,
+ 'php81+' => $stringified,
+ 'setup' => [
+ PDO::ATTR_EMULATE_PREPARES => false,
+ PDO::ATTR_STRINGIFY_FETCHES => true,
+ ],
+ ];
+
+ yield 'pdo_mysql, no stringify, no emulate' => [
+ 'connection' => ['driver' => 'pdo_mysql', 'host' => getenv('MYSQL_HOST')],
+ 'php80-' => $nativeMysql,
+ 'php81+' => $nativeMysql,
+ 'setup' => [PDO::ATTR_EMULATE_PREPARES => false],
+ ];
+
+ yield 'pdo_mysql, no stringify, emulate' => [
+ 'connection' => ['driver' => 'pdo_mysql', 'host' => getenv('MYSQL_HOST')],
+ 'php80-' => $stringified,
+ 'php81+' => $nativeMysql,
+ 'setup' => [], // defaults
+ ];
+
+ yield 'pdo_mysql, stringify, emulate' => [
+ 'connection' => ['driver' => 'pdo_mysql', 'host' => getenv('MYSQL_HOST')],
+ 'php80-' => $stringified,
+ 'php81+' => $stringified,
+ 'setup' => [
+ PDO::ATTR_STRINGIFY_FETCHES => true,
+ ],
+ ];
+
+ yield 'pdo_pgsql, stringify' => [
+ 'connection' => ['driver' => 'pdo_pgsql', 'host' => getenv('PGSQL_HOST')],
+
+ 'php80-' => $stringifiedOldPostgre,
+ 'php81+' => $stringified,
+ 'setup' => [PDO::ATTR_STRINGIFY_FETCHES => true],
+ ];
+
+ yield 'pdo_pgsql, no stringify' => [
+ 'connection' => ['driver' => 'pdo_pgsql', 'host' => getenv('PGSQL_HOST')],
+ 'php80-' => $nativePdoPg,
+ 'php81+' => $nativePdoPg,
+ 'setup' => [],
+ ];
+
+ yield 'pgsql' => [
+ 'connection' => ['driver' => 'pgsql', 'host' => getenv('PGSQL_HOST')],
+ 'php80-' => $nativePg,
+ 'php81+' => $nativePg,
+ 'setup' => [],
+ ];
+ }
+
+ /**
+ * @param mixed $nativeConnection
+ * @param array $attributes
+ */
+ private function setupAttributes($nativeConnection, array $attributes): void
+ {
+ if ($nativeConnection instanceof PDO) {
+ foreach ($attributes as $attribute => $value) {
+ $set = $nativeConnection->setAttribute($attribute, $value);
+ if (!$set) {
+ throw new LogicException(sprintf('Failed to set attribute %s to %s', $attribute, $value));
+ }
+ }
+
+ } elseif ($nativeConnection instanceof mysqli) {
+ foreach ($attributes as $attribute => $value) {
+ $set = $nativeConnection->options($attribute, $value);
+ if (!$set) {
+ throw new LogicException(sprintf('Failed to set attribute %s to %s', $attribute, $value));
+ }
+ }
+
+ } elseif (is_a($nativeConnection, 'PgSql\Connection', true)) {
+ if ($attributes !== []) {
+ throw new LogicException('Cannot set attributes for PgSql\Connection driver');
+ }
+
+ } elseif ($nativeConnection instanceof SQLite3) {
+ if ($attributes !== []) {
+ throw new LogicException('Cannot set attributes for ' . SQLite3::class . ' driver');
+ }
+
+ } elseif (is_resource($nativeConnection)) { // e.g. `resource (pgsql link)` on PHP < 8.1 with pgsql driver
+ if ($attributes !== []) {
+ throw new LogicException('Cannot set attributes for this resource');
+ }
+
+ } else {
+ throw new LogicException('Unexpected connection: ' . (function_exists('get_debug_type') ? get_debug_type($nativeConnection) : gettype($nativeConnection)));
+ }
+ }
+
+ /**
+ * @return mixed
+ */
+ private function getNativeConnection(Connection $connection)
+ {
+ if (method_exists($connection, 'getNativeConnection')) {
+ return $connection->getNativeConnection();
+ }
+
+ if (method_exists($connection, 'getWrappedConnection')) {
+ if ($connection->getWrappedConnection() instanceof PDO) {
+ return $connection->getWrappedConnection();
+ }
+
+ if (method_exists($connection->getWrappedConnection(), 'getWrappedResourceHandle')) {
+ return $connection->getWrappedConnection()->getWrappedResourceHandle();
+ }
+ }
+
+ throw new LogicException('Unable to get native connection');
+ }
+
+}
diff --git a/tests/Platform/README.md b/tests/Platform/README.md
new file mode 100644
index 00000000..b9c07d6a
--- /dev/null
+++ b/tests/Platform/README.md
@@ -0,0 +1,19 @@
+
+## How to run platform tests in docker
+
+Set current working directory to project root.
+
+```sh
+# Init services & dependencies
+- `printf "UID=$(id -u)\nGID=$(id -g)" > .env`
+- `docker-compose -f tests/Platform/docker/docker-compose.yml up -d`
+- `docker-compose -f tests/Platform/docker/docker-compose.yml run --rm php81 composer install`
+
+# Test behaviour with old stringification
+- `docker-compose -f tests/Platform/docker/docker-compose.yml run --rm php80 php -d memory_limit=1G vendor/bin/phpunit --group=platform`
+
+# Test behaviour with new stringification
+- `docker-compose -f tests/Platform/docker/docker-compose.yml run --rm php81 php -d memory_limit=1G vendor/bin/phpunit --group=platform`
+```
+
+You can also run utilize those containers for PHPStorm PHPUnit configuration.
diff --git a/tests/Platform/data/config.neon b/tests/Platform/data/config.neon
new file mode 100644
index 00000000..38f26ed7
--- /dev/null
+++ b/tests/Platform/data/config.neon
@@ -0,0 +1,5 @@
+includes:
+ - ../../../extension.neon
+parameters:
+ featureToggles:
+ listType: true
diff --git a/tests/Platform/docker/Dockerfile80 b/tests/Platform/docker/Dockerfile80
new file mode 100644
index 00000000..37b6694c
--- /dev/null
+++ b/tests/Platform/docker/Dockerfile80
@@ -0,0 +1,7 @@
+FROM php:8.0-cli
+
+COPY ./docker-setup.sh /opt/src/scripts/setup.sh
+RUN /opt/src/scripts/setup.sh
+
+COPY --from=composer:2 /usr/bin/composer /usr/local/bin/composer
+
diff --git a/tests/Platform/docker/Dockerfile81 b/tests/Platform/docker/Dockerfile81
new file mode 100644
index 00000000..4ef5c3df
--- /dev/null
+++ b/tests/Platform/docker/Dockerfile81
@@ -0,0 +1,7 @@
+FROM php:8.1-cli
+
+COPY ./docker-setup.sh /opt/src/scripts/setup.sh
+RUN /opt/src/scripts/setup.sh
+
+COPY --from=composer:2 /usr/bin/composer /usr/local/bin/composer
+
diff --git a/tests/Platform/docker/docker-compose.yml b/tests/Platform/docker/docker-compose.yml
new file mode 100644
index 00000000..5ff6fbb8
--- /dev/null
+++ b/tests/Platform/docker/docker-compose.yml
@@ -0,0 +1,46 @@
+# the setup here should be in sync with GitHub CI services, see platform-matrix-test.yml
+
+services:
+ mysql:
+ image: mysql:8.0
+ command: mysqld --default-authentication-plugin=mysql_native_password
+ ports:
+ - 3306:3306
+ environment:
+ MYSQL_ROOT_PASSWORD: secret
+ MYSQL_DATABASE: foo
+
+ pgsql:
+ image: postgres:13
+ ports:
+ - 5432:5432
+ environment:
+ POSTGRES_PASSWORD: secret
+ POSTGRES_USER: root
+ POSTGRES_DB: foo
+
+ php80:
+ depends_on: [mysql, pgsql]
+ build:
+ context: .
+ dockerfile: ./Dockerfile80
+ environment:
+ MYSQL_HOST: mysql
+ PGSQL_HOST: pgsql
+ working_dir: /app
+ user: ${UID:-1000}:${GID:-1000}
+ volumes:
+ - ../../../:/app
+
+ php81:
+ depends_on: [mysql, pgsql]
+ build:
+ context: .
+ dockerfile: ./Dockerfile81
+ environment:
+ MYSQL_HOST: mysql
+ PGSQL_HOST: pgsql
+ working_dir: /app
+ user: ${UID:-1000}:${GID:-1000}
+ volumes:
+ - ../../../:/app
diff --git a/tests/Platform/docker/docker-setup.sh b/tests/Platform/docker/docker-setup.sh
new file mode 100755
index 00000000..341c88c2
--- /dev/null
+++ b/tests/Platform/docker/docker-setup.sh
@@ -0,0 +1,7 @@
+set -ex \
+ && apt update \
+ && apt install -y bash zip libpq-dev libsqlite3-dev \
+ && pecl install xdebug mongodb \
+ && docker-php-ext-configure pgsql -with-pgsql=/usr/local/pgsql \
+ && docker-php-ext-install pdo mysqli pgsql pdo_mysql pdo_pgsql pdo_sqlite \
+ && docker-php-ext-enable xdebug mongodb
diff --git a/tests/Type/Doctrine/Query/QueryResultTypeWalkerTest.php b/tests/Type/Doctrine/Query/QueryResultTypeWalkerTest.php
index 983362c2..f2f349ed 100644
--- a/tests/Type/Doctrine/Query/QueryResultTypeWalkerTest.php
+++ b/tests/Type/Doctrine/Query/QueryResultTypeWalkerTest.php
@@ -13,7 +13,9 @@
use Doctrine\ORM\Tools\SchemaTool;
use PHPStan\Testing\PHPStanTestCase;
use PHPStan\Type\Accessory\AccessoryNumericStringType;
+use PHPStan\Type\BooleanType;
use PHPStan\Type\Constant\ConstantArrayTypeBuilder;
+use PHPStan\Type\Constant\ConstantBooleanType;
use PHPStan\Type\Constant\ConstantIntegerType;
use PHPStan\Type\Constant\ConstantStringType;
use PHPStan\Type\ConstantTypeHelper;
@@ -794,7 +796,8 @@ public function getTestData(): iterable
TypeCombinator::union(
new ConstantIntegerType(1),
new ConstantStringType('1'),
- new NullType()
+ new NullType(),
+ new ConstantBooleanType(true)
),
],
]),
@@ -810,7 +813,8 @@ public function getTestData(): iterable
new ConstantIntegerType(1),
TypeCombinator::union(
new StringType(),
- new IntegerType()
+ new IntegerType(),
+ new ConstantBooleanType(false)
),
],
[
@@ -839,6 +843,7 @@ public function getTestData(): iterable
new ConstantIntegerType(1),
TypeCombinator::union(
new StringType(),
+ new ConstantBooleanType(false),
new ConstantIntegerType(0)
),
],
@@ -859,6 +864,7 @@ public function getTestData(): iterable
new ConstantIntegerType(1),
TypeCombinator::union(
new StringType(),
+ new ConstantBooleanType(false),
new ConstantIntegerType(0)
),
],
@@ -881,7 +887,8 @@ public function getTestData(): iterable
new ConstantIntegerType(0),
new ConstantIntegerType(1),
new ConstantStringType('0'),
- new ConstantStringType('1')
+ new ConstantStringType('1'),
+ new BooleanType()
),
],
]),
@@ -902,7 +909,8 @@ public function getTestData(): iterable
new ConstantIntegerType(0),
new ConstantIntegerType(1),
new ConstantStringType('0'),
- new ConstantStringType('1')
+ new ConstantStringType('1'),
+ new BooleanType()
),
],
]),
@@ -921,28 +929,32 @@ public function getTestData(): iterable
new ConstantIntegerType(1),
TypeCombinator::union(
new ConstantIntegerType(1),
- new ConstantStringType('1')
+ new ConstantStringType('1'),
+ new ConstantBooleanType(true)
),
],
[
new ConstantIntegerType(2),
TypeCombinator::union(
new ConstantIntegerType(0),
- new ConstantStringType('0')
+ new ConstantStringType('0'),
+ new ConstantBooleanType(false)
),
],
[
new ConstantIntegerType(3),
TypeCombinator::union(
new ConstantIntegerType(1),
- new ConstantStringType('1')
+ new ConstantStringType('1'),
+ new ConstantBooleanType(true)
),
],
[
new ConstantIntegerType(4),
TypeCombinator::union(
new ConstantIntegerType(0),
- new ConstantStringType('0')
+ new ConstantStringType('0'),
+ new ConstantBooleanType(false)
),
],
]),