Helper issue: #2353411: Helper issue for #2164601 - stop auto-creating FieldItems
Sandbox branch: 2164601-list_auto_create in http://cgit.drupalcode.org/sandbox-yched-1736366

Problem/Motivation

Just trying to access an arbitrary delta in a FieldItemList automatically creates an "empty" FieldItem object (with all NULL properties) and adds it to the list at that delta.

This allows nice DX:

  • for accessing values without bothering whether the field is empty or not:
    $entity->field[0]->value;
    (you'll get NULL if the field is empty, which is what you want)
  • for easy assignment of field values on-the-fly:
    $entity->field[0]->value = 'foo';
    $entity->field[1]->value = 'bar';
    $entity->field[2]->value = 'baz';
    

    (works because accessing the delta creates an item ready for you to populate).

Yet this is very wrong for several reasons:

  • Looking at the object modifies the object :-p
  • This produces inconsistent/confusing behavior:
    isset($node->body[0]); // TRUE - OK
    isset($node->body[42]); // FALSE - OK 
    $node->body[42];        // TextWithSummaryItem - What ? You just said it didn't exist ?
    

    Offset doesn't exist, but I can still get it and it's a real, non NULL object.

  • The "ArrayAccess" metaphor doesn't really fly: the DX above is nice, but is not what you'd expect from a real array. $entity->field[2]->value would fatal out if $entity->field was an actual array with no entry at index 2.
  • In terms of data strutures, a FieldItemList (or more generally an ItemList) is supposed to be a sequence of Items with subsequent numeric indexes starting at 0. That's what the data model is, and that's what consumer code that "does stuff with ItemLists" expect. But the code above just created a "whole" in the deltas, and iterating on the list will jump from 0 to 42.
    Even though we present an array-like syntax for DX, the API should enforce the consistency of the underlying data structure, and make sure a List stays a List no matter what you try to do with it.
  • This makes it *very* easy to write code that inadvertently adds new "empty" Items at runtime, de-facto forcing any code that "does stuff with ItemLists" to account for the possibility of hitting one of those, which they typically do not expect.
    We have sprinkled ->filterEmptyItems() calls in various places in core to remove those Items before running the ItemList through some specific, identified steps (e.g formatters, entity save...). Still I found myself helping a participant to the Amsterdam Sprint, and saw she had to manually safeguard her foreach ($entity->field as $item) { with a manual if ($item->isEmpty()) {break;}. Good thing she bumped into it early; worst case, your code works, but breaks later when you enable another contrib module that tries to read field values from the entity before it reaches you.
    In other terms, big DX fail.

Proposed resolution

We discussed that issue with @fago, @Berdir, @plach, @amateescu at the post-conf sprint in Amsterdam.

We came up with the following proposal:
- No more creating ItemLists with an Item with NULL properties - that's being done in #1988492: Avoid having empty field items by default
- No more auto-creating Item objects on accessing List[$non_existent_delta] - this is what this issue is about.
More specifically:

When a (non computed) field has no value:
- ItemList::$list is an empty array()
- foreach ($entity->field as $item) produces 0 iterations
- $entity->field->first() returns NULL
- $entity->field->property (ItemList::__get()) returns NULL
- $entity->field->property = 'foo' (ItemList::__set()) creates an Item at delta 0 and populates the property

Exception for computed fields: those are never really "empty", Items will get auto-created, and will appear when reading values or iterating $entity->field. That's actually the only specific meaning of "computed field" (as opposed to "a regular field with computred properties").

More generally:
- $entity->field[unknown delta] (ItemList::offsetGet()) returns NULL
- $entity->field[unknown delta]->property / ->method() thus fails with a fatal
In other words: we safeguard $entity->field->property for nice DX (e.g for mono-valued or required fields), but if you try to do stuff on an explicit delta, you need to ensure it exists with an isset(), just like you would do with a real array.

New items can be added:
- For the first item at delta 0, with the $entity->field->property = 'foo' syntax mentioned above,
- For any delta, by a new ItemList::append() method (with support for the [] syntax):

$entity->field->append(array('property' => 'foo', 'property_2' => 'bar'));
$entity->field[] = array('property' => 'foo', 'property_2' => 'bar');
// Short syntax to only assign the 1st property:
$entity->field[] = 'foo';

There is still a possibility of explicitly creating an "empty" Item by doing $entity->field->append(NULL) and doing nothing with the item after that. Widgets need this for the "add new value" button, and some other code need it to access non-static methods that live on the Item class (e.g Field UI and Item::fieldSettingsForm()).
But then it's your explicit choice, and your fault if those $items leak out to other runtime code.

API changes

- $entity->field[$delta] / $entity->field->get($delta) returns NULL instead of a fresh, empty Item when delta doesn't exist.
- $entity->field->first() returns NULL instead of a fresh, empty Item when the field is empty.

In practice:

- If you want to do stuff with an item (read a value, call a method), you need to ensure it exists first (isset($entity->field[$delta]), as you would with a regular array.

- If you want to add *new* items:
$entity->field[2]->value = 'foo'; doesn't work anymore (fatal error if the delta doesn't already exist)
Instead you do one of:
$entity->field->appendItem(array('value' => 'foo'));
$entity->field[] = array('value' => 'foo');
$entity->field[] = 'foo';

- For DX, the short-hand syntax for the 1st item at delta 0 still works (which is the most common use case):
$entity->field->value; returns the value at delta 0 if the field is not empty, NULL otherwise
$entity->field->value = 'foo'; creates an Item at delta 0 if the field was empty, and populates the value.

CommentFileSizeAuthor
#102 interdiff.txt3.73 KByched
#102 2164601-list_auto_create-102.patch51.32 KByched
#100 interdiff.txt717 bytesyched
#100 2164601-list_auto_create-100.patch51.84 KByched
#90 patch-apply.png9.37 KBRavindraSingh
#89 2164601-list_auto_create-89.patch51.75 KBamateescu
#87 2164601-list_auto_create-87.patch51.75 KBamateescu
#85 2164601-list_auto_create-85.patch52.45 KByched
#81 2164601-list_auto_create-81.patch53.95 KBpfrenssen
#78 2164601-list_auto_create-77.patch53.93 KBjibran
#78 interdiff.txt1009 bytesjibran
#75 2164601-list_auto_create-74.patch53.9 KBjibran
#75 interdiff.txt1.19 KBjibran
#70 2164601-list_auto_create-70.patch53.56 KByched
#68 2164601-list_auto_create-68.patch53.14 KByched
#67 interdiff-63-67.txt1.41 KByched
#67 2164601-list_auto_create-67.patch54.41 KByched
#65 interdiff.txt1.51 KByched
#65 2164601-list_auto_create-65.patch53.06 KByched
#63 interdiff.txt5.4 KByched
#63 2164601-list_auto_create-63.patch54.56 KByched
#59 interdiff.txt9.12 KByched
#59 2164601-list_auto_create-59.patch53.91 KByched
#55 interdiff.txt637 bytesyched
#55 2164601-list_auto_create-55.patch53.58 KByched
#51 interdiff.txt2.03 KByched
#51 2164601-list_auto_create-51.patch52.5 KByched
#50 interdiff.txt5.39 KByched
#50 2164601-list_auto_create-50.patch51.93 KByched
#49 2164601-list_auto_create-49.patch53.22 KByched
#48 interdiff.txt1.51 KByched
#48 2164601-list_auto_create-48.patch53.22 KByched
#46 2164601-list_auto_create-46.patch51.71 KByched
#42 2164601-list_auto_create-42.patch54.1 KByched
#41 interdiff.txt770 bytesyched
#41 2164601-list_auto_create-41.patch58.91 KByched
#39 interdiff.txt636 bytesyched
#39 2164601-list_auto_create-39.patch58.91 KByched
#37 interdiff.txt5.86 KByched
#37 2164601-list_auto_create-37.patch58.91 KByched
#35 interdiff.txt18.64 KByched
#35 2164601-list_auto_create-35.patch61.76 KByched
#30 2164601-list_auto_create-29.patch50.78 KByched
#29 interdiff.txt1.77 KByched
#27 interdiff.txt43.24 KByched
#27 2164601-list_auto_create-27.patch49.01 KByched
#18 interdiff.txt9 KByched
#18 2164601-list_auto_create-18.patch9.77 KByched
#13 interdiff.txt6.99 KByched
#13 2164601-list_auto_create-13.patch6.99 KByched
#11 2164601-list_auto_create-11.patch6.94 KByched
#8 interdiff.txt2.69 KByched
#8 list_auto_create-2164601-8.patch7.95 KByched
#6 list_auto_create-2164601-6.patch5.49 KByched

Comments

yched’s picture

Issue summary: View changes
plopesc’s picture

Hello

It looks like it was done by design. This is the offsetGet() implementation in ItemList class:

public function offsetGet($offset) {
    if (!is_numeric($offset)) {
      throw new \InvalidArgumentException('Unable to get a value with a non-numeric delta in a list.');
    }
    // Allow getting not yet existing items as well.
    // @todo: Maybe add a public createItem() method in addition?
    elseif (!isset($this->list[$offset])) {
      $this->list[$offset] = $this->createItem($offset);
    }
    return $this->list[$offset];
  }

offsetGet() is overloaded in order to create new items at certain index.

Should we remove this overload and provide the public createItem() method as suggested in the @todo??

Regards.

fago’s picture

Offset doesn't exist, but I can still get it and it's a real, non NULL object.

Yeah, it's an object but containing only NULL/empty values, so if you ask it whether it's empty it is.

The thinking behind that was that it doesn't make any semantically difference and this eases creating/setting new items, in particular if you think of the single item case.

However, I guess there are cases where there is a difference between empty and non-existing values, e.g. when you present widgets for some items. I guess this works correctly right now, as the iterator only returns existing items, but I see that this is potentially confusing.

As an alternative I could see us doing a public createItem() method that appends it at the end of the list. But then, I think we still want to keep the current behaviour at least for the first item, so you can safely do $node->title->value? That said, the behaviour would still exist but be limited to the first item, is that less confusing?

yched’s picture

Yes, an explicit createItem() or appendItem() seems preferable to a magic "just ask for it and it suddenly exists".

(Also because the current behavior (->offsetGet(42)) generates non consecutive deltas, which is not something that is supposed to happen, and thus potentially requires renumbering before we do anything important)

So yeah, +1 on appendItem() + "no auto-creation for deltas > 0";
But I'm not sure about delta 0 either...

IMO this is very much related to #1988492: Avoid having empty field items by default and the random "Item objects with NULL values" that pop up at various times in our code flow and makes our code covered with safeguard filterEmptyValues() calls - which I still think is a real problem.

Our conceptual model relies on the ItemList being a mere glorified numeric array of items. In that sense,
- An Item object with NULL values is not the same as no object.
- An (Item)List with that has an Item with NULL values is not the same as an empty List.

During the lifecycle of an entity, our ItemLists keep going from one flavor of "empty" to the other, depending on the sequence of "accessing values" (which creates NULL-values Item objects) and filterEmptyValues() calls that get added here and there to clean them up before running code that needs "empty" to mean "empty".

So for a same field that is semantically "empty", a foreach ($items as $delta => $item) will sometimes produce one iteration (delta 0 / $item with NULL values) and sometimes none. That's ... not great :-)

Note that in D7, _field_filter_items() / hook_field_is_empty() was only there to process widget submitted form values (e.g discard textfields left empty) before promoting them to the $entity->field_foo model structures. That was 2 calls (of an '_' prefixed funtion...), one in form validate, one in form submit. Once the values were in the $entity model, empty was array(), and no further filtering was performed.
In current HEAD, there are 13 calls to FieldItemList::filterEmptyValues().

I think we should get back to that. There should be one flavor of "empty" IMO, and that's ItemList::$list == array().

Proposal in the next comment :-)

yched’s picture

So - I understand that it's handy to have $node->field->value not fatal out if the field is empty.

I'm not fully sure making it "just work even if empty" makes for consistent / sustainable DX - that's not a behavior that you would realistically expect from someting that is conceptually an array of values. If your data model is "the field might be empty", then doing isset() checks is only consistent with the data model. But let's say we try to preserve that for now.

We do have a "prototype" of an Item object with NULL values available somewhere, right ? What if :
- an empty ItemList is just $this->list == array()
Iterating on it makes 0 iterations, offsetGet(any $delta) returns NULL / possibly even triggers a notice ?
- the magic getters make $entity->field_foo->value return property values from the prototype *without* copying the prototype into $this->list - the list stays empty.
- the magic setters make $entity->field_foo->value = $value trigger the "clone proptotype as a new item in the previously empty list" behavior.

Would that work ?

yched’s picture

Status: Active » Needs review
StatusFileSize
new5.49 KB

Probably a bit rough still, but let's see what fails.

Status: Needs review » Needs work

The last submitted patch, 6: list_auto_create-2164601-6.patch, failed testing.

yched’s picture

Status: Needs work » Needs review
StatusFileSize
new7.95 KB
new2.69 KB

Should fix a couple fails.

Problem is of course that not auto-creating items breaks syntax like:

$entity = entity_create('entity_test', array());
$entity->field_foo[0]->value = 1;
$entity->field_foo[1]->value = 2;

Mulling on that...

The last submitted patch, 8: list_auto_create-2164601-8.patch, failed testing.

The last submitted patch, 8: list_auto_create-2164601-8.patch, failed testing.

yched’s picture

Priority: Normal » Critical
StatusFileSize
new6.94 KB

Restarting this - will try to summarize the discussion we had at the Amsterdam post-conf sprint today.

For now, just rerolling the current patch.

Status: Needs review » Needs work

The last submitted patch, 11: 2164601-list_auto_create-11.patch, failed testing.

yched’s picture

Status: Needs work » Needs review
StatusFileSize
new6.99 KB
new6.99 KB

Keep auto-create for the $entity->field->value = 'foo' syntax

Status: Needs review » Needs work

The last submitted patch, 13: 2164601-list_auto_create-13.patch, failed testing.

yched’s picture

[recap of the solution discussed in Amsterdam, moved to the issue summary]

yched’s picture

Exception for computed fields: [...] Items will get auto-created, and will appear when reading values or iterating $entity->field

It's not completely clear when this Item gets created though (straight in ItemList::__construct(), as we currently do for all fields ?)
Also, not clear how many items should be auto-created. Why couldn't there be computed fields with multiple values ?

yched’s picture

Side note: the only "computed field" we seem to have in core atm is Shortcut 'path'...

yched’s picture

StatusFileSize
new9.77 KB
new9 KB

OK, here's a patch that should install.

Ugly bit: in current HEAD, FieldItemList::applyDefaultValue() does:

if (no actual value was specified) {
  $this->first()->applyDefaultValue(FALSE);
}

so that the field type can provide a "default default value" in its Item::applyDefaultValue() method in case the field definition provides no default value.

That only worked because we always created one Item, which we need to stop doing. For now, I replaced that with the following:

if (no actual value was specified) {
  // Create one field item and let it apply its defaults. Remove the item if
  // this ended up doing nothing...
  $this->appendItem();
  $this->first()->applyDefaultValue(FALSE);
  $this->filterEmptyItems();
}

But this can only be a temporary stop gap. We need to get rid of that practice of field types using FieldItem::applyDefaultValues(), creating an item to figure out whether there should be an item is absurd. Field types that need this have other ways:
- use the 'default_value' annotation for the field type added by #2318605: uuid, created, changed, language default value implementations need to be updated
- implement FieldItemList::processDefaultValue(),
- override FieldItemList::applyDefaultValue() itself.

yched’s picture

Status: Needs work » Needs review

NR for testbot.

Also, from the plan in #15:

$entity->field->append(NULL / $empty_array); does nothing and is a no-op

Not sure we'll be able to do that. In addition to the ugly workaround mentioned in #18, widgets rely on that for the empty rows added by the "add new" button.

Status: Needs review » Needs work

The last submitted patch, 18: 2164601-list_auto_create-18.patch, failed testing.

yched’s picture

Issue summary: View changes

- Opened helper issue : #2353411: Helper issue for #2164601 - stop auto-creating FieldItems
- pushed the patch to 2164601-list_auto_create in http://cgit.drupalcode.org/sandbox-yched-1736366
- Expanded the issue summary with a description of the problems caused by the current situation, and a copy of #15 for the solution that was agreed in Amsterdam.

yched’s picture

Title: $entity->field_name->offsetGet(N) works for any N » Stop auto-creating FieldItems on mere reading of $entity->field[N]

Also, clearer title.

yched’s picture

Issue summary: View changes
yched’s picture

Issue summary: View changes

Added an "API changes" section

yched’s picture

Down to 30-ish fails in the helper issue, some of them hairy, will report soonish.

Meanwhile, found the following while working on those fails:
#2354177: Shortcut::getRouteParams() should be named getRouteParameters() for consistency
#2354199: Calling ContentEntity::updateOriginalValues() should not be the task of the SqlContentEntityStorage

yched’s picture

Issue summary: View changes

Update to the code samples in the issue summary : we'll also be supporting
$entity->field[] = 'value'

(same polymorphism as supported by TypedData's Item::setValue())

yched’s picture

Status: Needs work » Needs review
StatusFileSize
new49.01 KB
new43.24 KB

Should be down to just NodeTranslationUITest failing. More on that one in a later comment.

Summary of the interdiff:

  • Adapts code & tests to "$items[$unknown_delta] is NULL"
  • Makes ItemList::appendItem() return the new item - easier DX for most use cases.
  • Support $items[] = 'foo' (i.e allow same polymorphism for appendItem() than for Item::setValue())
  • Adds ItemList::clearItems() for API completeness, and use it in ContentEntityNormalizer::denormalize()
  • Moves the auto-creation of the Item for computed fields from FieldItemList::__construct() to ItemList::get(). This is not only about creation time.
  • Adapts places where we need an Item object just for the sake of calling some "field type" logic that doesn't live in static methods. Those are a consequnce of the bad design decision to make Item classes be the primary "field type class" instead of the List class. Too bad, but we have to live with it.
    That's:
    - Field UI calling Item::field(Storage)settingsForm(),
    - The current practice for "allowed values" (Item implements OptionsProviderInterface), which should get deprecated in #2002138: Use an adapter for supporting typed data on ContentEntities
    - The current practice of Item::applyDefaultValue(), which we really should get rid of. Separate discussion though.
  • (Field)ItemList::offsetExist() cannot use get(), because that's what Twig relies on for the entity.field.value syntax, and we cannot have exceptions in there.
  • Fixed FieldNormalizer / FieldItemNormalizer to use appendItem() instead of $items[$new_delta] (those normalizer classes could use a serious cleanup, but separate issue...)
  • Adapts DenormalizeTest::testMarkFieldForDeletion() to rely on a field's default value rather than on the dummy empty Item that was present in HEAD. This is what the test is actually about: ensure that an entity is created with default values for the fields that are not explicitly specified in the incoming data.
  • - For debugging sanity, renamed the assertXxx() methods in EntityFieldTest to regular protected sub methods. When something in there fails, Simpletest only reports an error on the top-most assertXxx() method in the callstack. So, definitely not a good idea for a method with 200+ lines and tons of sub-asserts.

Status: Needs review » Needs work

The last submitted patch, 27: 2164601-list_auto_create-27.patch, failed testing.

yched’s picture

Status: Needs work » Needs review
StatusFileSize
new1.77 KB

Fix NodeTranslationUITest.

In short: "comment as field" relies on the fact that the "comment status" field always returns a value - if no stored value, then the "default value" needs to be returned (which is not the behavior of Field API's "default value", which only applies at entity creation time).

This is currently done by CommentItem overriding FieldItemBase::__get(), which doesn't work now that "no stored value" means there is no Item object created.
Patch does it by CommentFieldItemList overriding ItemList::get().

yched’s picture

StatusFileSize
new50.78 KB

And the patch

yched’s picture

Ok, green.

Patch still has a couple @todos for docs / pending questions (will summarize those in a later comment), but should otherwise be ready for reviews.

berdir’s picture

+++ b/core/modules/system/tests/modules/entity_test/entity_test.module
@@ -322,13 +322,11 @@ function entity_test_entity_test_insert($entity) {
-    if ($items) {
-      if ($items[0]->value == 'no access value') {
-        return AccessResult::forbidden()->cacheUntilEntityChanges($items->getEntity());
-      }
-      elseif ($operation == 'delete' && $items[0]->value == 'no delete access value') {
-        return AccessResult::forbidden()->cacheUntilEntityChanges($items->getEntity());
-      }
+    if ($items->value == 'no access value') {
+      return AccessResult::forbidden()->cacheUntilEntityChanges($items->getEntity());
+    }
+    elseif ($operation == 'delete' && $items->value == 'no delete access value') {
+      return AccessResult::forbidden()->cacheUntilEntityChanges($items->getEntity());
     }

Why remove the $items check? it is defined as a n optional argument that can be NULL...

swentel’s picture

+++ b/core/lib/Drupal/Core/Field/FieldItemList.php
@@ -163,28 +158,39 @@ public function setValue($values, $notify = TRUE) {
+    // For empty fields, $entity->field->property = $value automatically ¶

nitpick - redundant space

yched’s picture

While working on deltas renumbering, it occured to me that filterEmptyItems() is currently inconsistent : #2365585: FieldItemList::filterEmptyItems() renumbers deltas but does not update the Items
The next patches here will most probably include that fix, but it's already a bug in HEAD so we might as well fix it separately.

yched’s picture

StatusFileSize
new61.76 KB
new18.64 KB

Updated patch:

- Fixes @berdir's and @swentel's remarks in #32 / #33

- Ensure subsequent deltas : renumber the subsequent deltas on unset($list[N])
Adds a $list->drop(N) method as a non-array-syntax equivalent.

- Ensure subsequent deltas : only support 0-based subsequent deltas in $list->setValue($array)
$list->setValue(array(1 => 'foo', 3 => 'bar')) raises an exception.
That revealed a couple places that currently set field values on non 0-based consecutive deltas:
- $user->removeRole()
- The custom form code for $term->parent / $forum->parent (the form values need to be massaged *once*, before building the entity - thus done in validate()).

- Ensure delta consistency : updates $item->getName() (which is supposed to be the item delta) when any renumbering happens (in unset() case above, or using filterEmptyItems()). This includes #2365585: FieldItemList::filterEmptyItems() renumbers deltas but does not update the Items, which would be best to get in first.

- Adds some missing docs, adds the newly added methods to ItemListInterface (and thus also adds implementations in Sequence.php) :
appendItem(), drop(), clearItems()

- Opened #2356623: Remove usages of FieldItem::applyDefaultValue() to stop using FieldItem::applyDefaultValue(), and referenced it in the existing @todo

- Dropped the protected ItemList::nextIndex(), not really useful.

yched’s picture

To be discussed :

- Instead of an exception on $list->setValue(array with non 0-based deltas), do we want to just silently renumber the passed-in values (array_values()) ?
Might be easier DX ?

- At some point I considered forbidding NULL items altogether, by:
dropping the item on $list->set(N, NULL) / $list[N] = NULL
filtering out NULL values in $list->setValue($array)

I abandoned that, because you can always assign NULL on the item directly : $list[N]->setValue(NULL).
Also, while items carring the NULL walue are not wanted for FieldItemLists, I cannot say for sure we don't want to support that in ItemLists generally.

In the helper issue, @amateescu suggested that we could have FieldItem::setValue(NULL) notify the FieldItemList parent, which in turn could remove the element from the list. I still think it would make some unintuitive/surprising behavior - e.g. :

$items[N]->setValue($value); // if $value is NULL, this drops the item and renumbers deltas
$items[N]->foo(); // now runs on the item that was at N+1, and could fatal because it could be NULL->foo().

// Or, when an item is passed as a param to a func / method:
function foo($item) {
  // happens to do :
  $item->setValue(NULL);
}
// Calling foo($items[N]) modifies $items - but I didn't pass $items... 

I guess we could forbid *FieldItem* to receive the NULL values :
- have FieldItem::set(NULL) raises an exception
- have FieldItemList::setValue($array) filters out NULL values
Not fully decided yet.

- FWIW, I stumbled across SplDoublyLinkedList, which is the SPL implementation of a "List" data structure. I'm actually thinking it would make sense to switch the internal ItemList::$list to a SplDoublyLinkedList instead of an array, since it already does exactly what we want in terms of index management / 0-based sequential deltas / renumber on drop - but in C code. And well, we want a List, this is a List...
Still mulling on that, but opinions welcome.

yched’s picture

StatusFileSize
new58.91 KB
new5.86 KB

Instead of an exception on $list->setValue(array with non 0-based deltas), do we want to just silently renumber the passed-in values (array_values()) ? Might be easier DX ?

Did that. Patch reverts the fixes in TermForm, ForumForm, User.

+ Opened #2368807: Remove special support for NULL values in FieldItemList as a side issue

Status: Needs review » Needs work

The last submitted patch, 37: 2164601-list_auto_create-37.patch, failed testing.

yched’s picture

Status: Needs work » Needs review
StatusFileSize
new58.91 KB
new636 bytes

array_walk() takes its array by ref, array_walk(func()) = no good.

Status: Needs review » Needs work

The last submitted patch, 39: 2164601-list_auto_create-39.patch, failed testing.

yched’s picture

Status: Needs work » Needs review
StatusFileSize
new58.91 KB
new770 bytes

Shakes fist at duplicate code.

yched’s picture

alexpott’s picture

isset($node->is_cat_dead[42]); // FALSE - OK 
isset($node->is_cat_dead[42]); // TRUE -OK

According to @dawehner this is how nature works.

fago’s picture

Did a first review - trying to stay out of quantum theory for now, but maybe this can make newton happy again ;-)

  1. +++ b/core/lib/Drupal/Core/Field/WidgetBase.php
    @@ -168,6 +168,11 @@ protected function formMultipleElements(FieldItemListInterface $items, array &$f
         for ($delta = 0; $delta <= $max; $delta++) {
    +      // Add a new empty item if it doesn't exist yet at this delta.
    +      if (!isset($items[$delta])) {
    +        $items->appendItem();
    

    That code reads a bit weird as it doesn't add it on a special delta. Maybe it could check teh count first and append atems as long until the target count is reached?
    Afterwards, it could decide to just loop over items?

  2. +++ b/core/lib/Drupal/Core/TypedData/ListInterface.php
    @@ -58,16 +58,25 @@ public function get($index);
    +  public function drop($index);
    

    I think "remove()" would be a better method name. Also, append has the item suffix, this one doesn't. Whatever we end up using should be consistent - I'd tend to have an item suffix as it makes it more clear what you are dealing with.

  3. +++ b/core/lib/Drupal/Core/TypedData/ListInterface.php
    @@ -80,4 +89,34 @@ public function set($index, $item);
    +   * Removes all items from the list.
    

    Comment uses different wording that method name.

  4. +++ b/core/lib/Drupal/Core/TypedData/ListInterface.php
    @@ -80,4 +89,34 @@ public function set($index, $item);
    +   * @return static
    

    $this

  5. +++ b/core/lib/Drupal/Core/TypedData/ListInterface.php
    @@ -80,4 +89,34 @@ public function set($index, $item);
    +  public function clearItems();
    

    Not sure that's needed, as you could just set it to an empty array?

    It clears the list, not the items imho.

  6. +++ b/core/lib/Drupal/Core/TypedData/Plugin/DataType/ItemList.php
    @@ -110,29 +112,61 @@ public function get($index) {
    +// @todo what would be an example of "getItemDefinition()->isComputed()" ?
    +    if ($index == 0 && !isset($this->list[0]) && ($this->definition->isComputed() || $this->definition->getItemDefinition()->isComputed())) {
    

    That's a bit hairy, but I could see things like the comment new property being a computed item, while the list is not computed but has one fixed item. Maybe, this could be better handled in a separate field item list class for single, computed fields?

  7. +++ b/core/lib/Drupal/Core/TypedData/Plugin/DataType/ItemList.php
    @@ -146,6 +180,9 @@ public function first() {
         return isset($this->list) && array_key_exists($offset, $this->list) && $this->get($offset)->getValue() !== NULL;
    

    As the API did not differentiate on having the $item being created or not, that followed that. I.e., it does not matter whether there is a NULL property or not.

    But this patch is going to change this, so should be this method. -> It should tell you whether the object exists imho.

  8. +++ b/core/modules/comment/src/CommentFieldItemList.php
    @@ -0,0 +1,33 @@
    +    // The Field API only applies the "field default value" to newly created
    +    // entities. In the specific case of the "comment status", though, we need
    +    // this default value to be also applied for existing entities created
    +    // before the comment field was added, which have no value stored for the
    +    // field.
    

    Interesting work a rount, but a bit weird as it would not work with queries. That#s another issue though.

  9. +++ b/core/modules/datetime/src/Tests/DateTimeItemTest.php
    @@ -98,7 +98,7 @@ public function testSetValue() {
    -    $entity->set('field_datetime', $value);
    +    $entity->field_datetime->value = $value;
    

    Is that change necessary? It shouldn't be imho?

  10. +++ b/core/modules/field_ui/src/Form/FieldStorageEditForm.php
    @@ -108,7 +109,9 @@ public function buildForm(array $form, FormStateInterface $form_state, FieldConf
    +    $item = $items->first() ?: $items->appendItem();
    

    Would it make sense to add an auto-append flag to first(), which auto-appends an item if none is there?

    We've got some code which follows recent "best practice" of doing an explicit:

    "$author_name = $items->first()->value;"
    Given that patch, that would change form being bad / unreliable :-/

  11. +++ b/core/modules/node/src/Tests/NodeAccessLanguageAwareTest.php
    @@ -97,7 +97,7 @@ protected function setUp() {
    -    $translation->field_private[0]->value = 0;
    +    $translation->field_private->value = 0;
    

    While I agree with this being a good change, it's a rather big API change now.

    A thought: Is it possible to migitate it by allowing write operations like this to create the object? Does it work to
    - let the magic getter of the list return a new empty item, pass it the parent but do not reference it in the list
    - use on change in the item, to notify the parent of the write
    - detect the write on the orphaned child item and so adopt it as a new child?

    That way we'd avoid the API change and could continue supporting the explicit usage of $entity->field->first()->value
    ?

xjm’s picture

Issue tags: +Triaged D8 critical
yched’s picture

StatusFileSize
new51.71 KB

Reroll after #2365585: FieldItemList::filterEmptyItems() renumbers deltas but does not update the Items got in.

Will process @fago's feedback from #44 after that.

Status: Needs review » Needs work

The last submitted patch, 46: 2164601-list_auto_create-46.patch, failed testing.

yched’s picture

Status: Needs work » Needs review
StatusFileSize
new53.22 KB
new1.51 KB

Fixed the EntityReferenceFormatterTest that were added in #2370703: ER's "autocreate" feature is mostly broken (and untested), and that used the "$items[$new_delta] = ..." syntax.

This revealed an existing bug, though : on EntityRefItem,
- $item->entity = $existing_entity populates $item->target_id with $existing_entity->id()
- but $item->setValue(array('entity' => $existing_entity)) does not, which triggers lots of interesting fails.

Opened #2386559: ERItem::setValue(array('entity' => $entity) produces broken Items for that - meanwhile, patch here uses a workaround syntax that works.

yched’s picture

StatusFileSize
new53.22 KB

Reroll

yched’s picture

StatusFileSize
new51.93 KB
new5.39 KB

Re: @fago's review in #44 :

1. Yeah, bugged me a bit too, but didn't really find a better shape for that code.

"check the count first and append items as long until the target count" : well, that would be

$nb_to_add = $max - $items->count();
for ($i = 0; $i < $nb_to_add, $i++) {
  $items->appendItem();
}

Is that really better than doing it in the existing loop ?

2. Agreed, renamed ItemList::drop($index) to removeItem($index)

3. 4. 5. Well, in the spirit of enriching the API to manipulate the lists (append, drop, filter...) I though it could be nice to add a method to enpty the list rather than just relying on setValue(array()), which is a TypedData-ism, but is less immediately clear in terms of "operation on a list data structure".
But well, turns out SplDoublyLinkedList, which is the data structure we could inspire from, has no method for emptying the list either. So, well... removed it :-)

6. $this->definition->isComputed() || $this->definition->getItemDefinition()->isComputed()
Yeah, the weird thing is that *I* was the one that added that code in patch #8 above - but I can't remember why :-)
I'll try to remove it in a next patch and see what happens :-)

7. You mean this ?

  public function offsetExists($offset) {
    -return isset($this->list) && array_key_exists($offset, $this->list) && $this->get($offset)->getValue() !== NULL;
    +return isset($this->list[$offset]);
  }

8. True, doesn't work with queries - but that's also the case with the current workaround in CommentItem::__get()
Query-wise, you currently need to differentiate "entities where the comment status is X" and "entities where the comment status not unspecified" (or do an "= X OR is NULL") anyway. Patch doesn't change that, it only slighlty cahnges the way "missing comment status" is filled in at runtime.

But that makes me think that the behavior added here in CommentFieldItemList::get() should let us remove the similar behavior in CommentItem::__get(). Will try that in a next patch.

9. Right, not really needed I think - it's just that $entity->get('field_datetime')->set(0, $value) / $entity->set('field_datetime', $value) feel very outdated :-) Reverted.

Will report on 10. & 11.

yched’s picture

StatusFileSize
new52.5 KB
new2.03 KB

As mentioned in #50 :
- Removes CommentItem::__get(), should be taken care of by CommentFieldItemList::get() now.
- Tries to simplify the code supporting computed fields, because "computed fields" are still a very blurry concept at this point.

Status: Needs review » Needs work

The last submitted patch, 51: 2164601-list_auto_create-51.patch, failed testing.

fago’s picture

ad 7.: yes exactly, this and interdiffs look good.

The last submitted patch, 50: 2164601-list_auto_create-50.patch, failed testing.

yched’s picture

Status: Needs work » Needs review
StatusFileSize
new53.58 KB
new637 bytes

Fix CommentItemList : if get() ensures there's always at least one Item, then offsetExists() needs to tell the same story.

That's interesting, because as per discussion with @fago here at the Ghent sprint, even though the comment fields are not computed fields strictly speaking (we do store the values, we just need to kick in and make sure there's a value even when there is none currently stored), CommentItemList in that latest patch is a good example of how a "computed field" should work :
- override ListClass::get() to make sure the ItemList contains the correct number of Items
- override ListClass::offsetExists() accordingly

It could be doable to provide helpers in a base ComputedFieldItemListBase class to provide "guidelines" for implementors. To be explored in a separate issue that clarifies what exactly are "computed fields" and how they should behave.

yched’s picture

Issue tags: +Ghent DA sprint
fago’s picture

Status: Needs review » Needs work

It could be doable to provide helpers in a base ComputedFieldItemListBase class to provide "guidelines" for implementors. To be explored in a separate issue that clarifies what exactly are "computed fields" and how they should behave.

Yep, do we already have an issue for clarifying computed fields usage?

  1. +++ b/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorage.php
    @@ -1077,7 +1077,7 @@ protected function mapToStorageRecord(ContentEntityInterface $entity, $table_nam
    -          $value = $entity->$field_name->first()->getValue();
    +          $value = $entity->$field_name->isEmpty() ? array() : $entity->$field_name->first()->getValue();
    

    ouch. isEmpty() checks will trigger that check down the tree, so having it to introduce here sucks. Why not just get the value of the whole list and then get the first item?

  2. +++ b/core/lib/Drupal/Core/Field/BaseFieldDefinition.php
    @@ -463,9 +463,12 @@ public function setDefaultValue($value) {
    +      // If the field is empty, create an item so that we can use it as the
    +      // "options provider".
    +      // @todo we should clear it afterwards ?
    +      return $items->first() ?: $items->appendItem();
    

    As discussed, we'll still need a way to make this things easier. For BC, first() should probably even default to it - let's dicuss?

  3. +++ b/core/lib/Drupal/Core/TypedData/ListInterface.php
    @@ -81,6 +80,27 @@ public function set($index, $item);
    +   * Appends a new item to the list
    

    Missing trailing point.

  4. +++ b/core/lib/Drupal/Core/TypedData/Plugin/DataType/ItemList.php
    @@ -110,29 +112,60 @@ public function get($index) {
    +    // Support setting values via typed data objects.
    +    if ($value instanceof TypedDataInterface) {
    +      $value = $value->getValue();
    +    }
    

    This should be in __set() only as it's not something what makes sense for typed data general, but for the entity field api only to support:
    $entity->field = $entity2->field;

  5. +++ b/core/modules/datetime/src/Tests/DateTimeItemTest.php
    @@ -88,7 +88,7 @@ public function testSetValue() {
    -    $entity->get('field_datetime')->set(0, $value);
    +    $entity->set('field_datetime', $value);
    

    Why is that 0 gone?

pfrenssen’s picture

Read through the patch, have not actually tried it out.

  1. +++ b/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorage.php
    @@ -1287,7 +1287,7 @@ protected function loadFieldItems(array $entities) {
    -            $entities[$row->entity_id]->getTranslation($row->langcode)->{$field_name}[$delta_count[$row->entity_id][$row->langcode]] = $item;
    +            $entities[$row->entity_id]->getTranslation($row->langcode)->{$field_name}->appendItem($item);
    

    Much better :)

  2. +++ b/core/lib/Drupal/Core/Field/FieldItemList.php
    @@ -133,23 +128,27 @@ public function setValue($values, $notify = TRUE) {
    +      // Non-numeric keys are not supported.
    +      array_walk($values, function ($value, $index) {
    +        if (!is_numeric($index)) {
               throw new \InvalidArgumentException('Unable to set a value with a non-numeric delta in a list.');
             }
    -        elseif (!isset($this->list[$delta])) {
    +      });
    +
    +      // Assign incoming values. Keys are renumbered to ensure 0-based
    +      // sequential deltas. If possible, reuse existing items rather than
    +      // creating new ones.
    +      foreach (array_values($values) as $delta => $value) {
    +        if (!isset($this->list[$delta])) {
               $this->list[$delta] = $this->createItem($delta, $value);
             }
             else {
               $this->list[$delta]->setValue($value, FALSE);
             }
           }
    

    We're looping twice over $values now. Wouldn't it be better to only loop once?

  3. +++ b/core/lib/Drupal/Core/Field/FieldItemList.php
    @@ -161,28 +160,39 @@ public function setValue($values, $notify = TRUE) {
       public function __isset($property_name) {
    -    return $this->first()->__isset($property_name);
    +    if ($item = $this->first()) {
    +      return $item->__isset($property_name);
    +    }
    +    return FALSE;
       }
    

    Can be rewritten in one line:

      return ($item = $this->first()) && $item->__isset($property_name);
    

    But this is perhaps less readable.

  4. +++ b/core/lib/Drupal/Core/TypedData/Plugin/DataType/ItemList.php
    @@ -61,27 +61,29 @@ public function setValue($values, $notify = TRUE) {
    +      array_walk($values, function ($value, $index) {
    +        if (!is_numeric($index)) {
               throw new \InvalidArgumentException('Unable to set a value with a non-numeric delta in a list.');
             }
    -        elseif (!isset($this->list[$delta])) {
    +      });
    +
    +      // Assign incoming values. Keys are renumbered to ensure 0-based
    +      // sequential deltas. If possible, reuse existing items rather than
    +      // creating new ones.
    +      foreach (array_values($values) as $delta => $value) {
    +        if (!isset($this->list[$delta])) {
               $this->list[$delta] = $this->createItem($delta, $value);
             }
             else {
               $this->list[$delta]->setValue($value);
             }
           }
    

    Looping twice over $values.

  5. +++ b/core/lib/Drupal/Core/TypedData/Plugin/DataType/ItemList.php
    @@ -110,29 +112,60 @@ public function get($index) {
    +    if ($index == 0 && !isset($this->list[0]) && ($this->definition->isComputed())) {
    

    Parentheses around $this->definition->isComputed() are not needed.

  6. +++ b/core/lib/Drupal/Core/TypedData/Plugin/DataType/ItemList.php
    @@ -110,29 +112,60 @@ public function get($index) {
    +  public function removeItem($index) {
    +    if (!is_numeric($index)) {
    +      throw new \InvalidArgumentException('Unable to drop a non-numeric delta.');
    +    }
    +    if (isset($this->list) && array_key_exists($index, $this->list)) {
    +      // Remove the item, and reassign deltas.
    +      unset($this->list[$index]);
    +      $this->rekey($index);
    +    }
    +    return $this;
    +  }
    

    Throw an exception if the item is not found in the list?

  7. +++ b/core/lib/Drupal/Core/TypedData/Plugin/DataType/ItemList.php
    @@ -110,29 +112,60 @@ public function get($index) {
    +    foreach ($this->list as $delta => $value) {
    +      if ($delta >= $from_index) {
    +        $value->setContext($delta, $this);
    +      }
    +    }
    

    You can use for ($i = $from_index, $i < count($this->list), $i++) instead of the foreach() to avoid looping over unneeded indexes.

  8. +++ b/core/modules/field_ui/src/Form/FieldStorageEditForm.php
    @@ -8,6 +8,7 @@
    +use Drupal\Core\Field\FieldItemListInterface;
    

    This does not need to be added.

  9. +++ b/core/modules/hal/src/Normalizer/ContentEntityNormalizer.php
    @@ -182,20 +182,16 @@ public function denormalize($data, $class, $format = NULL, array $context = arra
    +      $items->setValue(array());
    +      if ($field_data) {
    +        // Denormalize the field data into the FieldItemList object.
    +        $context['target_instance'] = $items;
    +        $this->serializer->denormalize($field_data, get_class($items), $format, $context);
    +      }
    

    I have trouble understanding what is going on here, but if we have no $field_data then we're not doing anything with $items. Is it then still necessary to retrieve them and set their values?

  10. +++ b/core/modules/hal/src/Normalizer/FieldItemNormalizer.php
    @@ -99,20 +99,17 @@ protected function constructValue($data, $context) {
    +    // instead, create a new item for the entity in the requested language.
    +    $entity_translation = $item->getEntity()->getTranslation($langcode);
    

    <nitpick>Capitalize 'instead'</nitpick>

yched’s picture

Status: Needs work » Needs review
StatusFileSize
new53.91 KB
new9.12 KB

re @fago #57:

do we already have an issue for clarifying computed fields usage?

Not yet, created #2392845: Add a trait to standardize handling of computed item lists

1. Fixed
2. Discussed live with @fago at the Ghent sprint, we agreed $items->first($autocreate_if_empty) is not needed in the end. Very little code actually has a use for "get the first item, add one if not exists". One of the places that do (BFD/FSConfig::getOptionsProvider()) actually should *not* leave the created item in the entity, patch fixes that.
3. Fixed
4. Existing code. @fago thinks it is incorrect, but that's for a separate issue
5. Because FieldItem::set($non_existing_delta, $value) now raises an exception. That's actually consistent with what the phpdoc for ListInterface::set() currently says : "*Replaces* the item at the specified position in this list".

re @pfrenssen #58:

1. :-)
2. Discussed live with @fago & @pfrenssen : we decided to drop the exception on non-numeric keys here, for perf reasons.
3. Yeah, matter of taste, but less readable IMO :-)
4. Same as 2. Note that #2381777: Unify setValue() implementations in ItemList & FieldItemList will remove the code duplication.
5. Fixed
6. Discussed with @fago & @pfrenssen, changed to : removeItem($index) throws an exception when the index is not in the list.
7. True - also, added the missing phpdoc
8. Fixed
9. Explained live at the Ghent sprint :-)
10. Fixed

fago’s picture

Thanks, patch looks already pretty solid.

5. Because FieldItem::set($non_existing_delta, $value) now raises an exception. That's actually consistent with what the phpdoc for ListInterface::set() currently says : "*Replaces* the item at the specified position in this list".

I still think that's an unnecessary API break at this stage - imo it should be totally valid to do: $node->title->set(0, array('value' => 'foo'));

  1. +++ b/core/lib/Drupal/Core/Field/FieldItemList.php
    @@ -205,17 +207,18 @@ public function defaultAccess($operation = 'view', AccountInterface $account = N
    +      // Create one field item and give it a chance to apply its defaults.
    +      // Remove it if this ended up doing nothing...
    +      // @todo Having to create an item in case it wants to set a value is
    +      // absurd. Remove that in https://www.drupal.org/node/2356623.
    +      $item = $this->first() ?: $this->appendItem();
    +      $item->applyDefaultValue(FALSE);
    +      $this->filterEmptyItems();
    

    I think that should just be removed. The default of a field has to be in the field definition.

    We should apply defaults when appending an item though, such that a possible default to an item property would be applied.

  2. +++ b/core/lib/Drupal/Core/Field/WidgetBase.php
    @@ -169,6 +169,11 @@ protected function formMultipleElements(FieldItemListInterface $items, array &$f
         for ($delta = 0; $delta <= $max; $delta++) {
    +      // Add a new empty item if it doesn't exist yet at this delta.
    +      if (!isset($items[$delta])) {
    +        $items->appendItem();
    +      }
    

    This is one example of previous valid usage which we should not break unnecessarily.

  3. +++ b/core/lib/Drupal/Core/TypedData/Plugin/DataType/ItemList.php
    @@ -146,16 +175,15 @@ public function first() {
       public function offsetUnset($offset) {
    -    if (isset($this->list)) {
    -      unset($this->list[$offset]);
    -    }
    +    $this->removeItem($offset);
    

    So this leads to an immediate re-key of the list. Does this break code like:
    foreach ($list as $i => $item) {
    if ($item is blue) {
    unset($item[$i]);
    }
    }

    Does the second unset still unset the right item?

pfrenssen’s picture

1. I agree. We shouldn't babysit missing default values.
2. Seeing it like this it indeed looks like bad DX.
3. I would assume that this would indeed break. We can either opt to not rekey, or to provide a method like $this->removeItems($offsets) to remove multiple items and then rekey at the end.

Status: Needs review » Needs work

The last submitted patch, 59: 2164601-list_auto_create-59.patch, failed testing.

yched’s picture

Status: Needs work » Needs review
StatusFileSize
new54.56 KB
new5.4 KB

Reroll + fix test :

(from my #59 above:)
we decided to drop the exception on non-numeric keys in ItemList::setValue(), for perf reasons

TypedDataTest needs to be adjusted accordingly

re @fago #60:

imo it should be totally valid to do: $node->title->set(0, array('value' => 'foo'));

OK, why not. Changed ItemList::set($delta, $value) to allow adding a new item if $delta is the next available position in the list.
This still maintains the consistency of sequential, while allowing you to populate your field on the fly :
$node->field[0] = 'foo';
$node->field[1] = 'bar';
$node->field[2] = 'baz';

As I pointed in Ghent, that means changing ListInterface::set() phpdoc, because it currently states it's only about "replacing" an item, not creating new ones at unpopulated indexes.

1. I think we had that discussion in Ghent. As the code comment you mention says, agreed that it would be way better to remove that and go with "The default of a field has to be in the field definition" if we could, but we can't at the moment since there are currently field types in core that provide their own business logic in their own override of applyDefaultValue(). See also #18 above.
I opened #2356623: Remove usages of FieldItem::applyDefaultValue() for that, and the code you quote has a @todo on that issue - but that's way outside the scope of this patch.

We should apply defaults when appending an item though

I don't think I agree, but that seems like a separate discussion anyway. Current HEAD doesn't apply defaults on a new Item that gets "magically autocreated" into a List, right ? The patch here replaces "new Items are magically autocreated" with "new Items need to be explicitely appended", but doesn't (and shouldn't) change whether defaults need to be applied to those new Items.

2. Er - yes, that BC break is exactly the title of this issue :-)
It is very precisely the intent of this patch, as was disscussed and agreed in Amsterdam 50 comments ago ?

For the case of the specific code instance pointed here : the "add more" button means widgets *add* new items to the current ItemList, and let you edit them.
HEAD pretends that an ItemList is an endless pit of Items - "just ask for more, I have as many as you want".
With patch, the widget takes an explicit action of adding a new item, which completely makes sense IMO.

3. Yep, such code would break, you cannot work that way on a data structure that is about subsequently indexed elements - this exact same code would break on a SplDoublyLinkedList (which also support an ArrayAccess $list[$i] syntax).
This is why we provide the ItemList::filter() method.

jibran’s picture

+++ b/core/modules/entity_reference/src/Tests/EntityReferenceFormatterTest.php
@@ -263,15 +263,18 @@ protected function buildRenderArray(array $referenced_entities, $formatter, $for
+      //   https://www.drupal.org/node/2386559 is fixed.

We can remove this @todo it is commit.

yched’s picture

StatusFileSize
new53.06 KB
new1.51 KB

Reroll,
+ adressed #64 : in fact that whole hunk can be removed now.

Status: Needs review » Needs work

The last submitted patch, 65: 2164601-list_auto_create-65.patch, failed testing.

yched’s picture

Status: Needs work » Needs review
StatusFileSize
new54.41 KB
new1.41 KB

EntityReferenceFormatterTest - in fact that whole hunk can be removed now

Silly me, spoke too soon.

Scratch #65, that is the correct change for #64.
Interdiff is with #63.

yched’s picture

StatusFileSize
new53.14 KB

Reroll + bump ?

fago’s picture

Status: Needs review » Needs work

OK, why not. Changed ItemList::set($delta, $value) to allow adding a new item if $delta is the next available position in the list.
This still maintains the consistency of sequential, while allowing you to populate your field on the fly :
$node->field[0] = 'foo';
$node->field[1] = 'bar';
$node->field[2] = 'baz';

Right, thanks :)

  1. +++ b/core/modules/field/src/Tests/FieldDataCountTest.php
    @@ -72,11 +72,9 @@ public function testEntityCountAndHasData() {
    -      $value = mt_rand(1,99);
    -      $value2 = mt_rand(1,99);
    -      $entity->field_int[0]->value = $value;
    -      $entity->field_int[1]->value = $value2;
    -      $entity->name->value = $this->randomMachineName();
    +      $entity->field_int[] = mt_rand(1,99);
    +      $entity->field_int[] = mt_rand(1,99);
    +      $entity->name[] = $this->randomMachineName();
    

    So the old code should continue to work now, right? That's what I meant with valid usages that the previous patch made invalid - but great to see this addressed now.

  2. +++ b/core/modules/node/src/Tests/NodeAccessLanguageAwareCombinationTest.php
    @@ -130,7 +130,7 @@ protected function setUp() {
    -    $translation->field_private[0]->value = 0;
    +    $translation->field_private->value = 0;
    

    Now unnecessary changes, but the new code is better readable anyway.

  3. +++ b/core/modules/system/src/Tests/Entity/EntityFieldTest.php
    @@ -277,20 +277,22 @@ protected function doTestReadWrite($entity_type) {
    -    $this->assertEqual($entity->name[1]->value, 'Another name', format_string('%entity_type: List item added via [].', array('%entity_type' => $entity_type)));
    -    $entity->name[2]->value = 'Third name';
    

    I think we should have some test-coverage for appending items with the right offset works. Settings to needs work for that, else patch looks ready to go to me.

Also, this needs a change record draft and update issues summary for committers.

yched’s picture

Status: Needs work » Needs review
StatusFileSize
new53.56 KB

@fago : thanks :-)

Reroll for now, then I'll address #69

yched’s picture

Status: Needs review » Needs work

Back to NW for #69

larowlan’s picture

I can guide someone through #69 during critical office hours (in 4 hours)

jibran’s picture

Assigned: Unassigned » jibran

Working on this during critical office hours.

larowlan’s picture

Issue tags: +Critical Office Hours
jibran’s picture

Status: Needs work » Needs review
Issue tags: +Needs change record
StatusFileSize
new1.19 KB
new53.9 KB

Thank you @larowlan for all the help.
For #69

  1. Nothing to fix there.
  2. Nothing to fix there.
  3. Added new assert for that.

Status: Needs review » Needs work

The last submitted patch, 75: 2164601-list_auto_create-74.patch, failed testing.

jibran’s picture

Status: Needs work » Needs review
Issue tags: -Needs change record
StatusFileSize
new1009 bytes
new53.93 KB

Fixed the test fails. Also created drafted change notice https://www.drupal.org/node/2404453

jibran’s picture

Assigned: jibran » Unassigned

It is ready for review again.

pfrenssen’s picture

Status: Needs review » Needs work
Issue tags: +Needs reroll
pfrenssen’s picture

Status: Needs work » Needs review
StatusFileSize
new53.95 KB

I rebased against HEAD, but this magically caused this hunk to appear in the patch:

diff --git a/core/modules/field/src/Tests/EntityReference/EntityReferenceFormatterTest.php b/core/modules/field/src/Tests/EntityReference/EntityReferenceFormatterTest.php
index 45f7bdc..6f38dac 100644
--- a/core/modules/field/src/Tests/EntityReference/EntityReferenceFormatterTest.php
+++ b/core/modules/field/src/Tests/EntityReference/EntityReferenceFormatterTest.php
@@ -263,15 +263,17 @@ protected function buildRenderArray(array $referenced_entities, $formatter, $for
     // Create the entity that will have the entity reference field.
     $referencing_entity = entity_create($this->entityType, array('name' => $this->randomMachineName()));
 
-    $delta = 0;
+    // Assign the referenced entities, and place the 'access' flag so that the
+    // formatter does not need to check field access.
+    $items = $referencing_entity->get($this->fieldName);
     foreach ($referenced_entities as $referenced_entity) {
-      $referencing_entity->{$this->fieldName}[$delta]->entity = $referenced_entity;
-      $referencing_entity->{$this->fieldName}[$delta++]->access = TRUE;
+      $items[] = [
+        'entity' => $referenced_entity,
+        'access' => TRUE,
+      ];
     }
 
     // Build the renderable array for the entity reference field.
-    $items = $referencing_entity->get($this->fieldName);
-
     return $items->view(array('type' => $formatter, 'settings' => $formatter_options));
   }

We're being kicked out of the sprint room now, I'll have another look tomorrow.

manningpete’s picture

Issue tags: -Needs reroll

I could apply the patch, so no reroll is needed.

pfrenssen’s picture

Thanks for having a look! Indeed I was mistaken, that hunk did not appear out of nowhere but was just moved around in the reroll, causing it to be highlighted in my difftool when I compared the "before" and "after" branches. All is well.

yched’s picture

Assigned: Unassigned » fago

Thanks folks !

Assigning to @fago for RTBC ?

yched’s picture

StatusFileSize
new52.45 KB

Reroll after #2235457: Use link field for shortcut entity. No need to touch anything in shortcut.module anymore.

yched’s picture

As a side note, we need to get this in before we can work on #2392845: Add a trait to standardize handling of computed item lists

amateescu’s picture

Status: Needs review » Reviewed & tested by the community
Issue tags: -Critical Office Hours
StatusFileSize
new51.75 KB

Rerolled for #2406749: Use a link field for custom menu link. Same as #85, no need to do anything in MenuLinkContent anymore.

@fago's feedback from #69 has been addressed, we have a change record draft, I think it's time to get this in.

Status: Reviewed & tested by the community » Needs work

The last submitted patch, 87: 2164601-list_auto_create-87.patch, failed testing.

amateescu’s picture

Status: Needs work » Reviewed & tested by the community
StatusFileSize
new51.75 KB

Hard to keep up with HEAD these days/hours. Yet Another Reroll!

RavindraSingh’s picture

StatusFileSize
new9.37 KB

Its needs reroll as per latest core api changes.

Getting failed when tried attached is a ref.

RavindraSingh’s picture

Issue tags: +Needs reroll

changing to needs work.

RavindraSingh’s picture

Status: Reviewed & tested by the community » Needs work

plach’s picture

Status: Needs work » Reviewed & tested by the community

#90 applies cleanly on the latest head here.

(didn't review the patch)

jibran’s picture

Issue tags: -Needs reroll

Doesn't need re-roll. @RavindraSingh are you sure you have the latest changes in HEAD on your local clone?

RavindraSingh’s picture

Yes, I think and now. And now its also sending the message
plach queued 89: 2164601-list_auto_create-89.patch for re-testing. see #90

fago’s picture

Assigned: fago » Unassigned

Yep, agreed this is ready. I had another look at the patch, looks all good to me.
I took a look at the change record draft and overhauled it, such that it provides a good summary of the changes. Please check it: https://www.drupal.org/node/2404453

Also, I guess this will need a beta evaluation summary.

yched’s picture

Thanks folks.

It is a "triaged critical" already, so not sure it needs a beta evaluation summary ?
I'll write one if needed :-)

Checking the draft record

yched’s picture

Made a couple adjustments to the change record, otherwise looks great
https://www.drupal.org/node/2404453/revisions/view/8079535/8079585

yched’s picture

StatusFileSize
new51.84 KB
new717 bytes

Er - not sure why I kept a now useles FieldItemList::__construct() for so long :-)
Trivial change, so keeping at RTBC.

+ yay at comment #100.

alexpott’s picture

Status: Reviewed & tested by the community » Needs work
  1. +++ b/core/lib/Drupal/Core/Field/BaseFieldDefinition.php
    @@ -511,9 +511,20 @@ public function setDefaultValue($value) {
    +      // If not, create a new item that has the entity as the parent, but do not
    +      // modify the actual entity by leaving it there.
    +      else {
    +        $item = $items->appendItem();
    +        $items->removeItem($item->getName());
    +        return $item;
    
    +++ b/core/modules/field/src/Entity/FieldStorageConfig.php
    @@ -593,9 +593,20 @@ public function getCardinality() {
    +        $item = $items->appendItem();
    +        $items->removeItem($item->getName());
    +        return $item;
    

    This looks dodgy - should we have a method to create a detached item? Also the duplicated code between BaseFieldDefinition and FieldSotrageConfig is painful. Looks like we could use \Drupal::typedDataManager()->getPropertyInstance($items, 0, NULL);.

  2. +++ b/core/lib/Drupal/Core/Field/FieldItemList.php
    @@ -177,17 +176,18 @@ public function defaultAccess($operation = 'view', AccountInterface $account = N
    +      // Remove it if this ended up doing nothing...
    

    No need for the ...

yched’s picture

Status: Needs work » Needs review
StatusFileSize
new51.32 KB
new3.73 KB

Reroll after #2374019: Cleanup the use of custom item properties in EntityReferenceFormatterBase

@alexpott #101:

1. "allowed values", yeah... :-( We discussed that with @fago in Ghent, and preferred not to introduce an official API in FieldItemList for creating an orphaned $item, a FieldItem is supposed to be created in a parent entity, we'd rather not make that too blurry (although #2227227: FieldTypePluginManager cannot instantiate FieldType plugins, good thing TypedDataManager can instantiate just about anything might or might not reconsider that)

There are still a couple places where we need to call logic on the FieldItem class without having an actual field value in an actual entity. In retrospect, it was a very early design mistake in Field API D8 to make the Item class be the main class holding the field type logic, and we've removed most cases along the months, but "allowed values" is one of the last remaining. There are/were plans for a better API for OptionsProvider (#2329937: Allow definition objects to provide options), but that's complex, and a bit stalled since Ghent :-/

But true, meanwhile, it seems \Drupal::typedDataManager()->getPropertyInstance($items, 0) should do the job in there. Trying that.

2. Fixed.

Status: Needs review » Needs work

The last submitted patch, 102: 2164601-list_auto_create-102.patch, failed testing.

RavindraSingh’s picture

yched’s picture

Status: Needs work » Reviewed & tested by the community

Thanks @larowlan
Green, back to RTBC ?

alexpott’s picture

Status: Reviewed & tested by the community » Fixed

This issue addresses a critical bug and is allowed per https://www.drupal.org/core/beta-changes. Committed 0509fbe and pushed to 8.0.x. Thanks!

  • alexpott committed 0509fbe on 8.0.x
    Issue #2164601 by yched, jibran, amateescu, pfrenssen: Stop auto-...
yched’s picture

pfrenssen’s picture

Yaay! Congratulations!

Status: Fixed » Closed (fixed)

Automatically closed - issue fixed for 2 weeks with no activity.