cms_content_sync-3.0.x-dev/src/Plugin/FieldHandlerBase.php
src/Plugin/FieldHandlerBase.php
<?php
namespace Drupal\cms_content_sync\Plugin;
use Drupal\cms_content_sync\Controller\LoggerProxy;
use Drupal\cms_content_sync\Helper\FieldHelper;
use Drupal\cms_content_sync\PullIntent;
use Drupal\cms_content_sync\PushIntent;
use Drupal\cms_content_sync\SyncIntent;
use Drupal\Core\Field\FieldStorageDefinitionInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\Plugin\PluginBase;
use Drupal\Core\TypedData\ComplexDataDefinitionInterface;
use EdgeBox\SyncCore\Interfaces\Configuration\IDefineEntityType;
use EdgeBox\SyncCore\Interfaces\Configuration\IDefineObject;
use Psr\Log\LoggerInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Drupal\Core\TypedData\DataDefinitionInterface;
use EdgeBox\SyncCore\Interfaces\Configuration\IDefineObjectProperty;
use EdgeBox\SyncCore\Interfaces\Configuration\IDefineProperty;
use EdgeBox\SyncCore\V2\Raw\Model\RemoteEntityTypePropertyFormat;
use function PHPSTORM_META\map;
/**
* Common base class for field handler plugins.
*
* @see \Drupal\cms_content_sync\Annotation\EntityHandler
* @see \Drupal\cms_content_sync\Plugin\FieldHandlerInterface
* @see plugin_api
*
* @ingroup third_party
*/
abstract class FieldHandlerBase extends PluginBase implements ContainerFactoryPluginInterface, FieldHandlerInterface {
/**
* A logger instance.
*
* @var \Psr\Log\LoggerInterface
*/
protected $logger;
/**
* @var string
*/
protected $entityTypeName;
/**
* @var string
*/
protected $bundleName;
/**
* @var string
*/
protected $fieldName;
/**
* @var \Drupal\Core\Field\FieldDefinitionInterface
*/
protected $fieldDefinition;
/**
* @var array
* Additional settings as provided by
* {@see FieldHandlerInterface::getHandlerSettings}
*/
protected $settings;
/**
* @var \Drupal\cms_content_sync\Entity\Flow
*/
protected $flow;
/**
* FieldHelper service.
*
* @var \Drupal\cms_content_sync\Helper\FieldHelper
*/
protected FieldHelper $fieldHelper;
/**
* Constructs a Drupal\rest\Plugin\ResourceBase object.
*
* @param array $configuration
* A configuration array containing information about the plugin instance.
* Must contain entity_type_name, bundle_name, field_name, field_definition,
* settings and sync (see above).
* @param string $plugin_id
* The plugin_id for the plugin instance.
* @param mixed $plugin_definition
* The plugin implementation definition.
* @param \Psr\Log\LoggerInterface $logger
* A logger instance.
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, LoggerInterface $logger, FieldHelper $field_helper) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->logger = $logger;
$this->entityTypeName = $configuration['entity_type_name'];
$this->bundleName = $configuration['bundle_name'];
$this->fieldName = $configuration['field_name'];
$this->fieldDefinition = $configuration['field_definition'];
$this->settings = $configuration['settings'];
$this->flow = $configuration['sync'];
$this->fieldHelper = $field_helper;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(
$configuration,
$plugin_id,
$plugin_definition,
LoggerProxy::get(),
$container->get('cms_content_sync.field_helper'),
);
}
/**
* @return string
*/
public function getFieldName() {
return $this->fieldName;
}
/**
* {@inheritdoc}
*/
public function getAllowedPushOptions() {
return [
PushIntent::PUSH_DISABLED,
PushIntent::PUSH_AUTOMATICALLY,
];
}
/**
* {@inheritdoc}
*/
public function getAllowedPullOptions() {
return [
PullIntent::PULL_DISABLED,
PullIntent::PULL_AUTOMATICALLY,
];
}
/**
* {@inheritdoc}
*/
public function getHandlerSettings($current_values, $type = 'both') {
// Nothing special here.
return [];
}
/**
* {@inheritdoc}
*/
public function validateHandlerSettings(array &$form, FormStateInterface $form_state, string $entity_type_name, string $bundle_name, string $field_name, $current_values) {
// No settings means no validation.
}
/**
* Get the target bundles for the reference field.
*
* @return array
* An array of target bundles.
*/
protected function getFieldTargetBundles(): array {
return $this->fieldHelper->getEntityReferenceFieldAllowedTargetBundles($this->fieldDefinition);
}
/**
* {@inheritdoc}
*/
public function pull(PullIntent $intent) {
$action = $intent->getAction();
$entity = $intent->getEntity();
// Deletion doesn't require any action on field basis for static data.
if (SyncIntent::ACTION_DELETE == $action) {
return FALSE;
}
if ($intent->shouldMergeChanges() && !$this->forceMergeOverwrite()) {
return FALSE;
}
if (PullIntent::PULL_AUTOMATICALLY != $this->settings['import']) {
return FALSE;
}
// These fields can't be changed.
if (!$entity->isNew()) {
if ('default_langcode' === $this->fieldName) {
return TRUE;
}
}
$data = $intent->getProperty($this->fieldName);
if (empty($data)) {
if (!$this->fieldDefinition->isRequired()) {
$entity->set($this->fieldName, NULL);
}
}
else {
$entity->set($this->fieldName, $data);
}
return TRUE;
}
/**
* {@inheritdoc}
*/
public function push(PushIntent $intent) {
$action = $intent->getAction();
$entity = $intent->getEntity();
if (PushIntent::PUSH_AUTOMATICALLY != $this->settings['export']) {
return FALSE;
}
// Deletion doesn't require any action on field basis for static data.
if (SyncIntent::ACTION_DELETE == $action) {
return FALSE;
}
$intent->setProperty($this->fieldName, $entity->get($this->fieldName)->getValue());
return TRUE;
}
/**
*
*/
protected function applyPropertySettings(IDefineProperty $property) {
$storage = $this->fieldDefinition->getFieldStorageDefinition();
$cardinality = $storage->getCardinality();
$constraints = $this->fieldDefinition->getConstraints();
if (isset($constraints['Count']['min'])) {
$property->setMinItems($constraints['Count']['min']);
}
if ($cardinality !== FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED) {
$property->setMaxItems($cardinality);
}
elseif (isset($constraints['Count']['max'])) {
$property->setMaxItems($constraints['Count']['max']);
}
$property->setLocalized($this->fieldDefinition->isTranslatable());
$property->setShared(!$this->fieldDefinition->getTargetBundle());
$description = $this->fieldDefinition->getDescription();
if ($description) {
if (!is_string($description)) {
$description = $description->__toString();
}
$property->setDescription($description);
}
}
/**
* {@inheritDoc}
*/
public function definePropertyAtType(IDefineEntityType $type_definition) {
$property = $type_definition->addObjectProperty($this->fieldName, $this->fieldDefinition->getLabel(), TRUE, $this->fieldDefinition->isRequired(), $this->fieldDefinition->getType());
$storage = $this->fieldDefinition->getFieldStorageDefinition();
$this->applyPropertySettings($property);
$remove_properties = [
// Remove unused properties.
'language' => [
'language' => 1,
],
// Remove calculated values.
'published_at' => [
'published_at_or_now' => 1,
],
'text_with_summary' => [
'processed' => 1,
'summary_processed' => 1,
],
'text_long' => [
'processed' => 1,
],
// Remove local IDs.
'entitygroupfield' => [
'target_id' => 1,
],
'path' => [
'pid' => 1,
],
'image' => [
'target_id' => 1,
],
'file' => [
'target_id' => 1,
],
'svg_image_field' => [
'target_id' => 1,
],
'viewfield' => [
'target_id' => 1,
],
'webform' => [
'target_id' => 1,
],
];
$property_names = $storage->getPropertyNames();
// Could use getColumns on top / instead to supplement poor typing.
$columns = $storage->getColumns();
if (empty($property_names)) {
foreach ($columns as $name => $column) {
if (!empty($remove_properties[$this->fieldDefinition->getType()][$name])) {
continue;
}
$this->definePropertyByDefinition($name, $property, new class($name, $column) implements DataDefinitionInterface {
protected array $column;
protected string $name;
/**
*
*/
public function __construct(string $name, array $column) {
$this->name = $name;
$this->column = $column;
}
/**
* @inheritDoc
*/
public static function createFromDataType($data_type) {
throw new \Exception("Not supported.");
}
/**
* @inheritDoc
*/
public function getDataType() {
if (!empty($this->column['serialize'])) {
return 'map';
}
return $this->column['type'];
}
/**
* @inheritDoc
*/
public function getLabel() {
return $this->name;
}
/**
* @inheritDoc
*/
public function getDescription() {
return NULL;
}
/**
* @inheritDoc
*/
public function isList() {
return FALSE;
}
/**
* @inheritDoc
*/
public function isReadOnly() {
return FALSE;
}
/**
* @inheritDoc
*/
public function isComputed() {
return FALSE;
}
/**
* @inheritDoc
*/
public function isRequired() {
return !empty($this->column['not null']);
}
/**
* @inheritDoc
*/
public function getClass() {
return '\\NoClass';
}
/**
* @inheritDoc
*/
public function getSettings() {
return [];
}
/**
* @inheritDoc
*/
public function getSetting($setting_name) {
return NULL;
}
/**
* @inheritDoc
*/
public function getConstraints() {
return [];
}
/**
* @inheritDoc
*/
public function getConstraint($constraint_name) {
throw new \Exception("Not supported.");
}
/**
* @inheritDoc
*/
public function addConstraint($constraint_name, $options = NULL) {
throw new \Exception("Not supported.");
}
/**
* @inheritDoc
*/
public function isInternal() {
return FALSE;
}
}, $name === $storage->getMainPropertyName(), $column);
}
}
else {
foreach ($property_names as $name) {
if (!empty($remove_properties[$this->fieldDefinition->getType()][$name])) {
continue;
}
$definition = $storage->getPropertyDefinition($name);
if ($definition->isComputed()) {
continue;
}
$this->definePropertyByDefinition($name, $property, $definition, $name === $storage->getMainPropertyName(), $columns[$name] ?? []);
}
}
return $property;
}
/**
*
*/
protected function definePropertyByDefinition(string $name, IDefineObjectProperty $property, DataDefinitionInterface $definition, $is_main_property = FALSE, array $column = []) {
static $map = [
'serial' => 'integer',
'int' => 'integer',
'integer' => 'integer',
'float' => 'float',
'numeric' => 'float',
'decimal' => 'float',
'varchar' => 'string',
'string' => 'string',
'language_reference' => 'string',
'timestamp' => 'integer',
'boolean' => 'boolean',
'filter_format' => 'string',
'char' => 'string',
'text' => 'string',
'blob' => 'string',
'datetime' => 'string',
'uri' => 'string',
'entity_reference' => 'reference',
'any' => 'string',
'map' => 'object',
'datetime_iso8601' => 'string',
'email' => 'string',
'metatag' => 'string',
'layout_section' => 'string',
'chart_config' => 'string',
];
// For arrays: Tiny, Medium, Small, Big, Normal.
static $stringLengths = [
'varchar' => 0xFFFF,
'string' => 0xFFFF,
// 12
'language_reference' => 0xC,
'filter_format' => 0xFF,
'char' => 0xFF,
'text' => [
0xFF,
0xFF,
0xFFFFFF,
0xFFFFFFFF,
// 16KB
0x4000,
],
// 32
'datetime' => 0x20,
'uri' => 0x800,
// 32
'datetime_iso8601' => 0x20,
'email' => 0xFF,
// 16KB
'metatag' => 0x4000,
'layout_section' => 0xFF,
// Blob / unknown.
'blob' => 0xFFFFFFFF,
'any' => 0xFFFFFFFF,
];
static $integerSizes = [
1,
2,
3,
8,
4,
];
static $floatSizes = [
4,
4,
4,
8,
4,
];
static $sizeIndices = [
'tiny',
'small',
'medium',
'big',
'normal',
];
$simple_type = $map[$definition->getDataType()] ?? NULL;
if (empty($simple_type)) {
throw new \Exception('Using unknown data definition type ' . $definition->getDataType() . ' at property ' . $this->fieldName);
}
$constraints = $definition->getConstraints();
$allowed_values = NULL;
if ($name === 'country_code' || $this->fieldDefinition->getType() === 'address_country') {
$allowed_values = $this->fieldDefinition->getSetting('available_countries');
}
elseif ($is_main_property) {
$allowed_values = $this->fieldDefinition->getSetting('allowed_values');
}
$any_property = NULL;
$size = $definition->getSetting('size');
if (!$size && !empty($column['size'])) {
$size = $column['size'];
}
else {
$size = 'normal';
}
$size_index = array_search($size, $sizeIndices);
switch ($simple_type) {
case 'boolean':
$boolean_property = $property->addBooleanProperty($name, $definition->getLabel(), FALSE, $definition->isRequired(), $definition->getDataType());
$any_property = $boolean_property;
break;
case 'integer':
$integer_property = $property->addIntegerProperty($name, $definition->getLabel(), FALSE, $definition->isRequired(), $definition->getDataType());
if (isset($constraints['Range']['min'])) {
$integer_property->setMinValue($constraints['Range']['min']);
}
if (isset($constraints['Range']['max'])) {
$integer_property->setMaxValue($constraints['Range']['max']);
}
if ($integerSizes[$size_index] > 0) {
$integer_property->setByteSize($integerSizes[$size_index]);
}
$any_property = $integer_property;
break;
case 'float':
$float_property = $property->addFloatProperty($name, $definition->getLabel(), FALSE, $definition->isRequired(), $definition->getDataType());
if (isset($constraints['Range']['min'])) {
$float_property->setMinValue($constraints['Range']['min']);
}
if (isset($constraints['Range']['max'])) {
$float_property->setMaxValue($constraints['Range']['max']);
}
if ($floatSizes[$size_index] > 0) {
$float_property->setByteSize($floatSizes[$size_index]);
}
$any_property = $float_property;
break;
case 'string':
$string_property = $property->addStringProperty($name, $definition->getLabel(), FALSE, $definition->isRequired(), $definition->getDataType());
if (isset($constraints['Length']['min'])) {
$string_property->setMinLength($constraints['Length']['min']);
}
if (isset($constraints['Length']['max'])) {
$string_property->setMaxLength($constraints['Length']['max']);
}
elseif (!empty($column['length'])) {
$string_property->setMaxLength($column['length']);
}
elseif ($length_type = $stringLengths[$definition->getDataType()]) {
$max_length = is_array($length_type) ? $length_type[$size_index] : $length_type;
if ($max_length > 0) {
$string_property->setMaxLength($max_length);
}
}
if (!empty($constraints['Email']['mode'])) {
$string_property->setFormat(RemoteEntityTypePropertyFormat::EMAIL_ADDRESS);
}
elseif (!empty($constraints['Uuid']['mode'])) {
$string_property->setFormat(RemoteEntityTypePropertyFormat::UUID);
}
if (!empty($constraints['Regex']['pattern'])) {
$string_property->setRegularExpressionFormat($constraints['Regex']['pattern']);
}
if ($definition->getDataType() === 'datetime_iso8601') {
$string_property->setFormat(RemoteEntityTypePropertyFormat::DATE_ISO8601);
}
if (!empty($constraints['Country']['availableCountries'])) {
$allowed_values = $constraints['Country']['availableCountries'];
}
$any_property = $string_property;
break;
case 'reference':
$reference_property = $property->addReferenceProperty($name, $definition->getLabel(), FALSE, $definition->isRequired(), $definition->getDataType());
$target_type = $definition->getSetting('target_type');
if ($target_type) {
$target_bundles = $this->getFieldTargetBundles();
if (empty($target_bundles)) {
$reference_property->addAllowedType($target_type);
}
else {
foreach ($target_bundles as $target_bundle) {
$reference_property->addAllowedType($target_type, $target_bundle);
}
}
}
else {
if (in_array($this->fieldDefinition->getType(), ['image', 'file_uri', 'file', 'svg_image_field'])) {
$reference_property->addAllowedType('file');
}
elseif ($name === 'entity') {
if ($this->fieldDefinition->getType() === 'entitygroupfield') {
$reference_property->addAllowedType('group');
}
elseif ($this->fieldDefinition->getType() === 'viewfield') {
$reference_property->addAllowedType('view');
}
elseif ($this->fieldDefinition->getType() === 'webform') {
$reference_property->addAllowedType('webform');
}
}
}
$any_property = $reference_property;
break;
case 'object':
$object_property = $property->addObjectProperty($name, $definition->getLabel(), FALSE, $definition->isRequired(), $definition->getDataType());
if ($definition instanceof ComplexDataDefinitionInterface) {
foreach ($definition->getPropertyDefinitions() as $key => $property_definition) {
$this->definePropertyByDefinition($key, $object_property, $property_definition);
}
if ($definition->getMainPropertyName()) {
$object_property->setMainProperty($definition->getMainPropertyName());
}
}
$any_property = $object_property;
break;
}
if ($any_property && !empty($allowed_values)) {
foreach ($allowed_values as $value => $name) {
$any_property->addAllowedValue($name, $value);
}
}
}
/**
* @return false
*/
protected function forceMergeOverwrite() {
return 'changed' == $this->fieldName;
}
}
