Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
"drupal/core-composer-scaffold": "^11.2",
"drupal/core-recommended": "^11.2",
"drupal/date_recur": "^3.6",
"drupal/driver_field_test": "*",
"drupal/mailsystem": "^4.4",
"drupal/name": "^1.2",
"drupal/smart_date": "^4.2",
Expand All @@ -57,6 +58,10 @@
"drupal": {
"type": "composer",
"url": "https://packages.drupal.org/8"
},
"driver_field_test": {
"type": "path",
"url": "tests/fixtures/modules/driver_field_test"
}
},
"minimum-stability": "beta",
Expand Down
86 changes: 85 additions & 1 deletion src/Drupal/Driver/Core/Core.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
use Drupal\Driver\Core\Field\FieldClassifier;
use Drupal\Driver\Core\Field\FieldClassifierInterface;
use Drupal\Driver\Core\Field\FieldHandlerInterface;
use Drupal\Driver\Core\Field\FieldShapeClassifier;
use Drupal\Driver\Core\Field\FieldShapeClassifierInterface;
use Drupal\Driver\Core\Alias\AuthorAlias;
use Drupal\Driver\Core\Alias\ParentTermAlias;
use Drupal\Driver\Core\Alias\VocabularyMachineNameAlias;
Expand Down Expand Up @@ -84,6 +86,11 @@ class Core implements CoreInterface, CreationAliasCapabilityInterface {
*/
protected ?FieldClassifierInterface $fieldClassifier = NULL;

/**
* Lazily created field shape classifier instance.
*/
protected ?FieldShapeClassifierInterface $fieldShapeClassifier = NULL;

/**
* Set up the Core implementation.
*
Expand Down Expand Up @@ -232,6 +239,28 @@ public function getFieldClassifier(): FieldClassifierInterface {
return $this->fieldClassifier;
}

/**
* Creates the field shape classifier instance for this Core.
*
* Subclasses override this method when they ship a version-specific value
* shape classifier. The default returns the base 'FieldShapeClassifier' which
* covers Drupal 10 and 11.
*/
protected function createFieldShapeClassifier(): FieldShapeClassifierInterface {
return new FieldShapeClassifier();
}

/**
* {@inheritdoc}
*/
public function getFieldShapeClassifier(): FieldShapeClassifierInterface {
if (!$this->fieldShapeClassifier instanceof FieldShapeClassifierInterface) {
$this->fieldShapeClassifier = $this->createFieldShapeClassifier();
}

return $this->fieldShapeClassifier;
}

/**
* {@inheritdoc}
*/
Expand All @@ -243,11 +272,66 @@ public function getFieldHandler(EntityStubInterface $stub, string $entity_type,
throw new \RuntimeException(sprintf('Field "%s" not found on entity type "%s".', $field_name, $entity_type));
}

$class = $this->fieldHandlers[$field_types[$field_name]] ?? DefaultHandler::class;
$field_type = $field_types[$field_name];
$class = $this->fieldHandlers[$field_type] ?? DefaultHandler::class;

if ($class === DefaultHandler::class) {
$this->assertDefaultHandlerCanMarshal($entity_type, $field_name, $field_type, $bundle);
}

return new $class($stub, $entity_type, $field_name);
}

/**
* Rejects a field the DefaultHandler fallback cannot marshal.
*
* Consulted only when no dedicated handler is registered for the field type.
* Delegates the value-shape decision to the field shape classifier and, when
* it reports the field is an entity reference or a complex/nested value,
* throws an actionable exception naming the field and why the default cannot
* relay it.
*
* @param string $entity_type
* The entity type ID.
* @param string $field_name
* The field name.
* @param string $field_type
* The field type ID, for the exception message.
* @param string $bundle
* The bundle name, for the exception message.
*
* @throws \RuntimeException
* When the field's stored shape is not one the default handler can relay.
*/
protected function assertDefaultHandlerCanMarshal(string $entity_type, string $field_name, string $field_type, string $bundle): void {
$storage = $this->getEntityFieldManager()->getFieldStorageDefinitions($entity_type)[$field_name] ?? NULL;

if ($storage === NULL) {
return;
}

$shape = $this->getFieldShapeClassifier();
$reason = NULL;

if ($shape->fieldIsEntityReference($storage)) {
$reason = 'it is an entity-reference value a dedicated handler must resolve to an id';
}
elseif ($shape->fieldIsComplexValue($storage)) {
$reason = 'it holds a complex or nested value with no single scalar shape';
}

if ($reason !== NULL) {
throw new \RuntimeException(sprintf(
'No dedicated handler is registered for field "%s" (type "%s") on entity type "%s" bundle "%s", and DefaultHandler cannot marshal it: %s. Register a dedicated handler via Core::registerFieldHandler().',
$field_name,
$field_type,
$entity_type,
$bundle,
$reason,
));
}
}

/**
* Expands values on the given stub through the field-handler pipeline.
*
Expand Down
13 changes: 13 additions & 0 deletions src/Drupal/Driver/Core/CoreInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
use Drupal\Driver\Capability\WatchdogCapabilityInterface;
use Drupal\Driver\Core\Field\FieldClassifierInterface;
use Drupal\Driver\Core\Field\FieldHandlerInterface;
use Drupal\Driver\Core\Field\FieldShapeClassifierInterface;
use Drupal\Driver\Entity\EntityStubInterface;

/**
Expand Down Expand Up @@ -150,4 +151,16 @@ public function getEntityFieldTypes(string $entity_type, ?string $bundle = NULL)
*/
public function getFieldClassifier(): FieldClassifierInterface;

/**
* Returns the field shape classifier, lazily instantiating on first access.
*
* Consumers call into the field shape classifier to ask a field's stored
* value shape - whether it is an entity reference or a complex/nested value -
* during handler selection. See 'src/Drupal/Driver/Core/Field/README.md'.
*
* @return \Drupal\Driver\Core\Field\FieldShapeClassifierInterface
* The field shape classifier instance.
*/
public function getFieldShapeClassifier(): FieldShapeClassifierInterface;

}
7 changes: 7 additions & 0 deletions src/Drupal/Driver/Core/Field/ColorFieldTypeHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,13 @@
* formatting and the opacity-disabled case, so the handler only relays the
* multi-column records through.
*
* @deprecated in drupal-driver:3.x and is removed from drupal-driver:4.0.0.
* The 'color'/'opacity' columns are plain scalars the generic DefaultHandler
* now relays, so this pass-through handler is redundant. It is retained for
* consumers that extend or reference it. Register a dedicated handler only
* for a field type whose author-facing input differs from its stored value.
*
* @see \Drupal\Driver\Core\Field\DefaultHandler
* @see https://www.drupal.org/project/color_field
*/
class ColorFieldTypeHandler extends AbstractHandler {
Expand Down
29 changes: 11 additions & 18 deletions src/Drupal/Driver/Core/Field/DefaultHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,32 +5,25 @@
namespace Drupal\Driver\Core\Field;

/**
* Fallback handler for field types that have no dedicated handler.
* Fallback handler for field types with no dedicated handler.
*
* Only correct for H1 (single-column scalar) fields. See
* 'src/Drupal/Driver/Core/Field/README.md' for the full handler-selection
* table and the loud-failure policy this class enforces.
* Relays the normalised records to storage verbatim. It is the resolved
* handler for any field type without a registered handler class. 'Core' asks
* the field shape classifier whether the field is a plain scalar before it
* falls back here (see 'FieldShapeClassifierInterface') and rejects a field
* this handler cannot marshal - an entity-reference target or a complex/nested
* value - so by the time this handler runs the field is known to be a
* plain-scalar shape safe to pass through.
*
* See 'src/Drupal/Driver/Core/Field/README.md' for the full handler-selection
* table.
*/
class DefaultHandler extends AbstractHandler {

/**
* {@inheritdoc}
*/
protected function doExpand(array $records): array {
$columns = $this->fieldInfo->getColumns();

if (count($columns) !== 1 || !array_key_exists('value', $columns)) {
throw new \RuntimeException(sprintf(
'No dedicated handler is registered for field "%s" (type "%s") on entity type "%s" bundle "%s", and DefaultHandler cannot marshal it: the field has %d column(s) (%s) and DefaultHandler only supports single-column scalar fields keyed by "value". Implement a dedicated handler for this field type and register it via Core::registerFieldHandler().',
$this->fieldInfo->getName(),
$this->fieldInfo->getType(),
$this->fieldInfo->getTargetEntityTypeId(),
$this->fieldConfig->getTargetBundle() ?? '(none)',
count($columns),
implode(', ', array_keys($columns)),
));
}

return $records;
}

Expand Down
66 changes: 66 additions & 0 deletions src/Drupal/Driver/Core/Field/FieldShapeClassifier.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
<?php

declare(strict_types=1);

namespace Drupal\Driver\Core\Field;

use Drupal\Core\Field\FieldStorageDefinitionInterface;
use Drupal\Core\TypedData\ComplexDataDefinitionInterface;
use Drupal\Core\TypedData\DataReferenceTargetDefinition;
use Drupal\Core\TypedData\ListDataDefinitionInterface;

/**
* Default Drupal 10/11 value-shape classifier.
*
* See 'src/Drupal/Driver/Core/Field/README.md' for the value-shape axis and how
* 'Core' consumes it during handler selection.
*/
class FieldShapeClassifier implements FieldShapeClassifierInterface {

/**
* {@inheritdoc}
*/
public function fieldIsEntityReference(FieldStorageDefinitionInterface $storage): bool {
foreach ($this->storedProperties($storage) as $definition) {
if ($definition instanceof DataReferenceTargetDefinition) {
return TRUE;
}
}

return FALSE;
}

/**
* {@inheritdoc}
*/
public function fieldIsComplexValue(FieldStorageDefinitionInterface $storage): bool {
foreach ($this->storedProperties($storage) as $definition) {
if ($definition instanceof ComplexDataDefinitionInterface || $definition instanceof ListDataDefinitionInterface) {
return TRUE;
}
}

return FALSE;
}

/**
* Yields a field's stored (non-computed) property definitions.
*
* Computed properties are storage-derived, not author-supplied, so they never
* bear on whether the caller can express the field as a plain scalar.
*
* @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage
* The field storage definition to inspect.
*
* @return iterable<\Drupal\Core\TypedData\DataDefinitionInterface>
* The stored property definitions.
*/
protected function storedProperties(FieldStorageDefinitionInterface $storage): iterable {
foreach ($storage->getPropertyDefinitions() as $definition) {
if (!$definition->isComputed()) {
yield $definition;
}
}
}

}
53 changes: 53 additions & 0 deletions src/Drupal/Driver/Core/Field/FieldShapeClassifierInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
<?php

declare(strict_types=1);

namespace Drupal\Driver\Core\Field;

use Drupal\Core\Field\FieldStorageDefinitionInterface;

/**
* Classifies a field's stored value shape for handler selection.
*
* Where 'FieldClassifierInterface' answers the pipeline-entry (F-row) question
* from a field's origin and storage profile, this answers the orthogonal
* value-shape question the README calls a "handler-selection input": is a
* field's stored value a plain scalar the default handler can relay, or a shape
* that needs a dedicated handler? Both predicates read only the storage
* definition's stored (non-computed) property definitions and enumerate no
* field-type or data-type strings, so a datetime, boolean, or list column is
* neither an entity reference nor complex - it is a plain scalar the default
* relays, and value translation for it belongs in a dedicated handler.
*
* See 'src/Drupal/Driver/Core/Field/README.md' for the value-shape axis and how
* 'Core' consumes it during handler selection.
*/
interface FieldShapeClassifierInterface {

/**
* Whether a stored property references another entity by id.
*
* @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage
* The field storage definition to inspect.
*
* @return bool
* TRUE when a non-computed property is a 'DataReferenceTargetDefinition' -
* the caller supplies a label, path, or name a dedicated handler must
* resolve to an id the author cannot know.
*/
public function fieldIsEntityReference(FieldStorageDefinitionInterface $storage): bool;

/**
* Whether a stored property holds a complex or nested value.
*
* @param \Drupal\Core\Field\FieldStorageDefinitionInterface $storage
* The field storage definition to inspect.
*
* @return bool
* TRUE when a non-computed property is a 'ComplexDataDefinitionInterface'
* (e.g. a map) or a 'ListDataDefinitionInterface' - there is no single
* scalar shape for the default handler to relay.
*/
public function fieldIsComplexValue(FieldStorageDefinitionInterface $storage): bool;

}
Loading
Loading