Passed
Push — master ( a6b330...399600 )
by
unknown
65:58
created

EloquentModelDataLayer::persistNew()   A

Complexity

Conditions 2
Paths 3

Size

Total Lines 17
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 12
nc 3
nop 2
dl 0
loc 17
rs 9.8666
c 0
b 0
f 0
1
<?php
2
3
namespace W2w\Laravel\Apie\Services\Retrievers;
4
5
use Illuminate\Database\Eloquent\ModelNotFoundException;
6
use RuntimeException;
7
use UnexpectedValueException;
8
use Illuminate\Contracts\Auth\Access\Gate;
9
use Illuminate\Database\Eloquent\Model;
10
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
11
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
12
use W2w\Lib\Apie\Exceptions\ResourceNotFoundException;
13
use W2w\Lib\Apie\Normalizers\ContextualNormalizer;
14
use W2w\Lib\Apie\Normalizers\EvilReflectionPropertyNormalizer;
15
use W2w\Lib\Apie\Persisters\ApiResourcePersisterInterface;
16
use W2w\Lib\Apie\Retrievers\ApiResourceRetrieverInterface;
17
use W2w\Lib\Apie\Retrievers\SearchFilterFromMetadataTrait;
18
use W2w\Lib\Apie\Retrievers\SearchFilterProviderInterface;
19
use W2w\Lib\Apie\SearchFilters\SearchFilterRequest;
20
21
/**
22
 * Maps a domain object to an eloquent model. Remember that foreign key constraints can be confusing, so it might be
23
 * a good idea to make your own retriever and persister class if the model becomes more complex.
24
 *
25
 * It uses the fill and toArray method of the Eloquent model. Mass alignment is disabled to map the fields as we
26
 * assume the domain object does the protection.
27
 */
28
class EloquentModelDataLayer implements ApiResourceRetrieverInterface, ApiResourcePersisterInterface, SearchFilterProviderInterface
29
{
30
    use SearchFilterFromMetadataTrait;
31
32
    private $normalizer;
33
34
    private $denormalizer;
35
36
    private $gate;
37
38
    /**
39
     * @param NormalizerInterface   $normalizer
40
     * @param DenormalizerInterface $denormalizer
41
     * @param Gate                  $gate
42
     */
43
    public function __construct(NormalizerInterface $normalizer, DenormalizerInterface $denormalizer, Gate $gate)
44
    {
45
        $this->normalizer = $normalizer;
46
        $this->denormalizer = $denormalizer;
47
        $this->gate = $gate;
48
    }
49
50
    /**
51
     * Retrieves all resources. Since the filtering whether you are allowed to see a model instance is done afterwards,
52
     * the pagination could show a less amount of records than indicated. This is only for performance reasons.
53
     *
54
     * @param  string              $resourceClass
55
     * @param  array               $context
56
     * @param  SearchFilterRequest $searchFilterRequest
57
     * @return array
58
     */
59
    public function retrieveAll(string $resourceClass, array $context, SearchFilterRequest $searchFilterRequest): iterable
60
    {
61
        $modelClass = $this->getModelClass($resourceClass, $context);
62
63
        $queryBuilder = $modelClass::where($searchFilterRequest->getSearches());
64
65
        $modelInstances = $queryBuilder->orderBy('id', 'ASC')
66
                                     ->skip($searchFilterRequest->getOffset())
67
                                     ->take($searchFilterRequest->getNumberOfItems())
68
                                     ->get();
69
70
        return array_filter(
71
            array_map(
72
                function ($modelInstance) use (&$resourceClass) {
73
                    return $this->denormalize($modelInstance->toArray(), $resourceClass);
74
                }, iterator_to_array($modelInstances)
75
            ),
76
            function ($resource) {
77
                return $this->gate->allows('get', $resource);
78
            }
79
        );
80
    }
81
82
    /**
83
     * Retrieves a single instance.
84
     *
85
     * @param  string $resourceClass
86
     * @param  mixed  $id
87
     * @param  array  $context
88
     * @return mixed
89
     */
90
    public function retrieve(string $resourceClass, $id, array $context)
91
    {
92
        $modelClass = $this->getModelClass($resourceClass, $context);
93
        try {
94
            $modelInstance = $modelClass::where($context[1] ?? $context['id'] ?? 'id', $id)->firstOrFail();
95
        } catch (ModelNotFoundException $notFoundException) {
96
            throw new ResourceNotFoundException($id);
97
        }
98
        $result = $this->denormalize($modelInstance->toArray(), $resourceClass);
99
        $this->gate->authorize('get', $result);
100
101
        return $result;
102
    }
103
104
    /**
105
     * Creates a new Eloquent model from an api resource.
106
     *
107
     * @param  mixed $resource
108
     * @param  array $context
109
     * @return mixed
110
     */
111
    public function persistNew($resource, array $context = [])
112
    {
113
        $this->gate->authorize('post', $resource);
114
        $resourceClass = get_class($resource);
115
        $array = $this->normalizer->normalize($resource);
116
        if (!is_array($array)) {
117
            throw new UnexpectedValueException('Resource ' . get_class($resource) . ' was normalized to a non array field');
118
        }
119
        $modelClass = $this->getModelClass($resourceClass, $context);
120
        $modelClass::unguard();
121
        try {
122
            $modelInstance = $modelClass::create($array);
123
        } finally {
124
            $modelClass::reguard();
125
        }
126
127
        return $this->denormalize($modelInstance->toArray(), $resourceClass);
128
    }
129
130
    /**
131
     * Stores an api resource to an existing Eloquent model instance.
132
     *
133
     * @param  mixed $resource
134
     * @param  mixed $id
135
     * @param  array $context
136
     * @return mixed
137
     */
138
    public function persistExisting($resource, $id, array $context = [])
139
    {
140
        $this->gate->authorize('put', $resource);
141
        $resourceClass = get_class($resource);
142
        $modelClass = $this->getModelClass($resourceClass, $context);
143
        $modelInstance = $modelClass::where(['id' => $id])->firstOrFail();
144
        $array = $this->normalizer->normalize($resource);
145
        if (!is_array($array)) {
146
            throw new UnexpectedValueException('Resource ' . get_class($resource) . ' was normalized to a non array field');
147
        }
148
        unset($array['id']);
149
        $modelInstance->unguard();
150
        try {
151
            $modelInstance->fill($array);
152
        } finally {
153
            $modelInstance->reguard();
154
        }
155
        $modelInstance->save();
156
157
        return $this->denormalize($modelInstance->toArray(), $resourceClass);
158
    }
159
160
    /**
161
     * Removes a resource from the database.
162
     *
163
     * @param string $resourceClass
164
     * @param mixed  $id
165
     * @param array  $context
166
     */
167
    public function remove(string $resourceClass, $id, array $context)
168
    {
169
        $modelClass = $this->getModelClass($resourceClass, $context);
170
        $modelInstance = $modelClass::where($context[1] ?? $context['id'] ?? 'id', $id)->first();
171
        if (!$modelInstance) {
172
            return;
173
        }
174
        $result = $this->denormalize($modelInstance->toArray(), $resourceClass);
175
        $this->gate->authorize('delete', $result);
176
        $modelClass::destroy($id);
177
    }
178
179
    /**
180
     * Denormalizes from an array to an api resource with the EvilReflectionPropertyNormalizer active, so a domain
181
     * with only a getter will be hydrated correctly.
182
     *
183
     * @param  array  $array
184
     * @param  string $resourceClass
185
     * @return mixed
186
     */
187
    private function denormalize(array $array, string $resourceClass)
188
    {
189
        ContextualNormalizer::enableDenormalizer(EvilReflectionPropertyNormalizer::class);
190
        try {
191
            $res = $this->denormalizer->denormalize(
192
                $array,
193
                $resourceClass,
194
                null,
195
                ['disable_type_enforcement' => true]
196
            );
197
        } finally {
198
            ContextualNormalizer::disableDenormalizer(EvilReflectionPropertyNormalizer::class);
199
        }
200
201
        return $res;
202
    }
203
204
    /**
205
     * Returns the name of the model class associated to a api resource class.
206
     *
207
     * @param  string $resourceClass
208
     * @return string
209
     */
210
    private function determineModel(string $resourceClass): string
211
    {
212
        return str_replace('\\ApiResources\\', '\\Models\\', $resourceClass);
213
    }
214
215
    /**
216
     * Returns the name of the model class associated to a api resource class and a context.
217
     *
218
     * @param  string $resourceClass
219
     * @param  array  $context
220
     * @return string
221
     */
222
    private function getModelClass(string $resourceClass, array $context): string
223
    {
224
        $modelClass = $context[0] ?? $context['model'] ?? $this->determineModel($resourceClass);
225
        if (!class_exists($modelClass)) {
226
            throw new RuntimeException('Class "' . $modelClass . '" not found!');
227
        }
228
        if (!is_a($modelClass, Model::class, true)) {
229
            throw new RuntimeException('Class "' . $modelClass . '" exists, but is not a Eloquent model!');
230
        }
231
232
        return $modelClass;
233
    }
234
}
235