xero-8.x-2.x-dev/src/Normalizer/XeroNormalizer.php
src/Normalizer/XeroNormalizer.php
<?php
namespace Drupal\xero\Normalizer;
use Drupal\Core\Datetime\DrupalDateTime;
use Drupal\Core\TypedData\ComplexDataInterface;
use Drupal\Core\TypedData\ListDataDefinitionInterface;
use Drupal\Core\TypedData\ListInterface;
use Drupal\Core\TypedData\Plugin\DataType\ItemList;
use Drupal\Core\TypedData\TypedDataManagerInterface;
use Drupal\serialization\Normalizer\ComplexDataNormalizer;
use Drupal\xero\Plugin\DataType\XeroComplexItemBase;
use Drupal\xero\Plugin\DataType\XeroItemBase;
use Drupal\xero\Plugin\DataType\XeroItemList;
use Drupal\xero\TypedData\Definition\XeroDefinitionInterface;
use Drupal\xero\TypedData\XeroComplexItemInterface;
use Drupal\xero\TypedData\XeroItemInterface;
use Symfony\Component\Serializer\Exception\UnexpectedValueException;
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
/**
* Implement denormalization for Xero complex data.
*/
class XeroNormalizer extends ComplexDataNormalizer implements DenormalizerInterface {
/**
* Typed Data manager.
*
* @var \Drupal\Core\TypedData\TypedDataManagerInterface
*/
protected $typedDataManager;
/**
* Old-style check for interface or class support.
*
* @var string
*/
protected $supportedInterfaceOrClass = 'Drupal\xero\TypedData\XeroItemInterface';
/**
* Initialization method.
*
* @param \Drupal\Core\TypedData\TypedDataManagerInterface $typed_data_manager
* The typed_data.manager service.
*/
public function __construct(TypedDataManagerInterface $typed_data_manager) {
$this->typedDataManager = $typed_data_manager;
}
/**
* {@inheritdoc}
*/
public function supportsNormalization($data, ?string $format = NULL, array $context = []): bool {
return $data instanceof XeroItemInterface;
}
/**
* {@inheritdoc}
*/
public function supportsDenormalization($data, string $type, ?string $format = NULL, array $context = []): bool {
if (isset($context['plugin_id'])) {
$definition = $this->typedDataManager->getDefinition($context['plugin_id'], FALSE);
return $definition && $definition['class'] === $type;
}
return FALSE;
}
/**
* {@inheritdoc}
*/
public function getSupportedTypes(?string $format): array {
return [
XeroComplexItemBase::class => TRUE,
XeroItemBase::class => TRUE,
];
}
/**
* {@inheritdoc}
*/
public function normalize($object, $format = NULL, array $context = []): \ArrayObject|array|string|int|float|bool|null {
$context['top'] = FALSE;
$ret = [];
// In order to make it easier to update Xero items when we don't know
// their full current state on Xero, we only want to send to Xero
// information about properties that have been explicitly specified on
// the typed data object, assuming that unspecified properties should
// be left unchanged on Xero.
/** @var \Drupal\Core\TypedData\TypedDataInterface $property */
foreach ($object->getSpecifiedProperties() as $propertyName => $property) {
$ret[$propertyName] = $this->serializer->normalize($property, $format, $context);
}
return $ret;
}
/**
* {@inheritdoc}
*/
public function denormalize($data, string $type, ?string $format = NULL, array $context = []): mixed {
// The context array requires the Xero data type to be known. If not, then
// cannot do anything. This is consistent with Entity normalization.
if (empty($context['plugin_id'])) {
throw new UnexpectedValueException('Plugin id parameter must be included in context.');
}
$plural_name = $type::getXeroProperty('plural_name');
if (isset($data[$plural_name])) {
return $this->denormalizeList($data[$plural_name], $type, $format, $context);
}
// This won't normally be reached when denormalizing Xero responses
// as they always include a plural name at the top level.
return $this->denormalizeItem($data, $type, $format, $context);
}
/**
* Denormalizes a list of Xero items.
*
* @param mixed $data
* The list data.
* @param string $class
* The expected class to instantiate the list items to.
* @param string|null $format
* Format the given data was extracted from.
* @param array $context
* Options available to the denormalizer.
*
* @return \Drupal\Core\TypedData\Plugin\DataType\ItemList|\Drupal\xero\Plugin\DataType\XeroItemList|\Drupal\Core\TypedData\ListInterface
* A typed data list object.
*
* @throws \Exception
*/
protected function denormalizeList($data, string $class, ?string $format, array $context): ItemList|XeroItemList|ListInterface {
$list_definition = $this->typedDataManager->createListDataDefinition($context['plugin_id']);
/** @var \Drupal\Core\TypedData\ComplexDataDefinitionInterface $definition */
$definition = $this->typedDataManager->createDataDefinition($context['plugin_id']);
// Typed Data Manager's createListDataDefinition method is dumb and creates
// lists of "any" by default so it needs to be set. DrupalWTF.
$list_definition->setItemDefinition($definition);
// Create an empty list and then populate each item.
/** @var \Drupal\Core\TypedData\Plugin\DataType\ItemList $items */
$items = $this->typedDataManager->create($list_definition, []);
foreach ($data as $index => $item_data) {
$item = $this->denormalizeItem($item_data, $class, $format, $context);
// Set the value, not the Typed Data object. Might have performance
// concerns about this.
$items->offsetSet(NULL, $item->getValue());
}
return $items;
}
/**
* Denormalizes a Xero item.
*
* @param mixed $data
* The item data.
* @param string $class
* The expected Xero item class to instantiate.
* @param string|null $format
* Format the given data was extracted from.
* @param array $context
* Options available to the denormalizer.
*
* @return \Drupal\xero\TypedData\XeroComplexItemInterface|\Drupal\Core\TypedData\ComplexDataInterface
* A Xero item object.
*
* @throws \Exception
*/
protected function denormalizeItem($data, string $class, ?string $format, array $context): XeroComplexItemInterface|ComplexDataInterface {
/** @var \Drupal\Core\TypedData\ComplexDataDefinitionInterface $definition */
$definition = $this->typedDataManager->createDataDefinition($context['plugin_id']);
/** @var \Drupal\Core\TypedData\ComplexDataInterface $item */
$item = $this->typedDataManager->create($definition, []);
// Go through each property definition.
foreach ($definition->getPropertyDefinitions() as $prop => $prop_definition) {
if (isset($data[$prop])) {
if ($prop_definition instanceof XeroDefinitionInterface) {
// If the definition is a "xero" type.
$prop_data = $this->denormalizeItem(
$data[$prop],
$prop_definition->getClass(),
$format,
['plugin_id' => $prop_definition->getDataType()]
);
$item->set($prop, $prop_data->getValue(), TRUE);
}
elseif ($prop_definition instanceof ListDataDefinitionInterface) {
// If the definition is a list of xero types.
$prop_data = $this->denormalizeList(
$data[$prop],
$prop_definition->getItemDefinition()->getClass(),
$format,
['plugin_id' => $prop_definition->getItemDefinition()->getDataType()]
);
$item->set($prop, $prop_data->getValue(), TRUE);
}
else {
// Otherwise set the property directly.
$value = $data[$prop];
// Detect Xero datetime format and convert to ISO8601.
if (!is_array($value) && preg_match('#^/Date\(([-]?[0-9]+)([0-9]{3})([+-][0-9]{4})?\)/$#', $value, $dateParts)) {
// Xero API DateTime format examples:
// "\/Date(1436961673000)\/" - unix timestamp (milliseconds).
// "\/Date(1436961673000+0100)\/" - with timezone.
// "\/Date(-1436961673000-0530)\/" - before the epoch.
// Regex matches for "\/Date(1436961673090+0100)\/":
// [1] = ([-]?[0-9]+) "1436961673" epoch seconds.
// [2] = ([0-9]{3}) "090" milliseconds.
// [3] = ([+-][0-9]{4})? "+0100"/"" optional timezone.
$datetime = DrupalDateTime::createFromFormat('U.u', $dateParts[1] . '.' . $dateParts[2] . '000')
->setTimezone(new \DateTimeZone($dateParts[3] ?? '+0000'));
$value = $datetime->format('c');
}
$item->set($prop, $value, TRUE);
}
}
}
return $item;
}
}
