From 3cce3931c37f247d846d6a86e2a5b1508dc498ac Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Wed, 7 Feb 2024 10:49:54 +0100 Subject: [PATCH 01/20] Build with Doctrine ORM 3 and DBAL 4 in CI --- .github/workflows/build.yml | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index ac3d7240..9f070d5e 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -106,6 +106,12 @@ jobs: dependencies: - "lowest" - "highest" + update-packages: + - "" + include: + - php-version: "8.3" + dependencies: "highest" + update-packages: "composer require --dev doctrine/orm:^3.0 doctrine/dbal:^4.0 carbonphp/carbon-doctrine-types:^3 -W" steps: - name: "Checkout" @@ -135,6 +141,9 @@ jobs: if: ${{ matrix.dependencies == 'highest' }} run: "composer update --no-interaction --no-progress" + - name: "Update packages" + run: ${{ matrix.update-packages }} + - name: "Tests" run: "make tests" @@ -152,6 +161,11 @@ jobs: - "8.1" - "8.2" - "8.3" + update-packages: + - "" + include: + - php-version: "8.3" + update-packages: "composer require --dev doctrine/orm:^3.0 doctrine/dbal:^4.0 carbonphp/carbon-doctrine-types:^3 -W" steps: - name: "Checkout" @@ -172,5 +186,8 @@ jobs: - name: "Install dependencies" run: "composer update --no-interaction --no-progress" + - name: "Update packages" + run: ${{ matrix.update-packages }} + - name: "PHPStan" run: "make phpstan" From bf665449d2a28ff49a395a57f853d1e432098081 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Wed, 7 Feb 2024 22:47:48 +0100 Subject: [PATCH 02/20] Add AnnotationDriver so that ORM PHPDoc annotations can be used with Doctrine ORM 3.0 --- .gitattributes | 1 + compatibility/AnnotationDriver.php | 910 ++++++++++++++++++ phpstan.neon | 1 + ...phpstan-without-object-manager-loader.neon | 3 + tests/DoctrineIntegration/ORM/phpstan.neon | 3 + tests/bootstrap.php | 2 + tests/orm-3-bootstrap.php | 8 + 7 files changed, 928 insertions(+) create mode 100644 compatibility/AnnotationDriver.php create mode 100644 tests/orm-3-bootstrap.php diff --git a/.gitattributes b/.gitattributes index cf5264fb..a1d69c90 100644 --- a/.gitattributes +++ b/.gitattributes @@ -2,6 +2,7 @@ .github export-ignore tests export-ignore +compatibility export-ignore tmp export-ignore .gitattributes export-ignore .gitignore export-ignore diff --git a/compatibility/AnnotationDriver.php b/compatibility/AnnotationDriver.php new file mode 100644 index 00000000..dae3d1e5 --- /dev/null +++ b/compatibility/AnnotationDriver.php @@ -0,0 +1,910 @@ + + */ + protected $entityAnnotationClasses = [ + Mapping\Entity::class => 1, + Mapping\MappedSuperclass::class => 2, + ]; + + /** @var bool */ + protected $reportFieldsWhereDeclared = false; + + /** + * Initializes a new AnnotationDriver that uses the given AnnotationReader for reading + * docblock annotations. + * + * @param Reader $reader The AnnotationReader to use + * @param string|string[]|null $paths One or multiple paths where mapping classes can be found. + */ + public function __construct($reader, $paths = null, bool $reportFieldsWhereDeclared = false) + { + Deprecation::trigger( + 'doctrine/orm', + 'https://github.com/doctrine/orm/issues/10098', + 'The annotation mapping driver is deprecated and will be removed in Doctrine ORM 3.0, please migrate to the attribute or XML driver.' + ); + $this->reader = $reader; + + $this->addPaths((array) $paths); + + if (! $reportFieldsWhereDeclared) { + Deprecation::trigger( + 'doctrine/orm', + 'https://github.com/doctrine/orm/pull/10455', + 'In ORM 3.0, the AttributeDriver will report fields for the classes where they are declared. This may uncover invalid mapping configurations. To opt into the new mode also with the AnnotationDriver today, set the "reportFieldsWhereDeclared" constructor parameter to true.', + self::class + ); + } + + $this->reportFieldsWhereDeclared = $reportFieldsWhereDeclared; + } + + /** + * {@inheritDoc} + * + * @psalm-param class-string $className + * @psalm-param ClassMetadata $metadata + * + * @template T of object + */ + public function loadMetadataForClass($className, PersistenceClassMetadata $metadata) + { + $class = $metadata->getReflectionClass() + // this happens when running annotation driver in combination with + // static reflection services. This is not the nicest fix + ?? new ReflectionClass($metadata->name); + + $classAnnotations = $this->reader->getClassAnnotations($class); + foreach ($classAnnotations as $key => $annot) { + if (! is_numeric($key)) { + continue; + } + + $classAnnotations[get_class($annot)] = $annot; + } + + // Evaluate Entity annotation + if (isset($classAnnotations[Mapping\Entity::class])) { + $entityAnnot = $classAnnotations[Mapping\Entity::class]; + assert($entityAnnot instanceof Mapping\Entity); + if ($entityAnnot->repositoryClass !== null) { + $metadata->setCustomRepositoryClass($entityAnnot->repositoryClass); + } + + if ($entityAnnot->readOnly) { + $metadata->markReadOnly(); + } + } elseif (isset($classAnnotations[Mapping\MappedSuperclass::class])) { + $mappedSuperclassAnnot = $classAnnotations[Mapping\MappedSuperclass::class]; + assert($mappedSuperclassAnnot instanceof Mapping\MappedSuperclass); + + $metadata->setCustomRepositoryClass($mappedSuperclassAnnot->repositoryClass); + $metadata->isMappedSuperclass = true; + } elseif (isset($classAnnotations[Mapping\Embeddable::class])) { + $metadata->isEmbeddedClass = true; + } else { + throw MappingException::classIsNotAValidEntityOrMappedSuperClass($className); + } + + // Evaluate Table annotation + if (isset($classAnnotations[Mapping\Table::class])) { + $tableAnnot = $classAnnotations[Mapping\Table::class]; + assert($tableAnnot instanceof Mapping\Table); + $primaryTable = [ + 'name' => $tableAnnot->name, + 'schema' => $tableAnnot->schema, + ]; + + foreach ($tableAnnot->indexes ?? [] as $indexAnnot) { + $index = []; + + if (! empty($indexAnnot->columns)) { + $index['columns'] = $indexAnnot->columns; + } + + if (! empty($indexAnnot->fields)) { + $index['fields'] = $indexAnnot->fields; + } + + if ( + isset($index['columns'], $index['fields']) + || ( + ! isset($index['columns']) + && ! isset($index['fields']) + ) + ) { + throw MappingException::invalidIndexConfiguration( + $className, + (string) ($indexAnnot->name ?? count($primaryTable['indexes'])) + ); + } + + if (! empty($indexAnnot->flags)) { + $index['flags'] = $indexAnnot->flags; + } + + if (! empty($indexAnnot->options)) { + $index['options'] = $indexAnnot->options; + } + + if (! empty($indexAnnot->name)) { + $primaryTable['indexes'][$indexAnnot->name] = $index; + } else { + $primaryTable['indexes'][] = $index; + } + } + + foreach ($tableAnnot->uniqueConstraints ?? [] as $uniqueConstraintAnnot) { + $uniqueConstraint = []; + + if (! empty($uniqueConstraintAnnot->columns)) { + $uniqueConstraint['columns'] = $uniqueConstraintAnnot->columns; + } + + if (! empty($uniqueConstraintAnnot->fields)) { + $uniqueConstraint['fields'] = $uniqueConstraintAnnot->fields; + } + + if ( + isset($uniqueConstraint['columns'], $uniqueConstraint['fields']) + || ( + ! isset($uniqueConstraint['columns']) + && ! isset($uniqueConstraint['fields']) + ) + ) { + throw MappingException::invalidUniqueConstraintConfiguration( + $className, + (string) ($uniqueConstraintAnnot->name ?? count($primaryTable['uniqueConstraints'])) + ); + } + + if (! empty($uniqueConstraintAnnot->options)) { + $uniqueConstraint['options'] = $uniqueConstraintAnnot->options; + } + + if (! empty($uniqueConstraintAnnot->name)) { + $primaryTable['uniqueConstraints'][$uniqueConstraintAnnot->name] = $uniqueConstraint; + } else { + $primaryTable['uniqueConstraints'][] = $uniqueConstraint; + } + } + + if ($tableAnnot->options) { + $primaryTable['options'] = $tableAnnot->options; + } + + $metadata->setPrimaryTable($primaryTable); + } + + // Evaluate @Cache annotation + if (isset($classAnnotations[Mapping\Cache::class])) { + $cacheAnnot = $classAnnotations[Mapping\Cache::class]; + $cacheMap = [ + 'region' => $cacheAnnot->region, + 'usage' => (int) constant('Doctrine\ORM\Mapping\ClassMetadata::CACHE_USAGE_' . $cacheAnnot->usage), + ]; + + $metadata->enableCache($cacheMap); + } + + // Evaluate NamedNativeQueries annotation + if (isset($classAnnotations[Mapping\NamedNativeQueries::class])) { + $namedNativeQueriesAnnot = $classAnnotations[Mapping\NamedNativeQueries::class]; + + foreach ($namedNativeQueriesAnnot->value as $namedNativeQuery) { + $metadata->addNamedNativeQuery( + [ + 'name' => $namedNativeQuery->name, + 'query' => $namedNativeQuery->query, + 'resultClass' => $namedNativeQuery->resultClass, + 'resultSetMapping' => $namedNativeQuery->resultSetMapping, + ] + ); + } + } + + // Evaluate SqlResultSetMappings annotation + if (isset($classAnnotations[Mapping\SqlResultSetMappings::class])) { + $sqlResultSetMappingsAnnot = $classAnnotations[Mapping\SqlResultSetMappings::class]; + + foreach ($sqlResultSetMappingsAnnot->value as $resultSetMapping) { + $entities = []; + $columns = []; + foreach ($resultSetMapping->entities as $entityResultAnnot) { + $entityResult = [ + 'fields' => [], + 'entityClass' => $entityResultAnnot->entityClass, + 'discriminatorColumn' => $entityResultAnnot->discriminatorColumn, + ]; + + foreach ($entityResultAnnot->fields as $fieldResultAnnot) { + $entityResult['fields'][] = [ + 'name' => $fieldResultAnnot->name, + 'column' => $fieldResultAnnot->column, + ]; + } + + $entities[] = $entityResult; + } + + foreach ($resultSetMapping->columns as $columnResultAnnot) { + $columns[] = [ + 'name' => $columnResultAnnot->name, + ]; + } + + $metadata->addSqlResultSetMapping( + [ + 'name' => $resultSetMapping->name, + 'entities' => $entities, + 'columns' => $columns, + ] + ); + } + } + + // Evaluate NamedQueries annotation + if (isset($classAnnotations[Mapping\NamedQueries::class])) { + $namedQueriesAnnot = $classAnnotations[Mapping\NamedQueries::class]; + + if (! is_array($namedQueriesAnnot->value)) { + throw new UnexpectedValueException('@NamedQueries should contain an array of @NamedQuery annotations.'); + } + + foreach ($namedQueriesAnnot->value as $namedQuery) { + if (! ($namedQuery instanceof Mapping\NamedQuery)) { + throw new UnexpectedValueException('@NamedQueries should contain an array of @NamedQuery annotations.'); + } + + $metadata->addNamedQuery( + [ + 'name' => $namedQuery->name, + 'query' => $namedQuery->query, + ] + ); + } + } + + // Evaluate InheritanceType annotation + if (isset($classAnnotations[Mapping\InheritanceType::class])) { + $inheritanceTypeAnnot = $classAnnotations[Mapping\InheritanceType::class]; + assert($inheritanceTypeAnnot instanceof Mapping\InheritanceType); + + $metadata->setInheritanceType( + constant('Doctrine\ORM\Mapping\ClassMetadata::INHERITANCE_TYPE_' . $inheritanceTypeAnnot->value) + ); + + if ($metadata->inheritanceType !== ClassMetadata::INHERITANCE_TYPE_NONE) { + // Evaluate DiscriminatorColumn annotation + if (isset($classAnnotations[Mapping\DiscriminatorColumn::class])) { + $discrColumnAnnot = $classAnnotations[Mapping\DiscriminatorColumn::class]; + assert($discrColumnAnnot instanceof Mapping\DiscriminatorColumn); + + $columnDef = [ + 'name' => $discrColumnAnnot->name, + 'type' => $discrColumnAnnot->type ?: 'string', + 'length' => $discrColumnAnnot->length ?? 255, + 'columnDefinition' => $discrColumnAnnot->columnDefinition, + 'enumType' => $discrColumnAnnot->enumType, + ]; + + if ($discrColumnAnnot->options) { + $columnDef['options'] = $discrColumnAnnot->options; + } + + $metadata->setDiscriminatorColumn($columnDef); + } else { + $metadata->setDiscriminatorColumn(['name' => 'dtype', 'type' => 'string', 'length' => 255]); + } + + // Evaluate DiscriminatorMap annotation + if (isset($classAnnotations[Mapping\DiscriminatorMap::class])) { + $discrMapAnnot = $classAnnotations[Mapping\DiscriminatorMap::class]; + assert($discrMapAnnot instanceof Mapping\DiscriminatorMap); + $metadata->setDiscriminatorMap($discrMapAnnot->value); + } + } + } + + // Evaluate DoctrineChangeTrackingPolicy annotation + if (isset($classAnnotations[Mapping\ChangeTrackingPolicy::class])) { + $changeTrackingAnnot = $classAnnotations[Mapping\ChangeTrackingPolicy::class]; + assert($changeTrackingAnnot instanceof Mapping\ChangeTrackingPolicy); + $metadata->setChangeTrackingPolicy(constant('Doctrine\ORM\Mapping\ClassMetadata::CHANGETRACKING_' . $changeTrackingAnnot->value)); + } + + // Evaluate annotations on properties/fields + foreach ($class->getProperties() as $property) { + if ($this->isRepeatedPropertyDeclaration($property, $metadata)) { + continue; + } + + $mapping = []; + $mapping['fieldName'] = $property->name; + + // Evaluate @Cache annotation + $cacheAnnot = $this->reader->getPropertyAnnotation($property, Mapping\Cache::class); + if ($cacheAnnot !== null) { + $mapping['cache'] = $metadata->getAssociationCacheDefaults( + $mapping['fieldName'], + [ + 'usage' => (int) constant('Doctrine\ORM\Mapping\ClassMetadata::CACHE_USAGE_' . $cacheAnnot->usage), + 'region' => $cacheAnnot->region, + ] + ); + } + + // Check for JoinColumn/JoinColumns annotations + $joinColumns = []; + + $joinColumnAnnot = $this->reader->getPropertyAnnotation($property, Mapping\JoinColumn::class); + if ($joinColumnAnnot) { + $joinColumns[] = $this->joinColumnToArray($joinColumnAnnot); + } else { + $joinColumnsAnnot = $this->reader->getPropertyAnnotation($property, Mapping\JoinColumns::class); + if ($joinColumnsAnnot) { + foreach ($joinColumnsAnnot->value as $joinColumn) { + if (is_array($joinColumn)) { + foreach ($joinColumn as $j) { + $joinColumns[] = $this->joinColumnToArray($j); + } + continue; + } + $joinColumns[] = $this->joinColumnToArray($joinColumn); + } + } + } + + // Field can only be annotated with one of: + // @Column, @OneToOne, @OneToMany, @ManyToOne, @ManyToMany + $columnAnnot = $this->reader->getPropertyAnnotation($property, Mapping\Column::class); + if ($columnAnnot) { + $mapping = $this->columnToArray($property->name, $columnAnnot); + + $idAnnot = $this->reader->getPropertyAnnotation($property, Mapping\Id::class); + if ($idAnnot) { + $mapping['id'] = true; + } + + $generatedValueAnnot = $this->reader->getPropertyAnnotation($property, Mapping\GeneratedValue::class); + if ($generatedValueAnnot) { + $metadata->setIdGeneratorType(constant('Doctrine\ORM\Mapping\ClassMetadata::GENERATOR_TYPE_' . $generatedValueAnnot->strategy)); + } + + if ($this->reader->getPropertyAnnotation($property, Mapping\Version::class)) { + $metadata->setVersionMapping($mapping); + } + + $metadata->mapField($mapping); + + // Check for SequenceGenerator/TableGenerator definition + $seqGeneratorAnnot = $this->reader->getPropertyAnnotation($property, Mapping\SequenceGenerator::class); + if ($seqGeneratorAnnot) { + $metadata->setSequenceGeneratorDefinition( + [ + 'sequenceName' => $seqGeneratorAnnot->sequenceName, + 'allocationSize' => $seqGeneratorAnnot->allocationSize, + 'initialValue' => $seqGeneratorAnnot->initialValue, + ] + ); + } else { + $customGeneratorAnnot = $this->reader->getPropertyAnnotation($property, Mapping\CustomIdGenerator::class); + if ($customGeneratorAnnot) { + $metadata->setCustomGeneratorDefinition( + [ + 'class' => $customGeneratorAnnot->class, + ] + ); + } + } + } else { + $this->loadRelationShipMapping( + $property, + $mapping, + $metadata, + $joinColumns, + $className + ); + } + } + + // Evaluate AssociationOverrides annotation + if (isset($classAnnotations[Mapping\AssociationOverrides::class])) { + $associationOverridesAnnot = $classAnnotations[Mapping\AssociationOverrides::class]; + assert($associationOverridesAnnot instanceof Mapping\AssociationOverrides); + + foreach ($associationOverridesAnnot->overrides as $associationOverride) { + $override = []; + $fieldName = $associationOverride->name; + + // Check for JoinColumn/JoinColumns annotations + if ($associationOverride->joinColumns) { + $joinColumns = []; + + foreach ($associationOverride->joinColumns as $joinColumn) { + $joinColumns[] = $this->joinColumnToArray($joinColumn); + } + + $override['joinColumns'] = $joinColumns; + } + + // Check for JoinTable annotations + if ($associationOverride->joinTable) { + $joinTableAnnot = $associationOverride->joinTable; + $joinTable = [ + 'name' => $joinTableAnnot->name, + 'schema' => $joinTableAnnot->schema, + ]; + + foreach ($joinTableAnnot->joinColumns as $joinColumn) { + $joinTable['joinColumns'][] = $this->joinColumnToArray($joinColumn); + } + + foreach ($joinTableAnnot->inverseJoinColumns as $joinColumn) { + $joinTable['inverseJoinColumns'][] = $this->joinColumnToArray($joinColumn); + } + + $override['joinTable'] = $joinTable; + } + + // Check for inversedBy + if ($associationOverride->inversedBy) { + $override['inversedBy'] = $associationOverride->inversedBy; + } + + // Check for `fetch` + if ($associationOverride->fetch) { + $override['fetch'] = constant(Mapping\ClassMetadata::class . '::FETCH_' . $associationOverride->fetch); + } + + $metadata->setAssociationOverride($fieldName, $override); + } + } + + // Evaluate AttributeOverrides annotation + if (isset($classAnnotations[Mapping\AttributeOverrides::class])) { + $attributeOverridesAnnot = $classAnnotations[Mapping\AttributeOverrides::class]; + assert($attributeOverridesAnnot instanceof Mapping\AttributeOverrides); + + foreach ($attributeOverridesAnnot->overrides as $attributeOverrideAnnot) { + $attributeOverride = $this->columnToArray($attributeOverrideAnnot->name, $attributeOverrideAnnot->column); + + $metadata->setAttributeOverride($attributeOverrideAnnot->name, $attributeOverride); + } + } + + // Evaluate EntityListeners annotation + if (isset($classAnnotations[Mapping\EntityListeners::class])) { + $entityListenersAnnot = $classAnnotations[Mapping\EntityListeners::class]; + assert($entityListenersAnnot instanceof Mapping\EntityListeners); + + foreach ($entityListenersAnnot->value as $item) { + $listenerClassName = $metadata->fullyQualifiedClassName($item); + + if (! class_exists($listenerClassName)) { + throw MappingException::entityListenerClassNotFound($listenerClassName, $className); + } + + $hasMapping = false; + $listenerClass = new ReflectionClass($listenerClassName); + + foreach ($listenerClass->getMethods(ReflectionMethod::IS_PUBLIC) as $method) { + // find method callbacks. + $callbacks = $this->getMethodCallbacks($method); + $hasMapping = $hasMapping ?: ! empty($callbacks); + + foreach ($callbacks as $value) { + $metadata->addEntityListener($value[1], $listenerClassName, $value[0]); + } + } + + // Evaluate the listener using naming convention. + if (! $hasMapping) { + EntityListenerBuilder::bindEntityListener($metadata, $listenerClassName); + } + } + } + + // Evaluate @HasLifecycleCallbacks annotation + if (isset($classAnnotations[Mapping\HasLifecycleCallbacks::class])) { + foreach ($class->getMethods(ReflectionMethod::IS_PUBLIC) as $method) { + foreach ($this->getMethodCallbacks($method) as $value) { + $metadata->addLifecycleCallback($value[0], $value[1]); + } + } + } + } + + /** + * @param mixed[] $joinColumns + * @param class-string $className + * @param array $mapping + */ + private function loadRelationShipMapping( + ReflectionProperty $property, + array &$mapping, + PersistenceClassMetadata $metadata, + array $joinColumns, + string $className + ): void { + $oneToOneAnnot = $this->reader->getPropertyAnnotation($property, Mapping\OneToOne::class); + if ($oneToOneAnnot) { + $idAnnot = $this->reader->getPropertyAnnotation($property, Mapping\Id::class); + if ($idAnnot) { + $mapping['id'] = true; + } + + $mapping['targetEntity'] = $oneToOneAnnot->targetEntity; + $mapping['joinColumns'] = $joinColumns; + $mapping['mappedBy'] = $oneToOneAnnot->mappedBy; + $mapping['inversedBy'] = $oneToOneAnnot->inversedBy; + $mapping['cascade'] = $oneToOneAnnot->cascade; + $mapping['orphanRemoval'] = $oneToOneAnnot->orphanRemoval; + $mapping['fetch'] = $this->getFetchMode($className, $oneToOneAnnot->fetch); + $metadata->mapOneToOne($mapping); + + return; + } + + $oneToManyAnnot = $this->reader->getPropertyAnnotation($property, Mapping\OneToMany::class); + if ($oneToManyAnnot) { + $mapping['mappedBy'] = $oneToManyAnnot->mappedBy; + $mapping['targetEntity'] = $oneToManyAnnot->targetEntity; + $mapping['cascade'] = $oneToManyAnnot->cascade; + $mapping['indexBy'] = $oneToManyAnnot->indexBy; + $mapping['orphanRemoval'] = $oneToManyAnnot->orphanRemoval; + $mapping['fetch'] = $this->getFetchMode($className, $oneToManyAnnot->fetch); + + $orderByAnnot = $this->reader->getPropertyAnnotation($property, Mapping\OrderBy::class); + if ($orderByAnnot) { + $mapping['orderBy'] = $orderByAnnot->value; + } + + $metadata->mapOneToMany($mapping); + } + + $manyToOneAnnot = $this->reader->getPropertyAnnotation($property, Mapping\ManyToOne::class); + if ($manyToOneAnnot) { + $idAnnot = $this->reader->getPropertyAnnotation($property, Mapping\Id::class); + if ($idAnnot) { + $mapping['id'] = true; + } + + $mapping['joinColumns'] = $joinColumns; + $mapping['cascade'] = $manyToOneAnnot->cascade; + $mapping['inversedBy'] = $manyToOneAnnot->inversedBy; + $mapping['targetEntity'] = $manyToOneAnnot->targetEntity; + $mapping['fetch'] = $this->getFetchMode($className, $manyToOneAnnot->fetch); + $metadata->mapManyToOne($mapping); + } + + $manyToManyAnnot = $this->reader->getPropertyAnnotation($property, Mapping\ManyToMany::class); + if ($manyToManyAnnot) { + $joinTable = []; + + $joinTableAnnot = $this->reader->getPropertyAnnotation($property, Mapping\JoinTable::class); + if ($joinTableAnnot) { + $joinTable = [ + 'name' => $joinTableAnnot->name, + 'schema' => $joinTableAnnot->schema, + ]; + + if ($joinTableAnnot->options) { + $joinTable['options'] = $joinTableAnnot->options; + } + + foreach ($joinTableAnnot->joinColumns as $joinColumn) { + $joinTable['joinColumns'][] = $this->joinColumnToArray($joinColumn); + } + + foreach ($joinTableAnnot->inverseJoinColumns as $joinColumn) { + $joinTable['inverseJoinColumns'][] = $this->joinColumnToArray($joinColumn); + } + } + + $mapping['joinTable'] = $joinTable; + $mapping['targetEntity'] = $manyToManyAnnot->targetEntity; + $mapping['mappedBy'] = $manyToManyAnnot->mappedBy; + $mapping['inversedBy'] = $manyToManyAnnot->inversedBy; + $mapping['cascade'] = $manyToManyAnnot->cascade; + $mapping['indexBy'] = $manyToManyAnnot->indexBy; + $mapping['orphanRemoval'] = $manyToManyAnnot->orphanRemoval; + $mapping['fetch'] = $this->getFetchMode($className, $manyToManyAnnot->fetch); + + $orderByAnnot = $this->reader->getPropertyAnnotation($property, Mapping\OrderBy::class); + if ($orderByAnnot) { + $mapping['orderBy'] = $orderByAnnot->value; + } + + $metadata->mapManyToMany($mapping); + } + + $embeddedAnnot = $this->reader->getPropertyAnnotation($property, Mapping\Embedded::class); + if ($embeddedAnnot) { + $mapping['class'] = $embeddedAnnot->class; + $mapping['columnPrefix'] = $embeddedAnnot->columnPrefix; + + $metadata->mapEmbedded($mapping); + } + } + + /** + * Attempts to resolve the fetch mode. + * + * @param class-string $className + * + * @psalm-return ClassMetadata::FETCH_* The fetch mode as defined in ClassMetadata. + * + * @throws MappingException If the fetch mode is not valid. + */ + private function getFetchMode(string $className, string $fetchMode): int + { + if (! defined('Doctrine\ORM\Mapping\ClassMetadata::FETCH_' . $fetchMode)) { + throw MappingException::invalidFetchMode($className, $fetchMode); + } + + return constant('Doctrine\ORM\Mapping\ClassMetadata::FETCH_' . $fetchMode); + } + + /** + * Attempts to resolve the generated mode. + * + * @psalm-return ClassMetadata::GENERATED_* + * + * @throws MappingException If the fetch mode is not valid. + */ + private function getGeneratedMode(string $generatedMode): int + { + if (! defined('Doctrine\ORM\Mapping\ClassMetadata::GENERATED_' . $generatedMode)) { + throw MappingException::invalidGeneratedMode($generatedMode); + } + + return constant('Doctrine\ORM\Mapping\ClassMetadata::GENERATED_' . $generatedMode); + } + + /** + * Parses the given method. + * + * @return list + * @psalm-return list + */ + private function getMethodCallbacks(ReflectionMethod $method): array + { + $callbacks = []; + $annotations = $this->reader->getMethodAnnotations($method); + + foreach ($annotations as $annot) { + if ($annot instanceof Mapping\PrePersist) { + $callbacks[] = [$method->name, Events::prePersist]; + } + + if ($annot instanceof Mapping\PostPersist) { + $callbacks[] = [$method->name, Events::postPersist]; + } + + if ($annot instanceof Mapping\PreUpdate) { + $callbacks[] = [$method->name, Events::preUpdate]; + } + + if ($annot instanceof Mapping\PostUpdate) { + $callbacks[] = [$method->name, Events::postUpdate]; + } + + if ($annot instanceof Mapping\PreRemove) { + $callbacks[] = [$method->name, Events::preRemove]; + } + + if ($annot instanceof Mapping\PostRemove) { + $callbacks[] = [$method->name, Events::postRemove]; + } + + if ($annot instanceof Mapping\PostLoad) { + $callbacks[] = [$method->name, Events::postLoad]; + } + + if ($annot instanceof Mapping\PreFlush) { + $callbacks[] = [$method->name, Events::preFlush]; + } + } + + return $callbacks; + } + + /** + * Parse the given JoinColumn as array + * + * @return mixed[] + * @psalm-return array{ + * name: string|null, + * unique: bool, + * nullable: bool, + * onDelete: mixed, + * columnDefinition: string|null, + * referencedColumnName: string, + * options?: array + * } + */ + private function joinColumnToArray(Mapping\JoinColumn $joinColumn): array + { + $mapping = [ + 'name' => $joinColumn->name, + 'unique' => $joinColumn->unique, + 'nullable' => $joinColumn->nullable, + 'onDelete' => $joinColumn->onDelete, + 'columnDefinition' => $joinColumn->columnDefinition, + 'referencedColumnName' => $joinColumn->referencedColumnName, + ]; + + if ($joinColumn->options) { + $mapping['options'] = $joinColumn->options; + } + + return $mapping; + } + + /** + * Parse the given Column as array + * + * @return mixed[] + * @psalm-return array{ + * fieldName: string, + * type: mixed, + * scale: int, + * length: int, + * unique: bool, + * nullable: bool, + * precision: int, + * notInsertable?: bool, + * notUpdateble?: bool, + * generated?: ClassMetadata::GENERATED_*, + * enumType?: class-string, + * options?: mixed[], + * columnName?: string, + * columnDefinition?: string + * } + */ + private function columnToArray(string $fieldName, Mapping\Column $column): array + { + $mapping = [ + 'fieldName' => $fieldName, + 'type' => $column->type, + 'scale' => $column->scale, + 'length' => $column->length, + 'unique' => $column->unique, + 'nullable' => $column->nullable, + 'precision' => $column->precision, + ]; + + if (! $column->insertable) { + $mapping['notInsertable'] = true; + } + + if (! $column->updatable) { + $mapping['notUpdatable'] = true; + } + + if ($column->generated) { + $mapping['generated'] = $this->getGeneratedMode($column->generated); + } + + if ($column->options) { + $mapping['options'] = $column->options; + } + + if (isset($column->name)) { + $mapping['columnName'] = $column->name; + } + + if (isset($column->columnDefinition)) { + $mapping['columnDefinition'] = $column->columnDefinition; + } + + if ($column->enumType !== null) { + $mapping['enumType'] = $column->enumType; + } + + return $mapping; + } + + /** + * Retrieve the current annotation reader + * + * @return Reader + */ + public function getReader() + { + Deprecation::trigger( + 'doctrine/orm', + 'https://github.com/doctrine/orm/pull/9587', + '%s is deprecated with no replacement', + __METHOD__ + ); + + return $this->reader; + } + + /** + * {@inheritDoc} + */ + public function isTransient($className) + { + $classAnnotations = $this->reader->getClassAnnotations(new ReflectionClass($className)); + + foreach ($classAnnotations as $annot) { + if (isset($this->entityAnnotationClasses[get_class($annot)])) { + return false; + } + } + + return true; + } + + /** + * Factory method for the Annotation Driver. + * + * @param mixed[]|string $paths + * + * @return AnnotationDriver + */ + public static function create($paths = [], ?AnnotationReader $reader = null) + { + if ($reader === null) { + $reader = new AnnotationReader(); + } + + return new self($reader, $paths); + } +} diff --git a/phpstan.neon b/phpstan.neon index b8a228bd..aabd4939 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -20,6 +20,7 @@ parameters: bootstrapFiles: - stubs/runtime/Enum/UnitEnum.php - stubs/runtime/Enum/BackedEnum.php + - tests/orm-3-bootstrap.php ignoreErrors: - diff --git a/tests/DoctrineIntegration/ORM/phpstan-without-object-manager-loader.neon b/tests/DoctrineIntegration/ORM/phpstan-without-object-manager-loader.neon index e1796c96..6a2360ae 100644 --- a/tests/DoctrineIntegration/ORM/phpstan-without-object-manager-loader.neon +++ b/tests/DoctrineIntegration/ORM/phpstan-without-object-manager-loader.neon @@ -6,3 +6,6 @@ parameters: doctrine: reportDynamicQueryBuilders: true queryBuilderClass: PHPStan\DoctrineIntegration\ORM\QueryBuilder\CustomQueryBuilder + + bootstrapFiles: + - ../../../tests/orm-3-bootstrap.php diff --git a/tests/DoctrineIntegration/ORM/phpstan.neon b/tests/DoctrineIntegration/ORM/phpstan.neon index a9604247..62ed8861 100644 --- a/tests/DoctrineIntegration/ORM/phpstan.neon +++ b/tests/DoctrineIntegration/ORM/phpstan.neon @@ -7,3 +7,6 @@ parameters: objectManagerLoader: entity-manager.php reportDynamicQueryBuilders: true queryBuilderClass: PHPStan\DoctrineIntegration\ORM\QueryBuilder\CustomQueryBuilder + + bootstrapFiles: + - ../../../tests/orm-3-bootstrap.php diff --git a/tests/bootstrap.php b/tests/bootstrap.php index f6c941c0..402e35ef 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -5,3 +5,5 @@ require_once __DIR__ . '/../vendor/autoload.php'; PHPStanTestCase::getContainer(); + +require_once __DIR__ . '/orm-3-bootstrap.php'; diff --git a/tests/orm-3-bootstrap.php b/tests/orm-3-bootstrap.php new file mode 100644 index 00000000..8dc7461f --- /dev/null +++ b/tests/orm-3-bootstrap.php @@ -0,0 +1,8 @@ + Date: Thu, 8 Feb 2024 09:59:40 +0100 Subject: [PATCH 03/20] Support Composer patches --- composer.json | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 98a407ba..d4c3b8f1 100644 --- a/composer.json +++ b/composer.json @@ -19,6 +19,7 @@ "require-dev": { "cache/array-adapter": "^1.1", "composer/semver": "^3.3.2", + "cweagans/composer-patches": "^1.7.3", "doctrine/annotations": "^1.11 || ^2.0", "doctrine/collections": "^1.6 || ^2.1", "doctrine/common": "^2.7 || ^3.0", @@ -38,7 +39,10 @@ "symfony/cache": "^5.4" }, "config": { - "sort-packages": true + "sort-packages": true, + "allow-plugins": { + "cweagans/composer-patches": true + } }, "extra": { "phpstan": { From 82f1b180b9f573ff3e4c2772b5446850b8c6d946 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Thu, 8 Feb 2024 10:00:46 +0100 Subject: [PATCH 04/20] Patch Attribute classes to support AnnotationDriver even with Doctrine ORM 3 for unit tests --- .github/workflows/build.yml | 4 +++- compatibility/patches/Column.patch | 14 ++++++++++++++ compatibility/patches/DiscriminatorColumn.patch | 16 ++++++++++++++++ compatibility/patches/DiscriminatorMap.patch | 16 ++++++++++++++++ compatibility/patches/Embeddable.patch | 13 +++++++++++++ compatibility/patches/Embedded.patch | 16 ++++++++++++++++ compatibility/patches/Entity.patch | 16 ++++++++++++++++ compatibility/patches/GeneratedValue.patch | 16 ++++++++++++++++ compatibility/patches/Id.patch | 13 +++++++++++++ compatibility/patches/InheritanceType.patch | 16 ++++++++++++++++ compatibility/patches/JoinColumn.patch | 16 ++++++++++++++++ compatibility/patches/JoinColumns.patch | 13 +++++++++++++ compatibility/patches/ManyToMany.patch | 16 ++++++++++++++++ compatibility/patches/ManyToOne.patch | 16 ++++++++++++++++ compatibility/patches/MappedSuperclass.patch | 17 +++++++++++++++++ compatibility/patches/OneToMany.patch | 16 ++++++++++++++++ compatibility/patches/OneToOne.patch | 16 ++++++++++++++++ compatibility/patches/OrderBy.patch | 16 ++++++++++++++++ compatibility/patches/UniqueConstraint.patch | 16 ++++++++++++++++ compatibility/patches/Version.patch | 13 +++++++++++++ 20 files changed, 294 insertions(+), 1 deletion(-) create mode 100644 compatibility/patches/Column.patch create mode 100644 compatibility/patches/DiscriminatorColumn.patch create mode 100644 compatibility/patches/DiscriminatorMap.patch create mode 100644 compatibility/patches/Embeddable.patch create mode 100644 compatibility/patches/Embedded.patch create mode 100644 compatibility/patches/Entity.patch create mode 100644 compatibility/patches/GeneratedValue.patch create mode 100644 compatibility/patches/Id.patch create mode 100644 compatibility/patches/InheritanceType.patch create mode 100644 compatibility/patches/JoinColumn.patch create mode 100644 compatibility/patches/JoinColumns.patch create mode 100644 compatibility/patches/ManyToMany.patch create mode 100644 compatibility/patches/ManyToOne.patch create mode 100644 compatibility/patches/MappedSuperclass.patch create mode 100644 compatibility/patches/OneToMany.patch create mode 100644 compatibility/patches/OneToOne.patch create mode 100644 compatibility/patches/OrderBy.patch create mode 100644 compatibility/patches/UniqueConstraint.patch create mode 100644 compatibility/patches/Version.patch diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 9f070d5e..d4162bd3 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -111,7 +111,9 @@ jobs: include: - php-version: "8.3" dependencies: "highest" - update-packages: "composer require --dev doctrine/orm:^3.0 doctrine/dbal:^4.0 carbonphp/carbon-doctrine-types:^3 -W" + update-packages: | + composer config extra.patches.doctrine/orm --json --merge '["compatibility/patches/Column.patch", "compatibility/patches/DiscriminatorColumn.patch", "compatibility/patches/DiscriminatorMap.patch", "compatibility/patches/Embeddable.patch", "compatibility/patches/Embedded.patch", "compatibility/patches/Entity.patch", "compatibility/patches/GeneratedValue.patch", "compatibility/patches/Id.patch", "compatibility/patches/InheritanceType.patch", "compatibility/patches/JoinColumn.patch", "compatibility/patches/JoinColumns.patch", "compatibility/patches/ManyToMany.patch", "compatibility/patches/ManyToOne.patch", "compatibility/patches/MappedSuperclass.patch", "compatibility/patches/OneToMany.patch", "compatibility/patches/OneToOne.patch", "compatibility/patches/OrderBy.patch", "compatibility/patches/UniqueConstraint.patch", "compatibility/patches/Version.patch"]' + composer require --dev doctrine/orm:^3.0 doctrine/dbal:^4.0 carbonphp/carbon-doctrine-types:^3 -W steps: - name: "Checkout" diff --git a/compatibility/patches/Column.patch b/compatibility/patches/Column.patch new file mode 100644 index 00000000..513d81fe --- /dev/null +++ b/compatibility/patches/Column.patch @@ -0,0 +1,14 @@ +--- src/Mapping/Column.php 2024-02-03 17:50:09 ++++ src/Mapping/Column.php 2024-02-08 14:19:31 +@@ -7,6 +7,11 @@ + use Attribute; + use BackedEnum; + ++/** ++ * @Annotation ++ * @NamedArgumentConstructor ++ * @Target({"PROPERTY","ANNOTATION"}) ++ */ + #[Attribute(Attribute::TARGET_PROPERTY)] + final class Column implements MappingAttribute + { diff --git a/compatibility/patches/DiscriminatorColumn.patch b/compatibility/patches/DiscriminatorColumn.patch new file mode 100644 index 00000000..62fa1bf5 --- /dev/null +++ b/compatibility/patches/DiscriminatorColumn.patch @@ -0,0 +1,16 @@ +--- src/Mapping/DiscriminatorColumn.php 2024-02-03 17:50:09 ++++ src/Mapping/DiscriminatorColumn.php 2024-02-08 14:25:37 +@@ -6,7 +6,13 @@ + + use Attribute; + use BackedEnum; ++use Doctrine\Common\Annotations\Annotation\NamedArgumentConstructor; + ++/** ++ * @Annotation ++ * @NamedArgumentConstructor() ++ * @Target("CLASS") ++ */ + #[Attribute(Attribute::TARGET_CLASS)] + final class DiscriminatorColumn implements MappingAttribute + { diff --git a/compatibility/patches/DiscriminatorMap.patch b/compatibility/patches/DiscriminatorMap.patch new file mode 100644 index 00000000..a8ecae11 --- /dev/null +++ b/compatibility/patches/DiscriminatorMap.patch @@ -0,0 +1,16 @@ +--- src/Mapping/DiscriminatorMap.php 2024-02-03 17:50:09 ++++ src/Mapping/DiscriminatorMap.php 2024-02-08 14:26:01 +@@ -5,7 +5,13 @@ + namespace Doctrine\ORM\Mapping; + + use Attribute; ++use Doctrine\Common\Annotations\Annotation\NamedArgumentConstructor; + ++/** ++ * @Annotation ++ * @NamedArgumentConstructor() ++ * @Target("CLASS") ++ */ + #[Attribute(Attribute::TARGET_CLASS)] + final class DiscriminatorMap implements MappingAttribute + { diff --git a/compatibility/patches/Embeddable.patch b/compatibility/patches/Embeddable.patch new file mode 100644 index 00000000..328c88c0 --- /dev/null +++ b/compatibility/patches/Embeddable.patch @@ -0,0 +1,13 @@ +--- src/Mapping/Embeddable.php 2024-02-03 17:50:09 ++++ src/Mapping/Embeddable.php 2024-02-08 14:23:25 +@@ -6,6 +6,10 @@ + + use Attribute; + ++/** ++ * @Annotation ++ * @Target("CLASS") ++ */ + #[Attribute(Attribute::TARGET_CLASS)] + final class Embeddable implements MappingAttribute + { diff --git a/compatibility/patches/Embedded.patch b/compatibility/patches/Embedded.patch new file mode 100644 index 00000000..180f14e9 --- /dev/null +++ b/compatibility/patches/Embedded.patch @@ -0,0 +1,16 @@ +--- src/Mapping/Embedded.php 2024-02-03 17:50:09 ++++ src/Mapping/Embedded.php 2024-02-08 14:26:23 +@@ -5,7 +5,13 @@ + namespace Doctrine\ORM\Mapping; + + use Attribute; ++use Doctrine\Common\Annotations\Annotation\NamedArgumentConstructor; + ++/** ++ * @Annotation ++ * @NamedArgumentConstructor() ++ * @Target("PROPERTY") ++ */ + #[Attribute(Attribute::TARGET_PROPERTY)] + final class Embedded implements MappingAttribute + { diff --git a/compatibility/patches/Entity.patch b/compatibility/patches/Entity.patch new file mode 100644 index 00000000..651d4a07 --- /dev/null +++ b/compatibility/patches/Entity.patch @@ -0,0 +1,16 @@ +--- src/Mapping/Entity.php 2024-02-08 09:55:51 ++++ src/Mapping/Entity.php 2024-02-08 09:55:54 +@@ -7,7 +7,12 @@ + use Attribute; + use Doctrine\ORM\EntityRepository; + +-/** @template T of object */ ++/** ++ * @Annotation ++ * @NamedArgumentConstructor() ++ * @Target("CLASS") ++ * @template T of object ++ */ + #[Attribute(Attribute::TARGET_CLASS)] + final class Entity implements MappingAttribute + { diff --git a/compatibility/patches/GeneratedValue.patch b/compatibility/patches/GeneratedValue.patch new file mode 100644 index 00000000..e9a09460 --- /dev/null +++ b/compatibility/patches/GeneratedValue.patch @@ -0,0 +1,16 @@ +--- src/Mapping/GeneratedValue.php 2024-02-03 17:50:09 ++++ src/Mapping/GeneratedValue.php 2024-02-08 14:20:21 +@@ -5,7 +5,13 @@ + namespace Doctrine\ORM\Mapping; + + use Attribute; ++use Doctrine\Common\Annotations\Annotation\NamedArgumentConstructor; + ++/** ++ * @Annotation ++ * @NamedArgumentConstructor() ++ * @Target("PROPERTY") ++ */ + #[Attribute(Attribute::TARGET_PROPERTY)] + final class GeneratedValue implements MappingAttribute + { diff --git a/compatibility/patches/Id.patch b/compatibility/patches/Id.patch new file mode 100644 index 00000000..45097bac --- /dev/null +++ b/compatibility/patches/Id.patch @@ -0,0 +1,13 @@ +--- src/Mapping/Id.php 2024-02-08 14:18:20 ++++ src/Mapping/Id.php 2024-02-08 14:18:23 +@@ -6,6 +6,10 @@ + + use Attribute; + ++/** ++ * @Annotation ++ * @Target("PROPERTY") ++ */ + #[Attribute(Attribute::TARGET_PROPERTY)] + final class Id implements MappingAttribute + { diff --git a/compatibility/patches/InheritanceType.patch b/compatibility/patches/InheritanceType.patch new file mode 100644 index 00000000..6e673a6d --- /dev/null +++ b/compatibility/patches/InheritanceType.patch @@ -0,0 +1,16 @@ +--- src/Mapping/InheritanceType.php 2024-02-03 17:50:09 ++++ src/Mapping/InheritanceType.php 2024-02-08 14:25:10 +@@ -5,7 +5,13 @@ + namespace Doctrine\ORM\Mapping; + + use Attribute; ++use Doctrine\Common\Annotations\Annotation\NamedArgumentConstructor; + ++/** ++ * @Annotation ++ * @NamedArgumentConstructor() ++ * @Target("CLASS") ++ */ + #[Attribute(Attribute::TARGET_CLASS)] + final class InheritanceType implements MappingAttribute + { diff --git a/compatibility/patches/JoinColumn.patch b/compatibility/patches/JoinColumn.patch new file mode 100644 index 00000000..887cd795 --- /dev/null +++ b/compatibility/patches/JoinColumn.patch @@ -0,0 +1,16 @@ +--- src/Mapping/JoinColumn.php 2024-02-03 17:50:09 ++++ src/Mapping/JoinColumn.php 2024-02-08 14:22:27 +@@ -5,7 +5,13 @@ + namespace Doctrine\ORM\Mapping; + + use Attribute; ++use Doctrine\Common\Annotations\Annotation\NamedArgumentConstructor; + ++/** ++ * @Annotation ++ * @NamedArgumentConstructor() ++ * @Target({"PROPERTY","ANNOTATION"}) ++ */ + #[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)] + final class JoinColumn implements MappingAttribute + { diff --git a/compatibility/patches/JoinColumns.patch b/compatibility/patches/JoinColumns.patch new file mode 100644 index 00000000..eb4e6e1b --- /dev/null +++ b/compatibility/patches/JoinColumns.patch @@ -0,0 +1,13 @@ +--- src/Mapping/JoinColumns.php 2024-02-03 17:50:09 ++++ src/Mapping/JoinColumns.php 2024-02-08 14:26:44 +@@ -4,6 +4,10 @@ + + namespace Doctrine\ORM\Mapping; + ++/** ++ * @Annotation ++ * @Target("PROPERTY") ++ */ + final class JoinColumns implements MappingAttribute + { + /** @param array $value */ diff --git a/compatibility/patches/ManyToMany.patch b/compatibility/patches/ManyToMany.patch new file mode 100644 index 00000000..813f2382 --- /dev/null +++ b/compatibility/patches/ManyToMany.patch @@ -0,0 +1,16 @@ +--- src/Mapping/ManyToMany.php 2024-02-03 17:50:09 ++++ src/Mapping/ManyToMany.php 2024-02-08 14:22:04 +@@ -5,7 +5,13 @@ + namespace Doctrine\ORM\Mapping; + + use Attribute; ++use Doctrine\Common\Annotations\Annotation\NamedArgumentConstructor; + ++/** ++ * @Annotation ++ * @NamedArgumentConstructor() ++ * @Target("PROPERTY") ++ */ + #[Attribute(Attribute::TARGET_PROPERTY)] + final class ManyToMany implements MappingAttribute + { diff --git a/compatibility/patches/ManyToOne.patch b/compatibility/patches/ManyToOne.patch new file mode 100644 index 00000000..854df89e --- /dev/null +++ b/compatibility/patches/ManyToOne.patch @@ -0,0 +1,16 @@ +--- src/Mapping/ManyToOne.php 2024-02-03 17:50:09 ++++ src/Mapping/ManyToOne.php 2024-02-08 14:20:37 +@@ -5,7 +5,13 @@ + namespace Doctrine\ORM\Mapping; + + use Attribute; ++use Doctrine\Common\Annotations\Annotation\NamedArgumentConstructor; + ++/** ++ * @Annotation ++ * @NamedArgumentConstructor() ++ * @Target("PROPERTY") ++ */ + #[Attribute(Attribute::TARGET_PROPERTY)] + final class ManyToOne implements MappingAttribute + { diff --git a/compatibility/patches/MappedSuperclass.patch b/compatibility/patches/MappedSuperclass.patch new file mode 100644 index 00000000..5a519f40 --- /dev/null +++ b/compatibility/patches/MappedSuperclass.patch @@ -0,0 +1,17 @@ +--- src/Mapping/MappedSuperclass.php 2024-02-03 17:50:09 ++++ src/Mapping/MappedSuperclass.php 2024-02-08 14:23:56 +@@ -5,8 +5,14 @@ + namespace Doctrine\ORM\Mapping; + + use Attribute; ++use Doctrine\Common\Annotations\Annotation\NamedArgumentConstructor; + use Doctrine\ORM\EntityRepository; + ++/** ++ * @Annotation ++ * @NamedArgumentConstructor() ++ * @Target("CLASS") ++ */ + #[Attribute(Attribute::TARGET_CLASS)] + final class MappedSuperclass implements MappingAttribute + { diff --git a/compatibility/patches/OneToMany.patch b/compatibility/patches/OneToMany.patch new file mode 100644 index 00000000..8abcf62d --- /dev/null +++ b/compatibility/patches/OneToMany.patch @@ -0,0 +1,16 @@ +--- src/Mapping/OneToMany.php 2024-02-03 17:50:09 ++++ src/Mapping/OneToMany.php 2024-02-08 14:21:43 +@@ -5,7 +5,13 @@ + namespace Doctrine\ORM\Mapping; + + use Attribute; ++use Doctrine\Common\Annotations\Annotation\NamedArgumentConstructor; + ++/** ++ * @Annotation ++ * @NamedArgumentConstructor() ++ * @Target("PROPERTY") ++ */ + #[Attribute(Attribute::TARGET_PROPERTY)] + final class OneToMany implements MappingAttribute + { diff --git a/compatibility/patches/OneToOne.patch b/compatibility/patches/OneToOne.patch new file mode 100644 index 00000000..7508b48b --- /dev/null +++ b/compatibility/patches/OneToOne.patch @@ -0,0 +1,16 @@ +--- src/Mapping/OneToOne.php 2024-02-03 17:50:09 ++++ src/Mapping/OneToOne.php 2024-02-08 14:23:03 +@@ -5,7 +5,13 @@ + namespace Doctrine\ORM\Mapping; + + use Attribute; ++use Doctrine\Common\Annotations\Annotation\NamedArgumentConstructor; + ++/** ++ * @Annotation ++ * @NamedArgumentConstructor() ++ * @Target("PROPERTY") ++ */ + #[Attribute(Attribute::TARGET_PROPERTY)] + final class OneToOne implements MappingAttribute + { diff --git a/compatibility/patches/OrderBy.patch b/compatibility/patches/OrderBy.patch new file mode 100644 index 00000000..5a8bc1a2 --- /dev/null +++ b/compatibility/patches/OrderBy.patch @@ -0,0 +1,16 @@ +--- src/Mapping/OrderBy.php 2024-02-03 17:50:09 ++++ src/Mapping/OrderBy.php 2024-02-08 18:01:12 +@@ -5,7 +5,13 @@ + namespace Doctrine\ORM\Mapping; + + use Attribute; ++use Doctrine\Common\Annotations\Annotation\NamedArgumentConstructor; + ++/** ++ * @Annotation ++ * @NamedArgumentConstructor() ++ * @Target("PROPERTY") ++ */ + #[Attribute(Attribute::TARGET_PROPERTY)] + final class OrderBy implements MappingAttribute + { diff --git a/compatibility/patches/UniqueConstraint.patch b/compatibility/patches/UniqueConstraint.patch new file mode 100644 index 00000000..a3c8479b --- /dev/null +++ b/compatibility/patches/UniqueConstraint.patch @@ -0,0 +1,16 @@ +--- src/Mapping/UniqueConstraint.php 2024-02-03 17:50:09 ++++ src/Mapping/UniqueConstraint.php 2024-02-08 14:24:37 +@@ -5,7 +5,13 @@ + namespace Doctrine\ORM\Mapping; + + use Attribute; ++use Doctrine\Common\Annotations\Annotation\NamedArgumentConstructor; + ++/** ++ * @Annotation ++ * @NamedArgumentConstructor() ++ * @Target("ANNOTATION") ++ */ + #[Attribute(Attribute::TARGET_CLASS | Attribute::IS_REPEATABLE)] + final class UniqueConstraint implements MappingAttribute + { diff --git a/compatibility/patches/Version.patch b/compatibility/patches/Version.patch new file mode 100644 index 00000000..fc7f172d --- /dev/null +++ b/compatibility/patches/Version.patch @@ -0,0 +1,13 @@ +--- src/Mapping/Version.php 2024-02-03 17:50:09 ++++ src/Mapping/Version.php 2024-02-08 14:24:16 +@@ -6,6 +6,10 @@ + + use Attribute; + ++/** ++ * @Annotation ++ * @Target("PROPERTY") ++ */ + #[Attribute(Attribute::TARGET_PROPERTY)] + final class Version implements MappingAttribute + { From 38e267d79b44b349e91b1e70b5bf16858e566695 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Thu, 8 Feb 2024 14:40:45 +0100 Subject: [PATCH 05/20] Fix PropertiesExtension --- src/Rules/Doctrine/ORM/PropertiesExtension.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Rules/Doctrine/ORM/PropertiesExtension.php b/src/Rules/Doctrine/ORM/PropertiesExtension.php index d9accb80..4fdbf57d 100644 --- a/src/Rules/Doctrine/ORM/PropertiesExtension.php +++ b/src/Rules/Doctrine/ORM/PropertiesExtension.php @@ -7,7 +7,6 @@ use PHPStan\Rules\Properties\ReadWritePropertiesExtension; use PHPStan\Type\Doctrine\ObjectMetadataResolver; use Throwable; -use function array_key_exists; use function in_array; class PropertiesExtension implements ReadWritePropertiesExtension @@ -47,7 +46,7 @@ public function isAlwaysWritten(PropertyReflection $property, string $propertyNa if (isset($metadata->fieldMappings[$propertyName])) { $mapping = $metadata->fieldMappings[$propertyName]; - if (array_key_exists('generated', $mapping) && $mapping['generated'] !== ClassMetadata::GENERATED_NEVER) { + if (isset($mapping['generated']) && $mapping['generated'] !== ClassMetadata::GENERATED_NEVER) { return true; } } From 3ce364bbf8bc8c9bc077666e8ba1e407a6723f60 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Thu, 8 Feb 2024 14:45:12 +0100 Subject: [PATCH 06/20] ORMException moved to another namespace --- src/Type/Doctrine/CreateQueryDynamicReturnTypeExtension.php | 2 +- .../QueryBuilderGetQueryDynamicReturnTypeExtension.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Type/Doctrine/CreateQueryDynamicReturnTypeExtension.php b/src/Type/Doctrine/CreateQueryDynamicReturnTypeExtension.php index 023828df..4c41613c 100644 --- a/src/Type/Doctrine/CreateQueryDynamicReturnTypeExtension.php +++ b/src/Type/Doctrine/CreateQueryDynamicReturnTypeExtension.php @@ -88,7 +88,7 @@ public function getTypeFromMethodCall( try { $query = $em->createQuery($queryString); QueryResultTypeWalker::walk($query, $typeBuilder, $this->descriptorRegistry); - } catch (ORMException | DBALException | NewDBALException | CommonException | MappingException $e) { + } catch (ORMException | DBALException | NewDBALException | CommonException | MappingException | \Doctrine\ORM\Exception\ORMException $e) { return new QueryType($queryString, null, null); } catch (AssertionError $e) { return new QueryType($queryString, null, null); diff --git a/src/Type/Doctrine/QueryBuilder/QueryBuilderGetQueryDynamicReturnTypeExtension.php b/src/Type/Doctrine/QueryBuilder/QueryBuilderGetQueryDynamicReturnTypeExtension.php index 1d0a1809..c5df245e 100644 --- a/src/Type/Doctrine/QueryBuilder/QueryBuilderGetQueryDynamicReturnTypeExtension.php +++ b/src/Type/Doctrine/QueryBuilder/QueryBuilderGetQueryDynamicReturnTypeExtension.php @@ -196,7 +196,7 @@ private function getQueryType(string $dql): Type try { $query = $em->createQuery($dql); QueryResultTypeWalker::walk($query, $typeBuilder, $this->descriptorRegistry); - } catch (ORMException | DBALException | CommonException | MappingException $e) { + } catch (ORMException | DBALException | CommonException | MappingException | \Doctrine\ORM\Exception\ORMException $e) { return new QueryType($dql, null); } catch (AssertionError $e) { return new QueryType($dql, null); From 5a667ee47bf495bec4fc229d328dfb86a533e156 Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Thu, 8 Feb 2024 15:45:19 +0100 Subject: [PATCH 07/20] Remove inheritdoc phpdoc --- .../Doctrine/Query/QueryResultTypeWalker.php | 118 ++++++++++-------- 1 file changed, 63 insertions(+), 55 deletions(-) diff --git a/src/Type/Doctrine/Query/QueryResultTypeWalker.php b/src/Type/Doctrine/Query/QueryResultTypeWalker.php index d034c1cc..3b113523 100644 --- a/src/Type/Doctrine/Query/QueryResultTypeWalker.php +++ b/src/Type/Doctrine/Query/QueryResultTypeWalker.php @@ -197,7 +197,7 @@ public function walkDeleteStatement(AST\DeleteStatement $AST): string } /** - * {@inheritdoc} + * @param string $identVariable */ public function walkEntityIdentificationVariable($identVariable): string { @@ -205,7 +205,8 @@ public function walkEntityIdentificationVariable($identVariable): string } /** - * {@inheritdoc} + * @param string $identificationVariable + * @param string|null $fieldName */ public function walkIdentificationVariable($identificationVariable, $fieldName = null): string { @@ -213,7 +214,7 @@ public function walkIdentificationVariable($identificationVariable, $fieldName = } /** - * {@inheritdoc} + * @param AST\PathExpression $pathExpr */ public function walkPathExpression($pathExpr): string { @@ -279,7 +280,7 @@ public function walkPathExpression($pathExpr): string } /** - * {@inheritdoc} + * @param AST\SelectClause $selectClause */ public function walkSelectClause($selectClause): string { @@ -287,7 +288,7 @@ public function walkSelectClause($selectClause): string } /** - * {@inheritdoc} + * @param AST\FromClause $fromClause */ public function walkFromClause($fromClause): string { @@ -301,7 +302,7 @@ public function walkFromClause($fromClause): string } /** - * {@inheritdoc} + * @param AST\IdentificationVariableDeclaration $identificationVariableDecl */ public function walkIdentificationVariableDeclaration($identificationVariableDecl): string { @@ -319,7 +320,7 @@ public function walkIdentificationVariableDeclaration($identificationVariableDec } /** - * {@inheritdoc} + * @param AST\IndexBy $indexBy */ public function walkIndexBy($indexBy): void { @@ -328,7 +329,7 @@ public function walkIndexBy($indexBy): void } /** - * {@inheritdoc} + * @param AST\RangeVariableDeclaration $rangeVariableDeclaration */ public function walkRangeVariableDeclaration($rangeVariableDeclaration): string { @@ -336,7 +337,9 @@ public function walkRangeVariableDeclaration($rangeVariableDeclaration): string } /** - * {@inheritdoc} + * @param AST\JoinAssociationDeclaration $joinAssociationDeclaration + * @param int $joinType + * @param AST\ConditionalExpression|AST\Phase2OptimizableConditional|null $condExpr */ public function walkJoinAssociationDeclaration($joinAssociationDeclaration, $joinType = AST\Join::JOIN_TYPE_INNER, $condExpr = null): string { @@ -344,7 +347,7 @@ public function walkJoinAssociationDeclaration($joinAssociationDeclaration, $joi } /** - * {@inheritdoc} + * @param AST\Functions\FunctionNode $function */ public function walkFunction($function): string { @@ -563,7 +566,7 @@ public function walkFunction($function): string } /** - * {@inheritdoc} + * @param AST\OrderByClause $orderByClause */ public function walkOrderByClause($orderByClause): string { @@ -571,7 +574,7 @@ public function walkOrderByClause($orderByClause): string } /** - * {@inheritdoc} + * @param AST\OrderByItem $orderByItem */ public function walkOrderByItem($orderByItem): string { @@ -579,7 +582,7 @@ public function walkOrderByItem($orderByItem): string } /** - * {@inheritdoc} + * @param AST\HavingClause $havingClause */ public function walkHavingClause($havingClause): string { @@ -587,7 +590,7 @@ public function walkHavingClause($havingClause): string } /** - * {@inheritdoc} + * @param AST\Join $join */ public function walkJoin($join): string { @@ -613,7 +616,7 @@ public function walkJoin($join): string } /** - * {@inheritdoc} + * @param AST\CoalesceExpression $coalesceExpression */ public function walkCoalesceExpression($coalesceExpression): string { @@ -642,7 +645,7 @@ public function walkCoalesceExpression($coalesceExpression): string } /** - * {@inheritdoc} + * @param AST\NullIfExpression $nullIfExpression */ public function walkNullIfExpression($nullIfExpression): string { @@ -695,7 +698,7 @@ public function walkGeneralCaseExpression(AST\GeneralCaseExpression $generalCase } /** - * {@inheritdoc} + * @param AST\SimpleCaseExpression $simpleCaseExpression */ public function walkSimpleCaseExpression($simpleCaseExpression): string { @@ -732,7 +735,7 @@ public function walkSimpleCaseExpression($simpleCaseExpression): string } /** - * {@inheritdoc} + * @param AST\SelectExpression $selectExpression */ public function walkSelectExpression($selectExpression): string { @@ -857,7 +860,7 @@ public function walkSelectExpression($selectExpression): string } /** - * {@inheritdoc} + * @param AST\QuantifiedExpression $qExpr */ public function walkQuantifiedExpression($qExpr): string { @@ -865,7 +868,7 @@ public function walkQuantifiedExpression($qExpr): string } /** - * {@inheritdoc} + * @param AST\Subselect $subselect */ public function walkSubselect($subselect): string { @@ -873,7 +876,7 @@ public function walkSubselect($subselect): string } /** - * {@inheritdoc} + * @param AST\SubselectFromClause $subselectFromClause */ public function walkSubselectFromClause($subselectFromClause): string { @@ -881,7 +884,7 @@ public function walkSubselectFromClause($subselectFromClause): string } /** - * {@inheritdoc} + * @param AST\SimpleSelectClause $simpleSelectClause */ public function walkSimpleSelectClause($simpleSelectClause): string { @@ -894,7 +897,8 @@ public function walkParenthesisExpression(AST\ParenthesisExpression $parenthesis } /** - * {@inheritdoc} + * @param AST\NewObjectExpression $newObjectExpression + * @param string|null $newObjectResultAlias */ public function walkNewObject($newObjectExpression, $newObjectResultAlias = null): string { @@ -908,7 +912,7 @@ public function walkNewObject($newObjectExpression, $newObjectResultAlias = null } /** - * {@inheritdoc} + * @param AST\SimpleSelectExpression $simpleSelectExpression */ public function walkSimpleSelectExpression($simpleSelectExpression): string { @@ -916,7 +920,7 @@ public function walkSimpleSelectExpression($simpleSelectExpression): string } /** - * {@inheritdoc} + * @param AST\AggregateExpression $aggExpression */ public function walkAggregateExpression($aggExpression): string { @@ -940,7 +944,7 @@ public function walkAggregateExpression($aggExpression): string } /** - * {@inheritdoc} + * @param AST\GroupByClause $groupByClause */ public function walkGroupByClause($groupByClause): string { @@ -948,7 +952,7 @@ public function walkGroupByClause($groupByClause): string } /** - * {@inheritdoc} + * @param AST\PathExpression|string $groupByItem */ public function walkGroupByItem($groupByItem): string { @@ -961,7 +965,7 @@ public function walkDeleteClause(AST\DeleteClause $deleteClause): string } /** - * {@inheritdoc} + * @param AST\UpdateClause $updateClause */ public function walkUpdateClause($updateClause): string { @@ -969,7 +973,7 @@ public function walkUpdateClause($updateClause): string } /** - * {@inheritdoc} + * @param AST\UpdateItem $updateItem */ public function walkUpdateItem($updateItem): string { @@ -977,7 +981,7 @@ public function walkUpdateItem($updateItem): string } /** - * {@inheritdoc} + * @param AST\WhereClause|null $whereClause */ public function walkWhereClause($whereClause): string { @@ -985,7 +989,7 @@ public function walkWhereClause($whereClause): string } /** - * {@inheritdoc} + * @param AST\ConditionalExpression|AST\Phase2OptimizableConditional $condExpr */ public function walkConditionalExpression($condExpr): string { @@ -993,7 +997,7 @@ public function walkConditionalExpression($condExpr): string } /** - * {@inheritdoc} + * @param AST\ConditionalTerm|AST\ConditionalPrimary|AST\ConditionalFactor $condTerm */ public function walkConditionalTerm($condTerm): string { @@ -1001,7 +1005,7 @@ public function walkConditionalTerm($condTerm): string } /** - * {@inheritdoc} + * @param AST\ConditionalFactor|AST\ConditionalPrimary $factor */ public function walkConditionalFactor($factor): string { @@ -1009,7 +1013,7 @@ public function walkConditionalFactor($factor): string } /** - * {@inheritdoc} + * @param AST\ConditionalPrimary $primary */ public function walkConditionalPrimary($primary): string { @@ -1017,7 +1021,7 @@ public function walkConditionalPrimary($primary): string } /** - * {@inheritdoc} + * @param AST\ExistsExpression $existsExpr */ public function walkExistsExpression($existsExpr): string { @@ -1025,7 +1029,7 @@ public function walkExistsExpression($existsExpr): string } /** - * {@inheritdoc} + * @param AST\CollectionMemberExpression $collMemberExpr */ public function walkCollectionMemberExpression($collMemberExpr): string { @@ -1033,7 +1037,7 @@ public function walkCollectionMemberExpression($collMemberExpr): string } /** - * {@inheritdoc} + * @param AST\EmptyCollectionComparisonExpression $emptyCollCompExpr */ public function walkEmptyCollectionComparisonExpression($emptyCollCompExpr): string { @@ -1041,7 +1045,7 @@ public function walkEmptyCollectionComparisonExpression($emptyCollCompExpr): str } /** - * {@inheritdoc} + * @param AST\NullComparisonExpression $nullCompExpr */ public function walkNullComparisonExpression($nullCompExpr): string { @@ -1049,15 +1053,15 @@ public function walkNullComparisonExpression($nullCompExpr): string } /** - * {@inheritdoc} + * @param mixed $inExpr */ - public function walkInExpression($inExpr) + public function walkInExpression($inExpr): string { return $this->marshalType(new MixedType()); } /** - * {@inheritdoc} + * @param AST\InstanceOfExpression $instanceOfExpr */ public function walkInstanceOfExpression($instanceOfExpr): string { @@ -1065,7 +1069,7 @@ public function walkInstanceOfExpression($instanceOfExpr): string } /** - * {@inheritdoc} + * @param mixed $inParam */ public function walkInParameter($inParam): string { @@ -1073,7 +1077,7 @@ public function walkInParameter($inParam): string } /** - * {@inheritdoc} + * @param AST\Literal $literal */ public function walkLiteral($literal): string { @@ -1110,7 +1114,7 @@ public function walkLiteral($literal): string } /** - * {@inheritdoc} + * @param AST\BetweenExpression $betweenExpr */ public function walkBetweenExpression($betweenExpr): string { @@ -1118,7 +1122,7 @@ public function walkBetweenExpression($betweenExpr): string } /** - * {@inheritdoc} + * @param AST\LikeExpression $likeExpr */ public function walkLikeExpression($likeExpr): string { @@ -1126,7 +1130,7 @@ public function walkLikeExpression($likeExpr): string } /** - * {@inheritdoc} + * @param AST\PathExpression $stateFieldPathExpression */ public function walkStateFieldPathExpression($stateFieldPathExpression): string { @@ -1134,7 +1138,7 @@ public function walkStateFieldPathExpression($stateFieldPathExpression): string } /** - * {@inheritdoc} + * @param AST\ComparisonExpression $compExpr */ public function walkComparisonExpression($compExpr): string { @@ -1142,7 +1146,7 @@ public function walkComparisonExpression($compExpr): string } /** - * {@inheritdoc} + * @param AST\InputParameter $inputParam */ public function walkInputParameter($inputParam): string { @@ -1150,7 +1154,7 @@ public function walkInputParameter($inputParam): string } /** - * {@inheritdoc} + * @param AST\ArithmeticExpression $arithmeticExpr */ public function walkArithmeticExpression($arithmeticExpr): string { @@ -1166,10 +1170,14 @@ public function walkArithmeticExpression($arithmeticExpr): string } /** - * {@inheritdoc} + * @param AST\Node|string $simpleArithmeticExpr */ public function walkSimpleArithmeticExpression($simpleArithmeticExpr): string { + if (!$simpleArithmeticExpr instanceof AST\SimpleArithmeticExpression) { + return $this->marshalType(new MixedType()); + } + $types = []; foreach ($simpleArithmeticExpr->arithmeticTerms as $term) { @@ -1188,7 +1196,7 @@ public function walkSimpleArithmeticExpression($simpleArithmeticExpr): string } /** - * {@inheritdoc} + * @param mixed $term */ public function walkArithmeticTerm($term): string { @@ -1214,7 +1222,7 @@ public function walkArithmeticTerm($term): string } /** - * {@inheritdoc} + * @param mixed $factor */ public function walkArithmeticFactor($factor): string { @@ -1231,7 +1239,7 @@ public function walkArithmeticFactor($factor): string } /** - * {@inheritdoc} + * @param mixed $primary */ public function walkArithmeticPrimary($primary): string { @@ -1248,7 +1256,7 @@ public function walkArithmeticPrimary($primary): string } /** - * {@inheritdoc} + * @param mixed $stringPrimary */ public function walkStringPrimary($stringPrimary): string { @@ -1256,7 +1264,7 @@ public function walkStringPrimary($stringPrimary): string } /** - * {@inheritdoc} + * @param string $resultVariable */ public function walkResultVariable($resultVariable): string { From 544bbeadd6314e77ad5b5d1e2f8d43406cf69aff Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Thu, 8 Feb 2024 17:53:36 +0100 Subject: [PATCH 08/20] More QueryResultTypeWalker fixes --- .../Doctrine/Query/QueryResultTypeWalker.php | 28 ++++++++++++------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/src/Type/Doctrine/Query/QueryResultTypeWalker.php b/src/Type/Doctrine/Query/QueryResultTypeWalker.php index 3b113523..9a5599bb 100644 --- a/src/Type/Doctrine/Query/QueryResultTypeWalker.php +++ b/src/Type/Doctrine/Query/QueryResultTypeWalker.php @@ -240,6 +240,7 @@ public function walkPathExpression($pathExpr): string case AST\PathExpression::TYPE_SINGLE_VALUED_ASSOCIATION: if (isset($class->associationMappings[$fieldName]['inherited'])) { + /** @var class-string $newClassName */ $newClassName = $class->associationMappings[$fieldName]['inherited']; $class = $this->em->getClassMetadata($newClassName); } @@ -255,6 +256,8 @@ public function walkPathExpression($pathExpr): string } $joinColumn = $assoc['joinColumns'][0]; + + /** @var class-string $assocClassName */ $assocClassName = $assoc['targetEntity']; $targetClass = $this->em->getClassMetadata($assocClassName); @@ -360,7 +363,7 @@ public function walkFunction($function): string return $function->getSql($this); case $function instanceof AST\Functions\AbsFunction: - $exprType = $this->unmarshalType($function->simpleArithmeticExpression->dispatch($this)); + $exprType = $this->unmarshalType($this->walkSimpleArithmeticExpression($function->simpleArithmeticExpression)); $type = TypeCombinator::union( IntegerRangeType::fromInterval(0, null), @@ -442,8 +445,8 @@ public function walkFunction($function): string return $this->marshalType($type); case $function instanceof AST\Functions\LocateFunction: - $firstExprType = $this->unmarshalType($function->firstStringPrimary->dispatch($this)); - $secondExprType = $this->unmarshalType($function->secondStringPrimary->dispatch($this)); + $firstExprType = $this->unmarshalType($this->walkStringPrimary($function->firstStringPrimary)); + $secondExprType = $this->unmarshalType($this->walkStringPrimary($function->secondStringPrimary)); $type = IntegerRangeType::fromInterval(0, null); if (TypeCombinator::containsNull($firstExprType) || TypeCombinator::containsNull($secondExprType)) { @@ -465,8 +468,8 @@ public function walkFunction($function): string return $this->marshalType($type); case $function instanceof AST\Functions\ModFunction: - $firstExprType = $this->unmarshalType($function->firstSimpleArithmeticExpression->dispatch($this)); - $secondExprType = $this->unmarshalType($function->secondSimpleArithmeticExpression->dispatch($this)); + $firstExprType = $this->unmarshalType($this->walkSimpleArithmeticExpression($function->firstSimpleArithmeticExpression)); + $secondExprType = $this->unmarshalType($this->walkSimpleArithmeticExpression($function->secondSimpleArithmeticExpression)); $type = IntegerRangeType::fromInterval(0, null); if (TypeCombinator::containsNull($firstExprType) || TypeCombinator::containsNull($secondExprType)) { @@ -481,7 +484,7 @@ public function walkFunction($function): string return $this->marshalType($type); case $function instanceof AST\Functions\SqrtFunction: - $exprType = $this->unmarshalType($function->simpleArithmeticExpression->dispatch($this)); + $exprType = $this->unmarshalType($this->walkSimpleArithmeticExpression($function->simpleArithmeticExpression)); $type = new FloatType(); if (TypeCombinator::containsNull($exprType)) { @@ -492,10 +495,10 @@ public function walkFunction($function): string case $function instanceof AST\Functions\SubstringFunction: $stringType = $this->unmarshalType($function->stringPrimary->dispatch($this)); - $firstExprType = $this->unmarshalType($function->firstSimpleArithmeticExpression->dispatch($this)); + $firstExprType = $this->unmarshalType($this->walkSimpleArithmeticExpression($function->firstSimpleArithmeticExpression)); if ($function->secondSimpleArithmeticExpression !== null) { - $secondExprType = $this->unmarshalType($function->secondSimpleArithmeticExpression->dispatch($this)); + $secondExprType = $this->unmarshalType($this->walkSimpleArithmeticExpression($function->secondSimpleArithmeticExpression)); } else { $secondExprType = new IntegerType(); } @@ -514,6 +517,8 @@ public function walkFunction($function): string assert(array_key_exists('metadata', $queryComp)); $class = $queryComp['metadata']; $assoc = $class->associationMappings[$assocField]; + + /** @var class-string $assocClassName */ $assocClassName = $assoc['targetEntity']; $targetClass = $this->em->getClassMetadata($assocClassName); @@ -930,7 +935,7 @@ public function walkAggregateExpression($aggExpression): string case 'AVG': case 'SUM': $type = $this->unmarshalType( - $aggExpression->pathExpression->dispatch($this) + $this->walkSimpleArithmeticExpression($aggExpression->pathExpression) ); return $this->marshalType(TypeCombinator::addNull($type)); @@ -1159,7 +1164,7 @@ public function walkInputParameter($inputParam): string public function walkArithmeticExpression($arithmeticExpr): string { if ($arithmeticExpr->simpleArithmeticExpression !== null) { - return $arithmeticExpr->simpleArithmeticExpression->dispatch($this); + return $this->walkSimpleArithmeticExpression($arithmeticExpr->simpleArithmeticExpression); } if ($arithmeticExpr->subselect !== null) { @@ -1302,7 +1307,10 @@ private function getTypeOfField(ClassMetadata $class, string $fieldName): array $metadata = $class->fieldMappings[$fieldName]; + /** @var string $type */ $type = $metadata['type']; + + /** @var class-string|null $enumType */ $enumType = $metadata['enumType'] ?? null; if (!is_string($enumType) || !class_exists($enumType)) { From 8b9df37efe786334905b80ca25f0d8b23084ce0b Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Thu, 8 Feb 2024 18:28:20 +0100 Subject: [PATCH 09/20] More fixes --- src/Type/Doctrine/Query/QueryResultTypeWalker.php | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/Type/Doctrine/Query/QueryResultTypeWalker.php b/src/Type/Doctrine/Query/QueryResultTypeWalker.php index 9a5599bb..b4b5ddf5 100644 --- a/src/Type/Doctrine/Query/QueryResultTypeWalker.php +++ b/src/Type/Doctrine/Query/QueryResultTypeWalker.php @@ -1180,7 +1180,7 @@ public function walkArithmeticExpression($arithmeticExpr): string public function walkSimpleArithmeticExpression($simpleArithmeticExpr): string { if (!$simpleArithmeticExpr instanceof AST\SimpleArithmeticExpression) { - return $this->marshalType(new MixedType()); + return $this->walkArithmeticTerm($simpleArithmeticExpr); } $types = []; @@ -1206,7 +1206,7 @@ public function walkSimpleArithmeticExpression($simpleArithmeticExpr): string public function walkArithmeticTerm($term): string { if (!$term instanceof AST\ArithmeticTerm) { - return $this->marshalType(new MixedType()); + return $this->walkArithmeticFactor($term); } $types = []; @@ -1232,7 +1232,7 @@ public function walkArithmeticTerm($term): string public function walkArithmeticFactor($factor): string { if (!$factor instanceof AST\ArithmeticFactor) { - return $this->marshalType(new MixedType()); + return $this->walkArithmeticPrimary($factor); } $primary = $factor->arithmeticPrimary; @@ -1265,6 +1265,10 @@ public function walkArithmeticPrimary($primary): string */ public function walkStringPrimary($stringPrimary): string { + if ($stringPrimary instanceof AST\Node) { + return $stringPrimary->dispatch($this); + } + return $this->marshalType(new MixedType()); } From 33aeae57bf206b11596534e87c213a83c1ad2dbb Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Fri, 9 Feb 2024 11:19:13 +0100 Subject: [PATCH 10/20] Fix test --- tests/Rules/Doctrine/ORM/DqlRuleTest.php | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/tests/Rules/Doctrine/ORM/DqlRuleTest.php b/tests/Rules/Doctrine/ORM/DqlRuleTest.php index c4cbe21d..c601fe54 100644 --- a/tests/Rules/Doctrine/ORM/DqlRuleTest.php +++ b/tests/Rules/Doctrine/ORM/DqlRuleTest.php @@ -2,9 +2,12 @@ namespace PHPStan\Rules\Doctrine\ORM; +use Composer\InstalledVersions; use PHPStan\Rules\Rule; use PHPStan\Testing\RuleTestCase; use PHPStan\Type\Doctrine\ObjectMetadataResolver; +use function sprintf; +use function strpos; /** * @extends RuleTestCase @@ -19,9 +22,15 @@ protected function getRule(): Rule public function testRule(): void { + $ormVersion = InstalledVersions::getVersion('doctrine/orm'); + if (strpos($ormVersion, '3.') === 0) { + $lexer = 'TokenType'; + } else { + $lexer = 'Lexer'; + } $this->analyse([__DIR__ . '/data/dql.php'], [ [ - 'DQL: [Syntax Error] line 0, col -1: Error: Expected Doctrine\ORM\Query\Lexer::T_IDENTIFIER, got end of string.', + sprintf('DQL: [Syntax Error] line 0, col -1: Error: Expected Doctrine\ORM\Query\%s::T_IDENTIFIER, got end of string.', $lexer), 35, ], [ From 4a3aa8bcc09f4f618fe831b24d388f702ad5d022 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Fri, 9 Feb 2024 11:25:16 +0100 Subject: [PATCH 11/20] Tests - bring back DBAL ArrayType --- compatibility/ArrayType.php | 57 +++++++++++++++++++ .../Doctrine/ORM/EntityColumnRuleTest.php | 3 + tests/bootstrap.php | 1 + tests/dbal-4-bootstrap.php | 8 +++ 4 files changed, 69 insertions(+) create mode 100644 compatibility/ArrayType.php create mode 100644 tests/dbal-4-bootstrap.php diff --git a/compatibility/ArrayType.php b/compatibility/ArrayType.php new file mode 100644 index 00000000..0263f426 --- /dev/null +++ b/compatibility/ArrayType.php @@ -0,0 +1,57 @@ +getClobTypeDeclarationSQL($column); + } + + public function convertToDatabaseValue(mixed $value, AbstractPlatform $platform): mixed + { + // @todo 3.0 - $value === null check to save real NULL in database + return serialize($value); + } + + public function convertToPHPValue(mixed $value, AbstractPlatform $platform): mixed + { + if ($value === null) { + return null; + } + + $value = is_resource($value) ? stream_get_contents($value) : $value; + + set_error_handler(function (int $code, string $message): bool { + if ($code === E_DEPRECATED || $code === E_USER_DEPRECATED) { + return false; + } + + throw ConversionException::conversionFailedUnserialization($this->getName(), $message); + }); + + try { + return unserialize($value); + } finally { + restore_error_handler(); + } + } +} diff --git a/tests/Rules/Doctrine/ORM/EntityColumnRuleTest.php b/tests/Rules/Doctrine/ORM/EntityColumnRuleTest.php index 4b9eaf80..cbb8dbe5 100644 --- a/tests/Rules/Doctrine/ORM/EntityColumnRuleTest.php +++ b/tests/Rules/Doctrine/ORM/EntityColumnRuleTest.php @@ -54,6 +54,9 @@ protected function getRule(): Rule if (!Type::hasType('carbon_immutable')) { Type::addType('carbon_immutable', CarbonImmutableType::class); } + if (!Type::hasType('array')) { + Type::addType('array', \Doctrine\DBAL\Types\ArrayType::class); + } return new EntityColumnRule( new ObjectMetadataResolver($this->objectManagerLoader, __DIR__ . '/../../../../tmp'), diff --git a/tests/bootstrap.php b/tests/bootstrap.php index 402e35ef..edf6599d 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -7,3 +7,4 @@ PHPStanTestCase::getContainer(); require_once __DIR__ . '/orm-3-bootstrap.php'; +require_once __DIR__ . '/dbal-4-bootstrap.php'; diff --git a/tests/dbal-4-bootstrap.php b/tests/dbal-4-bootstrap.php new file mode 100644 index 00000000..4b03a20c --- /dev/null +++ b/tests/dbal-4-bootstrap.php @@ -0,0 +1,8 @@ + Date: Fri, 9 Feb 2024 11:40:03 +0100 Subject: [PATCH 12/20] Patch Carbon return types --- .github/workflows/build.yml | 1 + compatibility/patches/DateTimeImmutableType.patch | 11 +++++++++++ compatibility/patches/DateTimeType.patch | 11 +++++++++++ 3 files changed, 23 insertions(+) create mode 100644 compatibility/patches/DateTimeImmutableType.patch create mode 100644 compatibility/patches/DateTimeType.patch diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index d4162bd3..66ab5b7f 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -113,6 +113,7 @@ jobs: dependencies: "highest" update-packages: | composer config extra.patches.doctrine/orm --json --merge '["compatibility/patches/Column.patch", "compatibility/patches/DiscriminatorColumn.patch", "compatibility/patches/DiscriminatorMap.patch", "compatibility/patches/Embeddable.patch", "compatibility/patches/Embedded.patch", "compatibility/patches/Entity.patch", "compatibility/patches/GeneratedValue.patch", "compatibility/patches/Id.patch", "compatibility/patches/InheritanceType.patch", "compatibility/patches/JoinColumn.patch", "compatibility/patches/JoinColumns.patch", "compatibility/patches/ManyToMany.patch", "compatibility/patches/ManyToOne.patch", "compatibility/patches/MappedSuperclass.patch", "compatibility/patches/OneToMany.patch", "compatibility/patches/OneToOne.patch", "compatibility/patches/OrderBy.patch", "compatibility/patches/UniqueConstraint.patch", "compatibility/patches/Version.patch"]' + composer config extra.patches.carbonphp/carbon-doctrine-types --json --merge '["compatibility/patches/DateTimeImmutableType.patch", "compatibility/patches/DateTimeType.patch"]' composer require --dev doctrine/orm:^3.0 doctrine/dbal:^4.0 carbonphp/carbon-doctrine-types:^3 -W steps: diff --git a/compatibility/patches/DateTimeImmutableType.patch b/compatibility/patches/DateTimeImmutableType.patch new file mode 100644 index 00000000..e8525247 --- /dev/null +++ b/compatibility/patches/DateTimeImmutableType.patch @@ -0,0 +1,11 @@ +--- src/Carbon/Doctrine/DateTimeImmutableType.php 2023-12-10 16:33:53 ++++ src/Carbon/Doctrine/DateTimeImmutableType.php 2024-02-09 11:36:50 +@@ -17,7 +17,7 @@ + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ +- public function convertToPHPValue(mixed $value, AbstractPlatform $platform): ?DateTimeImmutable ++ public function convertToPHPValue(mixed $value, AbstractPlatform $platform): ?CarbonImmutable + { + return $this->doConvertToPHPValue($value); + } diff --git a/compatibility/patches/DateTimeType.patch b/compatibility/patches/DateTimeType.patch new file mode 100644 index 00000000..0a36920f --- /dev/null +++ b/compatibility/patches/DateTimeType.patch @@ -0,0 +1,11 @@ +--- src/Carbon/Doctrine/DateTimeType.php 2023-12-10 16:33:53 ++++ src/Carbon/Doctrine/DateTimeType.php 2024-02-09 11:36:58 +@@ -17,7 +17,7 @@ + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ +- public function convertToPHPValue(mixed $value, AbstractPlatform $platform): ?DateTime ++ public function convertToPHPValue(mixed $value, AbstractPlatform $platform): ?Carbon + { + return $this->doConvertToPHPValue($value); + } From fdfc7e61b4080321f492ceb9095e0c2be93dab5c Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Fri, 9 Feb 2024 11:44:56 +0100 Subject: [PATCH 13/20] Fix tests for DBAL 4 --- .../ORM/EntityManagerTypeInferenceTest.php | 6 +++++- ...tyManagerWithoutObjectManagerLoaderTypeInferenceTest.php | 6 +++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/tests/DoctrineIntegration/ORM/EntityManagerTypeInferenceTest.php b/tests/DoctrineIntegration/ORM/EntityManagerTypeInferenceTest.php index efef0c5d..ad6c0b9f 100644 --- a/tests/DoctrineIntegration/ORM/EntityManagerTypeInferenceTest.php +++ b/tests/DoctrineIntegration/ORM/EntityManagerTypeInferenceTest.php @@ -21,8 +21,12 @@ public function dataFileAsserts(): iterable $version = InstalledVersions::getVersion('doctrine/dbal'); $hasDbal3 = $version !== null && strpos($version, '3.') === 0; + $hasDbal4 = $version !== null && strpos($version, '4.') === 0; - if ($hasDbal3) { + if ($hasDbal4) { + // nothing to test + yield from []; + } elseif ($hasDbal3) { yield from $this->gatherAssertTypes(__DIR__ . '/data/dbalQueryBuilderExecuteDynamicReturnDbal3.php'); } else { yield from $this->gatherAssertTypes(__DIR__ . '/data/dbalQueryBuilderExecuteDynamicReturn.php'); diff --git a/tests/DoctrineIntegration/ORM/EntityManagerWithoutObjectManagerLoaderTypeInferenceTest.php b/tests/DoctrineIntegration/ORM/EntityManagerWithoutObjectManagerLoaderTypeInferenceTest.php index cd39405d..c91f7fb1 100644 --- a/tests/DoctrineIntegration/ORM/EntityManagerWithoutObjectManagerLoaderTypeInferenceTest.php +++ b/tests/DoctrineIntegration/ORM/EntityManagerWithoutObjectManagerLoaderTypeInferenceTest.php @@ -20,8 +20,12 @@ public function dataFileAsserts(): iterable $version = InstalledVersions::getVersion('doctrine/dbal'); $hasDbal3 = $version !== null && strpos($version, '3.') === 0; + $hasDbal4 = $version !== null && strpos($version, '4.') === 0; - if ($hasDbal3) { + if ($hasDbal4) { + // nothing to test + yield from []; + } elseif ($hasDbal3) { yield from $this->gatherAssertTypes(__DIR__ . '/data/dbalQueryBuilderExecuteDynamicReturnDbal3.php'); } else { yield from $this->gatherAssertTypes(__DIR__ . '/data/dbalQueryBuilderExecuteDynamicReturn.php'); From 5dd48cffeb8690de1a1d4465ccf92ece2673d5a7 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Fri, 9 Feb 2024 11:53:38 +0100 Subject: [PATCH 14/20] Fix tests for ORM 3 --- .../ORM/EntityManagerTypeInferenceTest.php | 14 +++-- ...utObjectManagerLoaderTypeInferenceTest.php | 8 ++- .../ORM/data/entityManager-orm2.php | 54 +++++++++++++++++++ .../ORM/data/entityManagerDynamicReturn.php | 14 ----- 4 files changed, 71 insertions(+), 19 deletions(-) create mode 100644 tests/DoctrineIntegration/ORM/data/entityManager-orm2.php diff --git a/tests/DoctrineIntegration/ORM/EntityManagerTypeInferenceTest.php b/tests/DoctrineIntegration/ORM/EntityManagerTypeInferenceTest.php index ad6c0b9f..086a6e0e 100644 --- a/tests/DoctrineIntegration/ORM/EntityManagerTypeInferenceTest.php +++ b/tests/DoctrineIntegration/ORM/EntityManagerTypeInferenceTest.php @@ -14,14 +14,20 @@ class EntityManagerTypeInferenceTest extends TypeInferenceTestCase */ public function dataFileAsserts(): iterable { + $ormVersion = InstalledVersions::getVersion('doctrine/orm'); + $hasOrm2 = $ormVersion !== null && strpos($ormVersion, '2.') === 0; + if ($hasOrm2) { + yield from $this->gatherAssertTypes(__DIR__ . '/data/entityManager-orm2.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/entityManagerMergeReturn.php'); + } yield from $this->gatherAssertTypes(__DIR__ . '/data/entityManagerDynamicReturn.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/entityManagerMergeReturn.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/customRepositoryUsage.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/queryBuilder.php'); - $version = InstalledVersions::getVersion('doctrine/dbal'); - $hasDbal3 = $version !== null && strpos($version, '3.') === 0; - $hasDbal4 = $version !== null && strpos($version, '4.') === 0; + $dbalVersion = InstalledVersions::getVersion('doctrine/dbal'); + $hasDbal3 = $dbalVersion !== null && strpos($dbalVersion, '3.') === 0; + $hasDbal4 = $dbalVersion !== null && strpos($dbalVersion, '4.') === 0; if ($hasDbal4) { // nothing to test diff --git a/tests/DoctrineIntegration/ORM/EntityManagerWithoutObjectManagerLoaderTypeInferenceTest.php b/tests/DoctrineIntegration/ORM/EntityManagerWithoutObjectManagerLoaderTypeInferenceTest.php index c91f7fb1..25308635 100644 --- a/tests/DoctrineIntegration/ORM/EntityManagerWithoutObjectManagerLoaderTypeInferenceTest.php +++ b/tests/DoctrineIntegration/ORM/EntityManagerWithoutObjectManagerLoaderTypeInferenceTest.php @@ -14,8 +14,14 @@ class EntityManagerWithoutObjectManagerLoaderTypeInferenceTest extends TypeInfer */ public function dataFileAsserts(): iterable { + $ormVersion = InstalledVersions::getVersion('doctrine/orm'); + $hasOrm2 = $ormVersion !== null && strpos($ormVersion, '2.') === 0; + if ($hasOrm2) { + yield from $this->gatherAssertTypes(__DIR__ . '/data/entityManager-orm2.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/entityManagerMergeReturn.php'); + } + yield from $this->gatherAssertTypes(__DIR__ . '/data/entityManagerDynamicReturn.php'); - yield from $this->gatherAssertTypes(__DIR__ . '/data/entityManagerMergeReturn.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/customRepositoryUsage.php'); $version = InstalledVersions::getVersion('doctrine/dbal'); diff --git a/tests/DoctrineIntegration/ORM/data/entityManager-orm2.php b/tests/DoctrineIntegration/ORM/data/entityManager-orm2.php new file mode 100644 index 00000000..4fc3f60e --- /dev/null +++ b/tests/DoctrineIntegration/ORM/data/entityManager-orm2.php @@ -0,0 +1,54 @@ +entityManager = $entityManager; + } + + public function getPartialReferenceDynamicType(): void + { + $test = $this->entityManager->getPartialReference(MyEntity::class, 1); + + if ($test === null) { + throw new RuntimeException('Sorry, but no...'); + } + + assertType(MyEntity::class, $test); + + $test->doSomething(); + $test->doSomethingElse(); + } +} + +/** + * @ORM\Entity() + */ +class MyEntity +{ + /** + * @ORM\Id() + * @ORM\GeneratedValue() + * @ORM\Column(type="integer") + * + * @var int + */ + private $id; + + public function doSomething(): void + { + } +} diff --git a/tests/DoctrineIntegration/ORM/data/entityManagerDynamicReturn.php b/tests/DoctrineIntegration/ORM/data/entityManagerDynamicReturn.php index 5eb754ab..6882b535 100644 --- a/tests/DoctrineIntegration/ORM/data/entityManagerDynamicReturn.php +++ b/tests/DoctrineIntegration/ORM/data/entityManagerDynamicReturn.php @@ -45,20 +45,6 @@ public function getReferenceDynamicType(): void $test->doSomethingElse(); } - public function getPartialReferenceDynamicType(): void - { - $test = $this->entityManager->getPartialReference(MyEntity::class, 1); - - if ($test === null) { - throw new RuntimeException('Sorry, but no...'); - } - - assertType(MyEntity::class, $test); - - $test->doSomething(); - $test->doSomethingElse(); - } - /** * @param class-string $entityName */ From 7cfb25556aef4f83bce187ef613fd36812d0d56a Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Fri, 9 Feb 2024 13:05:46 +0100 Subject: [PATCH 15/20] Skip tests not working on ORM 3 Related: https://github.com/doctrine/orm/issues/11240 --- .../Query/QueryResultTypeWalkerTest.php | 41 +++++++++++-------- 1 file changed, 23 insertions(+), 18 deletions(-) diff --git a/tests/Type/Doctrine/Query/QueryResultTypeWalkerTest.php b/tests/Type/Doctrine/Query/QueryResultTypeWalkerTest.php index 1393a9bc..6ab3ee8c 100644 --- a/tests/Type/Doctrine/Query/QueryResultTypeWalkerTest.php +++ b/tests/Type/Doctrine/Query/QueryResultTypeWalkerTest.php @@ -49,6 +49,7 @@ use function count; use function property_exists; use function sprintf; +use function strpos; use function version_compare; use const PHP_VERSION_ID; @@ -1176,37 +1177,41 @@ public function getTestData(): iterable ', ]; - yield 'date_add function' => [ - $this->constantArray([ - [new ConstantIntegerType(1), new StringType()], - [new ConstantIntegerType(2), TypeCombinator::addNull(new StringType())], - [new ConstantIntegerType(3), TypeCombinator::addNull(new StringType())], - [new ConstantIntegerType(4), new StringType()], - ]), - ' + $ormVersion = InstalledVersions::getVersion('doctrine/orm'); + $hasOrm3 = strpos($ormVersion, '3.0') === 0; + if (!$hasOrm3) { + yield 'date_add function' => [ + $this->constantArray([ + [new ConstantIntegerType(1), new StringType()], + [new ConstantIntegerType(2), TypeCombinator::addNull(new StringType())], + [new ConstantIntegerType(3), TypeCombinator::addNull(new StringType())], + [new ConstantIntegerType(4), new StringType()], + ]), + ' SELECT DATE_ADD(m.datetimeColumn, m.intColumn, \'day\'), DATE_ADD(m.stringNullColumn, m.intColumn, \'day\'), DATE_ADD(m.datetimeColumn, NULLIF(m.intColumn, 1), \'day\'), DATE_ADD(\'2020-01-01\', 7, \'day\') FROM QueryResult\Entities\Many m ', - ]; + ]; - yield 'date_sub function' => [ - $this->constantArray([ - [new ConstantIntegerType(1), new StringType()], - [new ConstantIntegerType(2), TypeCombinator::addNull(new StringType())], - [new ConstantIntegerType(3), TypeCombinator::addNull(new StringType())], - [new ConstantIntegerType(4), new StringType()], - ]), - ' + yield 'date_sub function' => [ + $this->constantArray([ + [new ConstantIntegerType(1), new StringType()], + [new ConstantIntegerType(2), TypeCombinator::addNull(new StringType())], + [new ConstantIntegerType(3), TypeCombinator::addNull(new StringType())], + [new ConstantIntegerType(4), new StringType()], + ]), + ' SELECT DATE_SUB(m.datetimeColumn, m.intColumn, \'day\'), DATE_SUB(m.stringNullColumn, m.intColumn, \'day\'), DATE_SUB(m.datetimeColumn, NULLIF(m.intColumn, 1), \'day\'), DATE_SUB(\'2020-01-01\', 7, \'day\') FROM QueryResult\Entities\Many m ', - ]; + ]; + } yield 'date_diff function' => [ $this->constantArray([ From 1509192e0e2b742105716a050f2ca9bd63f06ec5 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Fri, 9 Feb 2024 13:47:21 +0100 Subject: [PATCH 16/20] Fix more tests on ORM 3 --- .../Doctrine/Query/QueryResultTypeWalkerTest.php | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/tests/Type/Doctrine/Query/QueryResultTypeWalkerTest.php b/tests/Type/Doctrine/Query/QueryResultTypeWalkerTest.php index 6ab3ee8c..5b49bff8 100644 --- a/tests/Type/Doctrine/Query/QueryResultTypeWalkerTest.php +++ b/tests/Type/Doctrine/Query/QueryResultTypeWalkerTest.php @@ -239,6 +239,12 @@ public function test(Type $expectedType, string $dql, ?string $expectedException */ public function getTestData(): iterable { + $ormVersion = InstalledVersions::getVersion('doctrine/orm'); + $hasOrm3 = strpos($ormVersion, '3.') === 0; + + $dbalVersion = InstalledVersions::getVersion('doctrine/dbal'); + $hasDbal4 = strpos($dbalVersion, '4.') === 0; + yield 'just root entity' => [ new ObjectType(One::class), ' @@ -355,7 +361,7 @@ public function getTestData(): iterable ]), $this->constantArray([ [new ConstantIntegerType(0), new ObjectType(One::class)], - [new ConstantStringType('id'), $this->numericString()], + [new ConstantStringType('id'), $hasDbal4 ? new IntegerType() : $this->numericString()], [new ConstantStringType('intColumn'), new IntegerType()], ]) ), @@ -377,7 +383,7 @@ public function getTestData(): iterable ]), $this->constantArray([ [new ConstantIntegerType(0), new ObjectType(Many::class)], - [new ConstantStringType('id'), $this->numericString()], + [new ConstantStringType('id'), $hasDbal4 ? new IntegerType() : $this->numericString()], [new ConstantStringType('intColumn'), new IntegerType()], ]) ), @@ -398,7 +404,7 @@ public function getTestData(): iterable ]), $this->constantArray([ [new ConstantStringType('one'), new ObjectType(One::class)], - [new ConstantStringType('id'), $this->numericString()], + [new ConstantStringType('id'), $hasDbal4 ? new IntegerType() : $this->numericString()], [new ConstantStringType('intColumn'), new IntegerType()], ]) ), @@ -508,7 +514,7 @@ public function getTestData(): iterable yield 'just root entity and scalars' => [ $this->constantArray([ [new ConstantIntegerType(0), new ObjectType(One::class)], - [new ConstantStringType('id'), $this->numericString()], + [new ConstantStringType('id'), $hasDbal4 ? new IntegerType() : $this->numericString()], ]), ' SELECT o, o.id @@ -1177,8 +1183,6 @@ public function getTestData(): iterable ', ]; - $ormVersion = InstalledVersions::getVersion('doctrine/orm'); - $hasOrm3 = strpos($ormVersion, '3.0') === 0; if (!$hasOrm3) { yield 'date_add function' => [ $this->constantArray([ From 8be5132968dfbe2cc0264237a5d52ca1822cbbfb Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Fri, 9 Feb 2024 13:55:20 +0100 Subject: [PATCH 17/20] BigIntType - always uses integer in DBAL 4 --- src/Type/Doctrine/Descriptors/BigIntType.php | 18 ++++++++ .../Doctrine/ORM/EntityColumnRuleTest.php | 43 +++++++++++++------ 2 files changed, 49 insertions(+), 12 deletions(-) diff --git a/src/Type/Doctrine/Descriptors/BigIntType.php b/src/Type/Doctrine/Descriptors/BigIntType.php index 213bf17b..f74eea95 100644 --- a/src/Type/Doctrine/Descriptors/BigIntType.php +++ b/src/Type/Doctrine/Descriptors/BigIntType.php @@ -2,11 +2,14 @@ namespace PHPStan\Type\Doctrine\Descriptors; +use Composer\InstalledVersions; use PHPStan\Type\Accessory\AccessoryNumericStringType; use PHPStan\Type\IntegerType; use PHPStan\Type\StringType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; +use function class_exists; +use function strpos; class BigIntType implements DoctrineTypeDescriptor { @@ -18,6 +21,10 @@ public function getType(): string public function getWritableToPropertyType(): Type { + if ($this->hasDbal4()) { + return new IntegerType(); + } + return TypeCombinator::intersect(new StringType(), new AccessoryNumericStringType()); } @@ -31,4 +38,15 @@ public function getDatabaseInternalType(): Type return new IntegerType(); } + private function hasDbal4(): bool + { + if (!class_exists(InstalledVersions::class)) { + return false; + } + + $dbalVersion = InstalledVersions::getVersion('doctrine/dbal'); + + return strpos($dbalVersion, '4.') === 0; + } + } diff --git a/tests/Rules/Doctrine/ORM/EntityColumnRuleTest.php b/tests/Rules/Doctrine/ORM/EntityColumnRuleTest.php index cbb8dbe5..b7648ae1 100644 --- a/tests/Rules/Doctrine/ORM/EntityColumnRuleTest.php +++ b/tests/Rules/Doctrine/ORM/EntityColumnRuleTest.php @@ -4,6 +4,7 @@ use Carbon\Doctrine\CarbonImmutableType; use Carbon\Doctrine\CarbonType; +use Composer\InstalledVersions; use Doctrine\DBAL\Types\Type; use Iterator; use PHPStan\Rules\Rule; @@ -23,6 +24,8 @@ use PHPStan\Type\Doctrine\Descriptors\SimpleArrayType; use PHPStan\Type\Doctrine\Descriptors\StringType; use PHPStan\Type\Doctrine\ObjectMetadataResolver; +use function array_unshift; +use function strpos; use const PHP_VERSION_ID; /** @@ -103,11 +106,8 @@ public function testRule(?string $objectManagerLoader): void { $this->allowNullablePropertyForRequiredField = false; $this->objectManagerLoader = $objectManagerLoader; - $this->analyse([__DIR__ . '/data/MyBrokenEntity.php'], [ - [ - 'Property PHPStan\Rules\Doctrine\ORM\MyBrokenEntity::$id type mapping mismatch: database can contain string but property expects int|null.', - 19, - ], + + $errors = [ [ 'Property PHPStan\Rules\Doctrine\ORM\MyBrokenEntity::$one type mapping mismatch: database can contain string|null but property expects string.', 25, @@ -168,7 +168,18 @@ public function testRule(?string $objectManagerLoader): void 'Property PHPStan\Rules\Doctrine\ORM\MyBrokenEntity::$invalidSimpleArray type mapping mismatch: property can contain array but database expects array.', 162, ], - ]); + ]; + + $dbalVersion = InstalledVersions::getVersion('doctrine/dbal'); + $hasDbal4 = strpos($dbalVersion, '4.') === 0; + if (!$hasDbal4) { + array_unshift($errors, [ + 'Property PHPStan\Rules\Doctrine\ORM\MyBrokenEntity::$id type mapping mismatch: database can contain string but property expects int|null.', + 19, + ]); + } + + $this->analyse([__DIR__ . '/data/MyBrokenEntity.php'], $errors); } /** @@ -178,11 +189,8 @@ public function testRuleWithAllowedNullableProperty(?string $objectManagerLoader { $this->allowNullablePropertyForRequiredField = true; $this->objectManagerLoader = $objectManagerLoader; - $this->analyse([__DIR__ . '/data/MyBrokenEntity.php'], [ - [ - 'Property PHPStan\Rules\Doctrine\ORM\MyBrokenEntity::$id type mapping mismatch: database can contain string but property expects int|null.', - 19, - ], + + $errors = [ [ 'Property PHPStan\Rules\Doctrine\ORM\MyBrokenEntity::$one type mapping mismatch: database can contain string|null but property expects string.', 25, @@ -231,7 +239,18 @@ public function testRuleWithAllowedNullableProperty(?string $objectManagerLoader 'Property PHPStan\Rules\Doctrine\ORM\MyBrokenEntity::$invalidSimpleArray type mapping mismatch: property can contain array but database expects array.', 162, ], - ]); + ]; + + $dbalVersion = InstalledVersions::getVersion('doctrine/dbal'); + $hasDbal4 = strpos($dbalVersion, '4.') === 0; + if (!$hasDbal4) { + array_unshift($errors, [ + 'Property PHPStan\Rules\Doctrine\ORM\MyBrokenEntity::$id type mapping mismatch: database can contain string but property expects int|null.', + 19, + ]); + } + + $this->analyse([__DIR__ . '/data/MyBrokenEntity.php'], $errors); } /** From 362f4784f4787f300f14df1592a791df71922f8b Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Fri, 9 Feb 2024 14:17:51 +0100 Subject: [PATCH 18/20] EntityColumnRuleTest - testGeneratedIds with less complicated type than bigint --- tests/Rules/Doctrine/ORM/EntityColumnRuleTest.php | 4 ++-- tests/Rules/Doctrine/ORM/data/CompositePrimaryKeyEntity1.php | 4 ++-- tests/Rules/Doctrine/ORM/data/GeneratedIdEntity1.php | 4 ++-- tests/Rules/Doctrine/ORM/data/GeneratedIdEntity2.php | 4 ++-- tests/Rules/Doctrine/ORM/data/GeneratedIdEntity3.php | 4 ++-- tests/Rules/Doctrine/ORM/data/GeneratedIdEntity4.php | 4 ++-- 6 files changed, 12 insertions(+), 12 deletions(-) diff --git a/tests/Rules/Doctrine/ORM/EntityColumnRuleTest.php b/tests/Rules/Doctrine/ORM/EntityColumnRuleTest.php index b7648ae1..36308989 100644 --- a/tests/Rules/Doctrine/ORM/EntityColumnRuleTest.php +++ b/tests/Rules/Doctrine/ORM/EntityColumnRuleTest.php @@ -300,7 +300,7 @@ public function generatedIdsProvider(): Iterator __DIR__ . '/data/GeneratedIdEntity2.php', [ [ - 'Property PHPStan\Rules\Doctrine\ORM\GeneratedIdEntity2::$id type mapping mismatch: database can contain string|null but property expects string.', + 'Property PHPStan\Rules\Doctrine\ORM\GeneratedIdEntity2::$id type mapping mismatch: database can contain int|null but property expects int.', 19, ], ], @@ -310,7 +310,7 @@ public function generatedIdsProvider(): Iterator __DIR__ . '/data/GeneratedIdEntity2.php', [ [ - 'Property PHPStan\Rules\Doctrine\ORM\GeneratedIdEntity2::$id type mapping mismatch: database can contain string|null but property expects string.', + 'Property PHPStan\Rules\Doctrine\ORM\GeneratedIdEntity2::$id type mapping mismatch: database can contain int|null but property expects int.', 19, ], ], diff --git a/tests/Rules/Doctrine/ORM/data/CompositePrimaryKeyEntity1.php b/tests/Rules/Doctrine/ORM/data/CompositePrimaryKeyEntity1.php index 75c33fc5..d9f69ae4 100644 --- a/tests/Rules/Doctrine/ORM/data/CompositePrimaryKeyEntity1.php +++ b/tests/Rules/Doctrine/ORM/data/CompositePrimaryKeyEntity1.php @@ -13,8 +13,8 @@ class CompositePrimaryKeyEntity1 /** * @ORM\Id() * @ORM\GeneratedValue() - * @ORM\Column(type="bigint", nullable=true) - * @var string + * @ORM\Column(type="integer", nullable=true) + * @var int */ private $id; diff --git a/tests/Rules/Doctrine/ORM/data/GeneratedIdEntity1.php b/tests/Rules/Doctrine/ORM/data/GeneratedIdEntity1.php index 28006ef5..546102ae 100644 --- a/tests/Rules/Doctrine/ORM/data/GeneratedIdEntity1.php +++ b/tests/Rules/Doctrine/ORM/data/GeneratedIdEntity1.php @@ -13,8 +13,8 @@ class GeneratedIdEntity1 /** * @ORM\Id() * @ORM\GeneratedValue() - * @ORM\Column(type="bigint") - * @var string + * @ORM\Column(type="integer") + * @var int */ private $id; diff --git a/tests/Rules/Doctrine/ORM/data/GeneratedIdEntity2.php b/tests/Rules/Doctrine/ORM/data/GeneratedIdEntity2.php index b3fc916f..5a2bf197 100644 --- a/tests/Rules/Doctrine/ORM/data/GeneratedIdEntity2.php +++ b/tests/Rules/Doctrine/ORM/data/GeneratedIdEntity2.php @@ -13,8 +13,8 @@ class GeneratedIdEntity2 /** * @ORM\Id() * @ORM\GeneratedValue() - * @ORM\Column(type="bigint", nullable=true) - * @var string + * @ORM\Column(type="integer", nullable=true) + * @var int */ private $id; diff --git a/tests/Rules/Doctrine/ORM/data/GeneratedIdEntity3.php b/tests/Rules/Doctrine/ORM/data/GeneratedIdEntity3.php index b97dba63..cf80ce02 100644 --- a/tests/Rules/Doctrine/ORM/data/GeneratedIdEntity3.php +++ b/tests/Rules/Doctrine/ORM/data/GeneratedIdEntity3.php @@ -13,8 +13,8 @@ class GeneratedIdEntity3 /** * @ORM\Id() * @ORM\GeneratedValue() - * @ORM\Column(type="bigint") - * @var string|null + * @ORM\Column(type="integer") + * @var int|null */ private $id; diff --git a/tests/Rules/Doctrine/ORM/data/GeneratedIdEntity4.php b/tests/Rules/Doctrine/ORM/data/GeneratedIdEntity4.php index 6575c9b7..d9299c88 100644 --- a/tests/Rules/Doctrine/ORM/data/GeneratedIdEntity4.php +++ b/tests/Rules/Doctrine/ORM/data/GeneratedIdEntity4.php @@ -13,8 +13,8 @@ class GeneratedIdEntity4 /** * @ORM\Id() * @ORM\GeneratedValue() - * @ORM\Column(type="bigint", nullable=true) - * @var string|null + * @ORM\Column(type="integer", nullable=true) + * @var int|null */ private $id; From d47e1194910ebc88b23b8b4f97e9c1ce20915ab7 Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Fri, 9 Feb 2024 14:24:18 +0100 Subject: [PATCH 19/20] More patches for Doctrine bugs Related: https://github.com/doctrine/orm/issues/11240 Related: https://github.com/doctrine/orm/issues/11241 --- .github/workflows/build.yml | 2 +- compatibility/patches/Base.patch | 13 +++++++++++++ compatibility/patches/DateAddFunction.patch | 10 ++++++++++ compatibility/patches/DateSubFunction.patch | 10 ++++++++++ 4 files changed, 34 insertions(+), 1 deletion(-) create mode 100644 compatibility/patches/Base.patch create mode 100644 compatibility/patches/DateAddFunction.patch create mode 100644 compatibility/patches/DateSubFunction.patch diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 66ab5b7f..6dc4a1e9 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -112,7 +112,7 @@ jobs: - php-version: "8.3" dependencies: "highest" update-packages: | - composer config extra.patches.doctrine/orm --json --merge '["compatibility/patches/Column.patch", "compatibility/patches/DiscriminatorColumn.patch", "compatibility/patches/DiscriminatorMap.patch", "compatibility/patches/Embeddable.patch", "compatibility/patches/Embedded.patch", "compatibility/patches/Entity.patch", "compatibility/patches/GeneratedValue.patch", "compatibility/patches/Id.patch", "compatibility/patches/InheritanceType.patch", "compatibility/patches/JoinColumn.patch", "compatibility/patches/JoinColumns.patch", "compatibility/patches/ManyToMany.patch", "compatibility/patches/ManyToOne.patch", "compatibility/patches/MappedSuperclass.patch", "compatibility/patches/OneToMany.patch", "compatibility/patches/OneToOne.patch", "compatibility/patches/OrderBy.patch", "compatibility/patches/UniqueConstraint.patch", "compatibility/patches/Version.patch"]' + composer config extra.patches.doctrine/orm --json --merge '["compatibility/patches/Base.patch", "compatibility/patches/Column.patch", "compatibility/patches/DateAddFunction.patch", "compatibility/patches/DateSubFunction.patch", "compatibility/patches/DiscriminatorColumn.patch", "compatibility/patches/DiscriminatorMap.patch", "compatibility/patches/Embeddable.patch", "compatibility/patches/Embedded.patch", "compatibility/patches/Entity.patch", "compatibility/patches/GeneratedValue.patch", "compatibility/patches/Id.patch", "compatibility/patches/InheritanceType.patch", "compatibility/patches/JoinColumn.patch", "compatibility/patches/JoinColumns.patch", "compatibility/patches/ManyToMany.patch", "compatibility/patches/ManyToOne.patch", "compatibility/patches/MappedSuperclass.patch", "compatibility/patches/OneToMany.patch", "compatibility/patches/OneToOne.patch", "compatibility/patches/OrderBy.patch", "compatibility/patches/UniqueConstraint.patch", "compatibility/patches/Version.patch"]' composer config extra.patches.carbonphp/carbon-doctrine-types --json --merge '["compatibility/patches/DateTimeImmutableType.patch", "compatibility/patches/DateTimeType.patch"]' composer require --dev doctrine/orm:^3.0 doctrine/dbal:^4.0 carbonphp/carbon-doctrine-types:^3 -W diff --git a/compatibility/patches/Base.patch b/compatibility/patches/Base.patch new file mode 100644 index 00000000..9a5f2ace --- /dev/null +++ b/compatibility/patches/Base.patch @@ -0,0 +1,13 @@ +--- src/Query/Expr/Base.php 2024-02-09 14:21:17 ++++ src/Query/Expr/Base.php 2024-02-09 14:21:24 +@@ -33,6 +33,10 @@ + + public function __construct(mixed $args = []) + { ++ if (is_array($args) && array_key_exists(0, $args) && is_array($args[0])) { ++ $args = $args[0]; ++ } ++ + $this->addMultiple($args); + } + diff --git a/compatibility/patches/DateAddFunction.patch b/compatibility/patches/DateAddFunction.patch new file mode 100644 index 00000000..79a7606a --- /dev/null +++ b/compatibility/patches/DateAddFunction.patch @@ -0,0 +1,10 @@ +--- src/Query/AST/Functions/DateAddFunction.php 2024-02-09 14:22:59 ++++ src/Query/AST/Functions/DateAddFunction.php 2024-02-09 14:23:02 +@@ -71,7 +71,6 @@ + private function dispatchIntervalExpression(SqlWalker $sqlWalker): string + { + $sql = $this->intervalExpression->dispatch($sqlWalker); +- assert(is_numeric($sql)); + + return $sql; + } diff --git a/compatibility/patches/DateSubFunction.patch b/compatibility/patches/DateSubFunction.patch new file mode 100644 index 00000000..12a8fcf3 --- /dev/null +++ b/compatibility/patches/DateSubFunction.patch @@ -0,0 +1,10 @@ +--- src/Query/AST/Functions/DateSubFunction.php 2024-02-09 14:22:31 ++++ src/Query/AST/Functions/DateSubFunction.php 2024-02-09 14:22:50 +@@ -64,7 +64,6 @@ + private function dispatchIntervalExpression(SqlWalker $sqlWalker): string + { + $sql = $this->intervalExpression->dispatch($sqlWalker); +- assert(is_numeric($sql)); + + return $sql; + } From 453e6fa1e69ba52bbb94f31c3ffb2316d145d67d Mon Sep 17 00:00:00 2001 From: Ondrej Mirtes Date: Fri, 9 Feb 2024 14:30:24 +0100 Subject: [PATCH 20/20] Fix PHPStan --- compatibility/orm-3-baseline.php | 16 ++++++ phpstan-baseline-orm-3.neon | 56 +++++++++++++++++++ phpstan.neon | 1 + src/Type/Doctrine/Descriptors/BigIntType.php | 3 + .../Doctrine/Query/QueryResultTypeWalker.php | 3 +- tests/Rules/Doctrine/ORM/DqlRuleTest.php | 2 +- .../Doctrine/ORM/EntityColumnRuleTest.php | 4 +- .../Doctrine/ORM/FakeTestingUuidType.php | 26 +-------- .../Query/QueryResultTypeWalkerTest.php | 4 +- 9 files changed, 86 insertions(+), 29 deletions(-) create mode 100644 compatibility/orm-3-baseline.php create mode 100644 phpstan-baseline-orm-3.neon diff --git a/compatibility/orm-3-baseline.php b/compatibility/orm-3-baseline.php new file mode 100644 index 00000000..69f1c589 --- /dev/null +++ b/compatibility/orm-3-baseline.php @@ -0,0 +1,16 @@ + but returns string\\.$#" + count: 1 + path: src/Type/Doctrine/Descriptors/ArrayType.php + + - + message: "#^Class Doctrine\\\\DBAL\\\\Types\\\\ObjectType not found\\.$#" + count: 1 + path: src/Type/Doctrine/Descriptors/ObjectType.php + + - + message: "#^Method PHPStan\\\\Type\\\\Doctrine\\\\Descriptors\\\\ObjectType\\:\\:getType\\(\\) should return class\\-string\\ but returns string\\.$#" + count: 1 + path: src/Type/Doctrine/Descriptors/ObjectType.php + + - + message: "#^Only booleans are allowed in a negated boolean, mixed given\\.$#" + count: 1 + path: src/Type/Doctrine/Query/QueryResultTypeWalker.php + + - + message: "#^Only booleans are allowed in \\|\\|, mixed given on the left side\\.$#" + count: 2 + path: src/Type/Doctrine/Query/QueryResultTypeWalker.php + + - + message: "#^Caught class Doctrine\\\\ORM\\\\ORMException not found\\.$#" + count: 1 + path: src/Type/Doctrine/QueryBuilder/QueryBuilderGetQueryDynamicReturnTypeExtension.php + + - + message: "#^Class Doctrine\\\\DBAL\\\\Types\\\\ArrayType not found\\.$#" + count: 1 + path: tests/Rules/Doctrine/ORM/EntityColumnRuleTest.php + + - + message: "#^Parameter \\#2 \\$className of static method Doctrine\\\\DBAL\\\\Types\\\\Type\\:\\:addType\\(\\) expects class\\-string\\, string given\\.$#" + count: 1 + path: tests/Rules/Doctrine/ORM/EntityColumnRuleTest.php diff --git a/phpstan.neon b/phpstan.neon index aabd4939..b3c5d642 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -3,6 +3,7 @@ includes: - rules.neon - phpstan-baseline.neon - phpstan-baseline-dbal-3.neon + - compatibility/orm-3-baseline.php - vendor/phpstan/phpstan-strict-rules/rules.neon - vendor/phpstan/phpstan-phpunit/extension.neon - vendor/phpstan/phpstan-phpunit/rules.neon diff --git a/src/Type/Doctrine/Descriptors/BigIntType.php b/src/Type/Doctrine/Descriptors/BigIntType.php index f74eea95..14b3ca2a 100644 --- a/src/Type/Doctrine/Descriptors/BigIntType.php +++ b/src/Type/Doctrine/Descriptors/BigIntType.php @@ -45,6 +45,9 @@ private function hasDbal4(): bool } $dbalVersion = InstalledVersions::getVersion('doctrine/dbal'); + if ($dbalVersion === null) { + return false; + } return strpos($dbalVersion, '4.') === 0; } diff --git a/src/Type/Doctrine/Query/QueryResultTypeWalker.php b/src/Type/Doctrine/Query/QueryResultTypeWalker.php index b4b5ddf5..c186a5e3 100644 --- a/src/Type/Doctrine/Query/QueryResultTypeWalker.php +++ b/src/Type/Doctrine/Query/QueryResultTypeWalker.php @@ -3,6 +3,7 @@ namespace PHPStan\Type\Doctrine\Query; use BackedEnum; +use Doctrine\DBAL\Types\Types; use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\Mapping\ClassMetadata; use Doctrine\ORM\Query; @@ -815,7 +816,7 @@ public function walkSelectExpression($selectExpression): string $type = $this->unmarshalType($expr->dispatch($this)); if (class_exists(TypedExpression::class) && $expr instanceof TypedExpression) { - $enforcedType = $this->resolveDoctrineType($expr->getReturnType()->getName()); + $enforcedType = $this->resolveDoctrineType(Types::INTEGER); $type = TypeTraverser::map($type, static function (Type $type, callable $traverse) use ($enforcedType): Type { if ($type instanceof UnionType || $type instanceof IntersectionType) { return $traverse($type); diff --git a/tests/Rules/Doctrine/ORM/DqlRuleTest.php b/tests/Rules/Doctrine/ORM/DqlRuleTest.php index c601fe54..007ed40b 100644 --- a/tests/Rules/Doctrine/ORM/DqlRuleTest.php +++ b/tests/Rules/Doctrine/ORM/DqlRuleTest.php @@ -23,7 +23,7 @@ protected function getRule(): Rule public function testRule(): void { $ormVersion = InstalledVersions::getVersion('doctrine/orm'); - if (strpos($ormVersion, '3.') === 0) { + if ($ormVersion !== null && strpos($ormVersion, '3.') === 0) { $lexer = 'TokenType'; } else { $lexer = 'Lexer'; diff --git a/tests/Rules/Doctrine/ORM/EntityColumnRuleTest.php b/tests/Rules/Doctrine/ORM/EntityColumnRuleTest.php index 36308989..57561ef4 100644 --- a/tests/Rules/Doctrine/ORM/EntityColumnRuleTest.php +++ b/tests/Rules/Doctrine/ORM/EntityColumnRuleTest.php @@ -171,7 +171,7 @@ public function testRule(?string $objectManagerLoader): void ]; $dbalVersion = InstalledVersions::getVersion('doctrine/dbal'); - $hasDbal4 = strpos($dbalVersion, '4.') === 0; + $hasDbal4 = $dbalVersion !== null && strpos($dbalVersion, '4.') === 0; if (!$hasDbal4) { array_unshift($errors, [ 'Property PHPStan\Rules\Doctrine\ORM\MyBrokenEntity::$id type mapping mismatch: database can contain string but property expects int|null.', @@ -242,7 +242,7 @@ public function testRuleWithAllowedNullableProperty(?string $objectManagerLoader ]; $dbalVersion = InstalledVersions::getVersion('doctrine/dbal'); - $hasDbal4 = strpos($dbalVersion, '4.') === 0; + $hasDbal4 = $dbalVersion !== null && strpos($dbalVersion, '4.') === 0; if (!$hasDbal4) { array_unshift($errors, [ 'Property PHPStan\Rules\Doctrine\ORM\MyBrokenEntity::$id type mapping mismatch: database can contain string but property expects int|null.', diff --git a/tests/Rules/Doctrine/ORM/FakeTestingUuidType.php b/tests/Rules/Doctrine/ORM/FakeTestingUuidType.php index 12388012..fcb28f7e 100644 --- a/tests/Rules/Doctrine/ORM/FakeTestingUuidType.php +++ b/tests/Rules/Doctrine/ORM/FakeTestingUuidType.php @@ -7,10 +7,7 @@ use Doctrine\DBAL\Types\GuidType; use Ramsey\Uuid\Uuid; use Ramsey\Uuid\UuidInterface; -use Throwable; -use function is_object; use function is_string; -use function method_exists; /** * From https://github.com/ramsey/uuid-doctrine/blob/fafebbe972cdaba9274c286ea8923e2de2579027/src/UuidType.php @@ -36,13 +33,7 @@ public function convertToPHPValue($value, AbstractPlatform $platform): ?UuidInte return null; } - try { - $uuid = Uuid::fromString($value); - } catch (Throwable $e) { - throw ConversionException::conversionFailed($value, self::NAME); - } - - return $uuid; + return Uuid::fromString($value); } /** @@ -56,18 +47,7 @@ public function convertToDatabaseValue($value, AbstractPlatform $platform): ?str return null; } - if ( - $value instanceof UuidInterface - || ( - (is_string($value) - || (is_object($value) && method_exists($value, '__toString'))) - && Uuid::isValid((string) $value) - ) - ) { - return (string) $value; - } - - throw ConversionException::conversionFailed($value, self::NAME); + return (string) $value; } public function getName(): string @@ -81,7 +61,7 @@ public function requiresSQLCommentHint(AbstractPlatform $platform): bool } /** - * @return string[] + * @return array */ public function getMappedDatabaseTypes(AbstractPlatform $platform): array { diff --git a/tests/Type/Doctrine/Query/QueryResultTypeWalkerTest.php b/tests/Type/Doctrine/Query/QueryResultTypeWalkerTest.php index 5b49bff8..27d1acab 100644 --- a/tests/Type/Doctrine/Query/QueryResultTypeWalkerTest.php +++ b/tests/Type/Doctrine/Query/QueryResultTypeWalkerTest.php @@ -240,10 +240,10 @@ public function test(Type $expectedType, string $dql, ?string $expectedException public function getTestData(): iterable { $ormVersion = InstalledVersions::getVersion('doctrine/orm'); - $hasOrm3 = strpos($ormVersion, '3.') === 0; + $hasOrm3 = $ormVersion !== null && strpos($ormVersion, '3.') === 0; $dbalVersion = InstalledVersions::getVersion('doctrine/dbal'); - $hasDbal4 = strpos($dbalVersion, '4.') === 0; + $hasDbal4 = $dbalVersion !== null && strpos($dbalVersion, '4.') === 0; yield 'just root entity' => [ new ObjectType(One::class),