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
36 changes: 26 additions & 10 deletions UPGRADING.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,26 +32,44 @@ DrupalExtension integrations) may need updating.
- `Drupal\Driver\Core\AbstractCore` - merged into `Core`. Custom cores should
extend `Core` directly and override the methods they need.

### Field classification moved to `FieldClassifier`

The predicates `fieldExists()` and `fieldIsBase()` are gone from `DrupalDriver`
and `Core`. The empty `FieldCapabilityInterface` is removed entirely. Field
classification into the nine F-row categories (F1-F9) now lives on
`Drupal\Driver\Core\Field\FieldClassifierInterface`, implemented by
`Drupal\Driver\Core\Field\FieldClassifier`. See
`src/Drupal/Driver/Core/Field/README.md` for the full truth table.

If consumer code called `$driver->fieldExists(...)` or `$driver->fieldIsBase(...)`,
replace with:

- `fieldExists($type, $name)` → `$core->classifier()->fieldIsConfigurable($type, $name)` (if you were checking for a configurable field)
- `fieldIsBase($type, $name)` → `$core->classifier()->fieldIsBaseStandard($type, $name)` (or one of the more specific F-row predicates, depending on intent)

The classifier is the single source of truth for field classification; the
old two-predicate API was insufficient to distinguish F1-F9 correctly and
caused downstream bugs (notably with computed writable base fields like
`moderation_state`).

### DrushDriver no longer supports Content or Field capabilities

`DrushDriver` used to rely on a companion module installed on the
site-under-test to provide entity CRUD and field introspection over Drush.
That indirection has been removed: `DrushDriver` now exposes only operations
that Drush services natively.

`DrushDriverInterface` no longer extends `ContentCapabilityInterface` or
`FieldCapabilityInterface`. The following methods are gone from `DrushDriver`:
`DrushDriverInterface` no longer extends `ContentCapabilityInterface`. The
following methods are gone from `DrushDriver`:

- `nodeCreate`, `nodeDelete`
- `termCreate`, `termDelete`
- `entityCreate`, `entityDelete`
- `fieldExists`, `fieldIsBase`

Consumers that need entity CRUD or field introspection should use
`DrupalDriver` (which bootstraps Drupal and delegates to `Core`) or
implement the missing behaviour themselves. Test the capability with
`instanceof ContentCapabilityInterface` / `instanceof FieldCapabilityInterface`
before calling.
Consumers that need entity CRUD should use `DrupalDriver` (which bootstraps
Drupal and delegates to `Core`) or implement the missing behaviour themselves.
Test the capability with `instanceof ContentCapabilityInterface` before
calling.

### CoreInterface expanded

Expand All @@ -73,8 +91,6 @@ that capability):
| `createNode` | `nodeCreate` | Content |
| `createTerm` | `termCreate` | Content |
| `createEntity` | `entityCreate` | Content |
| `isField` | `fieldExists` | Field |
| `isBaseField` | `fieldIsBase` | Field |
| `clearCache` | `cacheClear` | Cache |
| `clearStaticCaches` | `cacheClearStatic` | Cache |
| `runCron` | `cronRun` | Cron |
Expand Down
38 changes: 0 additions & 38 deletions src/Drupal/Driver/Capability/FieldCapabilityInterface.php

This file was deleted.

155 changes: 82 additions & 73 deletions src/Drupal/Driver/Core/Core.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,10 @@
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Driver\Core\Field\DefaultHandler;
use Drupal\Driver\Core\Field\FieldClassifier;
use Drupal\Driver\Core\Field\FieldClassifierInterface;
use Drupal\Driver\Core\Field\FieldHandlerInterface;
use Drupal\Driver\Exception\BootstrapException;
use Drupal\field\Entity\FieldStorageConfig;
use Drupal\language\Entity\ConfigurableLanguage;
use Drupal\mailsystem\MailsystemManager;
use Drupal\node\Entity\Node;
Expand Down Expand Up @@ -69,6 +70,11 @@ class Core implements CoreInterface {
*/
protected array $fieldHandlers = [];

/**
* Lazily created field classifier instance.
*/
protected ?FieldClassifierInterface $fieldClassifier = NULL;

/**
* Set up the Core implementation.
*
Expand Down Expand Up @@ -178,11 +184,34 @@ protected function deriveFieldType(string $short_class_name): string {
return strtolower((string) preg_replace('/([a-z0-9])([A-Z])/', '$1_$2', $bare));
}

/**
* Creates the field classifier instance for this Core.
*
* Subclasses override this method when they ship a version-specific
* classifier. The default returns the base 'FieldClassifier' which covers
* Drupal 10 and 11.
*/
protected function createFieldClassifier(): FieldClassifierInterface {
return new FieldClassifier($this->getEntityFieldManager());
}

/**
* {@inheritdoc}
*/
public function classifier(): FieldClassifierInterface {
if (!$this->fieldClassifier instanceof FieldClassifierInterface) {
$this->fieldClassifier = $this->createFieldClassifier();
}

return $this->fieldClassifier;
}

/**
* {@inheritdoc}
*/
public function getFieldHandler(object $entity, string $entity_type, string $field_name): FieldHandlerInterface {
$field_types = $this->getEntityFieldTypes($entity_type, [$field_name]);
$bundle = $this->resolveBundleFromEntity($entity_type, $entity);
$field_types = $this->getEntityFieldTypes($entity_type, $bundle);

if (!isset($field_types[$field_name])) {
throw new \RuntimeException(sprintf('Field "%s" not found on entity type "%s".', $field_name, $entity_type));
Expand All @@ -200,21 +229,26 @@ public function getFieldHandler(object $entity, string $entity_type, string $fie
* The entity type ID.
* @param \stdClass $entity
* Entity object.
* @param array<string> $base_fields
* Optional. Define base fields that will be expanded in addition to user
* defined fields.
*/
protected function expandEntityFields(string $entity_type, \stdClass $entity, array $base_fields = []): void {
// Include any base fields present as properties on the entity object so
// their values travel through the field-handler pipeline alongside
// configured fields. Without this, base entity-reference fields such as
// 'commerce_product.variations' or 'user.roles' would never reach
// EntityReferenceHandler and could not be populated from a stub.
$base_fields = array_values(array_unique(array_merge($base_fields, $this->detectBaseFieldsOnEntity($entity_type, $entity))));
protected function expandEntityFields(string $entity_type, \stdClass $entity): void {
$definition = $this->loadEntityTypeDefinition($entity_type);

$field_types = $this->getEntityFieldTypes($entity_type, $base_fields);
// The id key and bundle key identify the record itself and must not pass
// through the handler pipeline. For example, on 'commerce_product' the
// bundle key 'type' is also a base entity_reference field; expanding it
// would resolve the bundle machine name through EntityReferenceHandler
// and overwrite the scalar with ['target_id' => ...], corrupting every
// subsequent bundle lookup for the same stub.
$skip = array_filter([$definition->getKey('id'), $definition->getKey('bundle')]);

$bundle = $this->resolveBundleFromEntity($entity_type, $entity);
$field_types = $this->getEntityFieldTypes($entity_type, $bundle);

foreach (array_keys($field_types) as $field_name) {
if (in_array($field_name, $skip, TRUE)) {
continue;
}

if (isset($entity->$field_name)) {
$entity->$field_name = $this->getFieldHandler($entity, $entity_type, $field_name)
->expand($entity->$field_name);
Expand All @@ -223,39 +257,33 @@ protected function expandEntityFields(string $entity_type, \stdClass $entity, ar
}

/**
* Returns the names of base fields set as properties on the entity.
* Resolves the bundle for an entity stub.
*
* The entity type's id key and bundle key are excluded: they identify the
* record itself and must not pass through the field-handler pipeline, where
* they would be treated as look-up values (e.g. expanding node's 'type'
* through EntityReferenceHandler would try to resolve the bundle string as
* a NodeType config entity reference and discard the plain string form).
* Consults the entity type's bundle key first, then 'step_bundle', then
* falls back to the entity type id (single-bundle entities like 'user'
* use the type id as their bundle).
*
* @param string $entity_type
* The entity type ID.
* @param \stdClass $entity
* Entity stub whose properties are inspected.
* @param object $entity
* Entity stub.
*
* @return array<string>
* Base field names whose corresponding property is set on the stub.
* @return string
* Bundle name. Never empty.
*/
protected function detectBaseFieldsOnEntity(string $entity_type, \stdClass $entity): array {
protected function resolveBundleFromEntity(string $entity_type, object $entity): string {
$definition = $this->loadEntityTypeDefinition($entity_type);
$skip = array_filter([$definition->getKey('id'), $definition->getKey('bundle')]);

$detected = [];
$bundle_key = $definition->getKey('bundle');

foreach (array_keys($this->getEntityFieldManager()->getBaseFieldDefinitions($entity_type)) as $field_name) {
if (in_array($field_name, $skip, TRUE)) {
continue;
}
if ($bundle_key && !empty($entity->$bundle_key)) {
return (string) $entity->$bundle_key;
}

if (property_exists($entity, $field_name)) {
$detected[] = $field_name;
}
if (isset($entity->step_bundle) && $entity->step_bundle !== '') {
return (string) $entity->step_bundle;
}

return $detected;
return $entity_type;
}

/**
Expand Down Expand Up @@ -798,38 +826,35 @@ public function getExtensionPathList(): array {
return $paths;
}

/**
* Expands specified base fields on the entity object.
*
* @param string $entity_type
* The entity type for which to return the field types.
* @param \StdClass $entity
* Entity object.
* @param array<string> $base_fields
* Base fields to be expanded in addition to user defined fields.
*/
public function expandEntityBaseFields(string $entity_type, \StdClass $entity, array $base_fields): void {
$this->expandEntityFields($entity_type, $entity, $base_fields);
}

/**
* {@inheritdoc}
*/
public function getEntityFieldTypes(string $entity_type, array $base_fields = []): array {
public function getEntityFieldTypes(string $entity_type, ?string $bundle = NULL): array {
$entity_field_manager = $this->getEntityFieldManager();
$fields = $entity_field_manager->getFieldStorageDefinitions($entity_type);
$bundle_fields = [];
$fields = $entity_field_manager->getFieldStorageDefinitions($entity_type)
+ $entity_field_manager->getBaseFieldDefinitions($entity_type);

if ($base_fields !== []) {
$fields += $entity_field_manager->getBaseFieldDefinitions($entity_type);
if ($bundle !== NULL) {
$bundle_fields = $entity_field_manager->getFieldDefinitions($entity_type, $bundle);
$fields += $bundle_fields;
}

$types = [];

foreach ($fields as $field_name => $field) {
$is_configured = $this->fieldExists($entity_type, $field_name);
$is_requested_base = in_array($field_name, $base_fields) && $this->fieldIsBase($entity_type, $field_name);

if (!$is_configured && !$is_requested_base) {
// See src/Drupal/Driver/Core/Field/README.md. Only F1, F5, F9 enter the
// expansion pipeline; the OR below names those rows explicitly.
// F5 is additionally scoped to the bundle when known - otherwise a
// configurable field storage attached only to other bundles would slip
// into the type map and blow up in AbstractHandler::__construct().
$is_base_standard = $this->classifier()->fieldIsBaseStandard($entity_type, $field_name);
$is_configurable = $this->classifier()->fieldIsConfigurable($entity_type, $field_name)
&& ($bundle === NULL || isset($bundle_fields[$field_name]));
$is_bundle_storage_backed = $bundle !== NULL
&& $this->classifier()->fieldIsBundleStorageBacked($entity_type, $field_name, $bundle);

if (!$is_base_standard && !$is_configurable && !$is_bundle_storage_backed) {
continue;
}

Expand All @@ -839,22 +864,6 @@ public function getEntityFieldTypes(string $entity_type, array $base_fields = []
return $types;
}

/**
* {@inheritdoc}
*/
public function fieldExists(string $entity_type, string $field_name): bool {
$fields = $this->getEntityFieldManager()->getFieldStorageDefinitions($entity_type);
return (isset($fields[$field_name]) && $fields[$field_name] instanceof FieldStorageConfig);
}

/**
* {@inheritdoc}
*/
public function fieldIsBase(string $entity_type, string $field_name): bool {
$base_fields = $this->getEntityFieldManager()->getBaseFieldDefinitions($entity_type);
return isset($base_fields[$field_name]);
}

/**
* Returns the entity field manager service.
*
Expand Down
25 changes: 20 additions & 5 deletions src/Drupal/Driver/Core/CoreInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,13 @@
use Drupal\Driver\Capability\ConfigCapabilityInterface;
use Drupal\Driver\Capability\ContentCapabilityInterface;
use Drupal\Driver\Capability\CronCapabilityInterface;
use Drupal\Driver\Capability\FieldCapabilityInterface;
use Drupal\Driver\Capability\LanguageCapabilityInterface;
use Drupal\Driver\Capability\MailCapabilityInterface;
use Drupal\Driver\Capability\ModuleCapabilityInterface;
use Drupal\Driver\Capability\RoleCapabilityInterface;
use Drupal\Driver\Capability\UserCapabilityInterface;
use Drupal\Driver\Capability\WatchdogCapabilityInterface;
use Drupal\Driver\Core\Field\FieldClassifierInterface;
use Drupal\Driver\Core\Field\FieldHandlerInterface;

/**
Expand All @@ -34,7 +34,6 @@ interface CoreInterface extends
ConfigCapabilityInterface,
ContentCapabilityInterface,
CronCapabilityInterface,
FieldCapabilityInterface,
LanguageCapabilityInterface,
MailCapabilityInterface,
ModuleCapabilityInterface,
Expand Down Expand Up @@ -124,14 +123,30 @@ public function registerFieldHandler(string $field_type, string $class): void;
/**
* Returns the field types for the given entity type.
*
* Returns the map of every F1, F5, and F9 field that should be routed
* through the handler pipeline for this entity type. See
* 'src/Drupal/Driver/Core/Field/README.md' for the classification rules.
*
* @param string $entity_type
* The entity type ID.
* @param array<string> $base_fields
* Optional. Base fields to include alongside user-defined fields.
* @param string|null $bundle
* Optional. Bundle to consult for F9 (storage-backed bundle-attached)
* fields. Without a bundle only F1 and F5 fields surface.
*
* @return array<string, string>
* Map of field name to field type.
*/
public function getEntityFieldTypes(string $entity_type, array $base_fields = []): array;
public function getEntityFieldTypes(string $entity_type, ?string $bundle = NULL): array;

/**
* Returns the field classifier, lazily instantiating on first access.
*
* Consumers call into the classifier to ask which F-row a field belongs to
* (F1, F2, ..., F9). See 'src/Drupal/Driver/Core/Field/README.md'.
*
* @return \Drupal\Driver\Core\Field\FieldClassifierInterface
* The classifier instance.
*/
public function classifier(): FieldClassifierInterface;

}
Loading