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
Bug
introduced
by
![]() |
|||||
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
![]() |
|||||
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
![]() |
|||||
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
![]() |
|||||
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
![]() |
|||||
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
![]() |
|||||
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
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 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;
}
![]() |
|||||
173 | 1 | $this->qty = Arr::get($attributes, 'qty', $this->qty); |
|||
174 | 1 | $this->name = Arr::get($attributes, 'name', $this->name); |
|||
0 ignored issues
–
show
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 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;
}
![]() |
|||||
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 |