Issues (332)

Doctrine/MongoDbOdm/SubresourceDataProvider.php (1 issue)

1
<?php
2
3
/*
4
 * This file is part of the API Platform project.
5
 *
6
 * (c) Kévin Dunglas <[email protected]>
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\Core\Bridge\Doctrine\MongoDbOdm;
15
16
use ApiPlatform\Core\Bridge\Doctrine\Common\Util\IdentifierManagerTrait;
17
use ApiPlatform\Core\Bridge\Doctrine\MongoDbOdm\Extension\AggregationCollectionExtensionInterface;
18
use ApiPlatform\Core\Bridge\Doctrine\MongoDbOdm\Extension\AggregationItemExtensionInterface;
19
use ApiPlatform\Core\Bridge\Doctrine\MongoDbOdm\Extension\AggregationResultCollectionExtensionInterface;
20
use ApiPlatform\Core\Bridge\Doctrine\MongoDbOdm\Extension\AggregationResultItemExtensionInterface;
21
use ApiPlatform\Core\DataProvider\SubresourceDataProviderInterface;
22
use ApiPlatform\Core\Exception\ResourceClassNotSupportedException;
23
use ApiPlatform\Core\Exception\RuntimeException;
24
use ApiPlatform\Core\Identifier\IdentifierConverterInterface;
25
use ApiPlatform\Core\Metadata\Property\Factory\PropertyMetadataFactoryInterface;
26
use ApiPlatform\Core\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface;
27
use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface;
28
use Doctrine\Common\Persistence\ManagerRegistry;
29
use Doctrine\ODM\MongoDB\Aggregation\Builder;
30
use Doctrine\ODM\MongoDB\DocumentManager;
31
use Doctrine\ODM\MongoDB\Mapping\ClassMetadata;
32
use Doctrine\ODM\MongoDB\Repository\DocumentRepository;
33
34
/**
35
 * Subresource data provider for the Doctrine MongoDB ODM.
36
 *
37
 * @experimental
38
 *
39
 * @author Antoine Bluchet <[email protected]>
40
 * @author Alan Poulain <[email protected]>
41
 */
42
final class SubresourceDataProvider implements SubresourceDataProviderInterface
43
{
44
    use IdentifierManagerTrait;
45
46
    private $managerRegistry;
47
    private $resourceMetadataFactory;
48
    private $collectionExtensions;
49
    private $itemExtensions;
50
51
    /**
52
     * @param AggregationCollectionExtensionInterface[] $collectionExtensions
53
     * @param AggregationItemExtensionInterface[]       $itemExtensions
54
     */
55
    public function __construct(ManagerRegistry $managerRegistry, ResourceMetadataFactoryInterface $resourceMetadataFactory, PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory, iterable $collectionExtensions = [], iterable $itemExtensions = [])
56
    {
57
        $this->managerRegistry = $managerRegistry;
58
        $this->resourceMetadataFactory = $resourceMetadataFactory;
59
        $this->propertyNameCollectionFactory = $propertyNameCollectionFactory;
60
        $this->propertyMetadataFactory = $propertyMetadataFactory;
61
        $this->collectionExtensions = $collectionExtensions;
62
        $this->itemExtensions = $itemExtensions;
63
    }
64
65
    /**
66
     * {@inheritdoc}
67
     *
68
     * @throws RuntimeException
69
     */
70
    public function getSubresource(string $resourceClass, array $identifiers, array $context, string $operationName = null)
71
    {
72
        $manager = $this->managerRegistry->getManagerForClass($resourceClass);
73
        if (!$manager instanceof DocumentManager) {
74
            throw new ResourceClassNotSupportedException(sprintf('The manager for "%s" must be an instance of "%s".', $resourceClass, DocumentManager::class));
75
        }
76
77
        $repository = $manager->getRepository($resourceClass);
78
        if (!$repository instanceof DocumentRepository) {
79
            throw new RuntimeException(sprintf('The repository for "%s" must be an instance of "%s".', $resourceClass, DocumentRepository::class));
80
        }
81
82
        if (!isset($context['identifiers'], $context['property'])) {
83
            throw new ResourceClassNotSupportedException('The given resource class is not a subresource.');
84
        }
85
86
        $resourceMetadata = $this->resourceMetadataFactory->create($resourceClass);
87
        $attribute = $resourceMetadata->getSubresourceOperationAttribute($operationName, 'doctrine_mongodb', [], true);
88
        $executeOptions = $attribute['execute_options'] ?? [];
89
90
        $aggregationBuilder = $this->buildAggregation($identifiers, $context, $executeOptions, $repository->createAggregationBuilder(), \count($context['identifiers']));
91
92
        if (true === $context['collection']) {
93
            foreach ($this->collectionExtensions as $extension) {
94
                $extension->applyToCollection($aggregationBuilder, $resourceClass, $operationName, $context);
95
                if ($extension instanceof AggregationResultCollectionExtensionInterface && $extension->supportsResult($resourceClass, $operationName, $context)) {
96
                    return $extension->getResult($aggregationBuilder, $resourceClass, $operationName, $context);
97
                }
98
            }
99
        } else {
100
            foreach ($this->itemExtensions as $extension) {
101
                $extension->applyToItem($aggregationBuilder, $resourceClass, $identifiers, $operationName, $context);
102
                if ($extension instanceof AggregationResultItemExtensionInterface && $extension->supportsResult($resourceClass, $operationName, $context)) {
103
                    return $extension->getResult($aggregationBuilder, $resourceClass, $operationName, $context);
104
                }
105
            }
106
        }
107
108
        $iterator = $aggregationBuilder->hydrate($resourceClass)->execute($executeOptions);
109
110
        return $context['collection'] ? $iterator->toArray() : ($iterator->current() ?: null);
111
    }
112
113
    /**
114
     * @throws RuntimeException
115
     */
116
    private function buildAggregation(array $identifiers, array $context, array $executeOptions, Builder $previousAggregationBuilder, int $remainingIdentifiers, Builder $topAggregationBuilder = null): Builder
117
    {
118
        if ($remainingIdentifiers <= 0) {
119
            return $previousAggregationBuilder;
120
        }
121
122
        $topAggregationBuilder = $topAggregationBuilder ?? $previousAggregationBuilder;
123
124
        [$identifier, $identifierResourceClass] = $context['identifiers'][$remainingIdentifiers - 1];
125
        $previousAssociationProperty = $context['identifiers'][$remainingIdentifiers][0] ?? $context['property'];
126
127
        $manager = $this->managerRegistry->getManagerForClass($identifierResourceClass);
128
        if (!$manager instanceof DocumentManager) {
129
            throw new RuntimeException(sprintf('The manager for "%s" must be an instance of "%s".', $identifierResourceClass, DocumentManager::class));
130
        }
131
132
        $classMetadata = $manager->getClassMetadata($identifierResourceClass);
133
134
        if (!$classMetadata instanceof ClassMetadata) {
0 ignored issues
show
$classMetadata is always a sub-type of Doctrine\ODM\MongoDB\Mapping\ClassMetadata.
Loading history...
135
            throw new RuntimeException(sprintf('The class metadata for "%s" must be an instance of "%s".', $identifierResourceClass, ClassMetadata::class));
136
        }
137
138
        $aggregation = $manager->createAggregationBuilder($identifierResourceClass);
139
        $normalizedIdentifiers = [];
140
141
        if (isset($identifiers[$identifier])) {
142
            // if it's an array it's already normalized, the IdentifierManagerTrait is deprecated
143
            if ($context[IdentifierConverterInterface::HAS_IDENTIFIER_CONVERTER] ?? false) {
144
                $normalizedIdentifiers = $identifiers[$identifier];
145
            } else {
146
                $normalizedIdentifiers = $this->normalizeIdentifiers($identifiers[$identifier], $manager, $identifierResourceClass);
147
            }
148
        }
149
150
        if ($classMetadata->hasAssociation($previousAssociationProperty)) {
151
            $aggregation->lookup($previousAssociationProperty)->alias($previousAssociationProperty);
152
            foreach ($normalizedIdentifiers as $key => $value) {
153
                $aggregation->match()->field($key)->equals($value);
154
            }
155
        } elseif ($classMetadata->isIdentifier($previousAssociationProperty)) {
156
            foreach ($normalizedIdentifiers as $key => $value) {
157
                $aggregation->match()->field($key)->equals($value);
158
            }
159
160
            return $aggregation;
161
        }
162
163
        // Recurse aggregations
164
        $aggregation = $this->buildAggregation($identifiers, $context, $executeOptions, $aggregation, --$remainingIdentifiers, $topAggregationBuilder);
165
166
        $results = $aggregation->execute($executeOptions)->toArray();
167
        $in = array_reduce($results, function ($in, $result) use ($previousAssociationProperty) {
168
            return $in + array_map(function ($result) {
169
                return $result['_id'];
170
            }, $result[$previousAssociationProperty] ?? []);
171
        }, []);
172
        $previousAggregationBuilder->match()->field('_id')->in($in);
173
174
        return $previousAggregationBuilder;
175
    }
176
}
177