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); |
|
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); |
|
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); |
|
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); |
|
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); |
|
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); |
|
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
|
|||
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 |
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 theid
property of an instance of theAccount
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.