Problem/Motivation

Typed configuration elements defined for configuration schema inherit from Drupal\Core\TypedData\TypedData. TypedData has a $definition property implementing \Drupal\Core\TypedData\DataDefinitionInterface. Instead of using data definitions with this interface, typed configuration uses anonymous arrays instead. This is not conforming to the TypedData API it currently implements.

Proposed resolution

Implement data definitions as a DataDefinitionInterface. This still has array access already implemented in core built-in so accessing the definition properties as array elements is still possible.

Once the implementation uses DataDefinitionInterface properly, we can make the typed configuration manager extend the typed data manager and reuse lots of its code instead of duplicating logic.

Remaining tasks

Review.

User interface changes

None.

API changes

  • Configuration schema may now provide a definition_class additionally to a class implementing the type.
  • TypedConfigManager now extends directly from TypedDataManager instead of PluginManager.
  • TypedConfigManagerInterface::create() now takes a DataDefinitionInterface instance for the definition and a new buildDataDefinition method that creates the definition object from the YAML (config schema) plugin definition.

Original report by @Jose Reyero

This patch is the first step to add some consistency in the Typed Config / Typed Data issue.

The motivation is explained on the parent issue: #1928868: Typed config incorrectly implements Typed Data interfaces

What it does:
- Makes TypedConfigManager extend TypedDataManager and reuses a good part of it.
- Use DataDefinitionInterface objects instead of plain arrays for typed configuration elements.

Note it introduces small API changes just in order to add consistency between the existing two APIs. Mostly it replaces arrays by objects implementing DataDefinitionInterface.

Support from Acquia helps fund testing for Drupal Acquia logo

Comments

Jose Reyero’s picture

FileSize
14 KB

This first patch takes care of the objects in Drupal/Core/Config but not the rest of the system (locale?) nor updates tests that may rely on type definitions being arrays so it is not expected to pass tests yet.

Jose Reyero’s picture

Status: Active » Needs review

Just to let tests run

fago’s picture

Great to see you working on this, Jose++!

This looks like a good first step. I'm wondering whether it make sense to use composition instead of inheritance though, i.e. we won't pass in typed config manager instead of typed data manager somewhere, but explicitly add some additional functionality on top of typed data. So what about adding the typed data manager as dependency instead and forwarding things that are needed in typed config also? That makes it also more clear than how the typed config manager is actually used.

Status: Needs review » Needs work

The last submitted patch, 1: 2277945-01.patch, failed testing.

Jose Reyero’s picture

@fago,
Thanks for looking at this.

whether it make sense to use composition instead of inheritance

I think that may work too but it would be a conceptual rework and would need some more code. I'm not for major reworks atm, just into consolidating existing APIs.

TypedConfigManager is the same kind of animal as TypedDataManager, only it uses different discovery and needs some more definition pre-processing so extending it is OK IMO. But both use type definitions and produce TypedData objects. Ideally we would use a single class for both, just passing them different discovery but we are still far from that because of implementation details.

Could you please take a look at how DataDefinition interfaces are introduced through the patch and let me know whether they are bing 'used right', I mean if they are used how they are intended to be used?

Gábor Hojtsy’s picture

Looks like this simplifies things :) However some notes:

  1. +++ b/core/config/schema/core.data_types.schema.yml
    @@ -42,12 +42,14 @@ uri:
       class: '\Drupal\Core\Config\Schema\Mapping'
    +  definition_class: '\Drupal\Core\TypedData\MapDataDefinition'
    ...
       class: '\Drupal\Core\Config\Schema\Sequence'
    ...
    +  definition_class: '\Drupal\Core\TypedData\ListDataDefinition'
    

    This is not very clear at all. What's the difference between the two classes? This should have docs somewhere.

  2. +++ b/core/lib/Drupal/Core/Config/Schema/Sequence.php
    @@ -15,13 +15,22 @@
    +    // Get the generic definition and reprocess it for all items.
    +    $definition = isset($this->definition['sequence'][0]) ? $this->definition['sequence'][0] : array();
    
    @@ -37,7 +46,11 @@ public function isEmpty() {
    +      $definition = isset($this->definition['sequence'][0]) ? $this->definition['sequence'][0] : array();
    +      $this->itemDefinition = $this->buildDataDefinition($definition);
    

    Depending on what this definition will contain this may not be true. Sequences with dynamic types may resolve to wildly different things, eg. one may be an integer, another may be a mapping. So long as this keeps the dynamic nature and does not resolve it further, it looks fine.

  3. +++ b/core/lib/Drupal/Core/Config/TypedConfigManager.php
    @@ -80,19 +86,34 @@ public function __construct(StorageInterface $configStorage, StorageInterface $s
    +  public function buildDataDefinition(array $definition, $value = NULL, $name = NULL, $parent = NULL) {
    +    // Add default values for data type and replace variables.
    

    We found in #2183983: Find hidden configuration schema issues that requiring the name for all levels is the tree is not just possible but also desirable. Since this moves this method to its own, instead of extending create() with more things, it can make $name required, which would be preferred. It should also always get the name from get().

    That way, all items will know their local name and can traverse the parents to their full name, code which I built in #2183983: Find hidden configuration schema issues (but seen that create() cannot be made to require $name due to the interface mismatch). That is not a problem here :)

  4. +++ b/core/lib/Drupal/Core/Config/TypedConfigManagerInterface.php
    @@ -67,7 +69,26 @@ public function createInstance($data_type, array $configuration = array());
    +  public function create(DataDefinitionInterface $definition, $value = NULL, $name = NULL, $parent = NULL);
    

    create() is not even implemented in the patched typed data manager, no?! Looks like this should be removed from the interface.

  5. +++ b/core/lib/Drupal/Core/Config/TypedConfigManagerInterface.php
    @@ -67,7 +69,26 @@ public function createInstance($data_type, array $configuration = array());
    +  public function buildDataDefinition(array $definition, $value = NULL, $name = NULL, $parent = NULL);
    

    Since you added this on the interface, the implementation can keep using @inheritdoc. And you only need to fix typos and 80 char issues in here :)

Gábor Hojtsy’s picture

Issue tags: -Configuration systemD8MIlanguage-config +Configuration system, +D8MI, +language-config, +sprint

Split tags :D

Jose Reyero’s picture

Working on re-rolling the patch, some replies in the meanwhile:

1. MapDataDefinition / ListDataDefinition This is not very clear at all. What's the difference between the two classes? This should have docs somewhere.

Basically, these interfaces have API extension to return property definitions or list item definitions. Anyway, I don't think it's the place, that is part of the TypedData API.

2. Sequence->getItemDefinition().

Right, it's not true in our case as the data definition depends on the data itself. But as that method must be implemented because it's part of the interface so we just return what is possible, which is a 'generic item definition'.
Anyway that is not used for the items, item definitions are processed internally, see the parse() method, so we should be all right.

3. Requiring name for buildDataDefinition(array $definition, $value = NULL, $name = NULL, $parent = NULL)

Not sure. Depending on the definition itself, it may need value and name (if there are tokens to replace) or not. But anyway, at runtime, name is always passed. It doesn't make sense for the root element, as name is the name of the file itself (configuration name), not the name of a property and I don't think it would be good allowing the option of different schema types depending on the configuration name, unless there's a case for it. I mean in that case, the configuration object would be processed differently if it has a different name...?

Maybe yes for the value, so I am removing that $value = NULL

4. create() not implemented.
Nope, but some code that gets passed a TypedDataManagerInterface does use it, so it must be somewhere in the interface.
Ideally, there would be a TypedDataManagerInterface to extend, some day...

5. Right

Jose Reyero’s picture

Status: Needs work » Needs review
FileSize
33.34 KB
24.12 KB

New version of the patch:
- Addresses issues in #7
- Updates some more functions (config translation, locale)
- Updates some tests adding new schema properties (definition_class, type)

Status: Needs review » Needs work

The last submitted patch, 9: 2277945-08.patch, failed testing.

Jose Reyero’s picture

Uh, tests won't run :-(

Obviously I need to learn what to do with these 'Mock' objects,...

Help!

class ConfigMapperManagerTest extends UnitTestCase {
  /**
   * Returns a mocked schema element.
   *
   * @param array $definition
   *   The definition of the schema element.
   *
   * @return \Drupal\Core\Config\Schema\Element
   *   The mocked schema element.
   */
  protected function getElement(array $definition) {
    $data_definition = $this->typedConfigManager->buildDataDefinition($definition, NULL);
    $element = $this->getMock('Drupal\Core\TypedData\TypedDataInterface');
    $element->expects($this->any())
      ->method('getDataDefinition')
      ->will($this->returnValue($data_definition));
    return $element;
  }
  ....
}
Jose Reyero’s picture

Status: Needs work » Needs review
FileSize
33.35 KB

This should fix the issue with the tests, let's try

Gábor Hojtsy’s picture

Requiring the $name is not so we can work with it for whatever data definition perspective. If you look at #2183983: Find hidden configuration schema issues it can only return the "full path" in an error/exception to a broken element if we know the name of the file. Otherwise at the parsing level where we find the error, we cannot traverse up to a level that knew the file, because only create() could have known the file name (root $name) and it did not get in. The significant changes in #2183983: Find hidden configuration schema issues is passing on the root name (file name) to create(), making it pass it on and then a getFullName() method to form a full name for the bugos place it finds. So you get error messages like

views.view.content:display.default.display_options.fields.translation_link.text does not exist or does not have schema defined.

The only way an element can throw an error like this is if it knows the full name. The names of individual items are known at each level but not the root because create is not passed the $name for the root. If we just make that mandatory, then yay! Let's do that? :)

Jose Reyero’s picture

FileSize
1.01 KB

Yeah, test passes, attaching interdiff for last patch

@Gábor,
Good morning -if not at Drupalcon :-)

Yes, but: the name is not in the data definition.

If you want that then we need to add some 'root element' data type, so it handles its own name, and that one could be the parent for all trees

Anyway, I think that is out of scope for this issue.

Gábor Hojtsy’s picture

My mental concept of typed config is that "root elements" are just specialized types of other types. So a "system.maintenance" is a type that happens to be a specialized mapping. So that it should get its name is not anywhere different from internal elements passing on their names IMHO. It may be out of scope here, but you are changing the same code and introducing an interface for it, so looks related :)

Jose Reyero’s picture

I think we are talking about different things here. This is how I see it:

Since all this TypedData stuff is used for entities too, maybe we can think of the configuration name as the entity id, which is a different concept from a property/field name, which is what getName() is used for as per the interface definition (TypedDataInterface) or that I believe.
The difference here is that these TypedData elements exist by themselves instead of being contained in something else, maybe because it's some limited reuse, and in our case, the 'containing object' would be the Config object or the ConfigEntity object, not the TypedData parent object itself.
Maybe we shouldn't get the Typed config form the TypedDataManager but from the Config object itself, and then it would be the root element? And yes, that one actually has a configuration name (which is a property of the Config object, not of the data contained in it).

So yes, there's some 'conceptual gap' here that I don't really know how to fix. But it doesn't mean there's anything wrong with typed config, maybe you can think of it as a kind of TypedData that doesn't have a Root/Container entity. Also the TypedData implementation works fine as long as the topmost element in the tree is a Mapping or a Sequence object. For a 'perfect match' though I'd say it should be the Config/ConfigEntity object but that would require some important extension of these objects.

interface TypedDataInterface { 
  ...
  /**
   * Returns the root of the typed data tree.
   *
   * Returns the root data for a tree of typed data objects; e.g. for an entity
   * field item the root of the tree is its parent entity object.
   *
   * @return \Drupal\Core\TypedData\ComplexDataInterface|\Drupal\Core\TypedData\ListInterface
   *   The root data structure, either complex data or a list.
   */
  public function getRoot();
  ...
}

And anyway about that method (buildDataDefinition), passing the name there will only use it for variable replacement, that's not the name of the element as in the one retrieved by getName(). And that's why using the configuration name for that replacement may be wrong, since the data types of something shouln't depend on the name of that something.

Whatever, for validation we should be ok as these objects are (suppossed to be) run through validation one at a time so whatever code is handling that will 'know' the name of the configuration object that is validating to be able to print validation errors in context, if I got the issue right.

Maybe @fago could throw some more light at this.

Jose Reyero’s picture

FileSize
33.5 KB

Re-rolled patch for latest Drupal core updates.

Gábor Hojtsy’s picture

All right, well. Discussed this with Jose in IRC today. I see where he is coming from not to pass on the root name. We don't need it for this issue. We may or may not figure out a better solution for issues like #2183983: Find hidden configuration schema issues elsewhere.

Gábor Hojtsy’s picture

Status: Needs review » Needs work
  1. +++ b/core/config/schema/core.data_types.schema.yml
    @@ -42,12 +42,14 @@ uri:
    +  ¶
    ...
    + ¶
    

    Lets not add extra whitespace.

  2. +++ b/core/lib/Drupal/Core/Config/Schema/Mapping.php
    @@ -20,11 +20,20 @@
    +   * Note this only returns elements that have a data definition.
    

    We just discussed today that all will have data definition in the sense that they should get an undefined type, no? You said in chat that this behaviour is not right. Not sure if this should be in scope here. It is TOTALLY fine for it to be not in scope here, but we should have a @todo with an issue number then, no?

  3. +++ b/core/lib/Drupal/Core/Config/Schema/Sequence.php
    @@ -15,13 +15,22 @@
    +    // Get the generic definition and reprocess it for all items.
    

    I would explain we reprocess it because the definition may be referring to a dynamic type.

  4. +++ b/core/lib/Drupal/Core/Config/StorableConfigBase.php
    @@ -128,7 +128,8 @@ public function getStorage() {
    -      $this->schemaWrapper = $this->typedConfigManager->create($definition, $this->data);
    +      $data_definition = $this->typedConfigManager->buildDataDefinition($definition, $this->data);
    +      $this->schemaWrapper = $this->typedConfigManager->create($data_definition, $this->data);
    

    I think this is the only place we call create() directly. Can we call get() instead? That would help with consistency.

  5. +++ b/core/lib/Drupal/Core/Config/TypedConfigManagerInterface.php
    @@ -67,7 +69,27 @@ public function createInstance($data_type, array $configuration = array());
    +   * @param array $definition
    +   *   The base type definition array, for which a data definition should be
    +   *   created.
    

    So now there is either an array or a data definition object depending on which stage of the data handling you work with. Not sure this is a cleanup?

Jose Reyero’s picture

Status: Needs work » Needs review
FileSize
35.48 KB
5.55 KB

Fixing issues pointed out by @Gábor #19:

1. Fixed
2. Good point. Fixed providing 'undefined' type for data elements that don't have a definition.
* For the future we can still validate schema completeness checking for 'undefined' elements in the data.

3. Fixed, now it reads:

    // Creates a new data definition object for each item from the generic type
    // definition array and actual configuration data for that item. Type
    // definitions may contain variables to be replaced and those depend on
    // each item's data.

4. We call it also from /Drupal/Core/Config/Schema/Element::parseElement(). That is used to build nested elements while get() is used only for the root element.

5. No, it is always a data definition object. Note we are not doing the implementation but reusing the one
in TypedDataManager. Wondering whether we could leave this out of the interface...

In addition to that, fixed implementation of ComplexDataInterface::get()
It now returns the right exception, and only when there's no data for it. Otherwise, in case a missing mapping it returns an 'Undefined' property.

I don't expect tests to pass, just need to know which ones to fix..

Status: Needs review » Needs work

The last submitted patch, 20: 2277945-20.patch, failed testing.

Jose Reyero’s picture

Status: Needs work » Needs review
FileSize
36.97 KB

Fixed a problem with Mapping:get() implementation
Replaced SchemaIncompleteException by InvalidArgumentException

Another run for the tests.

Status: Needs review » Needs work

The last submitted patch, 22: 2277945-22.patch, failed testing.

Jose Reyero’s picture

We're almost there, just one test failing and considering whether that one makes any sense....

The problem is not tests but this quickly getting out of scope so I'm thinking getting it back to its original form (#17 + minor fixes asked for in #19)

However there are things in the schema that need fixing, and I've just bumped into one of those: 'undefined' data type, which as it reads in core.data_types.schema.yml

# Undefined type used by the system to assign to elements at any level where
# configuration schema is not defined. Using explicitly has the same effect as
# not defining schema, so there is no point in doing that.
undefined:
  label: 'Undefined'
  class: '\Drupal\Core\Config\Schema\Undefined'

So it is supposed not to be actually used in schemas, still it is used in two current schemas, namely: user.scyema.yml, views.cache.schema.yml

Funny paradox: Something that is defined as 'undefined'.... is it defined, or is it not?

So I think we should replace that with 'ignore' which is our current placeholder for 'any data'. But also I think our 'Ignore' should actually be 'Any' (Drupal\Core\TypedData\Plugin\DataType\Any)

Also I've found some trouble with StorableConfigBase::castValue() whose declaration says 'Casts the value to correct data type using the configuration schema' but actually it goes a bit futher than that (and it is the one test failing btw). Currently it does also some schema validation, throwing exceptions for mapping elements that are not defined in the schema. That is not casting, that is validating, and I'd say the method is kind of lying about its purpose... (And btw if the undefined* element's value is an array I think it will just break)

* Using 'undefined' as 'not defined' in this context, not as "defined as 'undefined' "... funny eh?

All these issues I had planned for a follow up patch, but bumped into them with this one.

So wondering.... Should we try and fix them all here? (Which is really little code but it may open up some never ending discussion about the real meaning of 'undefined' or 'type casting'... or life..).... or should we get back to the original intent, trimming down the patch to its original form and just fixing that definition arrays?

Gábor Hojtsy’s picture

+++ b/core/lib/Drupal/Core/Config/Schema/Mapping.php
@@ -28,15 +28,11 @@ class Mapping extends ArrayElement implements ComplexDataInterface {
     foreach ($this->getPropertyDefinitions() as $key => $definition) {
-      if (isset($this->value[$key]) || array_key_exists($key, $this->value)) {
-        $elements[$key] = $this->parseElement($key, $this->value[$key], $definition);
-      }
+      $elements[$key] = $this->parseElement($key, $this->value[$key], $definition);
     }

This seems to make a change in the wrong direction. Now elements that don't have data but have definition will show as if they have NULL as their data. Is save()-ing from the typed config object then resulting in new elements added to the configuration (that were not there but in the schema)?

Gábor Hojtsy’s picture

@reyero: sorry #25 was against the last interdiff you posted. I think its fine to get back to the original scope of this issue (#17 + minor fixes asked for in #19) as you said. No need to fix all the issues you find along the way :D

Jose Reyero’s picture

Status: Needs work » Needs review
FileSize
33.74 KB
1.6 KB

Ok, restarting from patch #17 (and fixing issues from #19):

1. Fixed
2. Good point. Fixed providing 'undefined' type for data elements that don't have a definition.
* For the future we can still validate schema completeness checking for 'undefined' elements in the data.

3. Fixed, now it reads:

    // Creates a new data definition object for each item from the generic type
    // definition array and actual configuration data for that item. Type
    // definitions may contain variables to be replaced and those depend on
    // each item's data.

I may create some new case for the issues mentioned in the last 3 comments.

New patch and inter-diff from #17

Gábor Hojtsy’s picture

  1. +++ b/core/config/schema/core.data_types.schema.yml
    @@ -42,9 +42,11 @@ uri:
       class: '\Drupal\Core\Config\Schema\Mapping'
    +  definition_class: '\Drupal\Core\TypedData\MapDataDefinition'
     sequence:
       label: Sequence
       class: '\Drupal\Core\Config\Schema\Sequence'
    +  definition_class: '\Drupal\Core\TypedData\ListDataDefinition'
    

    As I said before, I thin the use of definitions of different (even if very similar) types for these types is confusing. I understand the use of the Map/List types here is not possible. However, eg. MapDataDefinition::create() creates a data definition with type 'map'. That is not applicable to typed config.

  2. +++ b/core/lib/Drupal/Core/Config/TypedConfigManager.php
    @@ -12,11 +12,17 @@
    +use Drupal\Core\TypedData\DataDefinitionInterface;
    +use Drupal\Core\TypedData\DataDefinition;
    +use Drupal\Core\TypedData\ListDataDefinition;
    +use Drupal\Core\TypedData\ListDataDefinitionInterface;
    +use Drupal\Core\TypedData\MapDataDefinition;
    +use Drupal\Core\TypedData\TypedDataManager;
    

    Most of these are not actually used in the code.

Gábor Hojtsy’s picture

Title: Reconcile Typed Data, Typed Config and DataDefinitions » Typed configuration implements TypedDataInterface improperly
Category: Task » Bug report
Issue summary: View changes
Jose Reyero’s picture

Fixed @Gábor #28 (.2) removing all unneeded 'use' (all those and some other use Language in config_translation/ElementInterface)

About the first point (reuse of MapDataDefinition and ListDataDefinition) I think it is a fair point, but I don't know what to do, unless you want me to reimplement all these definition classes for typed config (and moving a good amount of schema logic here)... and then this will become pretty bigger. Well, the other option may be just using DataDefinition for all of them.. ?

However I don't think the MapDataDefinition::create() should be an issue, because that is the default but there's alreay a parameter in there to produce other types so my understanding is it is intended to work also with other types... Note it doesn't create a Map, it creates a MapDataDefinition...

public static function create($type = 'map') {
  $definition['type'] = $type;
  return new static($definition);
}

So admitting those definition classes should be ideally overridden and extended, I'd rather do it in a follow up patch because doing it properly would take moving part of mapping and sequence processing into them, which is beyond the intent of this one which just fixes the TypedDataInterface::getDataDefinition() method. And maybe as a part of that one we can also get Mapping and Sequence to extend or become Map and List.

Re-rolled for latest d8, tests may fail again because of the new schema validation, let's see...

Gábor Hojtsy’s picture

Status: Needs review » Reviewed & tested by the community

All right, let's move this to @alexpott's queue then.

Gábor Hojtsy’s picture

alexpott’s picture

Status: Reviewed & tested by the community » Fixed

This brings TypedData and TypedConfig closer together. It would be awesome if this was enforced on the manager level through a shared interface. Also we should be using the DataDefinition methods and not using ArrayAcesss - I will open an issue to do that.

But this is a step forward so...

Committed 9b32d95 and pushed to 8.x. Thanks!

  • Commit 9b32d95 on 8.x by alexpott:
    Issue #2277945 by Jose Reyero: Fixed Typed configuration implements...
Gábor Hojtsy’s picture

Issue tags: -sprint

Superb, thanks! We are getting there step by step.

alexpott’s picture

Gábor Hojtsy’s picture

Issue tags: -sprint

Cross-post I guess.

Gábor Hojtsy’s picture

The mapping data definition handling changes that were here temporarily are now at #2293063: Config schema mapping parsing is inconsistently forgiving, does not conform to the interface and are actually uncovering all kinds of interesting bugs.

Status: Fixed » Closed (fixed)

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