Project

General

Profile

Actions

Bug #103641

closed

Not possible so set nullable relation values in extbase domain models

Added by Torben Hansen 7 months ago. Updated 3 months ago.

Status:
Closed
Priority:
Must have
Assignee:
Category:
-
Target version:
-
Start date:
2024-04-16
Due date:
% Done:

100%

Estimated time:
TYPO3 Version:
13
PHP Version:
Tags:
Complexity:
Is Regression:
Sprint Focus:

Description

When an extbase domain Model has a property, which implements DomainObjectInterface and is nullable, it is impossible to set the property of an existing object to null, because the database field is defined does not accept NULL as value. When saved through DataHandler, a nullable (e.g. tca=group) field saves 0 as value for those fields.

Example:

protected ?Person $author = null;

If the object with the $author property is persisted with a valid relation to a Person model, Extbase persists the UID of the person. Setting the field to null e.g. in controller (e.g. $blog->setAuthor(null)), results in the following exception Column 'author cannot be null, since Extbase persistence tries to save a NULL value to the not nullable database field.

Extbase should consider this scenario and save 0 for nullable properties which implement DomainObjectInterface

The same applies to nullable DateTime fields, which show the same behavior. (see #88515)


Related issues 1 (0 open1 closed)

Related to TYPO3 Core - Bug #88515: Cannot unset DateTime value via nullClosed2019-06-07

Actions
Actions #1

Updated by Torben Hansen 7 months ago

  • Related to Bug #88515: Cannot unset DateTime value via null added
Actions #2

Updated by Torben Hansen 7 months ago

  • TYPO3 Version changed from 12 to 13
Actions #3

Updated by Gerrit Code Review 7 months ago

  • Status changed from New to Under Review

Patch set 1 for branch main of project Packages/TYPO3.CMS has been pushed to the review server.
It is available at https://review.typo3.org/c/Packages/TYPO3.CMS/+/83839

Actions #4

Updated by Gerrit Code Review 7 months ago

Patch set 2 for branch main of project Packages/TYPO3.CMS has been pushed to the review server.
It is available at https://review.typo3.org/c/Packages/TYPO3.CMS/+/83839

Actions #5

Updated by Gerrit Code Review 7 months ago

Patch set 3 for branch main of project Packages/TYPO3.CMS has been pushed to the review server.
It is available at https://review.typo3.org/c/Packages/TYPO3.CMS/+/83839

Actions #6

Updated by Gerrit Code Review 7 months ago

Patch set 4 for branch main of project Packages/TYPO3.CMS has been pushed to the review server.
It is available at https://review.typo3.org/c/Packages/TYPO3.CMS/+/83839

Actions #7

Updated by Gerrit Code Review 7 months ago

Patch set 5 for branch main of project Packages/TYPO3.CMS has been pushed to the review server.
It is available at https://review.typo3.org/c/Packages/TYPO3.CMS/+/83839

Actions #8

Updated by Gerrit Code Review 7 months ago

Patch set 6 for branch main of project Packages/TYPO3.CMS has been pushed to the review server.
It is available at https://review.typo3.org/c/Packages/TYPO3.CMS/+/83839

Actions #9

Updated by Gerrit Code Review 7 months ago

Patch set 7 for branch main of project Packages/TYPO3.CMS has been pushed to the review server.
It is available at https://review.typo3.org/c/Packages/TYPO3.CMS/+/83839

Actions #10

Updated by Torben Hansen 7 months ago

  • Status changed from Under Review to Resolved
  • % Done changed from 0 to 100
Actions #11

Updated by Benni Mack 5 months ago

  • Status changed from Resolved to Closed
Actions #12

Updated by Marcin Sągol 3 months ago

If someone would like to have it in version 12, you can use the composer patch (cweagans/composer-patches). For example (adjust the path and name if needed):

"extra": {
    "patches": {
        "typo3/cms-extbase": {
            "Allow persisting nullable properties in extbase": "patches/83839-bugfix-allow-persisting-nullable-properties-in-extbase-30399b0.diff" 
        }
    }
}

and the updated patch content (small fix was required due to code changes in v12):

diff --git a/Classes/Persistence/Generic/Backend.php b/Classes/Persistence/Generic/Backend.php
index 24815d1..2cfff89 100644
--- a/Classes/Persistence/Generic/Backend.php
+++ b/Classes/Persistence/Generic/Backend.php
@@ -19,6 +19,7 @@

 use Psr\EventDispatcher\EventDispatcherInterface;
 use TYPO3\CMS\Core\Context\LanguageAspect;
+use TYPO3\CMS\Core\Database\Query\QueryHelper;
 use TYPO3\CMS\Core\Database\ReferenceIndex;
 use TYPO3\CMS\Core\SingletonInterface;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
@@ -41,6 +42,7 @@
 use TYPO3\CMS\Extbase\Persistence\ObjectStorage;
 use TYPO3\CMS\Extbase\Persistence\PersistenceManagerInterface;
 use TYPO3\CMS\Extbase\Persistence\QueryInterface;
+use TYPO3\CMS\Extbase\Reflection\ClassSchema\Property;
 use TYPO3\CMS\Extbase\Reflection\ObjectAccess;
 use TYPO3\CMS\Extbase\Reflection\ReflectionService;

@@ -319,11 +321,11 @@
                     if ($propertyValue->_isNew()) {
                         $this->insertObject($propertyValue, $object, $propertyName);
                     }
-                    $row[$columnMap->getColumnName()] = $this->getPlainValue($propertyValue);
+                    $row[$columnMap->getColumnName()] = $this->getPlainValue($propertyValue, null, $property);
                 }
                 $queue[] = $propertyValue;
             } elseif ($object->_isNew() || $object->_isDirty($propertyName)) {
-                $row[$columnMap->getColumnName()] = $this->getPlainValue($propertyValue, $columnMap);
+                $row[$columnMap->getColumnName()] = $this->getPlainValue($propertyValue, $columnMap, $property);
             }
         }
         if (!empty($row)) {
@@ -596,7 +598,7 @@
                     $row[$columnMap->getColumnName()] = 0;
                 }
             } elseif ($propertyValue !== null) {
-                $row[$columnMap->getColumnName()] = $this->getPlainValue($propertyValue, $columnMap);
+                $row[$columnMap->getColumnName()] = $this->getPlainValue($propertyValue, $columnMap, $property);
             }
         }
         $this->addCommonFieldsToRow($object, $row);
@@ -930,19 +932,48 @@
     }

     /**
-     * Returns a plain value
-     *
-     * i.e. objects are flattened out if possible.
-     * Checks explicitly for null values as DataMapper's getPlainValue would convert this to 'NULL'
+     * Returns a plain value, i.e. objects are flattened out if possible.
+     * Checks explicitly for null values as DataMapper's getPlainValue would convert this to 'NULL'.
+     * For null values, the expected DB null value will be considered.
      *
      * @param mixed $input The value that will be converted
      * @param ColumnMap|null $columnMap Optional column map for retrieving the date storage format
+     * @param Property|null $property The current property
      * @return int|string|null
      */
-    protected function getPlainValue($input, ?ColumnMap $columnMap = null)
+    protected function getPlainValue(mixed $input, ColumnMap $columnMap = null, Property $property = null)
     {
-        return $input !== null
-            ? GeneralUtility::makeInstance(DataMapper::class)->getPlainValue($input, $columnMap)
-            : null;
+        if ($input !== null) {
+            return GeneralUtility::makeInstance(DataMapper::class)->getPlainValue($input, $columnMap);
+        }
+
+        if (!$property) {
+            return null;
+        }
+
+        $className = $property->getPrimaryType()->getClassName();
+
+        // Nullable domain model property
+        if (is_subclass_of($className, DomainObjectInterface::class)) {
+            return 0;
+        }
+
+        // Nullable DateTime property
+        if ($columnMap && is_subclass_of($className, \DateTimeInterface::class)) {
+            $datetimeFormats = QueryHelper::getDateTimeFormats();
+            $dateFormat = $columnMap->getDateTimeStorageFormat();
+            if (!$dateFormat) {
+                // Datetime property with no TCA dbType
+                return 0;
+            }
+
+            if (isset($datetimeFormats[$dateFormat])) {
+                // Datetime property with TCA dbType defined. Nullable fields will be saved with the empty value
+                // (e.g. "00:00:00" for dbType = time) as well, but DataMapper will correctly map those values to null
+                return $datetimeFormats[$dateFormat]['empty'];
+            }
+        }
+
+        return null;
     }
 }
Actions

Also available in: Atom PDF