Issues (14)

src/CartItem.php (8 issues)

1
<?php
2
3
/**
4
 * This file is part of dimtrovich/cart".
5
 *
6
 * (c) 2024 Dimitri Sitchet Tomkeu <[email protected]>
7
 *
8
 * For the full copyright and license information, please view
9
 * the LICENSE file that was distributed with this source code.
10
 */
11
12
namespace Dimtrovich\Cart;
13
14
use BlitzPHP\Contracts\Support\Arrayable;
15
use BlitzPHP\Contracts\Support\Jsonable;
16
use BlitzPHP\Utilities\Iterable\Arr;
17
use Closure;
18
use Dimtrovich\Cart\Contracts\Buyable;
19
use InvalidArgumentException;
20
21
/**
22
 * Representation of an item in a cart
23
 *
24
 * @property float|int $priceTax Price with TAX
25
 * @property float|int $subtotal Price for whole CartItem without TAX
26
 * @property float|int $tax      Applicable tax for one cart item
27
 * @property float|int $taxTotal Applicable tax for whole cart item
28
 * @property float|int $total    Price for whole CartItem with TAX
29
 */
30
class CartItem implements Arrayable, Jsonable
31
{
32
    /**
33
     * The rowID of the cart item.
34
     */
35
    public string $rowId;
36
37
    /**
38
     * The quantity for this cart item.
39
     */
40
    public float|int $qty;
41
42
    /**
43
     * The price without TAX of the cart item.
44
     */
45
    public float $price;
46
47
    /**
48
     * The options for this cart item.
49
     */
50
    public CartItemOptions $options;
51
52
    /**
53
     * The tax rate for the cart item.
54
     */
55
    private float|int $taxRate = 0;
56
57
    /**
58
     * Custom rowId generator
59
     */
60
    private static ?Closure $rowIdGenerator = null;
61
62
    /**
63
     * CartItem constructor.
64
     *
65
     * @param int|string           $id      The ID of the cart item.
66
     * @param string               $name    The name of the cart item.
67
     * @param array<string, mixed> $options
68
     */
69
    public function __construct(public int|string $id, public string $name, float|int $price, array $options = [])
70
    {
71
        if (empty($id)) {
72 5
            throw new InvalidArgumentException('Please supply a valid identifier.');
73
        }
74
        if (empty($name)) {
75 5
            throw new InvalidArgumentException('Please supply a valid name.');
76
        }
77
78 5
        $this->price   = (float) $price;
79 5
        $this->options = new CartItemOptions($options);
80 5
        $this->rowId   = $this->generateRowId($id, $options);
81
    }
82
83
    /**
84
     * Returns the formatted price without TAX.
85
     *
86
     * @return float|string
87
     */
88
    public function price(?int $decimals = null, ?string $decimalPoint = null, ?string $thousandSeperator = null)
89
    {
90 1
        return func_num_args() < 2 ? $this->price : $this->numberFormat($this->price, $decimals, $decimalPoint, $thousandSeperator);
0 ignored issues
show
It seems like $decimals can also be of type null; however, parameter $decimals of Dimtrovich\Cart\CartItem::numberFormat() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

90
        return func_num_args() < 2 ? $this->price : $this->numberFormat($this->price, /** @scrutinizer ignore-type */ $decimals, $decimalPoint, $thousandSeperator);
Loading history...
91
    }
92
93
    /**
94
     * Returns the formatted price with TAX.
95
     *
96
     * @return float|string
97
     */
98
    public function priceTax(?int $decimals = null, ?string $decimalPoint = null, ?string $thousandSeperator = null)
99
    {
100 1
        return func_num_args() < 2 ? $this->priceTax : $this->numberFormat($this->priceTax, $decimals, $decimalPoint, $thousandSeperator);
0 ignored issues
show
It seems like $decimals can also be of type null; however, parameter $decimals of Dimtrovich\Cart\CartItem::numberFormat() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

100
        return func_num_args() < 2 ? $this->priceTax : $this->numberFormat($this->priceTax, /** @scrutinizer ignore-type */ $decimals, $decimalPoint, $thousandSeperator);
Loading history...
101
    }
102
103
    /**
104
     * Returns the formatted subtotal.
105
     * Subtotal is price for whole CartItem without TAX
106
     *
107
     * @return float|string
108
     */
109
    public function subtotal(?int $decimals = null, ?string $decimalPoint = null, ?string $thousandSeperator = null)
110
    {
111 1
        return func_num_args() < 2 ? $this->subtotal : $this->numberFormat($this->subtotal, $decimals, $decimalPoint, $thousandSeperator);
0 ignored issues
show
It seems like $decimals can also be of type null; however, parameter $decimals of Dimtrovich\Cart\CartItem::numberFormat() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

111
        return func_num_args() < 2 ? $this->subtotal : $this->numberFormat($this->subtotal, /** @scrutinizer ignore-type */ $decimals, $decimalPoint, $thousandSeperator);
Loading history...
112
    }
113
114
    /**
115
     * Returns the formatted total.
116
     * Total is price for whole CartItem with TAX
117
     *
118
     * @return float|string
119
     */
120
    public function total(?int $decimals = null, ?string $decimalPoint = null, ?string $thousandSeperator = null)
121
    {
122 1
        return func_num_args() < 2 ? $this->total : $this->numberFormat($this->total, $decimals, $decimalPoint, $thousandSeperator);
0 ignored issues
show
It seems like $decimals can also be of type null; however, parameter $decimals of Dimtrovich\Cart\CartItem::numberFormat() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

122
        return func_num_args() < 2 ? $this->total : $this->numberFormat($this->total, /** @scrutinizer ignore-type */ $decimals, $decimalPoint, $thousandSeperator);
Loading history...
123
    }
124
125
    /**
126
     * Returns the formatted tax.
127
     *
128
     * @return float|string
129
     */
130
    public function tax(?int $decimals = null, ?string $decimalPoint = null, ?string $thousandSeperator = null)
131
    {
132 1
        return func_num_args() < 2 ? $this->tax : $this->numberFormat($this->tax, $decimals, $decimalPoint, $thousandSeperator);
0 ignored issues
show
It seems like $decimals can also be of type null; however, parameter $decimals of Dimtrovich\Cart\CartItem::numberFormat() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

132
        return func_num_args() < 2 ? $this->tax : $this->numberFormat($this->tax, /** @scrutinizer ignore-type */ $decimals, $decimalPoint, $thousandSeperator);
Loading history...
133
    }
134
135
    /**
136
     * Returns the formatted tax.
137
     *
138
     * @return float|string
139
     */
140
    public function taxTotal(?int $decimals = null, ?string $decimalPoint = null, ?string $thousandSeperator = null)
141
    {
142 1
        return func_num_args() < 2 ? $this->taxTotal : $this->numberFormat($this->taxTotal, $decimals, $decimalPoint, $thousandSeperator);
0 ignored issues
show
It seems like $decimals can also be of type null; however, parameter $decimals of Dimtrovich\Cart\CartItem::numberFormat() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

142
        return func_num_args() < 2 ? $this->taxTotal : $this->numberFormat($this->taxTotal, /** @scrutinizer ignore-type */ $decimals, $decimalPoint, $thousandSeperator);
Loading history...
143
    }
144
145
    /**
146
     * Set the quantity for this cart item.
147
     */
148
    public function setQuantity(float|int $qty): self
149
    {
150 5
        $this->qty = $qty;
151
152 5
        return $this;
153
    }
154
155
    /**
156
     * Update the cart item from a Buyable.
157
     */
158
    public function updateFromBuyable(Buyable $item): void
159
    {
160 1
        $this->id    = $item->getBuyableIdentifier($this->options);
161 1
        $this->name  = $item->getBuyableDescription($this->options);
162 1
        $this->price = $item->getBuyablePrice($this->options);
163
    }
164
165
    /**
166
     * Update the cart item from an array.
167
     *
168
     * @param array<string, mixed> $attributes
169
     */
170
    public function updateFromArray(array $attributes): void
171
    {
172 1
        $this->id      = Arr::get($attributes, 'id', $this->id);
0 ignored issues
show
Documentation Bug introduced by
It seems like BlitzPHP\Utilities\Itera...butes, 'id', $this->id) can also be of type array<string,mixed>. However, the property $id is declared as type integer|string. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
173 1
        $this->qty     = Arr::get($attributes, 'qty', $this->qty);
174 1
        $this->name    = Arr::get($attributes, 'name', $this->name);
0 ignored issues
show
Documentation Bug introduced by
It seems like BlitzPHP\Utilities\Itera...s, 'name', $this->name) can also be of type array<string,mixed>. However, the property $name is declared as type string. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
175 1
        $this->price   = Arr::get($attributes, 'price', $this->price);
176 1
        $this->options = new CartItemOptions(array_merge($options = $this->options->toArray(), Arr::get($attributes, 'options', $options)));
177
178 1
        $this->rowId = $this->generateRowId($this->id, $this->options->all());
179
    }
180
181
    /**
182
     * Set the tax rate.
183
     */
184
    public function setTaxRate(float|int $taxRate): self
185
    {
186 3
        $this->taxRate = $taxRate;
187
188 3
        return $this;
189
    }
190
191
    /**
192
     * Get an attribute from the cart item or get the associated model.
193
     */
194
    public function __get(string $attribute): mixed
195
    {
196
        if (property_exists($this, $attribute)) {
197 2
            return $this->{$attribute};
198
        }
199
200
        if ($attribute === 'priceTax') {
201 3
            return $this->price + $this->tax;
202
        }
203
204
        if ($attribute === 'subtotal') {
205 5
            return $this->qty * $this->price;
206
        }
207
208
        if ($attribute === 'total') {
209 3
            return $this->qty * ($this->priceTax);
210
        }
211
212
        if ($attribute === 'tax') {
213 5
            return $this->price * ($this->taxRate / 100);
214
        }
215
216
        if ($attribute === 'taxTotal') {
217 3
            return $this->tax * $this->qty;
218
        }
219
220 2
        return null;
221
    }
222
223
    /**
224
     * Create a new instance from a Buyable.
225
     *
226
     * @param array<string, mixed> $options
227
     */
228
    public static function fromBuyable(Buyable $item, array $options = []): static
229
    {
230 1
        return new static($item->getBuyableIdentifier($options), $item->getBuyableDescription($options), $item->getBuyablePrice($options), $options);
231
    }
232
233
    /**
234
     * Create a new instance from the given array.
235
     *
236
     * @param array<string, mixed> $attributes
237
     */
238
    public static function fromArray(array $attributes): static
239
    {
240 3
        $options = Arr::get($attributes, 'options', []);
241
242 3
        return new static($attributes['id'], $attributes['name'], $attributes['price'], $options);
243
    }
244
245
    /**
246
     * Create a new instance from the given attributes.
247
     *
248
     * @param array<string, mixed> $options
249
     */
250
    public static function fromAttributes(int|string $id, string $name, float $price, array $options = []): static
251
    {
252 3
        return new static($id, $name, $price, $options);
253
    }
254
255
    /**
256
     * Sets a custom row ID generator for cart items.
257
     *
258
     * This method allows you to provide a custom closure that generates a unique row ID for each cart item.
259
     * The closure should accept two parameters: $id (the item's identifier) and $options (an array of item options).
260
     * It should return a string representing the unique row ID.
261
     *
262
     * If no custom generator is provided, the default row ID generator will be used, which generates a unique MD5 hash
263
     * based on the item's identifier and serialized options.
264
     *
265
     * @param Closure(int|string $id, array<string,mixed> $options): string|null $generator The custom row ID generator closure or null to reset to default.
266
     */
267
    public static function setRowIdGenerator(?Closure $generator): void
268
    {
269 2
        self::$rowIdGenerator = $generator;
270
    }
271
272
    /**
273
     * Generate a unique id for the cart item.
274
     *
275
     * @param array<string, mixed> $options
276
     */
277
    protected function generateRowId(int|string $id, array $options): string
278
    {
279
        if (null !== self::$rowIdGenerator) {
280 2
            return call_user_func(self::$rowIdGenerator, $id, $options);
281
        }
282
283 5
        ksort($options);
284
285 5
        return md5($id . serialize($options));
286
    }
287
288
    /**
289
     * Get the instance as an array.
290
     *
291
     * @return array<string, mixed>
292
     */
293
    public function toArray(): array
294
    {
295
        return [
296
            'rowId'    => $this->rowId,
297
            'id'       => $this->id,
298
            'name'     => $this->name,
299
            'qty'      => $this->qty,
300
            'price'    => $this->price,
301
            'options'  => $this->options->toArray(),
302
            'tax'      => $this->tax,
303
            'subtotal' => $this->subtotal,
304 5
        ];
305
    }
306
307
    /**
308
     * Convert the object to its JSON representation.
309
     */
310
    public function toJson(int $options = 0): string
311
    {
312 2
        return json_encode($this->toArray(), $options);
313
    }
314
315
    /**
316
     * Get the formatted number.
317
     *
318
     * @param float $value
319
     */
320
    private function numberFormat($value, int $decimals, ?string $decimalPoint, ?string $thousandSeperator): string
321
    {
322
        if (null === $decimalPoint) {
323 1
            $decimalPoint = '.';
324
        }
325
326
        if (null === $thousandSeperator) {
327 1
            $thousandSeperator = ',';
328
        }
329
330 1
        return number_format($value, $decimals, $decimalPoint, $thousandSeperator);
331
    }
332
}
333