Completed
Push — master ( 0e53b5...b50ebe )
by Jodie
02:34
created

Pipeline::transformData()   B

Complexity

Conditions 7
Paths 8

Size

Total Lines 44

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 18
CRAP Score 7.049

Importance

Changes 0
Metric Value
dl 0
loc 44
ccs 18
cts 20
cp 0.9
rs 8.2826
c 0
b 0
f 0
cc 7
nc 8
nop 2
crap 7.049
1
<?php
2
3
namespace Rexlabs\Smokescreen\Transformer;
4
5
use Rexlabs\Smokescreen\Exception\IncludeException;
6
use Rexlabs\Smokescreen\Exception\InvalidTransformerException;
7
use Rexlabs\Smokescreen\Exception\UnhandledResourceType;
8
use Rexlabs\Smokescreen\Helpers\ArrayHelper;
9
use Rexlabs\Smokescreen\Relations\RelationLoaderInterface;
10
use Rexlabs\Smokescreen\Resource\Collection;
11
use Rexlabs\Smokescreen\Resource\Item;
12
use Rexlabs\Smokescreen\Resource\ResourceInterface;
13
use Rexlabs\Smokescreen\Serializer\SerializerInterface;
14
15
class Pipeline
16
{
17
    /** @var SerializerInterface */
18
    protected $serializer;
19
20
    /** @var TransformerResolverInterface|null */
21
    protected $transformerResolver;
22
23
    /** @var RelationLoaderInterface|null */
24
    protected $relationLoader;
25
26 16
    public function __construct()
27
    {
28 16
    }
29
30 2
    public function setTransformerResolver(TransformerResolverInterface $transformerResolver)
31
    {
32 2
        $this->transformerResolver = $transformerResolver;
33 2
    }
34
35 16
    public function setSerializer(SerializerInterface $serializer)
36
    {
37 16
        $this->serializer = $serializer;
38 16
    }
39
40 1
    public function setRelationLoader(RelationLoaderInterface $relationLoader)
41
    {
42 1
        $this->relationLoader = $relationLoader;
43 1
    }
44
45
    /**
46
     * @param Scope $scope
47
     *
48
     * @throws IncludeException
49
     *
50
     * @return array|null
51
     */
52 16
    public function transform(Scope $scope)
53
    {
54 16
        $resource = $scope->resource();
55 16
        if (!($resource instanceof ResourceInterface)) {
56 3
            if (\is_array($resource)) {
57 1
                return $resource;
58
            }
59 2
            if (\is_object($resource) && method_exists($resource, 'toArray')) {
60 1
                return $resource->toArray();
61
            }
62
63 1
            throw new UnhandledResourceType('Unable to serialize resource of type '.\gettype($resource));
64
        }
65
66 14
        if (!$resource->getData()) {
67
            // There is no data to transform.
68 1
            return null;
69
        }
70
71
        // Try to resolve a transformer for a resource that does not have one assigned.
72 14
        if (!$resource->hasTransformer()) {
73 9
            $transformer = $this->resolveTransformerForResource($resource);
74 9
            $resource->setTransformer($transformer);
75
        }
76
77
        // Call the relationship loader for any relations
78 14
        $this->loadRelations($resource, $scope->resolvedRelationshipKeys());
79
80
        // Build the output by recursively transforming each resource.
81 14
        $output = null;
82 14
        if ($resource instanceof Collection) {
83 5
            $output = $this->transformCollectionResource($scope);
84 12
        } elseif ($resource instanceof Item) {
85 12
            $output = $this->transformItemResource($scope);
86
        }
87
88 14
        return $output;
89
    }
90
91
    /**
92
     * Resolve the transformer to be used for a resource.
93
     * Returns an interface, callable or null when a transformer cannot be resolved.
94
     *
95
     * @param $resource
96
     *
97
     * @return TransformerInterface|mixed|null
98
     */
99 9
    protected function resolveTransformerForResource($resource)
100
    {
101 9
        $transformer = null;
102
103 9
        if ($this->transformerResolver !== null) {
104 2
            $transformer = $this->transformerResolver->resolve($resource);
105
        }
106
107 9
        return $transformer;
108
    }
109
110
    /**
111
     * Applies the serializer to the Item resource.
112
     *
113
     * @param Scope $scope
114
     *
115
     * @throws IncludeException
116
     *
117
     * @return array
118
     */
119 12
    protected function transformItemResource(Scope $scope): array
120
    {
121
        // Get the globally set serializer (resource may override)
122 12
        $defaultSerializer = $this->getSerializer();
123
124
        // The collection can have a custom serializer defined
125
        // TODO: Check resource type is item
126 12
        $item = $scope->resource();
127 12
        $serializer = $item->getSerializer() ?? $defaultSerializer;
128 12
        $isSerializerInterface = $serializer instanceof SerializerInterface;
129
130
        // Transform the item data
131 12
        $itemData = $this->transformData($scope, $item->getData());
132
133
        // Serialize the item data
134 12
        if ($isSerializerInterface) {
135
            // Serialize via object implementing SerializerInterface
136 10
            $output = $serializer->item($item->getResourceKey(), $itemData);
137 2
        } elseif (\is_callable($serializer)) {
138
            // Serialize via a callable/closure
139 1
            $output = $serializer($item->getResourceKey(), $itemData);
140
        } else {
141
            // No serialization
142 1
            $output = $itemData;
143
        }
144
145 12
        return $output;
146
    }
147
148
    /**
149
     * @param Scope $scope
150
     *
151
     * @throws IncludeException
152
     *
153
     * @return array
154
     */
155 5
    protected function transformCollectionResource(Scope $scope): array
156
    {
157
        // Get the globally set serializer (resource may override).
158 5
        $defaultSerializer = $this->getSerializer();
159
160
        // Collection resources implement IteratorAggregate ... so that's nice.
161
        // TODO: Check type?
162 5
        $collection = $scope->resource();
163
164 5
        $items = [];
165 5
        foreach ($collection as $itemData) {
166
            // $item might be a Model or an array etc.
167 5
            $items[] = $this->transformData($scope, $itemData);
168
        }
169
170
        // The collection can have a custom serializer defined.
171 5
        $serializer = $collection->getSerializer() ?? $defaultSerializer;
172
173 5
        if ($serializer instanceof SerializerInterface) {
174
            // Serialize via object implementing SerializerInterface
175 3
            $output = $serializer->collection($collection->getResourceKey(), $items);
176 3
            if ($collection->hasPaginator()) {
0 ignored issues
show
Bug introduced by
The method hasPaginator() does not exist on Rexlabs\Smokescreen\Resource\ResourceInterface. It seems like you code against a sub-type of Rexlabs\Smokescreen\Resource\ResourceInterface such as Rexlabs\Smokescreen\Resource\Collection. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

176
            if ($collection->/** @scrutinizer ignore-call */ hasPaginator()) {
Loading history...
177 3
                $output = array_merge($output, $serializer->paginator($collection->getPaginator()));
0 ignored issues
show
Bug introduced by
The method getPaginator() does not exist on Rexlabs\Smokescreen\Resource\ResourceInterface. It seems like you code against a sub-type of Rexlabs\Smokescreen\Resource\ResourceInterface such as Rexlabs\Smokescreen\Resource\Collection. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

177
                $output = array_merge($output, $serializer->paginator($collection->/** @scrutinizer ignore-call */ getPaginator()));
Loading history...
178
            }
179 2
        } elseif (\is_callable($serializer)) {
180
            // Serialize via a callable/closure
181 1
            $output = $serializer($collection->getResourceKey(), $items);
182
        } else {
183
            // Serialization disabled for this resource
184 1
            $output = $items;
185
        }
186
187 5
        return $output;
188
    }
189
190
    /**
191
     * Apply transformation to the item data.
192
     *
193
     * @param Scope $scope
194
     * @param mixed $data
195
     *
196
     * @throws IncludeException
197
     *
198
     * @return array
199
     */
200 14
    protected function transformData(Scope $scope, $data): array
201
    {
202 14
        $transformer = $scope->transformer();
203
204
        // Handle when no transformer is present
205 14
        if (empty($transformer)) {
206
            // No transformation
207 7
            return (array) $data;
208
        }
209
210
        // Handle when transformer is a callable
211 8
        if (\is_callable($transformer)) {
212
            // Simply run callable on the data and return the result
213 3
            return (array) $transformer($data);
214
        }
215
216
        // Ensure we're working with a real transformer from this point forward.
217 7
        if (!($transformer instanceof TransformerInterface)) {
218
            throw new InvalidTransformerException('Expected a valid transformer');
219
        }
220
221
        // Transform the data, and filter any sparse field-set for the scope.
222 7
        $transformedData = $scope->filterData($transformer->getTransformedData($data));
223
224
        // Add includes to the payload.
225 7
        $includeMap = $scope->includeMap();
226 7
        foreach ($scope->resolvedIncludeKeys() as $includeKey) {
227 6
            $child = $this->executeTransformerInclude($scope, $includeKey, $includeMap[$includeKey], $data);
228
229
            // Create a new child scope.
230 6
            $childScope = new Scope($child, $scope->includes()->splice($includeKey), $scope);
231
232
            // If working with a ResourceInterface, use it's own key (if present).
233 6
            $childKey = $child instanceof ResourceInterface && $child->getResourceKey() ?
234 6
                $child->getResourceKey() : $includeKey;
235
236 6
            ArrayHelper::mutate(
237
                $transformedData,
238 6
                $childKey,
239 6
                $this->transform($childScope)
240
            );
241
        }
242
243 7
        return $transformedData;
244
    }
245
246
    /**
247
     * Execute the transformer.
248
     *
249
     * @param Scope  $scope
250
     * @param string $includeKey
251
     * @param array  $includeDefinition
252
     * @param mixed  $data
253
     *
254
     * @throws IncludeException
255
     *
256
     * @return ResourceInterface
257
     */
258 6
    protected function executeTransformerInclude(Scope $scope, $includeKey, $includeDefinition, $data)
259
    {
260
        // Transformer explicitly provided an include method
261 6
        $transformer = $scope->transformer();
262 6
        $method = $includeDefinition['method'];
263 6
        if (method_exists($transformer, $method)) {
264 3
            return $transformer->$method($data, $scope);
265
        }
266
267
        // Otherwise try handle the include automatically
268 3
        return $this->autoWireInclude($includeKey, $includeDefinition, $data);
269
    }
270
271
    /**
272
     * Fire the relation loader (if defined) for this resource.
273
     *
274
     * @param ResourceInterface $resource
275
     * @param array             $relationshipKeys
276
     */
277 14
    protected function loadRelations(ResourceInterface $resource, array $relationshipKeys)
278
    {
279 14
        if ($this->relationLoader !== null && !empty($relationshipKeys)) {
280 1
            $this->relationLoader->load($resource, $relationshipKeys);
281
        }
282 14
    }
283
284
    /**
285
     * @param string $includeKey
286
     * @param array  $includeDefinition
287
     * @param        $item
288
     *
289
     * @throws \Rexlabs\Smokescreen\Exception\InvalidTransformerException
290
     * @throws \Rexlabs\Smokescreen\Exception\IncludeException
291
     *
292
     * @return Collection|Item|ResourceInterface
293
     */
294 3
    protected function autoWireInclude($includeKey, $includeDefinition, $item)
295
    {
296
        // Get the included data
297 3
        $data = null;
298 3
        if (\is_array($item) || $item instanceof \ArrayAccess) {
299 2
            $data = $item[$includeKey] ?? null;
300 1
        } elseif (\is_object($item)) {
301 1
            $data = $item->$includeKey ?? null;
302
        } else {
303
            throw new IncludeException("Cannot auto-wire include for $includeKey: Cannot get include data");
304
        }
305
306 3
        if (!empty($includeDefinition['resource_type']) && $includeDefinition['resource_type'] === 'collection') {
307 1
            return new Collection($data);
308
        }
309
310
        // Assume unless declared, that the resource is an item.
311 3
        return new Item($data);
312
    }
313
314 14
    protected function getSerializer()
315
    {
316 14
        return $this->serializer;
317
    }
318
}
319