Passed
Pull Request — master (#160)
by Alex
04:16
created

createBulkResourceforResourceSet()   B

Complexity

Conditions 5
Paths 12

Size

Total Lines 29
Code Lines 20

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 29
rs 8.439
c 0
b 0
f 0
cc 5
eloc 20
nc 12
nop 2
1
<?php
2
3
namespace AlgoWeb\PODataLaravel\Query;
4
5
use AlgoWeb\PODataLaravel\Auth\NullAuthProvider;
6
use AlgoWeb\PODataLaravel\Controllers\MetadataControllerContainer;
7
use AlgoWeb\PODataLaravel\Enums\ActionVerb;
8
use AlgoWeb\PODataLaravel\Interfaces\AuthInterface;
9
use AlgoWeb\PODataLaravel\Providers\MetadataProvider;
10
use Illuminate\Database\Eloquent\Model;
11
use Illuminate\Database\Eloquent\Relations\BelongsTo;
12
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
13
use Illuminate\Database\Eloquent\Relations\HasOneOrMany;
14
use Illuminate\Database\Eloquent\Relations\Relation;
15
use Illuminate\Http\JsonResponse;
16
use Illuminate\Support\Facades\App;
17
use Illuminate\Support\Facades\DB;
18
use POData\Common\InvalidOperationException;
19
use POData\Common\ODataException;
20
use POData\Providers\Metadata\ResourceProperty;
21
use POData\Providers\Metadata\ResourceSet;
22
use POData\Providers\Query\QueryResult;
23
use POData\Providers\Query\QueryType;
24
use POData\UriProcessor\QueryProcessor\ExpressionParser\FilterInfo;
25
use POData\UriProcessor\QueryProcessor\OrderByParser\InternalOrderByInfo;
26
use POData\UriProcessor\QueryProcessor\SkipTokenParser\SkipTokenInfo;
27
use POData\UriProcessor\ResourcePathProcessor\SegmentParser\KeyDescriptor;
28
use Symfony\Component\Process\Exception\InvalidArgumentException;
29
30
class LaravelBulkQuery
31
{
32
    protected $auth;
33
    protected $metadataProvider;
34
    protected $query;
35
    protected $controllerContainer;
36
37
    public function __construct(LaravelQuery &$query, AuthInterface $auth = null)
38
    {
39
        $this->auth = isset($auth) ? $auth : new NullAuthProvider();
40
        $this->metadataProvider = new MetadataProvider(App::make('app'));
41
        $this->query = $query;
42
        $this->controllerContainer = App::make('metadataControllers');
43
    }
44
45
46
    /**
47
     * Create multiple new resources in a resource set.
48
     *
49
     * @param ResourceSet $sourceResourceSet The entity set containing the entity to fetch
50
     * @param object[]    $data              The new data for the entity instance
51
     *
52
     * @return object[] returns the newly created model if successful, or throws an exception if model creation failed
53
     * @throw  \Exception
54
     */
55
    public function createBulkResourceforResourceSet(
56
        ResourceSet $sourceResourceSet,
57
        array $data
58
    ) {
59
        $verbName = 'bulkCreate';
60
        $mapping = $this->getOptionalVerbMapping($sourceResourceSet, $verbName);
61
62
        $result = [];
63
        try {
64
            DB::beginTransaction();
65
            if (null === $mapping) {
66
                foreach ($data as $newItem) {
67
                    $raw = $this->getQuery()->createResourceforResourceSet($sourceResourceSet, null, $newItem);
68
                    if (null === $raw) {
69
                        throw new \Exception('Bulk model creation failed');
70
                    }
71
                    $result[] = $raw;
72
                }
73
            } else {
74
                $keyDescriptor = null;
75
                $pastVerb = 'created';
76
                $result = $this->processBulkCustom($sourceResourceSet, $data, $mapping, $pastVerb, $keyDescriptor);
77
            }
78
            DB::commit();
79
        } catch (\Exception $e) {
80
            DB::rollBack();
81
            throw $e;
82
        }
83
        return $result;
84
    }
85
86
    /**
87
     * Updates a group of resources in a resource set.
88
     *
89
     * @param ResourceSet     $sourceResourceSet    The entity set containing the source entity
90
     * @param object          $sourceEntityInstance The source entity instance
91
     * @param KeyDescriptor[] $keyDescriptor        The key identifying the entity to fetch
92
     * @param object[]        $data                 The new data for the entity instances
93
     * @param bool            $shouldUpdate         Should undefined values be updated or reset to default
94
     *
95
     * @return object[] the new resource value if it is assignable, or throw exception for null
96
     * @throw  \Exception
97
     */
98
    public function updateBulkResource(
99
        ResourceSet $sourceResourceSet,
100
        $sourceEntityInstance,
101
        array $keyDescriptor,
102
        array $data,
103
        $shouldUpdate = false
104
    ) {
105
        $numKeys = count($keyDescriptor);
106
        if ($numKeys !== count($data)) {
107
            $msg = 'Key descriptor array and data array must be same length';
108
            throw new \InvalidArgumentException($msg);
109
        }
110
        $result = [];
111
112
        $verbName = 'bulkUpdate';
113
        $mapping = $this->getOptionalVerbMapping($sourceResourceSet, $verbName);
114
115
        try {
116
            DB::beginTransaction();
117
            if (null === $mapping) {
118
                for ($i = 0; $i < $numKeys; $i++) {
119
                    $newItem = $data[$i];
120
                    $newKey = $keyDescriptor[$i];
121
                    $raw = $this->getQuery()->
122
                        updateResource($sourceResourceSet, $sourceEntityInstance, $newKey, $newItem);
123
                    if (null === $raw) {
124
                        throw new \Exception('Bulk model update failed');
125
                    }
126
                    $result[] = $raw;
127
                }
128
            } else {
129
                $pastVerb = 'updated';
130
                $result = $this->processBulkCustom($sourceResourceSet, $data, $mapping, $pastVerb, $keyDescriptor);
131
            }
132
            DB::commit();
133
        } catch (\Exception $e) {
134
            DB::rollBack();
135
            throw $e;
136
        }
137
        return $result;
138
    }
139
140
    /**
141
     * Prepare bulk request from supplied data.  If $keyDescriptors is not null, its elements are assumed to
142
     * correspond 1-1 to those in $data.
143
     *
144
     * @param $paramList
145
     * @param array                $data
146
     * @param KeyDescriptor[]|null $keyDescriptors
147
     */
148
    protected function prepareBulkRequestInput($paramList, array $data, array $keyDescriptors = null)
149
    {
150
        $parms = [];
151
        $isCreate = null === $keyDescriptors;
152
153
        // for moment, we're only processing parameters of type Request
154
        foreach ($paramList as $spec) {
155
            $varType = isset($spec['type']) ? $spec['type'] : null;
156
            if (null !== $varType) {
157
                $var = new $varType();
158
                if ($spec['isRequest']) {
159
                    $var->setMethod($isCreate ? 'POST' : 'PUT');
160
                    $bulkData = ['data' => $data];
161
                    if (null !== $keyDescriptors) {
162
                        $keys = [];
163
                        foreach ($keyDescriptors as $desc) {
164
                            assert($desc instanceof KeyDescriptor, get_class($desc));
165
                            $rawPayload = $desc->getNamedValues();
166
                            $keyPayload = [];
167
                            foreach ($rawPayload as $keyName => $keyVal) {
168
                                $keyPayload[$keyName] = $keyVal[0];
169
                            }
170
                            $keys[] = $keyPayload;
171
                        }
172
                        $bulkData['keys'] = $keys;
173
                    }
174
                    $var->request = new \Symfony\Component\HttpFoundation\ParameterBag($bulkData);
175
                }
176
                $parms[] = $var;
177
            }
178
        }
179
        return $parms;
180
    }
181
182
    /**
183
     * @param ResourceSet $sourceResourceSet
184
     * @param array       $data
185
     * @param $mapping
186
     * @param $pastVerb
187
     * @param  KeyDescriptor[]|null $keyDescriptor
188
     * @throws ODataException
189
     * @return array
190
     */
191
    protected function processBulkCustom(
192
        ResourceSet $sourceResourceSet,
193
        array $data,
194
        $mapping,
195
        $pastVerb,
196
        array $keyDescriptor = null
197
    ) {
198
        $class = $sourceResourceSet->getResourceType()->getInstanceType()->getName();
199
        $controlClass = $mapping['controller'];
200
        $method = $mapping['method'];
201
        $paramList = $mapping['parameters'];
202
        $controller = App::make($controlClass);
203
        $parms = $this->prepareBulkRequestInput($paramList, $data, $keyDescriptor);
204
205
        $callResult = call_user_func_array(array($controller, $method), $parms);
206
        $payload = $this->createUpdateDeleteProcessOutput($callResult);
207
        $success = isset($payload['id']) && is_array($payload['id']);
208
209
        if ($success) {
210
            try {
211
                // return array of Model objects underlying collection returned by findMany
212
                $result = $class::findMany($payload['id'])->flatten()->all();
213
                foreach ($result as $model) {
214
                    LaravelQuery::queueModel($model);
215
                }
216
                return $result;
217
            } catch (\Exception $e) {
218
                throw new ODataException($e->getMessage(), 500);
219
            }
220
        } else {
221
            $msg = 'Target models not successfully ' . $pastVerb;
222
            throw new ODataException($msg, 422);
223
        }
224
    }
225
226
    /**
227
     * @param ResourceSet $sourceResourceSet
228
     * @param $verbName
229
     * @return array|null
230
     */
231
    protected function getOptionalVerbMapping(ResourceSet $sourceResourceSet, $verbName)
232
    {
233
        // dig up target class name
234
        $type = $sourceResourceSet->getResourceType()->getInstanceType();
235
        assert($type instanceof \ReflectionClass, get_class($type));
236
        $modelName = $type->getName();
237
        return $this->getControllerContainer()->getMapping($modelName, $verbName);
238
    }
239
240
    public function getQuery()
241
    {
242
        return $this->query;
243
    }
244
245
    /**
246
     * Dig out local copy of controller metadata mapping.
247
     *
248
     * @return MetadataControllerContainer
249
     */
250
    public function getControllerContainer()
251
    {
252
        assert(null !== $this->controllerContainer, get_class($this->controllerContainer));
253
        return $this->controllerContainer;
254
    }
255
256
    /**
257
     * @param $result
258
     * @throws ODataException
259
     * @return array|mixed
260
     */
261 View Code Duplication
    protected function createUpdateDeleteProcessOutput(JsonResponse $result)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
262
    {
263
        $outData = $result->getData();
264
        if (is_object($outData)) {
265
            $outData = (array) $outData;
266
        }
267
268
        if (!is_array($outData)) {
269
            throw ODataException::createInternalServerError('Controller response does not have an array.');
270
        }
271
        if (!(key_exists('id', $outData) && key_exists('status', $outData) && key_exists('errors', $outData))) {
272
            throw ODataException::createInternalServerError(
273
                'Controller response array missing at least one of id, status and/or errors fields.'
274
            );
275
        }
276
        return $outData;
277
    }
278
}
279