Completed
Pull Request — master (#70)
by
unknown
01:39
created

Promocodes   D

Complexity

Total Complexity 58

Size/Duplication

Total Lines 527
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 7

Importance

Changes 0
Metric Value
wmc 58
lcom 1
cbo 7
dl 0
loc 527
rs 4.5599
c 0
b 0
f 0

27 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 13 3
A createDisposable() 0 10 1
A create() 0 35 5
A output() 0 16 3
A getAmount() 0 4 2
A setAmount() 0 5 1
A generate() 0 26 3
A validate() 0 4 1
A getReward() 0 4 2
A setReward() 0 5 1
A getData() 0 4 2
A setData() 0 5 1
A getExpiresIn() 0 4 2
A setExpiresIn() 0 5 1
A getDisposable() 0 4 2
A setDisposable() 0 5 1
A getQuantity() 0 4 2
A setQuantity() 0 5 1
A setPrefix() 0 5 1
A setSuffix() 0 5 1
A redeem() 0 4 1
A apply() 0 26 5
A check() 0 10 3
A isSecondUsageAttempt() 0 5 2
A disable() 0 13 2
A clearRedundant() 0 9 5
A all() 0 6 4

How to fix   Complexity   

Complex Class

Complex classes like Promocodes often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Promocodes, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace Gabievi\Promocodes;
4
5
use Carbon\Carbon;
6
use Gabievi\Promocodes\Models\Promocode;
7
use Gabievi\Promocodes\Exceptions\AlreadyUsedException;
8
use Gabievi\Promocodes\Exceptions\UnauthenticatedException;
9
use Gabievi\Promocodes\Exceptions\InvalidPromocodeException;
10
11
class Promocodes
12
{
13
    /**
14
     * Prefix for code generation
15
     *
16
     * @var string
17
     */
18
    protected $prefix;
19
20
    /**
21
     * Suffix for code generation
22
     *
23
     * @var string
24
     */
25
    protected $suffix;
26
27
    /**
28
     * Number of codes to be generated
29
     *
30
     * @var int
31
     */
32
    protected $amount = 1;
33
34
    /**
35
     * Reward value which will be sticked to code
36
     *
37
     * @var null
38
     */
39
    protected $reward = null;
40
41
    /**
42
     * Additional data to be returned with code
43
     *
44
     * @var array
45
     */
46
    protected $data = [];
47
48
    /**
49
     * Number of days of code expiration
50
     *
51
     * @var null|int
52
     */
53
    protected $expires_in = null;
54
55
    /**
56
     * Maximum number of available usage of code
57
     *
58
     * @var null|int
59
     */
60
    protected $quantity = null;
61
62
    /**
63
     * If code should automatically invalidate after first use
64
     *
65
     * @var bool
66
     */
67
    protected $disposable = false;
68
69
    /**
70
     * Generated codes will be saved here
71
     * to be validated later.
72
     *
73
     * @var array
74
     */
75
    private $codes = [];
76
77
    /**
78
     * Length of code will be calculated from asterisks you have
79
     * set as mask in your config file.
80
     *
81
     * @var int
82
     */
83
    private $length;
84
85
    /**
86
     * Promocodes constructor.
87
     */
88
    public function __construct()
89
    {
90
        $this->codes = Promocode::pluck('code')->toArray();
91
        $this->length = substr_count(config('promocodes.mask'), '*');
92
93
        $this->prefix = (bool)config('promocodes.prefix')
94
            ? config('promocodes.prefix') . config('promocodes.separator')
95
            : '';
96
97
        $this->suffix = (bool)config('promocodes.suffix')
98
            ? config('promocodes.separator') . config('promocodes.suffix')
99
            : '';
100
    }
101
102
    /**
103
     * Save one-time use promocodes into database
104
     * Successful insert returns generated promocodes
105
     * Fail will return empty collection.
106
     *
107
     * @param int $amount
108
     * @param null $reward
109
     * @param array $data
110
     * @param int|null $expires_in
111
     * @param int|null $quantity
112
     *
113
     * @return \Illuminate\Support\Collection
114
     */
115
    public function createDisposable(
116
        $amount = null,
117
        $reward = null,
118
        $data = null,
119
        $expires_in = null,
120
        $quantity = null
121
    )
122
    {
123
        return $this->create($amount, $reward, $data, $expires_in, $quantity, true);
124
    }
125
126
    /**
127
     * Save promocodes into database
128
     * Successful insert returns generated promocodes
129
     * Fail will return empty collection.
130
     *
131
     * @param int $amount
132
     * @param null $reward
133
     * @param array $data
134
     * @param int|null $expires_in
135
     * @param int|null $quantity
136
     * @param bool $is_disposable
137
     * @param array $custom_codes
138
     *
139
     * @return \Illuminate\Support\Collection
140
     */
141
    public function create(
142
        $amount = null,
143
        $reward = null,
144
        $data = null,
145
        $expires_in = null,
146
        $quantity = null,
147
        $is_disposable = null,
148
        $custom_codes = []
149
    )
150
    {
151
        $records = [];
152
153
        $codes = count($custom_codes) > 0 ? $custom_codes : $this->output($amount);
154
155
        foreach ($codes as $code) {
156
            $records[] = [
157
                'code' => $code,
158
                'reward' => $this->getReward($reward),
159
                'data' => json_encode($this->getData($data)),
160
                'expires_at' => $this->getExpiresIn($expires_in) ? Carbon::now()->addDays($this->getExpiresIn($expires_in)) : null,
161
                'is_disposable' => $this->getDisposable($is_disposable),
162
                'quantity' => $this->getQuantity($quantity),
163
            ];
164
        }
165
166
        if (Promocode::insert($records)) {
167
            return collect($records)->map(function ($record) {
168
                $record['data'] = json_decode($record['data'], true);
169
170
                return $record;
171
            });
172
        }
173
174
        return collect([]);
175
    }
176
177
    /**
178
     * Generates promocodes as many as you wish.
179
     *
180
     * @param int $amount
181
     *
182
     * @return array
183
     */
184
    public function output($amount = null)
185
    {
186
        $collection = [];
187
188
        for ($i = 1; $i <= $this->getAmount($amount); $i++) {
189
            $random = $this->generate();
190
191
            while (!$this->validate($collection, $random)) {
192
                $random = $this->generate();
193
            }
194
195
            array_push($collection, $random);
196
        }
197
198
        return $collection;
199
    }
200
201
    /**
202
     * Get number of codes to be generated
203
     *
204
     * @param null|int $request
205
     * @return null|int
206
     */
207
    public function getAmount($request)
208
    {
209
        return $request !== null ? $request : $this->amount;
210
    }
211
212
    /**
213
     * Set how much code you want to be generated
214
     *
215
     * @param int $amount
216
     * @return $this
217
     */
218
    public function setAmount($amount)
219
    {
220
        $this->amount = $amount;
221
        return $this;
222
    }
223
224
    /**
225
     * Here will be generated single code using your parameters from config.
226
     *
227
     * @return string
228
     */
229
    private function generate()
230
    {
231
        $characters = config('promocodes.characters');
232
        $mask = config('promocodes.mask');
233
        $promocode = '';
234
        $random = [];
235
236
        for ($i = 1; $i <= $this->length; $i++) {
237
            $character = $characters[rand(0, strlen($characters) - 1)];
238
            $random[] = $character;
239
        }
240
241
        shuffle($random);
242
        $length = count($random);
243
244
        $promocode .= $this->prefix;
245
246
        for ($i = 0; $i < $length; $i++) {
247
            $mask = preg_replace('/\*/', $random[$i], $mask, 1);
248
        }
249
250
        $promocode .= $mask;
251
        $promocode .= $this->suffix;
252
253
        return $promocode;
254
    }
255
256
    /**
257
     * Your code will be validated to be unique for one request.
258
     *
259
     * @param $collection
260
     * @param $new
261
     *
262
     * @return bool
263
     */
264
    private function validate($collection, $new)
265
    {
266
        return !in_array($new, array_merge($collection, $this->codes));
267
    }
268
269
    /**
270
     * Get custom set reward value
271
     *
272
     * @param null|int $request
273
     * @return null|int
274
     */
275
    public function getReward($request)
276
    {
277
        return $request !== null ? $request : $this->reward;
278
    }
279
280
    /**
281
     * Set custom reward value
282
     *
283
     * @param int $reward
284
     * @return $this
285
     */
286
    public function setReward($reward)
287
    {
288
        $this->reward = $reward;
0 ignored issues
show
Documentation Bug introduced by
It seems like $reward of type integer is incompatible with the declared type null of property $reward.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
289
        return $this;
290
    }
291
292
    /**
293
     * Get custom set data value
294
     *
295
     * @param null|array $data
0 ignored issues
show
Bug introduced by
There is no parameter named $data. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
296
     * @return null|array
297
     */
298
    public function getData($request)
299
    {
300
        return $request !== null ? $request : $this->data;
301
    }
302
303
    /**
304
     * Set custom data value
305
     *
306
     * @param array $data
307
     * @return $this
308
     */
309
    public function setData($data)
310
    {
311
        $this->data = $data;
312
        return $this;
313
    }
314
315
    /**
316
     * Get custom set expiration days value
317
     *
318
     * @param null|int $request
319
     * @return null|int
320
     */
321
    public function getExpiresIn($request)
322
    {
323
        return $request !== null ? $request : $this->expires_in;
324
    }
325
326
    /**
327
     * Set custom expiration days value
328
     *
329
     * @param int $expires_in
330
     * @return $this
331
     */
332
    public function setExpiresIn($expires_in)
333
    {
334
        $this->expires_in = $expires_in;
335
        return $this;
336
    }
337
338
    /**
339
     * Get custom disposable value
340
     *
341
     * @param null|bool $request
342
     * @return null|bool
343
     */
344
    public function getDisposable($request)
345
    {
346
        return $request !== null ? $request : $this->disposable;
347
    }
348
349
    /**
350
     * Set custom disposable value
351
     *
352
     * @param bool $disposable
353
     * @return $this
354
     */
355
    public function setDisposable($disposable = true)
356
    {
357
        $this->disposable = $disposable;
358
        return $this;
359
    }
360
361
    /**
362
     * Get custom set quantity value
363
     *
364
     * @param null|int $request
365
     * @return null|int
366
     */
367
    public function getQuantity($request)
368
    {
369
        return $request !== null ? $request : $this->quantity;
370
    }
371
372
    /**
373
     * Set custom quantity value
374
     *
375
     * @param int $quantity
376
     * @return $this
377
     */
378
    public function setQuantity($quantity)
379
    {
380
        $this->quantity = $quantity;
381
        return $this;
382
    }
383
384
    /**
385
     * Set custom prefix for next generation
386
     *
387
     * @param string $prefix
388
     * @return $this
389
     */
390
    public function setPrefix($prefix)
391
    {
392
        $this->prefix = $prefix;
393
        return $this;
394
    }
395
396
    /**
397
     * Set custom suffix for next generation
398
     *
399
     * @param string $suffix
400
     * @return $this
401
     */
402
    public function setSuffix($suffix)
403
    {
404
        $this->suffix = $suffix;
405
        return $this;
406
    }
407
408
    /**
409
     * Reedem promocode to user that it's used from now.
410
     *
411
     * @param string $code
412
     *
413
     * @return bool|Promocode
414
     * @throws AlreadyUsedException
415
     * @throws UnauthenticatedException
416
     */
417
    public function redeem($code)
418
    {
419
        return $this->apply($code);
420
    }
421
422
    /**
423
     * Apply promocode to user that it's used from now.
424
     *
425
     * @param string $code
426
     *
427
     * @return bool|Promocode
428
     * @throws AlreadyUsedException
429
     * @throws UnauthenticatedException
430
     */
431
    public function apply($code)
432
    {
433
        if (!auth()->check()) {
0 ignored issues
show
Bug introduced by
The method check does only exist in Illuminate\Contracts\Auth\Guard, but not in Illuminate\Contracts\Auth\Factory.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
434
            throw new UnauthenticatedException;
435
        }
436
437
        if ($promocode = $this->check($code)) {
438
            if ($this->isSecondUsageAttempt($promocode)) {
0 ignored issues
show
Bug introduced by
It seems like $promocode defined by $this->check($code) on line 437 can also be of type boolean; however, Gabievi\Promocodes\Promo...:isSecondUsageAttempt() does only seem to accept object<Gabievi\Promocodes\Models\Promocode>, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
439
                throw new AlreadyUsedException;
440
            }
441
442
            $promocode->users()->attach(auth()->id(), [
0 ignored issues
show
Bug introduced by
The method id does only exist in Illuminate\Contracts\Auth\Guard, but not in Illuminate\Contracts\Auth\Factory.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
443
                'promocode_id' => $promocode->id,
444
                'used_at' => Carbon::now(),
445
            ]);
446
447
            if (!is_null($promocode->quantity)) {
448
                $promocode->quantity -= 1;
449
                $promocode->save();
450
            }
451
452
            return $promocode->load('users');
453
        }
454
455
        return false;
456
    }
457
458
    /**
459
     * Check promocode in database if it is valid.
460
     *
461
     * @param string $code
462
     *
463
     * @return bool|Promocode
464
     */
465
    public function check($code)
466
    {
467
        $promocode = Promocode::byCode($code)->first();
468
469
        if ($promocode === null || $promocode->isExpired()) {
470
            return false;
471
        }
472
473
        return $promocode;
474
    }
475
476
    /**
477
     * Check if user is trying to apply code again.
478
     *
479
     * @param Promocode $promocode
480
     *
481
     * @return bool
482
     */
483
    public function isSecondUsageAttempt(Promocode $promocode)
484
    {
485
        return $promocode->isDisposable() && $promocode->users()->wherePivot(config('promocodes.related_pivot_key', 'user_id'),
486
                auth()->id())->exists();
0 ignored issues
show
Bug introduced by
The method id does only exist in Illuminate\Contracts\Auth\Guard, but not in Illuminate\Contracts\Auth\Factory.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
487
    }
488
489
    /**
490
     * Expire code as it won't be usable anymore.
491
     *
492
     * @param string $code
493
     * @return bool
494
     * @throws InvalidPromocodeException
495
     */
496
    public function disable($code)
497
    {
498
        $promocode = Promocode::byCode($code)->first();
499
500
        if ($promocode === null) {
501
            throw new InvalidPromocodeException;
502
        }
503
504
        $promocode->expires_at = Carbon::now();
505
        $promocode->quantity = 0;
506
507
        return $promocode->save();
508
    }
509
510
    /**
511
     * Clear all expired and used promotion codes
512
     * that can not be used anymore.
513
     *
514
     * @return void
515
     */
516
    public function clearRedundant()
517
    {
518
        Promocode::all()->each(function (Promocode $promocode) {
519
            if ($promocode->isExpired() || ($promocode->isDisposable() && $promocode->users()->exists()) || $promocode->isOverAmount()) {
520
                $promocode->users()->detach();
521
                $promocode->delete();
522
            }
523
        });
524
    }
525
526
    /**
527
     * Get the list of valid promocodes
528
     *
529
     * @return Promocode[]|\Illuminate\Database\Eloquent\Collection
530
     */
531
    public function all()
532
    {
533
        return Promocode::all()->filter(function (Promocode $promocode) {
534
            return !$promocode->isExpired() && !($promocode->isDisposable() && $promocode->users()->exists()) && !$promocode->isOverAmount();
535
        });
536
    }
537
}
538