id = $holder->id(); } public function artistList(): array { if (!isset($this->artists)) { $this->load(); } return $this->artists; } public function contributorList(): array { if (!isset($this->contributors)) { $this->load(); } return $this->contributors; } public function entryCreated(CollageEntry $entry): string { if (!isset($this->created)) { $this->load(); } return $this->created[$entry->id()]; } /** * Does the entry already exist in this collage */ public function hasEntry(CollageEntry $entry): bool { return (bool)self::$db->scalar(" SELECT 1 FROM {$this->entryTable()} WHERE CollageID = ? AND {$this->entryColumn()} = ? ", $this->id, $entry->id() ); } public function entryUserId(CollageEntry $entry): int { return (int)self::$db->scalar(" SELECT UserID FROM {$this->entryTable()} WHERE CollageID = ? AND {$this->entryColumn()} = ? ", $this->id, $entry->id() ); } /** * Flush the cache keys associated with this collage. */ public function flushAll(array $keys = []): static { self::$db->prepared_query(" SELECT concat('collage_subs_user_new_', UserID) FROM users_collage_subs WHERE CollageID = ? ", $this->id ); if (self::$db->has_results()) { array_push($keys, ...self::$db->collect(0, false)); } self::$cache->delete_multi($keys); $this->holder->flush(); unset($this->artists); unset($this->contributors); unset($this->created); return $this; } /** * Update the database with the correct number of entries in this collage. * The caller of this method is responsible for invalidating the cache so * that the next instantiation will pick up the new value. * * @return int Number of entries */ protected function recalcTotal(): int { self::$db->prepared_query(" UPDATE collages SET updated = now(), NumTorrents = (SELECT count(*) FROM {$this->entryTable()} ca WHERE ca.CollageID = ?) WHERE ID = ? ", $this->id, $this->id ); return (int)self::$db->scalar(" SELECT count(*) FROM {$this->entryTable()} ca WHERE ca.CollageID = ? ", $this->id ); } /** * Add an entry to a collage. */ public function addEntry(CollageEntry $entry, \Gazelle\User $user): int { if ($this->hasEntry($entry)) { return 0; } self::$db->begin_transaction(); if ($this->holder->hasAttr('sort-newest')) { $mult = $this->holder->isPersonal() ? 1 : -1; } else { $mult = $this->holder->isPersonal() ? -1 : 1; } $func = $mult > 0 ? 'max' : 'min'; self::$db->prepared_query(" INSERT IGNORE INTO {$this->entryTable()} (CollageID, UserID, {$this->entryColumn()}, Sort) VALUES (?, ?, ?, ( SELECT coalesce($func(ca.Sort), 0) + (10 * ?) FROM {$this->entryTable()} ca WHERE ca.CollageID = ? ) ) ", $this->id, $user->id(), $entry->id(), $mult, $this->id ); $affected = self::$db->affected_rows(); if ($affected === 0) { self::$db->commit(); return 0; } $this->recalcTotal(); self::$db->commit(); $this->flushTarget($entry); return $affected; } /** * Remove an entry from a collage */ public function removeEntry(CollageEntry $entry): int { self::$db->begin_transaction(); self::$db->prepared_query(" DELETE FROM {$this->entryTable()} WHERE CollageID = ? AND {$this->entryColumn()} = ? ", $this->id, $entry->id() ); $affected = self::$db->affected_rows(); if ($affected === 0) { self::$db->commit(); return 0; } $this->recalcTotal(); self::$db->commit(); $this->flushTarget($entry); $this->load(); return $affected; } public function updateSequence(string $series): int { $series = $this->parseUrlArgs($series, 'li[]'); if (empty($series)) { return 0; } self::$db->prepared_query(" SELECT {$this->entryColumn()} AS cID, UserID FROM {$this->entryTable()} WHERE CollageID = ? ", $this->id ); $userMap = self::$db->to_pair('cID', 'UserID'); $id = $this->id; $args = array_merge(...array_map(fn($sort, $entryId) => [(int)$entryId, ($sort + 1) * 10, $id, $userMap[$entryId]], array_keys($series), $series)); self::$db->prepared_query(" INSERT INTO {$this->entryTable()} ({$this->entryColumn()}, Sort, CollageID, UserID) VALUES " . implode(', ', array_fill(0, count($series), '(?, ?, ?, ?)')) . " ON DUPLICATE KEY UPDATE Sort = VALUES(Sort) ", ...$args ); $affected = self::$db->affected_rows(); $this->load(); return $affected; } public function updateSequenceEntry(CollageEntry $entry, int $sequence): int { self::$db->prepared_query(" UPDATE {$this->entryTable()} SET Sort = ? WHERE CollageID = ? AND {$this->entryColumn()} = ? ", $sequence, $this->id, $entry->id() ); $affected = self::$db->affected_rows(); $this->load(); return $affected; } public function remove(): int { self::$db->prepared_query(" UPDATE collages SET Deleted = '1' WHERE Deleted = '0' AND ID = ? ", $this->id ); return self::$db->affected_rows(); } /** * Hydrate an array from a query string (everything that follow '?') * This reimplements parse_str() and side-steps the issue of max_input_vars limits. * * Example: * in: li[]=14&li[]=31&li[]=58&li[]=68&li[]=69&li[]=54&li[]=5, param=li[] * parsed: ['li[]' => ['14', '31, '58', '68', '69', '54', '5']] * out: [14, 31, 58, 68, 69, 54, 5] * * @param string $urlArgs query string from url * @param string $param url param to extract * returns hydrated equivalent */ public function parseUrlArgs(string $urlArgs, string $param): array { if (empty($urlArgs)) { return []; } $list = []; $pairs = explode('&', $urlArgs); foreach ($pairs as $p) { [$name, $value] = explode('=', $p, 2); if (!isset($list[$name])) { $list[$name] = (int)$value; } else { if (!is_array($list[$name])) { $list[$name] = [$list[$name]]; } $list[$name][] = (int)$value; } } return array_key_exists($param, $list) ? $list[$param] : []; /** @phpstan-ignore-line */ } }