Passed
Pull Request — master (#155)
by Alex
06:39
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
                return $result;
214
            } catch (\Exception $e) {
215
                throw new ODataException($e->getMessage(), 500);
216
            }
217
        } else {
218
            $msg = 'Target models not successfully ' . $pastVerb;
219
            throw new ODataException($msg, 422);
220
        }
221
    }
222
223
    /**
224
     * @param ResourceSet $sourceResourceSet
225
     * @param $verbName
226
     * @return array|null
227
     */
228
    protected function getOptionalVerbMapping(ResourceSet $sourceResourceSet, $verbName)
229
    {
230
        // dig up target class name
231
        $type = $sourceResourceSet->getResourceType()->getInstanceType();
232
        assert($type instanceof \ReflectionClass, get_class($type));
233
        $modelName = $type->getName();
234
        return $this->getControllerContainer()->getMapping($modelName, $verbName);
235
    }
236
237
    public function getQuery()
238
    {
239
        return $this->query;
240
    }
241
242
    /**
243
     * Dig out local copy of controller metadata mapping.
244
     *
245
     * @return MetadataControllerContainer
246
     */
247
    public function getControllerContainer()
248
    {
249
        assert(null !== $this->controllerContainer, get_class($this->controllerContainer));
250
        return $this->controllerContainer;
251
    }
252
253
    /**
254
     * @param $result
255
     * @throws ODataException
256
     * @return array|mixed
257
     */
258 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...
259
    {
260
        $outData = $result->getData();
261
        if (is_object($outData)) {
262
            $outData = (array) $outData;
263
        }
264
265
        if (!is_array($outData)) {
266
            throw ODataException::createInternalServerError('Controller response does not have an array.');
267
        }
268
        if (!(key_exists('id', $outData) && key_exists('status', $outData) && key_exists('errors', $outData))) {
269
            throw ODataException::createInternalServerError(
270
                'Controller response array missing at least one of id, status and/or errors fields.'
271
            );
272
        }
273
        return $outData;
274
    }
275
}
276