generated from boboko/starter
90 lines
3.0 KiB
PHP
90 lines
3.0 KiB
PHP
<?php
|
|
|
|
namespace App\Catalog;
|
|
|
|
use Illuminate\Http\Request;
|
|
use Modules\Core\Catalog\DTOs\ProductFilters;
|
|
use Modules\Core\Catalog\Enums\ProductSort;
|
|
|
|
/**
|
|
* The parsed filter/sort/page state of a product-listing request — used by the
|
|
* category page (scoped to a collection) and the all-products page. The query
|
|
* string is the single source of truth; build one of these from the request,
|
|
* read the applied values off it, and use query() to build links (sort options,
|
|
* pagination, "clear filter") that carry the rest of the state along.
|
|
*
|
|
* A param is only ever emitted when it differs from its default, so a pristine
|
|
* listing has no query string at all.
|
|
*/
|
|
final class ProductListing
|
|
{
|
|
private function __construct(
|
|
public readonly ?ProductSort $sort,
|
|
public readonly ?float $minPrice,
|
|
public readonly ?float $maxPrice,
|
|
public readonly bool $inStockOnly,
|
|
public readonly int $page,
|
|
) {}
|
|
|
|
public static function fromRequest(Request $request): self
|
|
{
|
|
return new self(
|
|
sort: ProductSort::tryFrom((string) $request->query('sort')),
|
|
minPrice: self::floatOrNull($request->query('price_min')),
|
|
maxPrice: self::floatOrNull($request->query('price_max')),
|
|
inStockOnly: $request->boolean('in_stock'),
|
|
page: max(1, (int) $request->query('page', 1)),
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @param ?int $collectionId scope to a collection (category page); null = every product
|
|
*/
|
|
public function filters(?int $collectionId = null): ProductFilters
|
|
{
|
|
return new ProductFilters(
|
|
collectionId: $collectionId,
|
|
minPrice: $this->minPrice,
|
|
maxPrice: $this->maxPrice,
|
|
inStockOnly: $this->inStockOnly,
|
|
);
|
|
}
|
|
|
|
/**
|
|
* The applied params as a clean array (defaults omitted), with `$overrides`
|
|
* merged on top — pass `['key' => null]` to drop one.
|
|
*
|
|
* @param array<string, string|int|null> $overrides
|
|
* @return array<string, string|int>
|
|
*/
|
|
public function query(array $overrides = []): array
|
|
{
|
|
return array_filter([
|
|
'sort' => $this->sort?->value,
|
|
'price_min' => $this->minPrice,
|
|
'price_max' => $this->maxPrice,
|
|
'in_stock' => $this->inStockOnly ? 1 : null,
|
|
'page' => $this->page > 1 ? $this->page : null,
|
|
...$overrides,
|
|
], fn ($value) => $value !== null);
|
|
}
|
|
|
|
/**
|
|
* Whether the listing is reordered/narrowed enough that it shouldn't be
|
|
* indexed as its own page (the canonical still points at the bare listing
|
|
* URL either way). A plain in-stock toggle is left indexable.
|
|
*/
|
|
public function isRefined(): bool
|
|
{
|
|
return $this->sort !== null
|
|
|| $this->minPrice !== null
|
|
|| $this->maxPrice !== null
|
|
|| $this->page > 1;
|
|
}
|
|
|
|
private static function floatOrNull(mixed $value): ?float
|
|
{
|
|
return is_numeric($value) ? (float) $value : null;
|
|
}
|
|
}
|