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) ), ], ]),