Completed
Push — master ( 173198...82f4fc )
by Zura
07:18
created

Promocodes::apply()   B

Complexity

Conditions 6
Paths 12

Size

Total Lines 30

Duplication

Lines 0
Ratio 0 %

Importance

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