GG.deals clone: CodeIgniter 4 + MongoDB game price tracker
- Full gg.deals-style UI: deal feed with filters, game pages with price history charts, rankings, vouchers, news, prepaids, subscriptions, bundles, franchises, wishlist, API docs - Real data crawlers: * gg:crawl (Steam Store API + GOG Catalog + Epic free games) * gg:catalog (252K game slugs from gg.deals sitemaps) * gg:ggdeals (per-game prices via FlareSolverr session, JSON-LD parsing) * gg:seed / gg:crawl-covers (demo seed + Steam CDN cover art) - 24/7 self-updating scheduler (scripts/scheduler_loop.php, lockfile-protected) - Tailwind v4 dark/light theme, Alpine.js, Chart.js
This commit is contained in:
@@ -0,0 +1,587 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Libraries\Mongo;
|
||||
use MongoDB\BSON\UTCDateTime;
|
||||
|
||||
/**
|
||||
* Game data access layer built on MongoDB aggregations.
|
||||
*/
|
||||
class GameModel
|
||||
{
|
||||
/**
|
||||
* Deal feed: best current deal per game, filtered + sorted.
|
||||
*/
|
||||
public function feed(array $filters, int $page, int $perPage): array
|
||||
{
|
||||
$match = [];
|
||||
|
||||
$stores = $filters['stores'] ?? [];
|
||||
if ($stores !== []) {
|
||||
$match['store_code'] = ['$in' => $stores];
|
||||
}
|
||||
if (! empty($filters['kind']) && in_array($filters['kind'], ['official', 'keyshop'], true)) {
|
||||
$match['kind'] = $filters['kind'];
|
||||
}
|
||||
|
||||
$priceCond = [];
|
||||
if ($filters['min_price'] !== null && $filters['min_price'] !== '') {
|
||||
$priceCond['$gte'] = (float) $filters['min_price'];
|
||||
}
|
||||
if ($filters['max_price'] !== null && $filters['max_price'] !== '') {
|
||||
$priceCond['$lte'] = (float) $filters['max_price'];
|
||||
}
|
||||
if ($priceCond !== []) {
|
||||
$match['price'] = $priceCond;
|
||||
}
|
||||
|
||||
// price tier presets (Under $5 / $10 / $15 / $20 / $30)
|
||||
$priceTier = (int) ($filters['price_tier'] ?? 0);
|
||||
if ($priceTier > 0) {
|
||||
$match['price'] = array_merge($match['price'] ?? [], ['$lte' => (float) $priceTier]);
|
||||
}
|
||||
|
||||
$minCut = (int) ($filters['min_cut'] ?? 0);
|
||||
if ($minCut > 0) {
|
||||
$match['cut'] = ['$gte' => $minCut];
|
||||
}
|
||||
|
||||
// discount tier presets (At least 50/66/75/80/85/90 %)
|
||||
$discountTier = (int) ($filters['discount_tier'] ?? 0);
|
||||
if ($discountTier > 0) {
|
||||
$match['cut'] = ['$gte' => $discountTier];
|
||||
}
|
||||
|
||||
// preset discount thresholds
|
||||
$preset = (string) ($filters['preset'] ?? '');
|
||||
if ($preset === 'cut75') {
|
||||
$match['cut'] = ['$gte' => 75];
|
||||
}
|
||||
if ($preset === 'cut85') {
|
||||
$match['cut'] = ['$gte' => 85];
|
||||
}
|
||||
|
||||
// deal age window (within 24h/48h/1w/2w) — deals have no created_at,
|
||||
// so approximate with the expiry window (ending soon) or leave empty.
|
||||
$timeRange = (string) ($filters['time_range'] ?? '');
|
||||
if ($timeRange !== '' && $timeRange !== 'any') {
|
||||
// apply on game.release_date is not right; instead ignore for now
|
||||
// (see post-group $match below for ending-soon handling)
|
||||
}
|
||||
|
||||
$basePipeline = [];
|
||||
if ($match !== []) {
|
||||
$basePipeline[] = ['$match' => $match];
|
||||
}
|
||||
$basePipeline[] = ['$sort' => ['price' => 1]];
|
||||
$basePipeline[] = [
|
||||
'$group' => [
|
||||
'_id' => '$game_slug',
|
||||
'best_price' => ['$first' => '$price'],
|
||||
'best_store' => ['$first' => '$store_code'],
|
||||
'best_kind' => ['$first' => '$kind'],
|
||||
'best_cut' => ['$first' => '$cut'],
|
||||
'best_hl' => ['$first' => '$historical_low'],
|
||||
'best_expiry' => ['$first' => '$expiry'],
|
||||
'best_deal_doc' => ['$first' => '$$ROOT'],
|
||||
],
|
||||
];
|
||||
$basePipeline[] = [
|
||||
'$lookup' => [
|
||||
'from' => 'games',
|
||||
'localField' => '_id',
|
||||
'foreignField' => 'slug',
|
||||
'as' => 'game',
|
||||
],
|
||||
];
|
||||
$basePipeline[] = ['$unwind' => '$game'];
|
||||
$basePipeline[] = [
|
||||
'$addFields' => [
|
||||
'deal_rating' => [
|
||||
'$cond' => [
|
||||
'if' => ['$gt' => ['$best_price', 0]],
|
||||
'then' => ['$divide' => ['$game.all_time_low.price', '$best_price']],
|
||||
'else' => 0,
|
||||
],
|
||||
],
|
||||
'ends_within' => [
|
||||
'$cond' => [
|
||||
'if' => ['$ne' => ['$best_expiry', null]],
|
||||
'then' => ['$subtract' => ['$best_expiry', '$$NOW']],
|
||||
'else' => 86400000 * 365,
|
||||
],
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
if (! empty($filters['q'])) {
|
||||
$regex = new \MongoDB\BSON\Regex(preg_quote((string) $filters['q'], '/'), 'i');
|
||||
$basePipeline[] = ['$match' => ['game.title' => $regex]];
|
||||
}
|
||||
|
||||
// genre filter (e.g. /games/{genre})
|
||||
if (! empty($filters['genre'])) {
|
||||
$basePipeline[] = ['$match' => ['game.genres' => $filters['genre']]];
|
||||
}
|
||||
|
||||
// multi-genre filter from the sidebar (matches any selected genre)
|
||||
$genresFilter = $filters['genres'] ?? [];
|
||||
if ($genresFilter !== []) {
|
||||
$basePipeline[] = ['$match' => ['game.genres' => ['$in' => $genresFilter]]];
|
||||
}
|
||||
|
||||
if (! empty($filters['hl_only'])) {
|
||||
$basePipeline[] = ['$match' => ['best_hl' => true]];
|
||||
}
|
||||
|
||||
// deal rating filter (hot/good/fair = ratio thresholds)
|
||||
$ratingFilter = (string) ($filters['deal_rating'] ?? '');
|
||||
if ($ratingFilter !== '' && $ratingFilter !== 'any') {
|
||||
$ratios = [
|
||||
'hot' => ['$gte' => 0.85],
|
||||
'good' => ['$gte' => 0.6],
|
||||
'fair' => ['$gte' => 0.35],
|
||||
];
|
||||
if (isset($ratios[$ratingFilter])) {
|
||||
$basePipeline[] = ['$match' => ['deal_rating' => $ratios[$ratingFilter]]];
|
||||
}
|
||||
}
|
||||
|
||||
// release year range
|
||||
$releaseRange = (string) ($filters['release_range'] ?? '');
|
||||
if ($releaseRange !== '' && $releaseRange !== 'any') {
|
||||
$ranges = [
|
||||
'pre2000' => ['$lte' => mktime(0, 0, 0, 1, 1, 2000) * 1000],
|
||||
'2000s' => ['$gte' => mktime(0, 0, 0, 1, 1, 2000) * 1000, '$lte' => mktime(0, 0, 0, 1, 1, 2010) * 1000],
|
||||
'2010s' => ['$gte' => mktime(0, 0, 0, 1, 1, 2010) * 1000, '$lte' => mktime(0, 0, 0, 1, 1, 2020) * 1000],
|
||||
'2020s' => ['$gte' => mktime(0, 0, 0, 1, 1, 2020) * 1000],
|
||||
];
|
||||
if (isset($ranges[$releaseRange])) {
|
||||
$basePipeline[] = ['$match' => ['game.release_date' => $ranges[$releaseRange]]];
|
||||
}
|
||||
}
|
||||
|
||||
// preset handling
|
||||
if ($preset === 'hl') {
|
||||
$basePipeline[] = ['$match' => ['best_hl' => true]];
|
||||
}
|
||||
if ($preset === 'ending') {
|
||||
$basePipeline[] = ['$match' => ['ends_within' => ['$lte' => 7 * 86400000]]];
|
||||
}
|
||||
|
||||
// wishlist-only filter: match slugs passed from the client
|
||||
$wishlist = $filters['wishlist'] ?? [];
|
||||
if ($wishlist !== []) {
|
||||
$basePipeline[] = ['$match' => ['game.slug' => ['$in' => $wishlist]]];
|
||||
}
|
||||
|
||||
// ---- count total (run same pipeline up to filters, then count)
|
||||
$countPipeline = $basePipeline;
|
||||
$countPipeline[] = ['$count' => 'total'];
|
||||
$countRes = Mongo::collection('deals')->aggregate($countPipeline)->toArray();
|
||||
$total = $countRes[0]['total'] ?? 0;
|
||||
|
||||
// ---- sort
|
||||
$sortStage = match ($filters['sort'] ?? 'trending') {
|
||||
'price' => ['best_price' => 1],
|
||||
'cut' => ['best_cut' => -1],
|
||||
'recent' => ['game.release_date' => -1],
|
||||
'hl' => ['game.all_time_low.price' => 1],
|
||||
'rating' => ['deal_rating' => -1],
|
||||
'title' => ['game.title' => 1],
|
||||
'metascore' => ['game.score' => -1],
|
||||
'length' => ['game.release_date' => 1],
|
||||
'ending' => ['ends_within' => 1],
|
||||
'hot' => ['best_cut' => -1, 'deal_rating' => -1],
|
||||
default => ['game.trending' => -1],
|
||||
};
|
||||
|
||||
$pipeline = $basePipeline;
|
||||
$pipeline[] = ['$sort' => $sortStage];
|
||||
$pipeline[] = ['$skip' => ($page - 1) * $perPage];
|
||||
$pipeline[] = ['$limit' => $perPage];
|
||||
|
||||
$rows = Mongo::collection('deals')->aggregate($pipeline)->toArray();
|
||||
|
||||
return [
|
||||
'rows' => $rows,
|
||||
'total' => $total,
|
||||
'pages' => (int) max(1, ceil($total / $perPage)),
|
||||
];
|
||||
}
|
||||
|
||||
public function findBySlug(string $slug): ?array
|
||||
{
|
||||
$res = Mongo::collection('games')->findOne(['slug' => $slug]);
|
||||
if ($res === null) {
|
||||
return null;
|
||||
}
|
||||
$res['_id'] = (string) $res['_id'];
|
||||
|
||||
return $res;
|
||||
}
|
||||
|
||||
/**
|
||||
* Steam AppID → game lookup for the public Prices API.
|
||||
*/
|
||||
public function findByAppId(int $appid): ?array
|
||||
{
|
||||
$slug = array_search($appid, \App\Commands\CrawlCovers::APP_IDS, true);
|
||||
if ($slug === false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->findBySlug((string) $slug);
|
||||
}
|
||||
|
||||
/**
|
||||
* All active deals for a game, sorted by price.
|
||||
*/
|
||||
public function dealsFor(string $slug): array
|
||||
{
|
||||
$rows = Mongo::collection('deals')
|
||||
->find(['game_slug' => $slug], ['sort' => ['price' => 1]])
|
||||
->toArray();
|
||||
|
||||
$official = [];
|
||||
$keyshop = [];
|
||||
|
||||
foreach ($rows as $r) {
|
||||
unset($r['_id']);
|
||||
if ($r['kind'] === 'official') {
|
||||
$official[] = $r;
|
||||
} else {
|
||||
$keyshop[] = $r;
|
||||
}
|
||||
}
|
||||
|
||||
return ['official' => $official, 'keyshop' => $keyshop];
|
||||
}
|
||||
|
||||
/**
|
||||
* Daily min price per store kind for the history chart.
|
||||
*/
|
||||
public function historyFor(string $slug): array
|
||||
{
|
||||
$pipeline = [
|
||||
['$match' => ['game_slug' => $slug]],
|
||||
[
|
||||
'$group' => [
|
||||
'_id' => [
|
||||
'day' => ['$dateToString' => ['date' => '$ts', 'format' => '%Y-%m-%d']],
|
||||
'kind' => '$kind',
|
||||
],
|
||||
'price' => ['$min' => '$price'],
|
||||
],
|
||||
],
|
||||
['$sort' => ['_id.day' => 1]],
|
||||
];
|
||||
|
||||
$rows = Mongo::collection('price_history')->aggregate($pipeline)->toArray();
|
||||
|
||||
$series = ['official' => [], 'keyshop' => []];
|
||||
foreach ($rows as $r) {
|
||||
$series[$r['_id']['kind']][] = ['day' => $r['_id']['day'], 'price' => round($r['price'], 2)];
|
||||
}
|
||||
|
||||
return $series;
|
||||
}
|
||||
|
||||
public function trending(int $limit = 12): array
|
||||
{
|
||||
$rows = Mongo::collection('games')
|
||||
->find([], ['sort' => ['trending' => -1], 'limit' => $limit])
|
||||
->toArray();
|
||||
|
||||
foreach ($rows as &$r) {
|
||||
$r['_id'] = (string) $r['_id'];
|
||||
}
|
||||
|
||||
return $rows;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ranking list by a numeric field (score, rating_pct, trending, etc).
|
||||
*/
|
||||
public function ranked(string $field, int $limit = 20): array
|
||||
{
|
||||
$sort = in_array($field, ['score', 'rating_pct', 'trending'], true)
|
||||
? [$field => -1]
|
||||
: ['trending' => -1];
|
||||
|
||||
$rows = Mongo::collection('games')
|
||||
->find([], ['sort' => $sort, 'limit' => $limit])
|
||||
->toArray();
|
||||
|
||||
foreach ($rows as &$r) {
|
||||
$r['_id'] = (string) $r['_id'];
|
||||
}
|
||||
|
||||
return $rows;
|
||||
}
|
||||
|
||||
/**
|
||||
* Genre counts for the games browser.
|
||||
*/
|
||||
public function genres(): array
|
||||
{
|
||||
$rows = Mongo::collection('games')->aggregate([
|
||||
['$unwind' => '$genres'],
|
||||
['$group' => ['_id' => '$genres', 'count' => ['$sum' => 1]]],
|
||||
['$sort' => ['count' => -1]],
|
||||
])->toArray();
|
||||
|
||||
return $rows;
|
||||
}
|
||||
|
||||
public function byGenre(string $genre, int $limit = 40): array
|
||||
{
|
||||
$rows = Mongo::collection('games')
|
||||
->find(['genres' => $genre], ['sort' => ['trending' => -1], 'limit' => $limit])
|
||||
->toArray();
|
||||
|
||||
foreach ($rows as &$r) {
|
||||
$r['_id'] = (string) $r['_id'];
|
||||
}
|
||||
|
||||
return $rows;
|
||||
}
|
||||
|
||||
/**
|
||||
* Similar games: same genre, excluding self.
|
||||
*/
|
||||
public function similar(array $game, int $limit = 8): array
|
||||
{
|
||||
$genres = array_map('strval', $game['genres'] ?? []);
|
||||
|
||||
if ($genres === []) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$rows = Mongo::collection('games')
|
||||
->find(
|
||||
['slug' => ['$ne' => $game['slug']], 'genres' => ['$in' => $genres]],
|
||||
['sort' => ['trending' => -1], 'limit' => $limit]
|
||||
)
|
||||
->toArray();
|
||||
|
||||
foreach ($rows as &$r) {
|
||||
$r['_id'] = (string) $r['_id'];
|
||||
}
|
||||
|
||||
return $rows;
|
||||
}
|
||||
|
||||
public function suggest(string $q, int $limit = 8): array
|
||||
{
|
||||
$regex = new \MongoDB\BSON\Regex(preg_quote($q, '/'), 'i');
|
||||
|
||||
$rows = Mongo::collection('games')
|
||||
->find(['title' => $regex], [
|
||||
'sort' => ['trending' => -1],
|
||||
'limit' => $limit,
|
||||
'projection' => ['title' => 1, 'slug' => 1, 'cover' => 1, 'all_time_low' => 1, 'best_official' => 1, 'best_keyshop' => 1, 'active_deals' => 1],
|
||||
])
|
||||
->toArray();
|
||||
|
||||
$out = [];
|
||||
foreach ($rows as $r) {
|
||||
$out[] = [
|
||||
'title' => $r['title'],
|
||||
'slug' => $r['slug'],
|
||||
'cover' => $r['cover']['file'] ?? '',
|
||||
'hl' => $r['all_time_low']['price'] ?? null,
|
||||
'deals' => $r['active_deals'] ?? 0,
|
||||
];
|
||||
}
|
||||
|
||||
return $out;
|
||||
}
|
||||
|
||||
public function storeStats(): array
|
||||
{
|
||||
$pipeline = [
|
||||
[
|
||||
'$group' => [
|
||||
'_id' => '$store_code',
|
||||
'deals' => ['$sum' => 1],
|
||||
'min_price' => ['$min' => '$price'],
|
||||
'avg_cut' => ['$avg' => '$cut'],
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
$rows = Mongo::collection('deals')->aggregate($pipeline)->toArray();
|
||||
|
||||
$stores = [];
|
||||
foreach (Mongo::collection('stores')->find()->toArray() as $s) {
|
||||
$stores[$s['code']] = [
|
||||
'code' => $s['code'],
|
||||
'name' => $s['name'],
|
||||
'kind' => $s['kind'],
|
||||
'color' => $s['color'],
|
||||
'textcolor' => $s['textcolor'],
|
||||
'logo' => $s['logo'] ?? '/img/stores/' . $s['code'] . '.png',
|
||||
'deals' => 0,
|
||||
'min_price' => null,
|
||||
'avg_cut' => 0,
|
||||
];
|
||||
}
|
||||
|
||||
foreach ($rows as $r) {
|
||||
if (isset($stores[$r['_id']])) {
|
||||
$stores[$r['_id']]['deals'] = $r['deals'];
|
||||
$stores[$r['_id']]['min_price'] = $r['min_price'];
|
||||
$stores[$r['_id']]['avg_cut'] = round($r['avg_cut'], 1);
|
||||
}
|
||||
}
|
||||
|
||||
usort($stores, static fn ($a, $b) => $b['deals'] <=> $a['deals']);
|
||||
|
||||
return array_values($stores);
|
||||
}
|
||||
|
||||
public function stats(): array
|
||||
{
|
||||
$games = Mongo::collection('games')->countDocuments();
|
||||
$deals = Mongo::collection('deals')->countDocuments();
|
||||
|
||||
return [
|
||||
'games' => $games,
|
||||
'deals' => $deals,
|
||||
'stores' => Mongo::collection('stores')->countDocuments(),
|
||||
'hl_now' => Mongo::collection('deals')->countDocuments(['historical_low' => true]),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Compact deal rows for the homepage dashboard sections.
|
||||
* $mode: best | new | hl | ending | hot | wanted | releases
|
||||
*/
|
||||
public function dashboardDeals(string $mode, int $limit = 12): array
|
||||
{
|
||||
$sort = match ($mode) {
|
||||
'new' => ['game.release_date' => -1, 'deal_rating' => -1],
|
||||
'hl' => ['deal_rating' => -1],
|
||||
'ending' => ['ends_within' => 1],
|
||||
'hot' => ['best_cut' => -1, 'deal_rating' => -1],
|
||||
'wanted' => ['game.trending' => -1],
|
||||
default => ['deal_rating' => -1, 'best_cut' => -1],
|
||||
};
|
||||
|
||||
$pipeline = [
|
||||
['$sort' => ['price' => 1]],
|
||||
[
|
||||
'$group' => [
|
||||
'_id' => '$game_slug',
|
||||
'best_price' => ['$first' => '$price'],
|
||||
'best_store' => ['$first' => '$store_code'],
|
||||
'best_kind' => ['$first' => '$kind'],
|
||||
'best_cut' => ['$first' => '$cut'],
|
||||
'best_hl' => ['$first' => '$historical_low'],
|
||||
'best_expiry' => ['$first' => '$expiry'],
|
||||
],
|
||||
],
|
||||
[
|
||||
'$lookup' => [
|
||||
'from' => 'games',
|
||||
'localField' => '_id',
|
||||
'foreignField' => 'slug',
|
||||
'as' => 'game',
|
||||
],
|
||||
],
|
||||
['$unwind' => '$game'],
|
||||
[
|
||||
'$addFields' => [
|
||||
'deal_rating' => [
|
||||
'$cond' => [
|
||||
'if' => ['$gt' => ['$best_price', 0]],
|
||||
'then' => ['$divide' => ['$game.all_time_low.price', '$best_price']],
|
||||
'else' => 0,
|
||||
],
|
||||
],
|
||||
'ends_within' => [
|
||||
'$cond' => [
|
||||
'if' => ['$ne' => ['$best_expiry', null]],
|
||||
'then' => ['$subtract' => ['$best_expiry', '$$NOW']],
|
||||
'else' => 86400000 * 365,
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
if ($mode === 'hl') {
|
||||
$pipeline[] = ['$match' => ['best_hl' => true]];
|
||||
}
|
||||
if ($mode === 'ending') {
|
||||
$pipeline[] = ['$match' => ['ends_within' => ['$lte' => 7 * 86400000]]];
|
||||
}
|
||||
|
||||
$pipeline[] = ['$sort' => $sort];
|
||||
$pipeline[] = ['$limit' => $limit];
|
||||
|
||||
return Mongo::collection('deals')->aggregate($pipeline)->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Most wanted / new releases / upcoming games (game-only lists).
|
||||
*/
|
||||
public function dashboardGames(string $mode, int $limit = 12): array
|
||||
{
|
||||
$sort = match ($mode) {
|
||||
'releases' => ['release_date' => -1],
|
||||
'upcoming' => ['release_date' => 1],
|
||||
default => ['trending' => -1],
|
||||
};
|
||||
|
||||
$filter = [];
|
||||
if ($mode === 'upcoming') {
|
||||
// treat games released after 2023 as "upcoming" in our demo data
|
||||
$filter = ['release_date' => ['$gt' => new UTCDateTime(mktime(0, 0, 0, 1, 1, 2024) * 1000)]];
|
||||
}
|
||||
|
||||
$rows = Mongo::collection('games')
|
||||
->find($filter, ['sort' => $sort, 'limit' => $limit])
|
||||
->toArray();
|
||||
|
||||
foreach ($rows as &$r) {
|
||||
$r['_id'] = (string) $r['_id'];
|
||||
}
|
||||
|
||||
return $rows;
|
||||
}
|
||||
|
||||
/**
|
||||
* Latest news posts for the homepage feed.
|
||||
*/
|
||||
public function recentNews(int $limit = 6): array
|
||||
{
|
||||
return Mongo::collection('news')
|
||||
->find([], ['sort' => ['date' => -1], 'limit' => $limit])
|
||||
->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Bundles that include this game.
|
||||
*/
|
||||
public function bundlesFor(string $slug): array
|
||||
{
|
||||
return Mongo::collection('bundles')
|
||||
->find(['games' => $slug], ['sort' => ['price' => 1]])
|
||||
->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Franchises containing this game.
|
||||
*/
|
||||
public function franchisesFor(string $slug): array
|
||||
{
|
||||
return Mongo::collection('franchises')
|
||||
->find(['games' => $slug])
|
||||
->toArray();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Libraries\Mongo;
|
||||
|
||||
class StoreModel
|
||||
{
|
||||
private ?array $all = null;
|
||||
|
||||
/** @return array<int, array> */
|
||||
public function all(): array
|
||||
{
|
||||
if ($this->all === null) {
|
||||
$this->all = Mongo::collection('stores')->find([], ['sort' => ['kind' => 1, 'name' => 1]])->toArray();
|
||||
}
|
||||
|
||||
return $this->all;
|
||||
}
|
||||
|
||||
/** @return array<string, array> code => store */
|
||||
public function map(): array
|
||||
{
|
||||
$map = [];
|
||||
foreach ($this->all() as $s) {
|
||||
$map[$s['code']] = $s;
|
||||
}
|
||||
|
||||
return $map;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user