Repository: phpstan/phpstan-doctrine (php)

Issue:
Generated fields not correctly treated as written by Doctrine
Having a generated field in an entity:
```
    #[Column(type: Types::DATETIME_MUTABLE, insertable: false, updatable: false,
        columnDefinition: 'datetime GENERATED ALWAYS AS (DATE_ADD(Start, INTERVAL Hours * 60 MINUTE))',
        generated: 'ALWAYS')]
    private \DateTimeInterface $end;
```
Obviously only with a plain getter:
```
    public function getEnd(): \DateTimeInterface
    {
        return $this->end;
    }
```
Makes phpstan not a happy camper:
```
  75     Property WorkEntry::$end is never written, only read.
         💡 See: https://phpstan.org/developing-extensions/always-read-written-properties
```
Solution is to properly parse the `generated` property of the attribute, and consider the property always-written when `ALWAYS` or `INSERT`, ref https://www.doctrine-project.org/projects/doctrine-orm/en/2.13/reference/annotations-reference.html#column

Feature has actually been in Doctrine ORM since 2.11: https://www.doctrine-project.org/2022/01/11/orm-2.11.html



Pull request description:
Add support for Doctrine generated fields
Closes #378

Patch:
diff --git a/src/Rules/Doctrine/ORM/PropertiesExtension.php b/src/Rules/Doctrine/ORM/PropertiesExtension.php
index 7328d6f..3be61f3 100644
--- a/src/Rules/Doctrine/ORM/PropertiesExtension.php
+++ b/src/Rules/Doctrine/ORM/PropertiesExtension.php
@@ -44,6 +44,11 @@ class PropertiesExtension implements ReadWritePropertiesExtension
 			return false;
 		}
 
+		$mapping = $metadata->getFieldMapping($propertyName);
+		if (array_key_exists('generated', $mapping) && $mapping['generated'] !== ClassMetadataInfo::GENERATED_NEVER) {
+			return true;
+		}
+
 		if ($metadata->isReadOnly && !$declaringClass->hasConstructor()) {
 			return true;
 		}
diff --git a/stubs/ORM/Mapping/ClassMetadataInfo.stub b/stubs/ORM/Mapping/ClassMetadataInfo.stub
index 262f8b8..a884894 100644
--- a/stubs/ORM/Mapping/ClassMetadataInfo.stub
+++ b/stubs/ORM/Mapping/ClassMetadataInfo.stub
@@ -17,7 +17,7 @@ use ReflectionClass;
  *      nullable?: bool,
  *      notInsertable?: bool,
  *      notUpdatable?: bool,
- *      generated?: string,
+ *      generated?: int,
  *      enumType?: class-string<BackedEnum>,
  *      columnDefinition?: string,
  *      precision?: int,


Test patch:
diff --git a/tests/Rules/DeadCode/data/unused-private-property.php b/tests/Rules/DeadCode/data/unused-private-property.php
index 88aca7c..f5455a3 100644
--- a/tests/Rules/DeadCode/data/unused-private-property.php
+++ b/tests/Rules/DeadCode/data/unused-private-property.php
@@ -55,5 +55,57 @@ class ReadOnlyEntityWithConstructor
 	public function __construct()
 	{
 	}
+}
+
+/**
+ * @ORM\Entity
+ */
+class EntityWithGeneratedField
+{
+	/**
+	 * @ORM\Id
+	 * @ORM\Column
+	 */
+	public int $id;
 
+	/**
+	 * @ORM\Column(type="int", insertable=false, updatable=false, generated="ALWAYS",
+	 *     columnDefinition="int GENERATED ALWAYS AS (1 + 2)")
+	 */
+	private int $generated;
+
+	public function __construct()
+	{
+	}
 }
+
+/**
+ * @ORM\Entity
+ */
+class EntityWithGeneratedFieldWithGetter
+{
+	/**
+	 * @ORM\Id
+	 * @ORM\Column
+	 */
+	public int $id;
+
+	/**
+	 * @ORM\Column(type="int", insertable=false, updatable=false, generated="ALWAYS",
+	 *     columnDefinition="int GENERATED ALWAYS AS (1 + 2)")
+	 */
+	private int $generated;
+
+	public function __construct()
+	{
+	}
+
+	/**
+	 * @return int
+	 */
+	public function getGenerated(): int
+	{
+		return $this->generated;
+	}
+}
+