SchemaCompatibilityChecker.php

Same filename in this branch
  1. 11.x core/modules/sdc/src/Component/SchemaCompatibilityChecker.php
Same filename and directory in other branches
  1. 10 core/modules/sdc/src/Component/SchemaCompatibilityChecker.php
  2. 10 core/lib/Drupal/Core/Theme/Component/SchemaCompatibilityChecker.php

Namespace

Drupal\Core\Theme\Component

File

core/lib/Drupal/Core/Theme/Component/SchemaCompatibilityChecker.php

View source
<?php

namespace Drupal\Core\Theme\Component;

use Drupal\Core\Render\Component\Exception\IncompatibleComponentSchema;

/**
 * Checks whether two schemas are compatible.
 *
 * This is used during component replacement. We do not allow replacing a
 * component if the schemas are not compatible. Component authors must consider
 * their component schema as part of their module/theme API. Breaking changes
 * to the schema must be done in a new major version.
 */
class SchemaCompatibilityChecker {
  
  /**
   * Checks if the replacement schema is compatible with the old one.
   *
   * The goal is to ensure existing usages of the original component will not
   * break when the new component takes place.
   *
   * For the new schema to be compatible with the original it needs to accept
   * all the possible input that the original component allow. Any optional
   * props in the original component, not present in the replacement component
   * should be ignored and not cause validation errors.
   *
   * @param array $original_schema
   *   The schema to check compatibility against.
   * @param array $new_schema
   *   The new schema that should be compatible with.
   *
   * @throws \Drupal\Core\Render\Component\Exception\IncompatibleComponentSchema
   */
  public function isCompatible(array $original_schema, array $new_schema) : void {
    $error_messages = [];
    // First check the required properties.
    $error_messages = [
      $error_messages,
      $this->checkRequired($original_schema, $new_schema),
    ];
    // Next, compare the property types to ensure compatibility.
    $error_messages = [
      $error_messages,
      $this->checkSharedProperties($original_schema, $new_schema),
    ];
    // Finally, raise any potential issues that we might have detected.
    if (!empty($error_messages)) {
      $message = implode("\n", $error_messages);
      throw new IncompatibleComponentSchema($message);
    }
  }
  
  /**
   * Checks that the required properties are compatible.
   *
   * @param array $original_schema
   *   The original schema.
   * @param array $new_schema
   *   The schema of the replacement component.
   *
   * @return array
   *   The detected error messages, if any.
   */
  private function checkRequired(array $original_schema, array $new_schema) : array {
    $error_messages = [];
    $original_required = $original_schema['required'] ?? [];
    $new_required = $new_schema['required'] ?? [];
    $missing_required = array_diff($original_required, $new_required);
    if (!empty($missing_required)) {
      $error_messages[] = sprintf('Some of the required properties are missing in the new schema: [%s].', implode(', ', $missing_required));
    }
    $additional_required = array_diff($new_required, $original_required);
    if (!empty($additional_required)) {
      $error_messages[] = sprintf('Some of the new required properties are not allowed by the original schema: [%s].', implode(', ', $additional_required));
    }
    return $error_messages;
  }
  
  /**
   * Checks that the shared properties are compatible.
   *
   * @param array $original_schema
   *   The original schema.
   * @param array $new_schema
   *   The schema of the replacement component.
   *
   * @return array
   *   The detected error messages, if any.
   */
  private function checkSharedProperties(array $original_schema, array $new_schema) : array {
    $error_messages = [];
    $original_properties = $original_schema['properties'] ?? [];
    $new_properties = $new_schema['properties'] ?? [];
    $shared_properties = array_intersect(array_keys($original_properties), array_keys($new_properties));
    return array_reduce($shared_properties, function (array $errors, string $property_name) use ($original_properties, $new_properties) {
      $original_types = $original_properties[$property_name]['type'] ?? [];
      $new_types = $new_properties[$property_name]['type'] ?? [];
      // The type for the new property should, at least, accept all types for
      // the original property. Type in JSON Schema can be either a string or
      // an array of strings.
      $original_types = is_string($original_types) ? [
        $original_types,
      ] : $original_types;
      $new_types = is_string($new_types) ? [
        $new_types,
      ] : $new_types;
      $unsupported_types = array_diff($original_types, $new_types);
      if (!empty($unsupported_types)) {
        $errors[] = sprintf('Property "%s" does not support the types [%s]. These types are supported in the original schema and should be supported in the new schema for compatibility.', $property_name, implode(', ', $unsupported_types));
      }
      // If there are enums, those also need to be compatible.
      $original_enums = $original_properties[$property_name]['enum'] ?? [];
      $new_enums = $new_properties[$property_name]['enum'] ?? [];
      $unsupported_enums = array_diff($original_enums, $new_enums);
      if (!empty($unsupported_enums)) {
        $errors[] = sprintf('Property "%s" does not allow some necessary enum values [%s]. These are supported in the original schema and should be supported in the new schema for compatibility.', $property_name, implode(', ', $unsupported_enums));
      }
      // If the property is an object, then ensure sub-schema compatibility.
      $original_subproperties = $original_properties[$property_name]['properties'] ?? [];
      $new_subproperties = $new_properties[$property_name]['properties'] ?? [];
      if (!empty($original_subproperties) && !empty($new_subproperties)) {
        try {
          $this->isCompatible($original_properties[$property_name], $new_properties[$property_name]);
        } catch (IncompatibleComponentSchema $exception) {
          $errors[] = $exception->getMessage();
        }
      }
      return $errors;
    }, $error_messages);
  }

}

Classes

Title Deprecated Summary
SchemaCompatibilityChecker Checks whether two schemas are compatible.

Buggy or inaccurate documentation? Please file an issue. Need support? Need help programming? Connect with the Drupal community.