Completed
Push — master ( 3871a8...57ae06 )
by Mahmoud
03:56
created

HashIdTrait::findKeyAndReturnValue()   B

Complexity

Conditions 5
Paths 4

Size

Total Lines 18
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 18
rs 8.8571
c 0
b 0
f 0
cc 5
eloc 8
nc 4
nop 3
1
<?php
2
3
namespace App\Ship\Engine\Traits;
4
5
use App\Ship\Features\Exceptions\IncorrectIdException;
6
use Illuminate\Support\Facades\Config;
7
use Route;
8
use Vinkla\Hashids\Facades\Hashids;
9
10
/**
11
 * Class HashIdTrait.
12
 *
13
 * @author  Mahmoud Zalt <[email protected]>
14
 */
15
trait HashIdTrait
16
{
17
18
    /**
19
     * endpoint to be skipped from decoding their ID's (example for external ID's)
20
     * @var  array
21
     */
22
    private $skippedEndpoints = [
23
//        'orders/{id}/external',
24
    ];
25
26
    /**
27
     * All ID's passed with all endpoints will be decoded before entering the Application
28
     */
29
    public function runEndpointsHashedIdsDecoder()
30
    {
31
        if (Config::get('hello.hash-id')) {
32
            Route::bind('id', function ($id, $route) {
33
                // skip decoding some endpoints
34
                if (!in_array($route->uri(), $this->skippedEndpoints)) {
35
36
                    // decode the ID in the URL
37
                    $decoded = $this->decoder($id);
38
39
                    if (empty($decoded)) {
40
                        throw new IncorrectIdException('ID (' . $id . ') is incorrect, consider using the hashed ID 
41
                        instead of the numeric ID.');
42
                    }
43
44
                    return $decoded[0];
45
                }
46
            });
47
        }
48
    }
49
50
    /**
51
     * Will be used by the Eloquent Models (since it's used as trait there).
52
     *
53
     * @param null $key
54
     *
55
     * @return  mixed
56
     */
57
    public function getHashedKey($key = null)
58
    {
59
        // hash the ID only if hash-id enabled in the config
60
        if (Config::get('hello.hash-id')) {
61
            return $this->encoder(($key) ? : $this->getKey());
0 ignored issues
show
Bug introduced by
It seems like getKey() must be provided by classes using this trait. How about adding it as abstract method to this trait?

This check looks for methods that are used by a trait but not required by it.

To illustrate, let’s look at the following code example

trait Idable {
    public function equalIds(Idable $other) {
        return $this->getId() === $other->getId();
    }
}

The trait Idable provides a method equalsId that in turn relies on the method getId(). If this method does not exist on a class mixing in this trait, the method will fail.

Adding the getId() as an abstract method to the trait will make sure it is available.

Loading history...
62
        }
63
64
        return $this->getKey();
0 ignored issues
show
Bug introduced by
It seems like getKey() must be provided by classes using this trait. How about adding it as abstract method to this trait?

This check looks for methods that are used by a trait but not required by it.

To illustrate, let’s look at the following code example

trait Idable {
    public function equalIds(Idable $other) {
        return $this->getId() === $other->getId();
    }
}

The trait Idable provides a method equalsId that in turn relies on the method getId(). If this method does not exist on a class mixing in this trait, the method will fail.

Adding the getId() as an abstract method to the trait will make sure it is available.

Loading history...
65
    }
66
67
    /**
68
     * without decoding the encoded ID's you won't be able to use
69
     * validation features like `exists:table,id`
70
     *
71
     * @param array $requestData
72
     *
73
     * @return  array
74
     */
75
    protected function decodeHashedIdsBeforeApplyingValidationRules(Array $requestData)
76
    {
77
        // the hash ID feature must be enabled to use this decoder feature.
78
        if (Config::get('hello.hash-id') && isset($this->decode) && !empty($this->decode)) {
79
80
            // iterate over each key (ID that needs to be decoded) and call keys locator to decode them
81
            foreach ($this->decode as $key) {
0 ignored issues
show
Bug introduced by
The property decode does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
82
83
                $requestData = $this->locateAndDecodeIds($requestData, $key);
84
85
            }
86
        }
87
88
        return $requestData;
89
    }
90
91
    /**
92
     * Expected Keys formats:
93
     *
94
     * Type 1:
95
     *   A
96
     * Type 2:
97
     *   A.*.B
98
     *   A.*.B.*.C
99
     * Type 3:
100
     *   A.*
101
     *   A.*.B.*
102
     *
103
     * @param $requestData
104
     * @param $key
105
     *
106
     * @return  mixed
107
     */
108
    private function locateAndDecodeIds($requestData, $key)
109
    {
110
111
        if ($this->stringEndsWithChars('.*', $key)) {
112
            // if the key of Type 3:
113
            $this->decodeType3Key($requestData, $key);
114
        } elseif (str_contains($key, '.*.')) {
115
            // if the key of Type 2:
116
            $this->decodeType2Key($requestData, $key);
117
        } else {
118
            // if the key of Type 1:
119
            $this->decodeType1Key($requestData, $key);
120
        }
121
122
        return $requestData;
123
    }
124
125
    /**
126
     * @param $requestData
127
     * @param $key
128
     */
129
    private function decodeType1Key(&$requestData, $key)
130
    {
131
        // decode single key
132
        if (isset($requestData[$key])) {
133
            $requestData[$key] = $this->decode($requestData[$key]);
134
        }
135
    }
136
137
    /**
138
     * @param $requestData
139
     * @param $key
140
     */
141
    private function decodeType2Key(&$requestData, $key)
142
    {
143
        // get the last part of the key, which should be the ID that needs decoding
144
        $idToDecode = substr($key, strrpos($key, '.*.') + 3);
145
146
        array_walk_recursive($requestData, function (&$value, $key) use ($idToDecode) {
147
148
            if ($key == $idToDecode) {
149
150
                $value = $this->decode($value);
151
            }
152
153
        });
154
    }
155
156
    /**
157
     * @param $requestData
158
     * @param $key
159
     */
160
    private function decodeType3Key(&$requestData, $key)
161
    {
162
163
        $idToDecode = $this->removeLastOccurrenceFromString($key, '.*');
164
165
        $this->findKeyAndReturnValue($requestData, $idToDecode, function ($ids) {
166
167
            if (!is_array($ids)) {
168
                throw new IncorrectIdException('Expected ID\'s to be in array. Please wrap your ID\'s in an Array and send them back.');
169
            }
170
171
            $decodedIds = [];
172
173
            foreach ($ids as $id) {
174
                $decodedIds[] = $this->decode($id);
175
            }
176
177
            // callback return
178
            return $decodedIds;
179
        });
180
181
    }
182
183
    /**
184
     * @param $subject
185
     * @param $findKey
186
     * @param $callback
187
     *
188
     * @return  array
189
     */
190
    public function findKeyAndReturnValue(&$subject, $findKey, $callback)
191
    {
192
        // if the value is not an array, then you have reached the deepest point of the branch, so return the value.
193
        if (!is_array($subject)) {
194
            return $subject;
195
        }
196
197
        foreach ($subject as $key => $value) {
198
199
            if ($key == $findKey && isset($subject[$findKey])) {
200
                $subject[$key] = $callback($subject[$findKey]);
201
                break;
202
            }
203
204
            // add the value with the recursive call
205
            $this->findKeyAndReturnValue($value, $findKey, $callback);
206
        }
207
    }
208
209
    /**
210
     * @param $search
211
     * @param $subject
212
     *
213
     * @return  mixed
214
     */
215
    private function removeLastOccurrenceFromString($subject, $search)
216
    {
217
        $replace = '';
218
219
        $pos = strrpos($subject, $search);
220
221
        if ($pos !== false) {
222
            $subject = substr_replace($subject, $replace, $pos, strlen($search));
223
        }
224
225
        return $subject;
226
    }
227
228
    /**
229
     * @param $needle
230
     * @param $haystack
231
     *
232
     * @return  int
233
     */
234
    private function stringEndsWithChars($needle, $haystack)
235
    {
236
        return preg_match('/' . preg_quote($needle, '/') . '$/', $haystack);
237
    }
238
239
240
    /**
241
     * @param array $ids
242
     *
243
     * @return  array
244
     */
245
    public function decodeArray(array $ids)
246
    {
247
        $result = [];
248
        foreach ($ids as $id) {
249
            $result[] = $this->decode($id);
250
        }
251
252
        return $result;
253
    }
254
255
    /**
256
     * @param $id
257
     *
258
     * @return  mixed
259
     */
260
    public function decode($id)
261
    {
262
        if (is_int($id)) {
263
            throw new IncorrectIdException('Only Hashed ID\'s allowed.');
264
        }
265
266
        return empty($this->decoder($id)) ? [] : $this->decoder($id)[0];
267
    }
268
269
    /**
270
     * @param $id
271
     *
272
     * @return  mixed
273
     */
274
    public function encode($id)
275
    {
276
        return $this->encoder($id);
277
    }
278
279
    /**
280
     * @param $id
281
     *
282
     * @return  mixed
283
     */
284
    private function decoder($id)
285
    {
286
        return Hashids::decode($id);
287
    }
288
289
    /**
290
     * @param $id
291
     *
292
     * @return  mixed
293
     */
294
    public function encoder($id)
295
    {
296
        return Hashids::encode($id);
297
    }
298
299
}
300