• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

api-platform / core / 7355537923

29 Dec 2023 09:42AM UTC coverage: 37.321% (-0.009%) from 37.33%
7355537923

push

github

web-flow
GraphQL: Nested Collections (#6038)

* feat(graphql): support nested collections

* null safe operator

---------

Co-authored-by: josef.wagner <josef.wagner@hf-solutions.co>
Co-authored-by: Antoine Bluchet <soyuka@users.noreply.github.com>

11 of 29 new or added lines in 4 files covered. (37.93%)

71 existing lines in 9 files now uncovered.

10360 of 27759 relevant lines covered (37.32%)

28.59 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

4.08
/src/GraphQl/Serializer/ItemNormalizer.php
1
<?php
2

3
/*
4
 * This file is part of the API Platform project.
5
 *
6
 * (c) Kévin Dunglas <dunglas@gmail.com>
7
 *
8
 * For the full copyright and license information, please view the LICENSE
9
 * file that was distributed with this source code.
10
 */
11

12
declare(strict_types=1);
13

14
namespace ApiPlatform\GraphQl\Serializer;
15

16
use ApiPlatform\Metadata\ApiProperty;
17
use ApiPlatform\Metadata\GraphQl\Query;
18
use ApiPlatform\Metadata\IdentifiersExtractorInterface;
19
use ApiPlatform\Metadata\IriConverterInterface;
20
use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface;
21
use ApiPlatform\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface;
22
use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface;
23
use ApiPlatform\Metadata\ResourceClassResolverInterface;
24
use ApiPlatform\Metadata\Util\ClassInfoTrait;
25
use ApiPlatform\Serializer\CacheKeyTrait;
26
use ApiPlatform\Serializer\ItemNormalizer as BaseItemNormalizer;
27
use ApiPlatform\Symfony\Security\ResourceAccessCheckerInterface;
28
use Psr\Log\LoggerInterface;
29
use Psr\Log\NullLogger;
30
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
31
use Symfony\Component\Serializer\Exception\UnexpectedValueException;
32
use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactoryInterface;
33
use Symfony\Component\Serializer\NameConverter\NameConverterInterface;
34

35
/**
36
 * GraphQL normalizer.
37
 *
38
 * @author Kévin Dunglas <dunglas@gmail.com>
39
 */
40
final class ItemNormalizer extends BaseItemNormalizer
41
{
42
    use CacheKeyTrait;
43
    use ClassInfoTrait;
44

45
    public const FORMAT = 'graphql';
46
    public const ITEM_RESOURCE_CLASS_KEY = '#itemResourceClass';
47
    public const ITEM_IDENTIFIERS_KEY = '#itemIdentifiers';
48

49
    private array $safeCacheKeysCache = [];
50

51
    public function __construct(PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory, IriConverterInterface $iriConverter, private readonly IdentifiersExtractorInterface $identifiersExtractor, ResourceClassResolverInterface $resourceClassResolver, PropertyAccessorInterface $propertyAccessor = null, NameConverterInterface $nameConverter = null, ClassMetadataFactoryInterface $classMetadataFactory = null, LoggerInterface $logger = null, ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory = null, ResourceAccessCheckerInterface $resourceAccessChecker = null)
52
    {
53
        parent::__construct($propertyNameCollectionFactory, $propertyMetadataFactory, $iriConverter, $resourceClassResolver, $propertyAccessor, $nameConverter, $classMetadataFactory, $logger ?: new NullLogger(), $resourceMetadataCollectionFactory, $resourceAccessChecker);
116✔
54
    }
55

56
    /**
57
     * {@inheritdoc}
58
     */
59
    public function supportsNormalization(mixed $data, string $format = null, array $context = []): bool
60
    {
61
        return self::FORMAT === $format && parent::supportsNormalization($data, $format, $context);
×
62
    }
63

64
    public function getSupportedTypes($format): array
65
    {
66
        return self::FORMAT === $format ? parent::getSupportedTypes($format) : [];
72✔
67
    }
68

69
    /**
70
     * {@inheritdoc}
71
     *
72
     * @param array<string, mixed> $context
73
     *
74
     * @throws UnexpectedValueException
75
     */
76
    public function normalize(mixed $object, string $format = null, array $context = []): array|string|int|float|bool|\ArrayObject|null
77
    {
78
        $resourceClass = $this->getObjectClass($object);
×
79

80
        if ($this->getOutputClass($context)) {
×
81
            $context['graphql_identifiers'] = [
×
82
                self::ITEM_RESOURCE_CLASS_KEY => $context['operation']->getClass(),
×
83
                self::ITEM_IDENTIFIERS_KEY => $this->identifiersExtractor->getIdentifiersFromItem($object, $context['operation'] ?? null),
×
84
            ];
×
85

86
            return parent::normalize($object, $format, $context);
×
87
        }
88

89
        if ($this->isCacheKeySafe($context)) {
×
90
            $context['cache_key'] = $this->getCacheKey($format, $context);
×
91
        }
92

93
        unset($context['operation_name'], $context['operation']); // Remove operation and operation_name only when cache key has been created
×
94
        $data = parent::normalize($object, $format, $context);
×
95
        if (!\is_array($data)) {
×
96
            throw new UnexpectedValueException('Expected data to be an array.');
×
97
        }
98

99
        if (isset($context['graphql_identifiers'])) {
×
100
            $data += $context['graphql_identifiers'];
×
101
        } elseif (!($context['no_resolver_data'] ?? false)) {
×
102
            $data[self::ITEM_RESOURCE_CLASS_KEY] = $resourceClass;
×
103
            $data[self::ITEM_IDENTIFIERS_KEY] = $this->identifiersExtractor->getIdentifiersFromItem($object, $context['operation'] ?? null);
×
104
        }
105

106
        return $data;
×
107
    }
108

109
    /**
110
     * {@inheritdoc}
111
     */
112
    protected function normalizeCollectionOfRelations(ApiProperty $propertyMetadata, iterable $attributeValue, string $resourceClass, ?string $format, array $context): array
113
    {
114
        // check for nested collection
NEW
115
        $operation = $this?->resourceMetadataCollectionFactory->create($resourceClass)->getOperation(forceCollection: true, forceGraphQl: true);
×
NEW
116
        if ($operation && $operation instanceof Query && $operation->getNested() && !$operation->getResolver() && !$operation->getProvider()) {
×
NEW
117
            return [...$attributeValue];
×
118
        }
119

120
        // to-many are handled directly by the GraphQL resolver
121
        return [];
×
122
    }
123

124
    /**
125
     * {@inheritdoc}
126
     */
127
    public function supportsDenormalization(mixed $data, string $type, string $format = null, array $context = []): bool
128
    {
129
        return self::FORMAT === $format && parent::supportsDenormalization($data, $type, $format, $context);
×
130
    }
131

132
    /**
133
     * {@inheritdoc}
134
     */
135
    protected function getAllowedAttributes(string|object $classOrObject, array $context, bool $attributesAsString = false): array|bool
136
    {
137
        $allowedAttributes = parent::getAllowedAttributes($classOrObject, $context, $attributesAsString);
×
138

139
        if (($context['api_denormalize'] ?? false) && \is_array($allowedAttributes) && false !== ($indexId = array_search('id', $allowedAttributes, true))) {
×
140
            $allowedAttributes[] = '_id';
×
141
            array_splice($allowedAttributes, (int) $indexId, 1);
×
142
        }
143

144
        return $allowedAttributes;
×
145
    }
146

147
    /**
148
     * {@inheritdoc}
149
     */
150
    protected function setAttributeValue($object, $attribute, $value, $format = null, array $context = []): void
151
    {
152
        if ('_id' === $attribute) {
×
153
            $attribute = 'id';
×
154
        }
155

156
        parent::setAttributeValue($object, $attribute, $value, $format, $context);
×
157
    }
158

159
    /**
160
     * Check if any property contains a security grants, which makes the cache key not safe,
161
     * as allowed_properties can differ for 2 instances of the same object.
162
     */
163
    private function isCacheKeySafe(array $context): bool
164
    {
165
        if (!isset($context['resource_class']) || !$this->resourceClassResolver->isResourceClass($context['resource_class'])) {
×
166
            return false;
×
167
        }
168
        $resourceClass = $this->resourceClassResolver->getResourceClass(null, $context['resource_class']);
×
169
        if (isset($this->safeCacheKeysCache[$resourceClass])) {
×
170
            return $this->safeCacheKeysCache[$resourceClass];
×
171
        }
172
        $options = $this->getFactoryOptions($context);
×
173
        $propertyNames = $this->propertyNameCollectionFactory->create($resourceClass, $options);
×
174

175
        $this->safeCacheKeysCache[$resourceClass] = true;
×
176
        foreach ($propertyNames as $propertyName) {
×
177
            $propertyMetadata = $this->propertyMetadataFactory->create($resourceClass, $propertyName, $options);
×
178
            if (null !== $propertyMetadata->getSecurity()) {
×
179
                $this->safeCacheKeysCache[$resourceClass] = false;
×
180
                break;
×
181
            }
182
        }
183

184
        return $this->safeCacheKeysCache[$resourceClass];
×
185
    }
186
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc