generated from boboko/starter
127 lines
3.4 KiB
PHP
127 lines
3.4 KiB
PHP
<?php
|
|||
|
|
|
||
|
|
namespace App\Services;
|
||
|
|
|
||
|
|
use App\Models\WishlistItem;
|
||
|
|
use Illuminate\Contracts\Auth\Authenticatable;
|
||
|
|
use Illuminate\Support\Facades\Auth;
|
||
|
|
use Illuminate\Support\Facades\Cookie;
|
||
|
|
|
||
|
|
/**
|
||
|
|
* The current shopper's wishlist, product ids only.
|
||
|
|
*
|
||
|
|
* Logged in: rows in wishlist_items. Guest: a 1-year cookie holding the ids
|
||
|
|
* (encrypted like every cookie, by the web group's EncryptCookies), so nothing
|
||
|
|
* is written to the database for anonymous visitors. On login the cookie is
|
||
|
|
* merged into the account and cleared (MergeGuestWishlistOnLogin).
|
||
|
|
*/
|
||
|
|
class Wishlist
|
||
|
|
{
|
||
|
|
public const COOKIE = 'wishlist';
|
||
|
|
|
||
|
|
private const COOKIE_MINUTES = 60 * 24 * 365;
|
||
|
|
|
||
|
|
// Keeps the cookie well under the 4KB browser limit.
|
||
|
|
private const GUEST_MAX = 100;
|
||
|
|
|
||
|
|
/** @var array<int>|null ids for this request, including a toggle just made */
|
||
|
|
private ?array $guestIds = null;
|
||
|
|
|
||
|
|
/** @return array<int> newest first */
|
||
|
|
public function ids(): array
|
||
|
|
{
|
||
|
|
if ($user = Auth::user()) {
|
||
|
|
return WishlistItem::where('user_id', $user->id)
|
||
|
|
->latest('id')
|
||
|
|
->pluck('product_id')
|
||
|
|
->all();
|
||
|
|
}
|
||
|
|
|
||
|
|
return $this->guestIds();
|
||
|
|
}
|
||
|
|
|
||
|
|
public function has(int $productId): bool
|
||
|
|
{
|
||
|
|
return in_array($productId, $this->ids(), true);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @return bool whether the product is on the wishlist afterwards
|
||
|
|
*/
|
||
|
|
public function toggle(int $productId): bool
|
||
|
|
{
|
||
|
|
if ($user = Auth::user()) {
|
||
|
|
$deleted = WishlistItem::where('user_id', $user->id)->where('product_id', $productId)->delete();
|
||
|
|
|
||
|
|
if ($deleted) {
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
WishlistItem::create(['user_id' => $user->id, 'product_id' => $productId]);
|
||
|
|
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
|
||
|
|
$ids = $this->guestIds();
|
||
|
|
|
||
|
|
if (in_array($productId, $ids, true)) {
|
||
|
|
$this->storeGuestIds(array_values(array_diff($ids, [$productId])));
|
||
|
|
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
$this->storeGuestIds(array_slice([$productId, ...$ids], 0, self::GUEST_MAX));
|
||
|
|
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
|
||
|
|
public function remove(int $productId): void
|
||
|
|
{
|
||
|
|
if ($this->has($productId)) {
|
||
|
|
$this->toggle($productId);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Moves the guest cookie's products onto $user's wishlist and clears it.
|
||
|
|
*/
|
||
|
|
public function mergeGuestInto(Authenticatable $user): void
|
||
|
|
{
|
||
|
|
$ids = $this->guestIds();
|
||
|
|
|
||
|
|
if ($ids === []) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Oldest first, so the newest cookie item also ends up newest here.
|
||
|
|
foreach (array_reverse($ids) as $productId) {
|
||
|
|
WishlistItem::firstOrCreate(['user_id' => $user->id, 'product_id' => $productId]);
|
||
|
|
}
|
||
|
|
|
||
|
|
$this->guestIds = [];
|
||
|
|
Cookie::queue(Cookie::forget(self::COOKIE));
|
||
|
|
}
|
||
|
|
|
||
|
|
/** @return array<int> */
|
||
|
|
private function guestIds(): array
|
||
|
|
{
|
||
|
|
if ($this->guestIds !== null) {
|
||
|
|
return $this->guestIds;
|
||
|
|
}
|
||
|
|
|
||
|
|
$decoded = json_decode((string) request()->cookie(self::COOKIE), true);
|
||
|
|
|
||
|
|
return $this->guestIds = is_array($decoded)
|
||
|
|
? array_values(array_unique(array_filter(array_map('intval', $decoded))))
|
||
|
|
: [];
|
||
|
|
}
|
||
|
|
|
||
|
|
/** @param array<int> $ids */
|
||
|
|
private function storeGuestIds(array $ids): void
|
||
|
|
{
|
||
|
|
$this->guestIds = $ids;
|
||
|
|
|
||
|
|
Cookie::queue(self::COOKIE, json_encode($ids), self::COOKIE_MINUTES);
|
||
|
|
}
|
||
|
|
}
|