Completed
Push — master ( b65575...5d3719 )
by Zura
01:31
created

Promocodes::setSuffix()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 5
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 1
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
     * Generated codes will be saved here
29
     * to be validated later.
30
     *
31
     * @var array
32
     */
33
    private $codes = [];
34
35
    /**
36
     * Length of code will be calculated from asterisks you have
37
     * set as mask in your config file.
38
     *
39
     * @var int
40
     */
41
    private $length;
42
43
    /**
44
     * Promocodes constructor.
45
     */
46
    public function __construct()
47
    {
48
        $this->codes = Promocode::pluck('code')->toArray();
49
        $this->length = substr_count(config('promocodes.mask'), '*');
50
51
        $this->prefix = (bool)config('promocodes.prefix')
52
            ? config('promocodes.prefix') . config('promocodes.separator')
53
            : '';
54
55
        $this->suffix = (bool)config('promocodes.suffix')
56
            ? config('promocodes.separator') . config('promocodes.suffix')
57
            : '';
58
    }
59
60
    /**
61
     * Save one-time use promocodes into database
62
     * Successful insert returns generated promocodes
63
     * Fail will return empty collection.
64
     *
65
     * @param int $amount
66
     * @param null $reward
67
     * @param array $data
68
     * @param int|null $expires_in
69
     * @param int|null $quantity
70
     *
71
     * @return \Illuminate\Support\Collection
72
     */
73
    public function createDisposable(
74
        $amount = 1,
75
        $reward = null,
76
        array $data = [],
77
        $expires_in = null,
78
        $quantity = null
79
    )
80
    {
81
        return $this->create($amount, $reward, $data, $expires_in, $quantity, true);
82
    }
83
84
    /**
85
     * Save promocodes into database
86
     * Successful insert returns generated promocodes
87
     * Fail will return empty collection.
88
     *
89
     * @param int $amount
90
     * @param null $reward
91
     * @param array $data
92
     * @param int|null $expires_in
93
     * @param bool $is_disposable
94
     * @param int|null $quantity
95
     *
96
     * @return \Illuminate\Support\Collection
97
     */
98
    public function create(
99
        $amount = 1,
100
        $reward = null,
101
        array $data = [],
102
        $expires_in = null,
103
        $quantity = null,
104
        $is_disposable = false
105
    )
106
    {
107
        $records = [];
108
109
        foreach ($this->output($amount) as $code) {
110
            $records[] = [
111
                'code' => $code,
112
                'reward' => $reward,
113
                'data' => json_encode($data),
114
                'expires_at' => $expires_in ? Carbon::now()->addDays($expires_in) : null,
115
                'is_disposable' => $is_disposable,
116
                'quantity' => $quantity,
117
            ];
118
        }
119
120
        if (Promocode::insert($records)) {
121
            return collect($records)->map(function ($record) {
122
                $record['data'] = json_decode($record['data'], true);
123
124
                return $record;
125
            });
126
        }
127
128
        return collect([]);
129
    }
130
131
    /**
132
     * Generates promocodes as many as you wish.
133
     *
134
     * @param int $amount
135
     *
136
     * @return array
137
     */
138
    public function output($amount = 1)
139
    {
140
        $collection = [];
141
142
        for ($i = 1; $i <= $amount; $i++) {
143
            $random = $this->generate();
144
145
            while (!$this->validate($collection, $random)) {
146
                $random = $this->generate();
147
            }
148
149
            array_push($collection, $random);
150
        }
151
152
        return $collection;
153
    }
154
155
    /**
156
     * Set custom prefix for next generation
157
     *
158
     * @param $prefix
159
     * @return $this
160
     */
161
    public function setPrefix($prefix)
162
    {
163
        $this->prefix = $prefix;
164
        return $this;
165
    }
166
167
    /**
168
     * Set custom suffix for next generation
169
     *
170
     * @param $suffix
171
     * @return $this
172
     */
173
    public function setSuffix($suffix)
174
    {
175
        $this->suffix = $suffix;
176
        return $this;
177
    }
178
179
    /**
180
     * Here will be generated single code using your parameters from config.
181
     *
182
     * @return string
183
     */
184
    private function generate()
185
    {
186
        $characters = config('promocodes.characters');
187
        $mask = config('promocodes.mask');
188
        $promocode = '';
189
        $random = [];
190
191
        for ($i = 1; $i <= $this->length; $i++) {
192
            $character = $characters[rand(0, strlen($characters) - 1)];
193
            $random[] = $character;
194
        }
195
196
        shuffle($random);
197
        $length = count($random);
198
199
        $promocode .= $this->prefix;
200
201
        for ($i = 0; $i < $length; $i++) {
202
            $mask = preg_replace('/\*/', $random[$i], $mask, 1);
203
        }
204
205
        $promocode .= $mask;
206
        $promocode .= $this->suffix;
207
208
        return $promocode;
209
    }
210
211
    /**
212
     * Your code will be validated to be unique for one request.
213
     *
214
     * @param $collection
215
     * @param $new
216
     *
217
     * @return bool
218
     */
219
    private function validate($collection, $new)
220
    {
221
        return !in_array($new, array_merge($collection, $this->codes));
222
    }
223
224
    /**
225
     * Reedem promocode to user that it's used from now.
226
     *
227
     * @param string $code
228
     *
229
     * @return bool|Promocode
230
     * @throws AlreadyUsedException
231
     * @throws UnauthenticatedException
232
     */
233
    public function redeem($code)
234
    {
235
        return $this->apply($code);
236
    }
237
238
    /**
239
     * Apply promocode to user that it's used from now.
240
     *
241
     * @param string $code
242
     *
243
     * @return bool|Promocode
244
     * @throws AlreadyUsedException
245
     * @throws UnauthenticatedException
246
     */
247
    public function apply($code)
248
    {
249
        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...
250
            throw new UnauthenticatedException;
251
        }
252
253
        try {
254
            if ($promocode = $this->check($code)) {
255
                if ($this->isSecondUsageAttempt($promocode)) {
0 ignored issues
show
Bug introduced by
It seems like $promocode defined by $this->check($code) on line 254 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...
256
                    throw new AlreadyUsedException;
257
                }
258
259
                $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...
260
                    'promocode_id' => $promocode->id,
261
                    'used_at' => Carbon::now(),
262
                ]);
263
264
                if (!is_null($promocode->quantity)) {
265
                    $promocode->quantity -= 1;
266
                    $promocode->save();
267
                }
268
269
                return $promocode->load('users');
270
            }
271
        } catch (InvalidPromocodeException $exception) {
272
            //
273
        }
274
275
        return false;
276
    }
277
278
    /**
279
     * Check promocode in database if it is valid.
280
     *
281
     * @param string $code
282
     *
283
     * @return bool|Promocode
284
     * @throws InvalidPromocodeException
285
     */
286
    public function check($code)
287
    {
288
        $promocode = Promocode::byCode($code)->first();
289
290
        if ($promocode === null) {
291
            throw new InvalidPromocodeException;
292
        }
293
294
        if ($promocode->isExpired() || ($promocode->isDisposable() && $promocode->users()->exists()) || $promocode->isOverAmount()) {
295
            return false;
296
        }
297
298
        return $promocode;
299
    }
300
301
    /**
302
     * Check if user is trying to apply code again.
303
     *
304
     * @param Promocode $promocode
305
     *
306
     * @return bool
307
     */
308
    public function isSecondUsageAttempt(Promocode $promocode)
309
    {
310
        return $promocode->users()->wherePivot(config('promocodes.related_pivot_key', 'user_id'),
311
            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...
312
    }
313
314
    /**
315
     * Expire code as it won't usable anymore.
316
     *
317
     * @param string $code
318
     * @return bool
319
     * @throws InvalidPromocodeException
320
     */
321
    public function disable($code)
322
    {
323
        $promocode = Promocode::byCode($code)->first();
324
325
        if ($promocode === null) {
326
            throw new InvalidPromocodeException;
327
        }
328
329
        $promocode->expires_at = Carbon::now();
330
        $promocode->quantity = 0;
331
332
        return $promocode->save();
333
    }
334
335
    /**
336
     * Clear all expired and used promotion codes
337
     * that can not be used anymore.
338
     *
339
     * @return void
340
     */
341
    public function clearRedundant()
342
    {
343
        Promocode::all()->each(function (Promocode $promocode) {
344
            if ($promocode->isExpired() || ($promocode->isDisposable() && $promocode->users()->exists()) || $promocode->isOverAmount()) {
345
                $promocode->users()->detach();
346
                $promocode->delete();
347
            }
348
        });
349
    }
350
351
    /**
352
     * Get the list of valid promocodes
353
     *
354
     * @return Promocode[]|\Illuminate\Database\Eloquent\Collection
355
     */
356
    public function all()
357
    {
358
        return Promocode::all()->filter(function (Promocode $promocode) {
359
            return !$promocode->isExpired() && !($promocode->isDisposable() && $promocode->users()->exists()) && !$promocode->isOverAmount();
360
        });
361
    }
362
}
363