HashIdTrait   B
last analyzed

Complexity

Total Complexity 39

Size/Duplication

Total Lines 281
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 2

Importance

Changes 0
Metric Value
wmc 39
lcom 1
cbo 2
dl 0
loc 281
rs 8.2857
c 0
b 0
f 0

15 Methods

Rating   Name   Duplication   Size   Complexity  
A getHashedKey() 0 9 3
B decodeHashedIdsBeforeValidation() 0 12 5
A locateAndDecodeIds() 0 15 3
A decodeType1Key() 0 7 2
A decodeType2Key() 0 13 2
A decodeType3Key() 0 20 3
B findKeyAndReturnValue() 0 18 5
A removeLastOccurrenceFromString() 0 12 2
A stringEndsWithChars() 0 4 1
A decodeArray() 0 9 2
A decode() 0 8 4
A encode() 0 4 1
A decoder() 0 4 1
A encoder() 0 4 1
A runHashedIdsDecoder() 0 20 4
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
     * Will be used by the Eloquent Models (since it's used as trait there).
28
     *
29
     * @param null $key
30
     *
31
     * @return  mixed
32
     */
33
    public function getHashedKey($key = null)
34
    {
35
        // hash the ID only if hash-id enabled in the config
36
        if (Config::get('hello.hash-id')) {
37
            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...
38
        }
39
40
        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...
41
    }
42
43
    /**
44
     * without decoding the encoded ID's you won't be able to use
45
     * validation features like `exists:table,id`
46
     *
47
     * @param array $requestData
48
     *
49
     * @return  array
50
     */
51
    protected function decodeHashedIdsBeforeValidation(Array $requestData)
52
    {
53
        // the hash ID feature must be enabled to use this decoder feature.
54
        if (Config::get('hello.hash-id') && isset($this->decode) && !empty($this->decode)) {
55
            // iterate over each key (ID that needs to be decoded) and call keys locator to decode them
56
            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...
57
                $requestData = $this->locateAndDecodeIds($requestData, $key);
58
            }
59
        }
60
61
        return $requestData;
62
    }
63
64
    /**
65
     * Expected Keys formats:
66
     *
67
     * Type 1:
68
     *   A
69
     * Type 2:
70
     *   A.*.B
71
     *   A.*.B.*.C
72
     * Type 3:
73
     *   A.*
74
     *   A.*.B.*
75
     *
76
     * @param $requestData
77
     * @param $key
78
     *
79
     * @return  mixed
80
     */
81
    private function locateAndDecodeIds($requestData, $key)
82
    {
83
        if ($this->stringEndsWithChars('.*', $key)) {
84
            // if the key of Type 3:
85
            $this->decodeType3Key($requestData, $key);
86
        } elseif (str_contains($key, '.*.')) {
87
            // if the key of Type 2:
88
            $this->decodeType2Key($requestData, $key);
89
        } else {
90
            // if the key of Type 1:
91
            $this->decodeType1Key($requestData, $key);
92
        }
93
94
        return $requestData;
95
    }
96
97
    /**
98
     * @param $requestData
99
     * @param $key
100
     */
101
    private function decodeType1Key(&$requestData, $key)
102
    {
103
        // decode single key
104
        if (isset($requestData[$key])) {
105
            $requestData[$key] = $this->decode($requestData[$key], $key);
106
        }
107
    }
108
109
    /**
110
     * @param $requestData
111
     * @param $key
112
     */
113
    private function decodeType2Key(&$requestData, $key)
114
    {
115
        // get the last part of the key, which should be the ID that needs decoding
116
        $idToDecode = substr($key, strrpos($key, '.*.') + 3);
117
118
        array_walk_recursive($requestData, function (&$value, $key) use ($idToDecode) {
119
120
            if ($key == $idToDecode) {
121
                $value = $this->decode($value, $key);
122
            }
123
124
        });
125
    }
126
127
    /**
128
     * @param $requestData
129
     * @param $key
130
     */
131
    private function decodeType3Key(&$requestData, $key)
132
    {
133
        $idToDecode = $this->removeLastOccurrenceFromString($key, '.*');
134
135
        $this->findKeyAndReturnValue($requestData, $idToDecode, function ($ids) use ($key) {
136
137
            if (!is_array($ids)) {
138
                throw new IncorrectIdException('Expected ID\'s to be in array. Please wrap your ID\'s in an Array and send them back.');
139
            }
140
141
            $decodedIds = [];
142
143
            foreach ($ids as $id) {
144
                $decodedIds[] = $this->decode($id, $key);
145
            }
146
147
            // callback return
148
            return $decodedIds;
149
        });
150
    }
151
152
    /**
153
     * @param $subject
154
     * @param $findKey
155
     * @param $callback
156
     *
157
     * @return  array
158
     */
159
    public function findKeyAndReturnValue(&$subject, $findKey, $callback)
160
    {
161
        // if the value is not an array, then you have reached the deepest point of the branch, so return the value.
162
        if (!is_array($subject)) {
163
            return $subject;
164
        }
165
166
        foreach ($subject as $key => $value) {
167
168
            if ($key == $findKey && isset($subject[$findKey])) {
169
                $subject[$key] = $callback($subject[$findKey]);
170
                break;
171
            }
172
173
            // add the value with the recursive call
174
            $this->findKeyAndReturnValue($value, $findKey, $callback);
175
        }
176
    }
177
178
    /**
179
     * @param $search
180
     * @param $subject
181
     *
182
     * @return  mixed
183
     */
184
    private function removeLastOccurrenceFromString($subject, $search)
185
    {
186
        $replace = '';
187
188
        $pos = strrpos($subject, $search);
189
190
        if ($pos !== false) {
191
            $subject = substr_replace($subject, $replace, $pos, strlen($search));
192
        }
193
194
        return $subject;
195
    }
196
197
    /**
198
     * @param $needle
199
     * @param $haystack
200
     *
201
     * @return  int
202
     */
203
    private function stringEndsWithChars($needle, $haystack)
204
    {
205
        return preg_match('/' . preg_quote($needle, '/') . '$/', $haystack);
206
    }
207
208
    /**
209
     * @param array $ids
210
     *
211
     * @return  array
212
     */
213
    public function decodeArray(array $ids)
214
    {
215
        $result = [];
216
        foreach ($ids as $id) {
217
            $result[] = $this->decode($id);
218
        }
219
220
        return $result;
221
    }
222
223
    /**
224
     * @param      $id
225
     * @param null $parameter
226
     *
227
     * @return  array
228
     */
229
    public function decode($id, $parameter = null)
230
    {
231
        if (is_numeric($id)) {
232
            throw new IncorrectIdException('Only Hashed ID\'s allowed' . (!is_null($parameter) ? " ($parameter)." : '.'));
233
        }
234
235
        return empty($this->decoder($id)) ? [] : $this->decoder($id)[0];
236
    }
237
238
    /**
239
     * @param $id
240
     *
241
     * @return  mixed
242
     */
243
    public function encode($id)
244
    {
245
        return $this->encoder($id);
246
    }
247
248
    /**
249
     * @param $id
250
     *
251
     * @return  mixed
252
     */
253
    private function decoder($id)
254
    {
255
        return Hashids::decode($id);
256
    }
257
258
    /**
259
     * @param $id
260
     *
261
     * @return  mixed
262
     */
263
    public function encoder($id)
264
    {
265
        return Hashids::encode($id);
266
    }
267
268
    /**
269
     * Automatically decode any found `id` in the URL, no need to be used anymore.
270
     * Since now the user will define what needs to be decoded in the request.
271
     *
272
     * All ID's passed with all endpoints will be decoded before entering the Application
273
     */
274
    public function runHashedIdsDecoder()
275
    {
276
        if (Config::get('hello.hash-id')) {
277
            Route::bind('id', function ($id, $route) {
278
                // skip decoding some endpoints
279
                if (!in_array($route->uri(), $this->skippedEndpoints)) {
280
281
                    // decode the ID in the URL
282
                    $decoded = $this->decoder($id);
283
284
                    if (empty($decoded)) {
285
                        throw new IncorrectIdException('ID (' . $id . ') is incorrect, consider using the hashed ID
286
                        instead of the numeric ID.');
287
                    }
288
289
                    return $decoded[0];
290
                }
291
            });
292
        }
293
    }
294
295
}
296