Skip to content
Draft
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
51 changes: 32 additions & 19 deletions src/Context.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
namespace JMS\Serializer;

use JMS\Serializer\Exception\LogicException;
use JMS\Serializer\Exception\RuntimeException;
use JMS\Serializer\Exclusion\DepthExclusionStrategy;
use JMS\Serializer\Exclusion\DisjunctExclusionStrategy;
use JMS\Serializer\Exclusion\ExclusionStrategyInterface;
Expand Down Expand Up @@ -50,12 +49,11 @@ abstract class Context
*/
private $initialized = false;

/** @var \SplStack */
private $metadataStack;
/** @var array<ClassMetadata|PropertyMetadata> */
private array $metadataStack = [];

public function __construct()
{
$this->metadataStack = new \SplStack();
}

public function initialize(string $format, VisitorInterface $visitor, GraphNavigatorInterface $navigator, MetadataFactoryInterface $factory): void
Expand All @@ -68,7 +66,7 @@ public function initialize(string $format, VisitorInterface $visitor, GraphNavig
$this->visitor = $visitor;
$this->navigator = $navigator;
$this->metadataFactory = $factory;
$this->metadataStack = new \SplStack();
$this->metadataStack = [];

if (isset($this->attributes['groups'])) {
$this->addExclusionStrategy(new GroupsExclusionStrategy($this->attributes['groups']));
Expand Down Expand Up @@ -210,35 +208,50 @@ public function getFormat(): string

public function pushClassMetadata(ClassMetadata $metadata): void
{
$this->metadataStack->push($metadata);
$this->metadataStack[] = $metadata;
}

public function pushPropertyMetadata(PropertyMetadata $metadata): void
{
$this->metadataStack->push($metadata);
$this->metadataStack[] = $metadata;
}

public function popPropertyMetadata(): void
{
$metadata = $this->metadataStack->pop();

if (!$metadata instanceof PropertyMetadata) {
throw new RuntimeException('Context metadataStack not working well');
}
array_pop($this->metadataStack);
}

public function popClassMetadata(): void
{
$metadata = $this->metadataStack->pop();
array_pop($this->metadataStack);
}

if (!$metadata instanceof ClassMetadata) {
throw new RuntimeException('Context metadataStack not working well');
}
/**
* Returns the metadata stack count without creating a copy.
*/
public function getMetadataStackSize(): int
{
return \count($this->metadataStack);
}

/**
* Returns the top element of the metadata stack.
*
* @return ClassMetadata|PropertyMetadata
*/
public function getMetadataStackTop()
{
return $this->metadataStack[\count($this->metadataStack) - 1];
}

public function getMetadataStack(): \SplStack
/**
* Returns the metadata stack as an array with LIFO index order (0 = top/most recent).
*
* @return array<ClassMetadata|PropertyMetadata>
*/
public function getMetadataStack(): array
{
return $this->metadataStack;
return array_reverse($this->metadataStack);
}

public function getCurrentPath(): array
Expand All @@ -250,7 +263,7 @@ public function getCurrentPath(): array
$paths = [];
foreach ($this->metadataStack as $metadata) {
if ($metadata instanceof PropertyMetadata) {
array_unshift($paths, $metadata->name);
$paths[] = $metadata->name;
}
}

Expand Down
60 changes: 58 additions & 2 deletions src/Exclusion/DepthExclusionStrategy.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@
*/
final class DepthExclusionStrategy implements ExclusionStrategyInterface
{
private bool $cachedResult = false;
private int $cachedStackCount = -1;
private bool $hasMaxDepthOnStack = false;

public function shouldSkipClass(ClassMetadata $metadata, Context $context): bool
{
return $this->isTooDeep($context);
Expand All @@ -25,24 +29,76 @@ public function shouldSkipProperty(PropertyMetadata $property, Context $context)

private function isTooDeep(Context $context): bool
{
$currentCount = $context->getMetadataStackSize();

if ($currentCount === $this->cachedStackCount) {
return $this->cachedResult;
}

if ($currentCount < $this->cachedStackCount && !$this->cachedResult) {
$this->cachedStackCount = $currentCount;

return false;
}

$stack = $context->getMetadataStack();

if (!$this->hasMaxDepthOnStack && !$this->cachedResult && $currentCount > $this->cachedStackCount) {
$delta = $currentCount - $this->cachedStackCount;
$found = false;
$i = 0;
foreach ($stack as $metadata) {
if ($i >= $delta) {
break;
}

if ($metadata instanceof PropertyMetadata && null !== $metadata->maxDepth) {
$found = true;
break;
}

$i++;
}

if (!$found) {
$this->cachedStackCount = $currentCount;

return false;
}
}

// Full scan
$relativeDepth = 0;
$top = $currentCount > 0 ? $stack[0] : null;
$foundMaxDepth = false;

foreach ($context->getMetadataStack() as $metadata) {
foreach ($stack as $metadata) {
if (!$metadata instanceof PropertyMetadata) {
continue;
}

$relativeDepth++;

if (0 === $metadata->maxDepth && $context->getMetadataStack()->top() === $metadata) {
if (null !== $metadata->maxDepth) {
$foundMaxDepth = true;
}

if (0 === $metadata->maxDepth && $top === $metadata) {
continue;
}

if (null !== $metadata->maxDepth && $relativeDepth > $metadata->maxDepth) {
$this->cachedResult = true;
$this->cachedStackCount = $currentCount;

return true;
}
}

$this->hasMaxDepthOnStack = $foundMaxDepth;
$this->cachedResult = false;
$this->cachedStackCount = $currentCount;

return false;
}
}
33 changes: 20 additions & 13 deletions src/Exclusion/GroupsExclusionStrategy.php
Original file line number Diff line number Diff line change
Expand Up @@ -54,11 +54,29 @@ public function shouldSkipProperty(PropertyMetadata $property, Context $navigato
if ($this->nestedGroups) {
$groups = $this->getGroupsFor($navigatorContext);

$groupsMap = [];
foreach ($groups as $k => $v) {
if (is_string($k)) {
// nested group entry (key = property name, value = sub-groups)
continue;
}

if (is_string($v)) {
$groupsMap[$v] = true;
}
}

if (!$property->groups) {
return !in_array(self::DEFAULT_GROUP, $groups);
return !isset($groupsMap[self::DEFAULT_GROUP]);
}

return $this->shouldSkipUsingGroups($property, $groups);
foreach ($property->groups as $group) {
if (isset($groupsMap[$group])) {
return false;
}
}

return true;
} else {
if (!$property->groups) {
return !isset($this->groups[self::DEFAULT_GROUP]);
Expand All @@ -74,17 +92,6 @@ public function shouldSkipProperty(PropertyMetadata $property, Context $navigato
}
}

private function shouldSkipUsingGroups(PropertyMetadata $property, array $groups): bool
{
foreach ($property->groups as $group) {
if (in_array($group, $groups)) {
return false;
}
}

return true;
}

public function getGroupsFor(Context $navigatorContext): array
{
if (!$this->nestedGroups) {
Expand Down
18 changes: 15 additions & 3 deletions src/GraphNavigator/SerializationGraphNavigator.php
Original file line number Diff line number Diff line change
Expand Up @@ -203,9 +203,14 @@ public function accept($data, ?array $type = null)

// If we're serializing a polymorphic type, then we'll be interested in the
// metadata for the actual type of the object, not the base class.
$typeNameBeforeEvents = $type['name'];
$cachedHandler = false; // false = not looked up yet
if (class_exists($type['name'], false) || interface_exists($type['name'], false)) {
if (is_subclass_of($data, $type['name'], false) && null === $this->handlerRegistry->getHandler(GraphNavigatorInterface::DIRECTION_SERIALIZATION, $type['name'], $this->format)) {
$type = ['name' => \get_class($data), 'params' => $type['params'] ?? []];
if (is_subclass_of($data, $type['name'], false)) {
$cachedHandler = $this->handlerRegistry->getHandler(GraphNavigatorInterface::DIRECTION_SERIALIZATION, $type['name'], $this->format);
if (null === $cachedHandler) {
$type = ['name' => \get_class($data), 'params' => $type['params'] ?? []];
}
}
}

Expand All @@ -219,7 +224,14 @@ public function accept($data, ?array $type = null)
// First, try whether a custom handler exists for the given type. This is done
// before loading metadata because the type name might not be a class, but
// could also simply be an artificial type.
if (null !== $handler = $this->handlerRegistry->getHandler(GraphNavigatorInterface::DIRECTION_SERIALIZATION, $type['name'], $this->format)) {
// Reuse handler lookup if type hasn't changed since the polymorphic check
if (false !== $cachedHandler && $type['name'] === $typeNameBeforeEvents) {
$handler = $cachedHandler;
} else {
$handler = $this->handlerRegistry->getHandler(GraphNavigatorInterface::DIRECTION_SERIALIZATION, $type['name'], $this->format);
}

if (null !== $handler) {
try {
$rs = \call_user_func($handler, $this->visitor, $data, $type, $this->context);
$this->context->stopVisiting($data);
Expand Down
2 changes: 1 addition & 1 deletion src/Handler/ArrayCollectionHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ public function deserializeCollection(
return $elements;
}

$propertyMetadata = $context->getMetadataStack()->top();
$propertyMetadata = $context->getMetadataStackTop();
if (!$propertyMetadata instanceof PropertyMetadata) {
return $elements;
}
Expand Down
16 changes: 15 additions & 1 deletion src/Handler/DateHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,11 @@ final class DateHandler implements SubscribingHandlerInterface
*/
private $xmlCData;

/**
* @var array<string, \DateTimeZone>
*/
private $timezoneCache = [];

/**
* {@inheritdoc}
*/
Expand Down Expand Up @@ -250,7 +255,7 @@ public function deserializeDateIntervalFromJson(DeserializationVisitorInterface
*/
private function parseDateTime($data, array $type, bool $immutable = false): \DateTimeInterface
{
$timezone = !empty($type['params'][1]) ? new \DateTimeZone($type['params'][1]) : $this->defaultTimezone;
$timezone = !empty($type['params'][1]) ? $this->resolveTimezone($type['params'][1]) : $this->defaultTimezone;
$formats = $this->getDeserializationFormats($type);

$formatTried = [];
Expand Down Expand Up @@ -298,6 +303,15 @@ private function parseDateInterval(string $data): \DateInterval
return $dateInterval;
}

private function resolveTimezone(string $timezone): \DateTimeZone
{
if (!isset($this->timezoneCache[$timezone])) {
$this->timezoneCache[$timezone] = new \DateTimeZone($timezone);
}

return $this->timezoneCache[$timezone];
}

/**
* @param TypeArray $type
*/
Expand Down
15 changes: 14 additions & 1 deletion src/Handler/UnionHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -123,9 +123,22 @@ public function deserializeUnion(DeserializationVisitorInterface $visitor, mixed
throw new NotAcceptableException();
}

private static $primitiveTypes = [
'int' => true,
'integer' => true,
'float' => true,
'double' => true,
'bool' => true,
'boolean' => true,
'true' => true,
'false' => true,
'string' => true,
'array' => true,
];

private function isPrimitiveType(string $type): bool
{
return in_array($type, ['int', 'integer', 'float', 'double', 'bool', 'boolean', 'true', 'false', 'string', 'array'], true);
return isset(self::$primitiveTypes[$type]);
}

private function testPrimitive(mixed $data, string $type): bool
Expand Down
Loading
Loading