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
7 changes: 7 additions & 0 deletions src/Drupal/Driver/Alias/RolesAlias.php
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,13 @@ public function applyAfterCreate(EntityStubInterface $stub, object $entity): voi
}

foreach ($roles as $role) {
// EntityReferenceHandler expands 'roles' into records like
// '['target_id' => 'editor']'. Unwrap that here so the alias can
// operate on the same role name the caller supplied originally.
if (is_array($role) && array_key_exists('target_id', $role)) {
$role = $role['target_id'];
}

if (!is_scalar($role) && !$role instanceof \Stringable) {
throw new CreationAliasResolutionException("Cannot assign role because one of the 'roles' entries is not a scalar or stringable value.");
}
Expand Down
10 changes: 9 additions & 1 deletion src/Drupal/Driver/Core/Alias/AuthorAlias.php
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,15 @@ public function applyToStub(EntityStubInterface $stub): void {
throw new CreationAliasResolutionException(sprintf("Cannot create node because the 'author' lookup returned an object without an 'id()' method while resolving '%s'.", $name));
}

$stub->setValue('uid', $user->id());
$resolved_uid = $user->id();

if (!is_numeric($resolved_uid) || (int) $resolved_uid <= 0) {
throw new CreationAliasResolutionException(sprintf("Cannot create node because the user resolved from 'author' = '%s' has an invalid id.", $name));
}

// Cast to int so the downstream entity-reference handler treats the
// value as a pre-resolved id and skips its own validation query.
$stub->setValue('uid', (int) $resolved_uid);
$stub->removeValue('author');
}

Expand Down
8 changes: 7 additions & 1 deletion src/Drupal/Driver/Core/Alias/ParentTermAlias.php
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,13 @@ public function applyToStub(EntityStubInterface $stub): void {
throw new CreationAliasResolutionException(sprintf("Cannot create term because parent term '%s' does not exist in vocabulary '%s'.", $parent_name, $vid));
}

$stub->setValue('parent', $tid);
if (!is_numeric($tid) || (int) $tid <= 0) {
throw new CreationAliasResolutionException(sprintf("Cannot resolve parent term '%s' in vocabulary '%s' because the lookup returned an invalid tid.", $parent_name, $vid));
}

// Cast to int so the downstream entity-reference handler treats the
// value as a pre-resolved tid and skips its own validation query.
$stub->setValue('parent', (int) $tid);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}

}
48 changes: 27 additions & 21 deletions src/Drupal/Driver/Core/Field/AbstractHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
* Base class for field handlers.
*/
abstract class AbstractHandler implements FieldHandlerInterface {

/**
* Field storage definition.
*/
Expand All @@ -25,13 +26,9 @@ abstract class AbstractHandler implements FieldHandlerInterface {
/**
* Main property name of the field's storage definition.
*
* Cached once at construction so 'normalise()' and subclass 'expand()'
* methods do not have to look it up on every call. Resolves to the
* column key that a bare scalar maps to ('target_id' for image/file/
* entity_reference, 'value' for datetime/boolean/list/text, 'uri' for
* link, etc.). NULL for field types without a single main column (e.g.
* 'address', 'name'); those handlers must override 'normalise()' or
* 'expand()' to interpret records themselves.
* NULL for field types without a single main column (e.g. 'address',
* 'name'); those handlers must override 'normalise()' to interpret
* records themselves.
*/
protected ?string $mainProperty = NULL;

Expand Down Expand Up @@ -81,12 +78,14 @@ public function __construct(EntityStubInterface $stub, string $entity_type, stri
}

/**
* Normalises loose handler input into a canonical list of records.
*
* Consumers should not have to know whether a field is single- or
* multi-column; this method accepts any of the natural shapes a caller
* is likely to produce and returns a uniform 'array<int, array<string,
* mixed>>' that 'expand()' implementations can iterate without sniffing.
* {@inheritdoc}
*/
final public function expand(mixed $values): array {
return $this->doExpand($this->normalise($values));
}

/**
* Folds loose input into a canonical list of records.
*
* Recognised input shapes:
* - Bare scalar -> wrapped as a single record using the main property.
Expand All @@ -95,19 +94,15 @@ public function __construct(EntityStubInterface $stub, string $entity_type, stri
* - List of records -> returned unchanged.
* - Mixed list of scalars and records -> scalars wrapped, records kept.
*
* The main property name (the column a bare scalar maps to) is pulled
* from the field's storage definition - 'target_id' for image/file/
* entity_reference, 'value' for datetime/boolean/list/text, 'uri' for
* link, etc. Subclasses with custom shorthand (e.g. 'NameHandler's
* 'Family, Given' string, 'AddressHandler's first-visible-field
* shorthand) should override this method and call 'parent::normalise()'
* for the residual cases they do not handle themselves.
* Subclasses with custom shorthand override this method and may call
* 'parent::normalise()' for the residual shapes they do not handle
* themselves.
*
* @param mixed $values
* Whatever shape the caller produced.
*
* @return array<int, array<string, mixed>>
* A canonical list of records.
* Canonical list of records.
*/
protected function normalise(mixed $values): array {
if ($this->mainProperty === NULL) {
Expand Down Expand Up @@ -172,4 +167,15 @@ protected function normalise(mixed $values): array {
return $records;
}

/**
* Transforms canonical records into the storage shape.
*
* @param array<int, array<string, mixed>> $records
* Canonical list of records.
*
* @return array<int|string, mixed>
* Field values in the format expected by Drupal's entity storage.
*/
abstract protected function doExpand(array $records): array;

}
61 changes: 51 additions & 10 deletions src/Drupal/Driver/Core/Field/AddressHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,22 +5,67 @@
namespace Drupal\Driver\Core\Field;

/**
* Address field handler for Drupal 8.
* Field handler for 'address' fields.
*/
class AddressHandler extends AbstractHandler {

/**
* {@inheritdoc}
*/
public function expand($values): array {
protected function normalise(mixed $values): array {
if ($values === []) {
return [];
}

$visible_fields = $this->getVisibleAddressFields();
$result = [];

if (is_string($values)) {
return [$this->normaliseDelta($values, $visible_fields)];
}

if (!is_array($values)) {
throw new \InvalidArgumentException(sprintf('Address field value must be a string or array. Got %s.', get_debug_type($values)));
}

// A top-level list of scalars is a single positional address (the
// visible-field positions), not a multi-delta list. Only a list whose
// first element is an array iterates as multi-delta.
$is_list_of_records = array_is_list($values) && is_array($values[0] ?? NULL);

if (!$is_list_of_records) {
return [$this->normaliseDelta($values, $visible_fields)];
}

$records = [];

foreach ($values as $value) {
$result[] = $this->normaliseDelta($value, $visible_fields);
if (!is_array($value) && !is_string($value)) {
throw new \InvalidArgumentException(sprintf('Address field delta must be a string or array. Got %s.', get_debug_type($value)));
}

$records[] = $this->normaliseDelta($value, $visible_fields);
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

return $result;
return $records;
}

/**
* {@inheritdoc}
*/
protected function doExpand(array $records): array {
// 'available_countries' is empty when the field accepts every country;
// 'reset([])' returns FALSE, which would land a boolean in storage.
// Leave 'country_code' unset in that case so the field's own default
// wins instead.
$available = $this->fieldConfig->getSettings()['available_countries'] ?? [];

foreach ($records as &$record) {
if (!isset($record['country_code']) && $available !== []) {
$record['country_code'] = reset($available);
}
}

return $records;
}

/**
Expand Down Expand Up @@ -64,7 +109,7 @@ protected function getVisibleAddressFields(): array {
}

/**
* Normalises a single address delta into a keyed array.
* Folds one address value into a keyed sub-field array.
*
* @param mixed $value
* A single address value (string or array).
Expand Down Expand Up @@ -100,10 +145,6 @@ protected function normaliseDelta(mixed $value, array $visible_fields): array {
$position++;
}

if (!isset($normalised['country_code'])) {
$normalised['country_code'] = reset($this->fieldConfig->getSettings()['available_countries']);
}

return $normalised;
}

Expand Down
28 changes: 4 additions & 24 deletions src/Drupal/Driver/Core/Field/BooleanHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,42 +5,22 @@
namespace Drupal\Driver\Core\Field;

/**
* Field handler for the 'boolean' field type.
*
* Scenarios authored against a BDD driver read naturally when boolean columns
* carry human words ('Yes', 'Published') rather than the raw 1/0 the Drupal
* field API stores. This handler resolves an incoming value in two stages:
*
* 1. The field's own 'on_label' / 'off_label' settings, compared
* case-insensitively. These are the labels the site builder configured;
* they already reflect the site's active language, so translated sites
* get translation matching for free.
* 2. A canonical allow-list via 'filter_var(FILTER_VALIDATE_BOOLEAN)':
* '1', 'true', 'on', 'yes' map to 1; '0', 'false', 'off', 'no', '' map
* to 0. Case-insensitive.
*
* A value that matches neither throws a descriptive 'RuntimeException' listing
* both the field-specific labels and the canonical set, so scenario authors
* get an actionable error instead of a silent coercion to FALSE.
*
* Subclasses in a 'Core{N}\Field\' tree can override 'resolveBoolean()' to
* extend the mapping (e.g. site-specific synonyms) without reimplementing the
* per-delta loop.
* Field handler for 'boolean' fields.
*/
class BooleanHandler extends AbstractHandler {

/**
* {@inheritdoc}
*/
public function expand(mixed $values): array {
protected function doExpand(array $records): array {
$settings = $this->fieldConfig->getSettings();
$on_label = (string) ($settings['on_label'] ?? '');
$off_label = (string) ($settings['off_label'] ?? '');

$resolved = [];

foreach ((array) $values as $value) {
$resolved[] = $this->resolveBoolean((string) $value, $on_label, $off_label);
foreach ($records as $record) {
$resolved[] = $this->resolveBoolean((string) $record['value'], $on_label, $off_label);
}

return $resolved;
Expand Down
49 changes: 42 additions & 7 deletions src/Drupal/Driver/Core/Field/DaterangeHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,23 +7,58 @@
use Drupal\datetime\Plugin\Field\FieldType\DateTimeItemInterface;

/**
* Daterange field handler for Drupal 8.
*
* Extends DatetimeHandler to reuse date formatting logic.
* Field handler for 'daterange' fields.
*/
class DaterangeHandler extends DatetimeHandler {

/**
* {@inheritdoc}
*/
public function expand($values): array {
protected function normalise(mixed $values): array {
if (!is_array($values) || $values === []) {
return [];
}

// A top-level positional pair like ['start', 'end'] is itself a list,
// so iterating directly would treat each scalar as its own delta and
// reject it. Only a list whose first element is an array is treated
// as a multi-delta list.
$is_list_of_records = array_is_list($values) && is_array($values[0] ?? NULL);

if (!$is_list_of_records) {
$values = [$values];
}

$records = [];

foreach ($values as $value) {
if (!is_array($value)) {
throw new \InvalidArgumentException(sprintf(
'Daterange field record must be an array (positional [start, end] or keyed value/end_value). Got %s.',
get_debug_type($value),
));
}

$records[] = [
'value' => $value['value'] ?? $value[0] ?? NULL,
'end_value' => $value['end_value'] ?? $value[1] ?? NULL,
];
}

return $records;
}

/**
* {@inheritdoc}
*/
protected function doExpand(array $records): array {
$site_timezone = new \DateTimeZone(\Drupal::config('system.date')->get('timezone.default') ?: 'UTC');
$storage_timezone = new \DateTimeZone(DateTimeItemInterface::STORAGE_TIMEZONE);
$result = [];

foreach ($values as $value) {
$start = $value['value'] ?? $value[0] ?? NULL;
$end = $value['end_value'] ?? $value[1] ?? NULL;
foreach ($records as $record) {
$start = $record['value'];
$end = $record['end_value'];

$result[] = [
'value' => $start ? $this->formatDateValue($start, $site_timezone, $storage_timezone) : NULL,
Expand Down
11 changes: 2 additions & 9 deletions src/Drupal/Driver/Core/Field/DatetimeHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,26 +9,19 @@
use Drupal\datetime\Plugin\Field\FieldType\DateTimeItemInterface;

/**
* Datetime field handler for Drupal 8.
* Field handler for 'datetime' fields.
*/
class DatetimeHandler extends AbstractHandler {

/**
* {@inheritdoc}
*
* Accepts whatever shape the caller naturally has: a bare date string,
* a list of date strings, a single record, or a list of records.
* 'normalise()' folds all of those into a canonical list of records
* before iteration. Returns a uniform list of records with 'value'
* formatted for storage.
*/
public function expand($values): array {
protected function doExpand(array $records): array {
// Fresh Drupal installs leave system.date:timezone.default NULL until the
// installer writes a value; fall back to UTC so the handler never passes
// NULL to DateTimeZone.
$site_timezone = new \DateTimeZone(\Drupal::config('system.date')->get('timezone.default') ?: 'UTC');
$storage_timezone = new \DateTimeZone(DateTimeItemInterface::STORAGE_TIMEZONE);
$records = $this->normalise($values);
$formatted = [];

foreach ($records as $record) {
Expand Down
4 changes: 2 additions & 2 deletions src/Drupal/Driver/Core/Field/DefaultHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ class DefaultHandler extends AbstractHandler {
/**
* {@inheritdoc}
*/
public function expand(mixed $values): array {
protected function doExpand(array $records): array {
$columns = $this->fieldInfo->getColumns();

if (count($columns) !== 1 || !array_key_exists('value', $columns)) {
Expand All @@ -31,7 +31,7 @@ public function expand(mixed $values): array {
));
}

return (array) $values;
return $records;
}

}
2 changes: 1 addition & 1 deletion src/Drupal/Driver/Core/Field/EmbridgeAssetItemHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
namespace Drupal\Driver\Core\Field;

/**
* Embridge Asset Item field handler for Drupal 8.
* Field handler for 'embridge_asset_item' fields (Embridge contrib).
*/
class EmbridgeAssetItemHandler extends EntityReferenceHandler {

Expand Down
Loading
Loading