I've started looking into typed data resolver and how to improve it. This is going to be very long, but I have to write down some basics and my thoughts to be able to discuss this.

I don't think typed data is as bad as you make it. It definitely gives you the API's and definitions to access any value in it's data structure. The problem is that the structure is more complex than you try to make it. Typed Data consists of 3/4 different things:

- lists: ListInterface/ListDataDefinitionInterface
- complex data: ComplexDataInterface/ComplexDataDefinitionInterface
- "simple" data: DataInterface/ComplexDataInterface
- a special kind of data, references: DataReferenceDefinitionInterfac

In regards to type data, you can combine that in any way. you can have lists of lists, multiple nested complex data structures and eventually, you have the actual (sometimes but not always primitive) data.

It's not completely up to date, but there are actually fairly good docs on how content entities use typed data here: https://www.drupal.org/node/1795854

content entity structure, unlike typed data in general, is fixed. (at least down to the property) It always is:

Entity:Field:FieldItem:Property

Or, using the typed data names:
complex:list:complex:*

Property can be multiple things, a primitive, a reference, or complex data.

From looking at the current resolver code, the main problem is that you try to access entities with too simplified assumptions about it's structure and are to achieve that, use methods that actually have nothing to do with typed data.

Some examples, assuming we want to get the value of node:langcode:

$value = $this->getFirstApplicableTypeResolver($property)->getValueFromProperty($context->getContextValue()->$property_name);

getContextValue() doesn't return typed data. It returns whatever the value is and in this case, a node object. You also rely on magic __get(), which is a content entity specific feature. You want getContextData(). That just gives you typed data, but the method is getContextFromProperty(), so we can assume that we actually have complex data here. Which has a get($property_name) method.

  /**
   * {@inheritdoc}
   */
  public function appliesToProperty(ListDataDefinitionInterface $property) {
    return $property->getType() == 'entity_reference';
  }

ListDataDefinitionInterface has no getType() method. That is actually from FieldStorageDefinitionInterface, and it returns the *field type*. The typed data type is return with getDataType() and is... "list" (surprise! ;)). That's because FieldStorageDefinition/Field is simply a list of field items for typed data. So, the list definition itself is not relevant for figuring out what's actually inside, you need to check the list item definition. $property->getItemDefinition()->getDataType() gives you for the langcode field "field_item:langcode". It's *not* a langcode/language. It's a langcode field item.

So we need to look at the properties. $property->getItemDefinition()->getPropertyDefinitions() returns two, value and language. value is a simple string and contains the language code, language is the reference to the language object, a DataReferenceDefinition with the data type language_reference. It also has a getTargetDefinition() that finally contains the definition/type for the object we want, a language.

So, for your node:langcode, the typed data property path for the thing you actually want is node.langcode.0.language (the value of a reference is it's target, it does have its own definition and that of the target, though). And for node:type, to get the node type object, it's node.type.0.entity.

So the question is how do we actually get there in a generic way. It's pretty clear to me that "node:langcode" is a too simplified representation, but what's not clear to me is what ctools actually wants to achieve:

* What about multiple values. getCardinality() is again a field definition specific concept. An automatism that returns a list if there are multiple and just one if not is something imho turned out to be one of the worst decisions in entity.module in 7.x, which is why, in 8.x, we made the decision that fields are *always* a list. We could make the same assumption as content entities and default to delta 0. But we can only do that when accessing a property below, as otherwise it's not possible to access the list. For entities, $node->langcode->value is the same as $node->langcode[0]->value. But $node->langcode must return the full thing.

* How do we know which field property we actually want to access? As I said above, node.langcode has two, value and the reference (language). In this case, we want the reference. But what would we want for e.g. an integer field type. Or a TextWithSummary that has a text, format and a summary. Or a field that has two references to other entities? The resolvers are asked to extract "the data" from possibly indefinitely complex typed data structures, without being told anything about what is actually desired.

I think if we make that relationship notation or how we want to call it just slightly more explicit and closer to entities/typed data, then we can actually implement it in a generic way. What I think would work is "node:langcode:language". As written above, then we can imply delta 0 and access this information in a generic, typed data compliant way:

$context->getContextData()->get('langcode')->first()->get('language')->getValue().

Attaching a first patch for this. Not very well tested, but PathautoUnitTest::testPatternLoadByEntity() that uses exactly this passes now. One thing it can't do yet is deal with a list of references. Not sure yet how to handle this, maybe node:some_field:*:node? i don't think that is needed very often?

Support from Acquia helps fund testing for Drupal Acquia logo

Comments

Berdir created an issue. See original summary.

Berdir’s picture

Status: Active » Needs review
FileSize
5.46 KB
Berdir’s picture

Title: Make typed data actually work with typed data interfaces » Make typed data resolver actually work with typed data interfaces
fago’s picture

talked about this with berdir: I totally agree that there is no need for having typed data resolvers as the tree of typed data can directly map to a property path or data selector (that's how we call this in Rules). I don't think making something for abbreviations is worth it - also there is a difference between the "node.type" (the reference) and "node.type.entity" the entity referenced. Entity metadata wrappers actually got this wrong in d7 what caused in some issues in affect - so let's not repeat that mistake.

  1. +++ b/src/TypedDataResolver.php
    @@ -118,22 +120,64 @@ class TypedDataResolver {
    +  public function getContextFromProperty($property_path, ContextInterface $context) {
    

    the method says it gets the context form a property, but the argument is a context. that's a bit inconsistent

  2. +++ b/src/TypedDataResolver.php
    @@ -118,22 +120,64 @@ class TypedDataResolver {
    +            $data = $data->get($name);
    

    this can throw an exception what should be handled, e.g. unexisting offset given.

  3. +++ b/src/TypedDataResolver.php
    @@ -118,22 +120,64 @@ class TypedDataResolver {
    +        $data = $data->get($name);
    

    $data can be NULL if first() is not set -> boom

  4. +++ b/src/TypedDataResolver.php
    @@ -118,22 +120,64 @@ class TypedDataResolver {
    +    $context_definition = new ContextDefinition($data_definition->getDataType());
    

    This can loose quite some metadata, like constraints or information about a list item. We lack some createFromDataDefinition() or so here imho.

Generally, we have code for this at https://github.com/fago/rules/blob/8.x-3.x/src/Engine/RulesState.php#L90 which right now is coupled to Rules. We plan to provide this as re-usable component though, so we'll factor that out. In fact I'm going to work on this during the upcoming weeks as I'll need it for Rules' token evaluation / processing. The plan is to provide this re-usable parts of Rules as separate composer packages (via sub-tree splits), so if adding this in via a composer package would be an option for you that would be totally a way to go and something we could prioritize to do if it helps you to move forward.
Otherwise, you can still just copy some code over ;-)

Berdir’s picture

Here's a patch that provides the tokens/relationship part for this. I renamed a few things and removed the data types argument, instead I'm currently hardcoding the assumption that we only want references.

The labels should look exactly like they do right now, despite the additional level in the token. I'm using the following rule to get that: If a nested property only returns a single token, then I'm ignoring its nested label and just use the property label. Only if there is more than one, then I'm adding the nested labels too.

Berdir’s picture

Ok, this fixes some bugs so that nested contexts (like the roles of a user, although there it gets tricky with multiple values, which aren't fully supported yet, not by the token/relationship UI/API at least) work too.

Also improved the RelationshipConfigure form a bit by fixing getLabelByToken. Also thought that it would make sense to use that as default for the context label and instead use an actual label for the data type, the current form was IMHO very confusing.

And, removed a lot of code that isn't needed anymore, including the whole tagged services stuff.

juampynr’s picture

Rebasing against 8.x-3.x.

Thanks to this patch I can successfully run one of Pathauto's tests. Without it, I get Error when running pathauto test..

The error I was getting is because the test sets a context of type "node:langcode:language". When Ctools extracts contexts and it gets to "language" it crashes as it does not expect this data type:

class Drupal\Core\TypedData\DataDefinition#1833 (1) {
  protected $definition =>
  array(1) {
    'type' =>
    string(8) "language"
  }
}
EclipseGc’s picture

+++ b/ctools.services.yml
@@ -20,18 +20,3 @@ services:
-  ctools.access:
-    class: Drupal\ctools\Access\TempstoreAccess
-    arguments: ['@user.shared_tempstore']
-    tags:
-      - { name: access_check, applies_to: _ctools_access }

This is an unrelated service that needs to continue existing.

There are aspects of this patch I still find confusing, and I'm trying to wrap my brain around them. In the mean time, I've applied this patch to test it and am fairly happy with how it's performing in my tests. My greater worry lies around my own inability to understand certain aspects of typed data and I need to see how this will work for non-reference items. Strings and other such thing are definitely in the realm of what I want this code to be able to handle.

Eclipse

EclipseGc’s picture

Status: Needs review » Needs work
Berdir’s picture

The removed access service I think went wrong in the latest reroll, yeah, that shouldn't be removed :)

Happy to try and explain this. I might also find time to write a few tests. This works for any field type. It should even be able to go through references, but I haven't tried that yet. E.g. something like node:uid:entity:mail:value *should* just work :)

Berdir’s picture

Status: Needs work » Needs review
FileSize
28.73 KB
5.48 KB

Tests! Atleast for getContextFromProperty(), as a start. But shouldn't be too difficult to also add test for other methods.

Turns out that I had a bug that prevented node:uid:entity:mail:value from working. It does now :) Tests++

The tests are btw also helpful to follow the code path in getContextFromProperty in a debugger if you want to see how it traverses the typed data object for a given property path.

Also restored the access service.

  • EclipseGc committed 6e4df80 on 8.x-3.x authored by Berdir
    Issue #2615444 by Berdir, juampynr: Make typed data resolver actually...
EclipseGc’s picture

Status: Needs review » Fixed

Ok, this seems like a big improvement over what we have and has a start at some tests. Ideally I'd like more tests, but given that others are already depending on this patch and that it is working as I'd hoped and is overall better, I'm committing this.

Eclipse

Status: Fixed » Closed (fixed)

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