Completed
Push — master ( e7b516...6a1685 )
by Zura
10:42
created

Promocodes::apply()   B

Complexity

Conditions 5
Paths 8

Size

Total Lines 24
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
dl 0
loc 24
rs 8.5125
c 1
b 0
f 0
cc 5
eloc 12
nc 8
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\AlreadyUsedExceprion;
8
use Gabievi\Promocodes\Exceptions\UnauthenticatedExceprion;
9
use Gabievi\Promocodes\Exceptions\InvalidPromocodeExceprion;
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
36
        $this->length = substr_count(config('promocodes.mask'), '*');
37
    }
38
39
    /**
40
     * Generates promocodes as many as you wish.
41
     *
42
     * @param int $amount
43
     *
44
     * @return array
45
     */
46
    public function output($amount = 1)
47
    {
48
        $collection = [];
49
50
        for ($i = 1; $i <= $amount; $i++) {
51
            $random = $this->generate();
52
53
            while (!$this->validate($collection, $random)) {
54
                $random = $this->generate();
55
            }
56
57
            array_push($collection, $random);
58
        }
59
60
        return $collection;
61
    }
62
63
    /**
64
     * Save promocodes into database
65
     * Successful insert returns generated promocodes
66
     * Fail will return empty collection.
67
     *
68
     * @param int $amount
69
     * @param null $reward
70
     * @param array $data
71
     * @param int|null $expires_in
72
     * @param bool $is_disposable
73
     *
74
     * @return \Illuminate\Support\Collection
75
     */
76
    public function create($amount = 1, $reward = null, array $data = [], $expires_in = null, $is_disposable = false)
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
            ];
88
        }
89
90
        if (Promocode::insert($records)) {
91
            return collect($records)->map(function ($record) {
92
                $record['data'] = json_decode($record['data'], true);
93
                return $record;
94
            });
95
        }
96
97
        return collect([]);
98
    }
99
100
    /**
101
     * Save one-time use promocodes into database
102
     * Successful insert returns generated promocodes
103
     * Fail will return empty collection.
104
     *
105
     * @param int $amount
106
     * @param null $reward
107
     * @param array $data
108
     * @param int|null $expires_in
109
     *
110
     * @return \Illuminate\Support\Collection
111
     */
112
    public function createDisposable($amount = 1, $reward = null, array $data = [], $expires_in = null)
113
    {
114
        return $this->create($amount, $reward, $data, $expires_in, true);
115
    }
116
117
    /**
118
     * Check promocode in database if it is valid.
119
     *
120
     * @param string $code
121
     *
122
     * @return bool|\Gabievi\Promocodes\Model\Promocode
123
     * @throws \Gabievi\Promocodes\Exceptions\InvalidPromocodeExceprion
124
     */
125
    public function check($code)
126
    {
127
        $promocode = Promocode::byCode($code)->first();
128
129
        if ($promocode === null) {
130
            throw new InvalidPromocodeExceprion;
131
        }
132
133
        if ($promocode->isExpired() || ($promocode->isDisposable() && $promocode->users()->exists())) {
134
            return false;
135
        }
136
137
        return $promocode;
138
    }
139
140
    /**
141
     * Apply promocode to user that it's used from now.
142
     *
143
     * @param string $code
144
     *
145
     * @return bool|\Gabievi\Promocodes\Model\Promocode
146
     * @throws \Gabievi\Promocodes\Exceptions\UnauthenticatedExceprion|\Gabievi\Promocodes\Exceptions\AlreadyUsedExceprion
147
     */
148
    public function apply($code)
149
    {
150
        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...
151
            throw new UnauthenticatedExceprion;
152
        }
153
154
        try {
155
            if ($promocode = $this->check($code)) {
156
                if ($this->isSecondUsageAttempt($promocode)) {
157
                    throw new AlreadyUsedExceprion;
158
                }
159
160
                $promocode->users()->attach(auth()->user()->id, [
0 ignored issues
show
Bug introduced by
The method user 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...
161
                    'used_at' => Carbon::now(),
162
                ]);
163
164
                return $promocode->load('users');
165
            }
166
        } catch(InvalidPromocodeExceprion $exception) {
167
            //
168
        }
169
170
        return false;
171
    }
172
173
    /**
174
     * Reedem promocode to user that it's used from now.
175
     *
176
     * @param string $code
177
     *
178
     * @return bool|\Gabievi\Promocodes\Model\Promocode
179
     * @throws \Gabievi\Promocodes\Exceptions\UnauthenticatedExceprion|\Gabievi\Promocodes\Exceptions\AlreadyUsedExceprion
180
     */
181
    public function redeem($code)
182
    {
183
        return $this->apply($code);
184
    }
185
186
    /**
187
     * Expire code as it won't usable anymore.
188
     *
189
     * @param string $code
190
     * @return bool
191
     * @throws \Gabievi\Promocodes\Exceptions\InvalidPromocodeExceprion
192
     */
193
    public function disable($code)
194
    {
195
        $promocode = Promocode::byCode($code)->first();
196
197
        if ($promocode === null) {
198
            throw new InvalidPromocodeExceprion;
199
        }
200
201
        $promocode->expires_at = Carbon::now();
202
203
        return $promocode->save();
204
    }
205
206
    /**
207
     * Clear all expired and used promotion codes
208
     * that can not be used anymore.
209
     *
210
     * @return void
211
     */
212
    public function clearRedundant()
213
    {
214
        Promocode::all()->each(function ($promocode) {
215
            if ($promocode->isExpired() || ($promocode->isDisposable() && $promocode->users()->exists())) {
216
                $promocode->users()->detach();
217
                $promocode->delete();
218
            }
219
        });
220
    }
221
222
    /**
223
     * Here will be generated single code using your parameters from config.
224
     *
225
     * @return string
226
     */
227
    private function generate()
228
    {
229
        $characters = config('promocodes.characters');
230
        $mask = config('promocodes.mask');
231
        $promocode = '';
232
        $random = [];
233
234
        // take needed length of string from characters and randomize it
235
        for ($i = 1; $i <= $this->length; $i++) {
236
            $character = $characters[rand(0, strlen($characters) - 1)];
237
            $random[] = $character;
238
        }
239
240
        // shuffle randomized characters
241
        shuffle($random);
242
243
        // set prefix for promocode
244
        $promocode .= $this->getPrefix();
245
246
        // loop through asterisks and change with random symbol
247
        for ($i = 0; $i < count($random); $i++) {
0 ignored issues
show
Performance Best Practice introduced by
It seems like you are calling the size function count() as part of the test condition. You might want to compute the size beforehand, and not on each iteration.

If the size of the collection does not change during the iteration, it is generally a good practice to compute it beforehand, and not on each iteration:

for ($i=0; $i<count($array); $i++) { // calls count() on each iteration
}

// Better
for ($i=0, $c=count($array); $i<$c; $i++) { // calls count() just once
}
Loading history...
248
            $mask = preg_replace('/\*/', $random[$i], $mask, 1);
249
        }
250
251
        // set updated mask as code
252
        $promocode .= $mask;
253
254
        // set suffix for promocode
255
        $promocode .= $this->getSuffix();
256
257
        return $promocode;
258
    }
259
260
    /**
261
     * Generate prefix with separator for promocode.
262
     *
263
     * @return string
264
     */
265
    private function getPrefix()
266
    {
267
        return (bool)config('promocodes.prefix')
268
            ? config('promocodes.prefix') . config('promocodes.separator')
269
            : '';
270
    }
271
272
    /**
273
     * Generate suffix with separator for promocode.
274
     *
275
     * @return string
276
     */
277
    private function getSuffix()
278
    {
279
        return (bool)config('promocodes.suffix')
280
            ? config('promocodes.separator') . config('promocodes.suffix')
281
            : '';
282
    }
283
284
    /**
285
     * Your code will be validated to be unique for one request.
286
     *
287
     * @param $collection
288
     * @param $new
289
     *
290
     * @return bool
291
     */
292
    private function validate($collection, $new)
293
    {
294
        return !in_array($new, array_merge($collection, $this->codes));
295
    }
296
297
    /**
298
     * Check if user is trying to apply code again.
299
     *
300
     * @param $promocode
301
     *
302
     * @return bool
303
     */
304
    private function isSecondUsageAttempt($promocode) {
305
        return $promocode->users()->wherePivot('user_id', auth()->user()->id)->exists();
0 ignored issues
show
Bug introduced by
The method user 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...
306
    }
307
}
308