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/.github/workflows/build.yml b/.github/workflows/build.yml index eac6b0f3..caa87f32 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -106,6 +106,15 @@ jobs: dependencies: - "lowest" - "highest" + update-packages: + - "" + include: + - php-version: "8.3" + dependencies: "highest" + update-packages: | + 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 steps: - name: "Checkout" @@ -135,6 +144,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 +164,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 +189,8 @@ jobs: - name: "Install dependencies" run: "composer update --no-interaction --no-progress" + - name: "Update packages" + run: ${{ matrix.update-packages }} + - name: "PHPStan" run: "make phpstan" 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/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/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 @@ +addMultiple($args); + } + 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/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; + } 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); + } 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 + { diff --git a/composer.json b/composer.json index be301022..6818f33d 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": { diff --git a/phpstan-baseline-orm-3.neon b/phpstan-baseline-orm-3.neon new file mode 100644 index 00000000..92e5f87c --- /dev/null +++ b/phpstan-baseline-orm-3.neon @@ -0,0 +1,56 @@ +parameters: + ignoreErrors: + - + message: "#^Call to function method_exists\\(\\) with 'Doctrine\\\\\\\\ORM\\\\\\\\EntityManager' and 'create' will always evaluate to false\\.$#" + count: 1 + path: src/Doctrine/Mapping/ClassMetadataFactory.php + + - + message: "#^Caught class Doctrine\\\\ORM\\\\ORMException not found\\.$#" + count: 1 + path: src/Type/Doctrine/CreateQueryDynamicReturnTypeExtension.php + + - + message: "#^Class Doctrine\\\\DBAL\\\\Types\\\\ArrayType not found\\.$#" + count: 1 + path: src/Type/Doctrine/Descriptors/ArrayType.php + + - + message: "#^Method PHPStan\\\\Type\\\\Doctrine\\\\Descriptors\\\\ArrayType\\:\\:getType\\(\\) should return class\\-string\\ 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 b8a228bd..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 @@ -20,6 +21,7 @@ parameters: bootstrapFiles: - stubs/runtime/Enum/UnitEnum.php - stubs/runtime/Enum/BackedEnum.php + - tests/orm-3-bootstrap.php ignoreErrors: - 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; } } 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/Descriptors/BigIntType.php b/src/Type/Doctrine/Descriptors/BigIntType.php index 213bf17b..14b3ca2a 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,18 @@ public function getDatabaseInternalType(): Type return new IntegerType(); } + private function hasDbal4(): bool + { + if (!class_exists(InstalledVersions::class)) { + return false; + } + + $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 d034c1cc..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; @@ -197,7 +198,7 @@ public function walkDeleteStatement(AST\DeleteStatement $AST): string } /** - * {@inheritdoc} + * @param string $identVariable */ public function walkEntityIdentificationVariable($identVariable): string { @@ -205,7 +206,8 @@ public function walkEntityIdentificationVariable($identVariable): string } /** - * {@inheritdoc} + * @param string $identificationVariable + * @param string|null $fieldName */ public function walkIdentificationVariable($identificationVariable, $fieldName = null): string { @@ -213,7 +215,7 @@ public function walkIdentificationVariable($identificationVariable, $fieldName = } /** - * {@inheritdoc} + * @param AST\PathExpression $pathExpr */ public function walkPathExpression($pathExpr): string { @@ -239,6 +241,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); } @@ -254,6 +257,8 @@ public function walkPathExpression($pathExpr): string } $joinColumn = $assoc['joinColumns'][0]; + + /** @var class-string $assocClassName */ $assocClassName = $assoc['targetEntity']; $targetClass = $this->em->getClassMetadata($assocClassName); @@ -279,7 +284,7 @@ public function walkPathExpression($pathExpr): string } /** - * {@inheritdoc} + * @param AST\SelectClause $selectClause */ public function walkSelectClause($selectClause): string { @@ -287,7 +292,7 @@ public function walkSelectClause($selectClause): string } /** - * {@inheritdoc} + * @param AST\FromClause $fromClause */ public function walkFromClause($fromClause): string { @@ -301,7 +306,7 @@ public function walkFromClause($fromClause): string } /** - * {@inheritdoc} + * @param AST\IdentificationVariableDeclaration $identificationVariableDecl */ public function walkIdentificationVariableDeclaration($identificationVariableDecl): string { @@ -319,7 +324,7 @@ public function walkIdentificationVariableDeclaration($identificationVariableDec } /** - * {@inheritdoc} + * @param AST\IndexBy $indexBy */ public function walkIndexBy($indexBy): void { @@ -328,7 +333,7 @@ public function walkIndexBy($indexBy): void } /** - * {@inheritdoc} + * @param AST\RangeVariableDeclaration $rangeVariableDeclaration */ public function walkRangeVariableDeclaration($rangeVariableDeclaration): string { @@ -336,7 +341,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 +351,7 @@ public function walkJoinAssociationDeclaration($joinAssociationDeclaration, $joi } /** - * {@inheritdoc} + * @param AST\Functions\FunctionNode $function */ public function walkFunction($function): string { @@ -357,7 +364,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), @@ -439,8 +446,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)) { @@ -462,8 +469,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)) { @@ -478,7 +485,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)) { @@ -489,10 +496,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(); } @@ -511,6 +518,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); @@ -563,7 +572,7 @@ public function walkFunction($function): string } /** - * {@inheritdoc} + * @param AST\OrderByClause $orderByClause */ public function walkOrderByClause($orderByClause): string { @@ -571,7 +580,7 @@ public function walkOrderByClause($orderByClause): string } /** - * {@inheritdoc} + * @param AST\OrderByItem $orderByItem */ public function walkOrderByItem($orderByItem): string { @@ -579,7 +588,7 @@ public function walkOrderByItem($orderByItem): string } /** - * {@inheritdoc} + * @param AST\HavingClause $havingClause */ public function walkHavingClause($havingClause): string { @@ -587,7 +596,7 @@ public function walkHavingClause($havingClause): string } /** - * {@inheritdoc} + * @param AST\Join $join */ public function walkJoin($join): string { @@ -613,7 +622,7 @@ public function walkJoin($join): string } /** - * {@inheritdoc} + * @param AST\CoalesceExpression $coalesceExpression */ public function walkCoalesceExpression($coalesceExpression): string { @@ -642,7 +651,7 @@ public function walkCoalesceExpression($coalesceExpression): string } /** - * {@inheritdoc} + * @param AST\NullIfExpression $nullIfExpression */ public function walkNullIfExpression($nullIfExpression): string { @@ -695,7 +704,7 @@ public function walkGeneralCaseExpression(AST\GeneralCaseExpression $generalCase } /** - * {@inheritdoc} + * @param AST\SimpleCaseExpression $simpleCaseExpression */ public function walkSimpleCaseExpression($simpleCaseExpression): string { @@ -732,7 +741,7 @@ public function walkSimpleCaseExpression($simpleCaseExpression): string } /** - * {@inheritdoc} + * @param AST\SelectExpression $selectExpression */ public function walkSelectExpression($selectExpression): string { @@ -807,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); @@ -857,7 +866,7 @@ public function walkSelectExpression($selectExpression): string } /** - * {@inheritdoc} + * @param AST\QuantifiedExpression $qExpr */ public function walkQuantifiedExpression($qExpr): string { @@ -865,7 +874,7 @@ public function walkQuantifiedExpression($qExpr): string } /** - * {@inheritdoc} + * @param AST\Subselect $subselect */ public function walkSubselect($subselect): string { @@ -873,7 +882,7 @@ public function walkSubselect($subselect): string } /** - * {@inheritdoc} + * @param AST\SubselectFromClause $subselectFromClause */ public function walkSubselectFromClause($subselectFromClause): string { @@ -881,7 +890,7 @@ public function walkSubselectFromClause($subselectFromClause): string } /** - * {@inheritdoc} + * @param AST\SimpleSelectClause $simpleSelectClause */ public function walkSimpleSelectClause($simpleSelectClause): string { @@ -894,7 +903,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 +918,7 @@ public function walkNewObject($newObjectExpression, $newObjectResultAlias = null } /** - * {@inheritdoc} + * @param AST\SimpleSelectExpression $simpleSelectExpression */ public function walkSimpleSelectExpression($simpleSelectExpression): string { @@ -916,7 +926,7 @@ public function walkSimpleSelectExpression($simpleSelectExpression): string } /** - * {@inheritdoc} + * @param AST\AggregateExpression $aggExpression */ public function walkAggregateExpression($aggExpression): string { @@ -926,7 +936,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)); @@ -940,7 +950,7 @@ public function walkAggregateExpression($aggExpression): string } /** - * {@inheritdoc} + * @param AST\GroupByClause $groupByClause */ public function walkGroupByClause($groupByClause): string { @@ -948,7 +958,7 @@ public function walkGroupByClause($groupByClause): string } /** - * {@inheritdoc} + * @param AST\PathExpression|string $groupByItem */ public function walkGroupByItem($groupByItem): string { @@ -961,7 +971,7 @@ public function walkDeleteClause(AST\DeleteClause $deleteClause): string } /** - * {@inheritdoc} + * @param AST\UpdateClause $updateClause */ public function walkUpdateClause($updateClause): string { @@ -969,7 +979,7 @@ public function walkUpdateClause($updateClause): string } /** - * {@inheritdoc} + * @param AST\UpdateItem $updateItem */ public function walkUpdateItem($updateItem): string { @@ -977,7 +987,7 @@ public function walkUpdateItem($updateItem): string } /** - * {@inheritdoc} + * @param AST\WhereClause|null $whereClause */ public function walkWhereClause($whereClause): string { @@ -985,7 +995,7 @@ public function walkWhereClause($whereClause): string } /** - * {@inheritdoc} + * @param AST\ConditionalExpression|AST\Phase2OptimizableConditional $condExpr */ public function walkConditionalExpression($condExpr): string { @@ -993,7 +1003,7 @@ public function walkConditionalExpression($condExpr): string } /** - * {@inheritdoc} + * @param AST\ConditionalTerm|AST\ConditionalPrimary|AST\ConditionalFactor $condTerm */ public function walkConditionalTerm($condTerm): string { @@ -1001,7 +1011,7 @@ public function walkConditionalTerm($condTerm): string } /** - * {@inheritdoc} + * @param AST\ConditionalFactor|AST\ConditionalPrimary $factor */ public function walkConditionalFactor($factor): string { @@ -1009,7 +1019,7 @@ public function walkConditionalFactor($factor): string } /** - * {@inheritdoc} + * @param AST\ConditionalPrimary $primary */ public function walkConditionalPrimary($primary): string { @@ -1017,7 +1027,7 @@ public function walkConditionalPrimary($primary): string } /** - * {@inheritdoc} + * @param AST\ExistsExpression $existsExpr */ public function walkExistsExpression($existsExpr): string { @@ -1025,7 +1035,7 @@ public function walkExistsExpression($existsExpr): string } /** - * {@inheritdoc} + * @param AST\CollectionMemberExpression $collMemberExpr */ public function walkCollectionMemberExpression($collMemberExpr): string { @@ -1033,7 +1043,7 @@ public function walkCollectionMemberExpression($collMemberExpr): string } /** - * {@inheritdoc} + * @param AST\EmptyCollectionComparisonExpression $emptyCollCompExpr */ public function walkEmptyCollectionComparisonExpression($emptyCollCompExpr): string { @@ -1041,7 +1051,7 @@ public function walkEmptyCollectionComparisonExpression($emptyCollCompExpr): str } /** - * {@inheritdoc} + * @param AST\NullComparisonExpression $nullCompExpr */ public function walkNullComparisonExpression($nullCompExpr): string { @@ -1049,15 +1059,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 +1075,7 @@ public function walkInstanceOfExpression($instanceOfExpr): string } /** - * {@inheritdoc} + * @param mixed $inParam */ public function walkInParameter($inParam): string { @@ -1073,7 +1083,7 @@ public function walkInParameter($inParam): string } /** - * {@inheritdoc} + * @param AST\Literal $literal */ public function walkLiteral($literal): string { @@ -1110,7 +1120,7 @@ public function walkLiteral($literal): string } /** - * {@inheritdoc} + * @param AST\BetweenExpression $betweenExpr */ public function walkBetweenExpression($betweenExpr): string { @@ -1118,7 +1128,7 @@ public function walkBetweenExpression($betweenExpr): string } /** - * {@inheritdoc} + * @param AST\LikeExpression $likeExpr */ public function walkLikeExpression($likeExpr): string { @@ -1126,7 +1136,7 @@ public function walkLikeExpression($likeExpr): string } /** - * {@inheritdoc} + * @param AST\PathExpression $stateFieldPathExpression */ public function walkStateFieldPathExpression($stateFieldPathExpression): string { @@ -1134,7 +1144,7 @@ public function walkStateFieldPathExpression($stateFieldPathExpression): string } /** - * {@inheritdoc} + * @param AST\ComparisonExpression $compExpr */ public function walkComparisonExpression($compExpr): string { @@ -1142,7 +1152,7 @@ public function walkComparisonExpression($compExpr): string } /** - * {@inheritdoc} + * @param AST\InputParameter $inputParam */ public function walkInputParameter($inputParam): string { @@ -1150,12 +1160,12 @@ public function walkInputParameter($inputParam): string } /** - * {@inheritdoc} + * @param AST\ArithmeticExpression $arithmeticExpr */ public function walkArithmeticExpression($arithmeticExpr): string { if ($arithmeticExpr->simpleArithmeticExpression !== null) { - return $arithmeticExpr->simpleArithmeticExpression->dispatch($this); + return $this->walkSimpleArithmeticExpression($arithmeticExpr->simpleArithmeticExpression); } if ($arithmeticExpr->subselect !== null) { @@ -1166,10 +1176,14 @@ public function walkArithmeticExpression($arithmeticExpr): string } /** - * {@inheritdoc} + * @param AST\Node|string $simpleArithmeticExpr */ public function walkSimpleArithmeticExpression($simpleArithmeticExpr): string { + if (!$simpleArithmeticExpr instanceof AST\SimpleArithmeticExpression) { + return $this->walkArithmeticTerm($simpleArithmeticExpr); + } + $types = []; foreach ($simpleArithmeticExpr->arithmeticTerms as $term) { @@ -1188,12 +1202,12 @@ public function walkSimpleArithmeticExpression($simpleArithmeticExpr): string } /** - * {@inheritdoc} + * @param mixed $term */ public function walkArithmeticTerm($term): string { if (!$term instanceof AST\ArithmeticTerm) { - return $this->marshalType(new MixedType()); + return $this->walkArithmeticFactor($term); } $types = []; @@ -1214,12 +1228,12 @@ public function walkArithmeticTerm($term): string } /** - * {@inheritdoc} + * @param mixed $factor */ public function walkArithmeticFactor($factor): string { if (!$factor instanceof AST\ArithmeticFactor) { - return $this->marshalType(new MixedType()); + return $this->walkArithmeticPrimary($factor); } $primary = $factor->arithmeticPrimary; @@ -1231,7 +1245,7 @@ public function walkArithmeticFactor($factor): string } /** - * {@inheritdoc} + * @param mixed $primary */ public function walkArithmeticPrimary($primary): string { @@ -1248,15 +1262,19 @@ public function walkArithmeticPrimary($primary): string } /** - * {@inheritdoc} + * @param mixed $stringPrimary */ public function walkStringPrimary($stringPrimary): string { + if ($stringPrimary instanceof AST\Node) { + return $stringPrimary->dispatch($this); + } + return $this->marshalType(new MixedType()); } /** - * {@inheritdoc} + * @param string $resultVariable */ public function walkResultVariable($resultVariable): string { @@ -1294,7 +1312,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)) { 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); diff --git a/tests/DoctrineIntegration/ORM/EntityManagerTypeInferenceTest.php b/tests/DoctrineIntegration/ORM/EntityManagerTypeInferenceTest.php index efef0c5d..086a6e0e 100644 --- a/tests/DoctrineIntegration/ORM/EntityManagerTypeInferenceTest.php +++ b/tests/DoctrineIntegration/ORM/EntityManagerTypeInferenceTest.php @@ -14,15 +14,25 @@ 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; + $dbalVersion = InstalledVersions::getVersion('doctrine/dbal'); + $hasDbal3 = $dbalVersion !== null && strpos($dbalVersion, '3.') === 0; + $hasDbal4 = $dbalVersion !== null && strpos($dbalVersion, '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..25308635 100644 --- a/tests/DoctrineIntegration/ORM/EntityManagerWithoutObjectManagerLoaderTypeInferenceTest.php +++ b/tests/DoctrineIntegration/ORM/EntityManagerWithoutObjectManagerLoaderTypeInferenceTest.php @@ -14,14 +14,24 @@ 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'); $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/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 */ 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/Rules/Doctrine/ORM/DqlRuleTest.php b/tests/Rules/Doctrine/ORM/DqlRuleTest.php index c4cbe21d..007ed40b 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 ($ormVersion !== null && 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, ], [ diff --git a/tests/Rules/Doctrine/ORM/EntityColumnRuleTest.php b/tests/Rules/Doctrine/ORM/EntityColumnRuleTest.php index 4b9eaf80..57561ef4 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; /** @@ -54,6 +57,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'), @@ -100,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, @@ -165,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 = $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.', + 19, + ]); + } + + $this->analyse([__DIR__ . '/data/MyBrokenEntity.php'], $errors); } /** @@ -175,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, @@ -228,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 = $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.', + 19, + ]); + } + + $this->analyse([__DIR__ . '/data/MyBrokenEntity.php'], $errors); } /** @@ -278,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, ], ], @@ -288,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/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/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; diff --git a/tests/Type/Doctrine/Query/QueryResultTypeWalkerTest.php b/tests/Type/Doctrine/Query/QueryResultTypeWalkerTest.php index 1393a9bc..27d1acab 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; @@ -238,6 +239,12 @@ public function test(Type $expectedType, string $dql, ?string $expectedException */ public function getTestData(): iterable { + $ormVersion = InstalledVersions::getVersion('doctrine/orm'); + $hasOrm3 = $ormVersion !== null && strpos($ormVersion, '3.') === 0; + + $dbalVersion = InstalledVersions::getVersion('doctrine/dbal'); + $hasDbal4 = $dbalVersion !== null && strpos($dbalVersion, '4.') === 0; + yield 'just root entity' => [ new ObjectType(One::class), ' @@ -354,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()], ]) ), @@ -376,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()], ]) ), @@ -397,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()], ]) ), @@ -507,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 @@ -1176,37 +1183,39 @@ 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()], - ]), - ' + 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([ diff --git a/tests/bootstrap.php b/tests/bootstrap.php index f6c941c0..edf6599d 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -5,3 +5,6 @@ require_once __DIR__ . '/../vendor/autoload.php'; 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 @@ +