Problem/Motivation

When a form launches an AJAX call (like in the yoast_seo module), then the form_state is build by the request. (core/lib/Drupal/Core/Form/EventSubscriber/FormAjaxSubscriber.php r:103)
Therefore, the DateTime fields have their values as an array and not a string (array containing date and time elements), like in the form names: field_date[0][value][date] and field_date[0][value][time].
Result: in the DateTimeComputed.php (r:47) =>

$value = $item->{($this->definition->getSetting('date source'))};

the value is now an array, instead of a string, which on line 58 -> 251, throws an error:

TypeError: DateTime::createFromFormat(): Argument #2 ($datetime) must be of type string, array given in DateTime::createFromFormat() (line 251 of /var/app/web/core/lib/Drupal/Component/Datetime/DateTimePlus.php)

This bug will occur when the following conditions exist:

  1. An ajax submission is triggered on a nested form that contains a datetime (or date range) field.
  2. The submitted datetime field value is invalid and fails Datetime::validateDatetime which means the formState value for the element remains as an array.
  3. The nested form calls EntityFormDisplayInterface::extractFormValues from a validate callback (e.g. NestedEntityTestForm::validateForm or ParagraphsWidget::elementValidate which results in DateTimeItem::setValue getting called and the item's values property gets set to the value array. Then when the nested form is rebuilt on ajax and DateTimeWidgetBase::formElement is called, the call to the date property on line 35 results in DateTimeComputed::getValue being called and because the value is an array, it throws an throwableerror which is not caught but the try/catch and Ajax fails.

Steps to reproduce

  1. Install Drupal core with the Standard profile.
  2. Install and enable the Paragraphs module.
  3. Create a Test paragraph type with a required date field (Date type: Date only).
  4. Add an entity-reference revisions (ERR) field to the page content type, referencing the Test paragraph type (number of values: unlimited).
  5. Create a page node. Leave the date field empty. Click the "Add Test" button.

Proposed resolution


I'll share a patch in comments, but it probably can be handled better.
As we don't know which key is being handled (date/time) I just check if the value is an array, and if so just use the first element instead.
feel free to further search for a cleaner solution.

The issue is caused by the form/widget logic setting the field item to a value type that is not expected by DateTimeComputed::getValue. This method expects the value to be a string or NULL. Given the purpose of \Drupal\Core\Field\WidgetInterface::massageFormValues is to massage the form values into the format expected for field values, I propose DateTimeWidgetBase::massageFormValues and DateRangeWidgetBase::massageFormValues are the best places to fix the problem. Refactor these methods to set $item['value'] to NULL if it is not a DrupalDateTime.

Remaining tasks

  1. Find steps to reproduce without using contributed modules.
  2. Decide on the right approach. (See the current MR (details in comment #21), previous commit on MR (details in comment #18), patch in Comment #2 and the MR on #3052608: DateTimeComputed tries to compute from an empty value.)
  3. Add tests.

Issue fork drupal-3313231

Command icon Show commands

Start within a Git clone of the project using the version control instructions.

Or, if you do not have SSH keys set up on git.drupalcode.org:

Comments

Tim Lammar created an issue. See original summary.

tim@lammar.be’s picture

StatusFileSize
new776 bytes

Basic quick patch.

LeoAlcci’s picture

Title: DateTimeComputed: Fatal error when Ajax call in form. (strlen() on arraay) » DateTimeComputed: Fatal error when Ajax call in form. (strlen() on array)

Version: 9.4.x-dev » 9.5.x-dev

Drupal 9.4.9 was released on December 7, 2022 and is the final full bugfix release for the Drupal 9.4.x series. Drupal 9.4.x will not receive any further development aside from security fixes. Drupal 9 bug reports should be targeted for the 9.5.x-dev branch from now on, and new development or disruptive changes should be targeted for the 10.1.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

bvoynick’s picture

bvoynick’s picture

Also running in to this due to a combination of the Paragraphs module & stable widget with AJAX from the Media Library widget. It seems to only occur if the datetime field in question is A) Required and B) happens to be empty.

Here's an example callstack for the Paragraphs issue, though based on your post it seems like this is most associated with form rebuilding.

TypeError: DateTime::createFromFormat(): Argument #2 ($datetime) must be of type string, array given in DateTime::createFromFormat() (line 251 of /var/www/html/dist/core/lib/Drupal/Component/Datetime/DateTimePlus.php)

#0 /var/www/html/dist/core/lib/Drupal/Component/Datetime/DateTimePlus.php(251): DateTime::createFromFormat()
#1 /var/www/html/dist/core/modules/datetime/src/DateTimeComputed.php(57): Drupal\Component\Datetime\DateTimePlus::createFromFormat()
#2 /var/www/html/dist/core/lib/Drupal/Core/Field/FieldItemBase.php(140): Drupal\datetime\DateTimeComputed->getValue()
#3 /var/www/html/dist/core/modules/datetime/src/Plugin/Field/FieldWidget/DateTimeWidgetBase.php(35): Drupal\Core\Field\FieldItemBase->__get()
#4 /var/www/html/dist/core/modules/datetime/src/Plugin/Field/FieldWidget/DateTimeDefaultWidget.php(59): Drupal\datetime\Plugin\Field\FieldWidget\DateTimeWidgetBase->formElement()
#5 /var/www/html/dist/core/lib/Drupal/Core/Field/WidgetBase.php(353): Drupal\datetime\Plugin\Field\FieldWidget\DateTimeDefaultWidget->formElement()
#6 /var/www/html/dist/core/lib/Drupal/Core/Field/WidgetBase.php(220): Drupal\Core\Field\WidgetBase->formSingleElement()
#7 /var/www/html/dist/core/lib/Drupal/Core/Field/WidgetBase.php(111): Drupal\Core\Field\WidgetBase->formMultipleElements()
#8 /var/www/html/dist/core/lib/Drupal/Core/Entity/Entity/EntityFormDisplay.php(183): Drupal\Core\Field\WidgetBase->form()
#9 /var/www/html/dist/modules/contrib/paragraphs/src/Plugin/Field/FieldWidget/ParagraphsWidget.php(781): Drupal\Core\Entity\Entity\EntityFormDisplay->buildForm()
#10 /var/www/html/dist/core/lib/Drupal/Core/Field/WidgetBase.php(353): Drupal\paragraphs\Plugin\Field\FieldWidget\ParagraphsWidget->formElement()
#11 /var/www/html/dist/modules/contrib/paragraphs/src/Plugin/Field/FieldWidget/ParagraphsWidget.php(1116): Drupal\Core\Field\WidgetBase->formSingleElement()
#12 /var/www/html/dist/core/lib/Drupal/Core/Field/WidgetBase.php(111): Drupal\paragraphs\Plugin\Field\FieldWidget\ParagraphsWidget->formMultipleElements()
#13 /var/www/html/dist/modules/contrib/paragraphs/src/Plugin/Field/FieldWidget/ParagraphsWidget.php(1238): Drupal\Core\Field\WidgetBase->form()
#14 /var/www/html/dist/core/lib/Drupal/Core/Entity/Entity/EntityFormDisplay.php(183): Drupal\paragraphs\Plugin\Field\FieldWidget\ParagraphsWidget->form()
#15 /var/www/html/dist/core/lib/Drupal/Core/Entity/ContentEntityForm.php(121): Drupal\Core\Entity\Entity\EntityFormDisplay->buildForm()
#16 /var/www/html/dist/core/lib/Drupal/Core/Entity/EntityForm.php(106): Drupal\Core\Entity\ContentEntityForm->form()
#17 [internal function]: Drupal\Core\Entity\EntityForm->buildForm()
#18 /var/www/html/dist/core/lib/Drupal/Core/Form/FormBuilder.php(534): call_user_func_array()
#19 /var/www/html/dist/core/lib/Drupal/Core/Form/FormBuilder.php(373): Drupal\Core\Form\FormBuilder->retrieveForm()
#20 /var/www/html/dist/core/lib/Drupal/Core/Form/FormBuilder.php(631): Drupal\Core\Form\FormBuilder->rebuildForm()
#21 /var/www/html/dist/core/lib/Drupal/Core/Form/FormBuilder.php(323): Drupal\Core\Form\FormBuilder->processForm()
#22 /var/www/html/dist/core/lib/Drupal/Core/Controller/FormController.php(73): Drupal\Core\Form\FormBuilder->buildForm()
#23 [internal function]: Drupal\Core\Controller\FormController->getContentResult()
#24 /var/www/html/dist/core/lib/Drupal/Core/EventSubscriber/EarlyRenderingControllerWrapperSubscriber.php(123): call_user_func_array()
#25 /var/www/html/dist/core/lib/Drupal/Core/Render/Renderer.php(580): Drupal\Core\EventSubscriber\EarlyRenderingControllerWrapperSubscriber->Drupal\Core\EventSubscriber\{closure}()
#26 /var/www/html/dist/core/lib/Drupal/Core/EventSubscriber/EarlyRenderingControllerWrapperSubscriber.php(124): Drupal\Core\Render\Renderer->executeInRenderContext()
#27 /var/www/html/dist/core/lib/Drupal/Core/EventSubscriber/EarlyRenderingControllerWrapperSubscriber.php(97): Drupal\Core\EventSubscriber\EarlyRenderingControllerWrapperSubscriber->wrapControllerExecutionInRenderContext()
#28 /var/www/html/vendor/symfony/http-kernel/HttpKernel.php(169): Drupal\Core\EventSubscriber\EarlyRenderingControllerWrapperSubscriber->Drupal\Core\EventSubscriber\{closure}()
#29 /var/www/html/vendor/symfony/http-kernel/HttpKernel.php(81): Symfony\Component\HttpKernel\HttpKernel->handleRaw()
#30 /var/www/html/dist/core/lib/Drupal/Core/StackMiddleware/Session.php(58): Symfony\Component\HttpKernel\HttpKernel->handle()
#31 /var/www/html/dist/core/lib/Drupal/Core/StackMiddleware/KernelPreHandle.php(48): Drupal\Core\StackMiddleware\Session->handle()
#32 /var/www/html/dist/core/lib/Drupal/Core/StackMiddleware/ReverseProxyMiddleware.php(48): Drupal\Core\StackMiddleware\KernelPreHandle->handle()
#33 /var/www/html/dist/core/lib/Drupal/Core/StackMiddleware/NegotiationMiddleware.php(51): Drupal\Core\StackMiddleware\ReverseProxyMiddleware->handle()
#34 /var/www/html/vendor/stack/builder/src/Stack/StackedHttpKernel.php(23): Drupal\Core\StackMiddleware\NegotiationMiddleware->handle()
#35 /var/www/html/dist/core/lib/Drupal/Core/DrupalKernel.php(713): Stack\StackedHttpKernel->handle()
#36 /var/www/html/dist/index.php(19): Drupal\Core\DrupalKernel->handle()
#37 {main}TypeError: DateTime::createFromFormat(): Argument #2 ($datetime) must be of type string, array given in DateTime::createFromFormat() (line 251 of /var/www/html/dist/core/lib/Drupal/Component/Datetime/DateTimePlus.php)

It looks to me like this originates in the static valueCallback() method in Drupal\Core\Datetime\Element\Datetime and Drupal\Core\Datetime\Element\DateList. Both implementations of valueCallback() act to ensure that the input will be transformed into an array.

However, this transformation to an array is not reversed if the element is required & incomplete. (Or simply incomplete, in the case of DateList.) $form_state->setValueForElement() is called in _some_ cases but not in others. It seems like this inconsistency is not a problem for simpler forms, I suppose because the widget doesn't have to be fully rebuilt. But it can become an issue when AJAX/form rebuilding causes the array set by valueCallback() to "wrap around" into rebuilding the form element once again.

Possibly this is a Paragraphs issue and there is some way in which it is failing to mirror Core, but I'm doubtful about that. Especially since you report the Yoast module is also hitting this. Seems to me these widgets should be reversing the valueCallback transformation to array in all cases to ensure that other modules' form handling doesn't have to worry about this.

bvoynick’s picture

This could potentially be fixed by adding $form_state->setValueForElement($element, NULL); to all conditions in the validation callbacks for these two elements (DateList, Datetime) that do not currently set the element value.

However, there is a downside to doing this in the case of DateList, which is that partial (and therefore invalid) values would be wiped back to a clean slate in instances where the DateList field is re-rendered.

So I wonder if work in DateTimeComputed such as https://www.drupal.org/project/drupal/issues/3052608 - or something adjacent, such as a type check in DrupalDateTime::createFromFormat() - would be best.

bvoynick’s picture

benjifisher’s picture

Issue summary: View changes

I am adding steps to reproduce the bug to the issue summary. I used the steps in #3306746: DateTime::createFromFormat() expects parameter 2 to be string and #3052608-8: DateTimeComputed tries to compute from an empty value as a starting point.

I am pretty sure that this issue and #3306746: DateTime::createFromFormat() expects parameter 2 to be string are duplicates. The other issue was created first and has a better description of how to reproduce the bug. This issue has a better explanation of what goes wrong. Each issue has a patch, and they change different parts of the process.

igorbiki’s picture

Patch #2 works for me. Used with paragraphs in a custom block.

cilefen’s picture

Version: 9.5.x-dev » 11.x-dev
sonyavpaa’s picture

Implemented patch #2 and works great with latest Paragraphs 1.17 & Drupal core 10.2.7!

jldust’s picture

Confirming that patch #2 works cleanly on Drupal 10.3.2 with Paragraphs v1.18. Does this change need tests to be updated?

cilefen’s picture

Status: Active » Needs work
Issue tags: -PHP 8.1 +Needs merge request

Tests are expected by default. Maybe a unit test will do.

jldust’s picture

I've created a MR with the patch applied, I'm new to test writing so I'm not sure where to start for this specific issue. If there is a specific guide page I would be happy to look at it!

eelkeblok made their first commit to this issue’s fork.

eelkeblok’s picture

From my limited testing, I noticed that only when the field is empty, the value will indeed be an array. I think the solution from the previous patch is than in fact not quite appropriate; it just takes one of the empty values and passes it through the test of the code. Instead, I changed it to treat it similarly to a NULL value; just bail out when it is an array.

Still needs a test, of course :)

tame4tex made their first commit to this issue’s fork.

tame4tex’s picture

Assigned: Unassigned » tame4tex
tame4tex’s picture

Assigned: tame4tex » Unassigned
Issue summary: View changes
Status: Needs work » Needs review

I was able to reproduce the bug in core in a FunctionalJavascript test using NestedEntityTestForm from the field_test module. That test has been pushed to the MR.

This bug will occur when the following conditions exist:

  1. An ajax submission is triggered on a nested form that contains a datetime (or date range) field.
  2. The submitted datetime field value is invalid and fails Datetime::validateDatetime which means the formState value for the element remains as an array.
  3. The nested form calls EntityFormDisplayInterface::extractFormValues from a validate callback (e.g. NestedEntityTestForm::validateForm or ParagraphsWidget::elementValidate which results in DateTimeItem::setValue getting called and the item's values property gets set to the value array. Then when the nested form is rebuilt on ajax and DateTimeWidgetBase::formElement is called, the call to the date property on line 35 results in DateTimeComputed::getValue being called and because the value is an array, it throws an throwableerror which is not caught but the try/catch and Ajax fails.

Based on these findings I feel the current proposed solution both here and on issue #3052608: DateTimeComputed tries to compute from an empty value, while preventing the bug, is fixing a symptom rather than the cause. I propose instead that we modify DateTimeWidgetBase::massageFormValues and DateRangeWidgetBase::massageFormValues to ensure both valid and invalid values are handled correctly to match the expectations of DateTimeComputed::getValue. If $item['value'] is not a DrupalDateTime, it did not pass validation so it is still an array, we should set it to NULL.

I have pushed this change to the MR and updated the Issue Summary with my findings and the updated proposed resolution.

tame4tex’s picture

Issue summary: View changes
tame4tex’s picture

Issue tags: -Needs merge request
qusai taha’s picture

StatusFileSize
new20.1 KB

Patch file to work with Drupal 10.4.7

altcom_alan’s picture

We had this issue with AJAX loaded paragraphs with date and date range fields and the patch in #24 fixes it for us on Drupal 10.5.2

smustgrave’s picture

Status: Needs review » Needs work
Issue tags: +Needs Review Queue Initiative

Left comments on the MR.

casey made their first commit to this issue’s fork.

tcrawford’s picture

Patch #24 worked for us on Drupal 11.2.8.

Version: 11.x-dev » main

Drupal core is now using the main branch as the primary development branch. New developments and disruptive changes should now be targeted to the main branch.

Read more in the announcement.

herved made their first commit to this issue’s fork.

herved’s picture

StatusFileSize
new20.45 KB

Coming from #3052608: DateTimeComputed tries to compute from an empty value

The current fix here looks correct to me and also solves the issue I'm having, which is the same as the IS (paragraphs with empty required date field).
I think #21 is correct and the other issue is fixing a symptom while this here is fixing the cause by not creating field items if the date is invalid.
Attaching current MR snapshot.