Completed
Push — master ( 18a348...dbee4c )
by Mahmoud
03:28
created

HashIdTrait::locateAndDecodeIds()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 16
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 16
rs 9.4285
c 0
b 0
f 0
cc 3
eloc 8
nc 3
nop 2
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
            return $decodedIds;
178
        });
179
180
    }
181
182
    /**
183
     * @param $subject
184
     * @param $findKey
185
     * @param $callback
186
     *
187
     * @return  array
188
     */
189
    public function findKeyAndReturnValue(&$subject, $findKey, $callback)
190
    {
191
        // if the value is not an array, then you have reached the deepest point of the branch, so return the value.
192
        if (!is_array($subject)) {
193
            return $subject;
194
        }
195
196
        foreach ($subject as $key => $value) {
197
198
            if ($key == $findKey) {
199
                $subject[$key] = $callback($subject[$findKey]);
200
                break;
201
            }
202
203
            // add the value with the recursive call
204
            $this->findKeyAndReturnValue($value, $findKey, $callback);
205
        }
206
    }
207
208
    /**
209
     * @param $search
210
     * @param $subject
211
     *
212
     * @return  mixed
213
     */
214
    private function removeLastOccurrenceFromString($subject, $search)
215
    {
216
        $replace = '';
217
218
        $pos = strrpos($subject, $search);
219
220
        if ($pos !== false) {
221
            $subject = substr_replace($subject, $replace, $pos, strlen($search));
222
        }
223
224
        return $subject;
225
    }
226
227
    /**
228
     * @param $needle
229
     * @param $haystack
230
     *
231
     * @return  int
232
     */
233
    private function stringEndsWithChars($needle, $haystack)
234
    {
235
        return preg_match('/' . preg_quote($needle, '/') . '$/', $haystack);
236
    }
237
238
239
    /**
240
     * @param array $ids
241
     *
242
     * @return  array
243
     */
244
    public function decodeArray(array $ids)
245
    {
246
        $result = [];
247
        foreach ($ids as $id) {
248
            $result[] = $this->decode($id);
249
        }
250
251
        return $result;
252
    }
253
254
    /**
255
     * @param $id
256
     *
257
     * @return  mixed
258
     */
259
    public function decode($id)
260
    {
261
        if (is_int($id)) {
262
            throw new IncorrectIdException('Only Hashed ID\'s allowed.');
263
        }
264
265
        return empty($this->decoder($id)) ? [] : $this->decoder($id)[0];
266
    }
267
268
    /**
269
     * @param $id
270
     *
271
     * @return  mixed
272
     */
273
    public function encode($id)
274
    {
275
        return $this->encoder($id);
276
    }
277
278
    /**
279
     * @param $id
280
     *
281
     * @return  mixed
282
     */
283
    private function decoder($id)
284
    {
285
        return Hashids::decode($id);
286
    }
287
288
    /**
289
     * @param $id
290
     *
291
     * @return  mixed
292
     */
293
    public function encoder($id)
294
    {
295
        return Hashids::encode($id);
296
    }
297
298
}
299