Files
ops-Gazelle/app/Collage/AbstractCollage.php

268 lines
8.1 KiB
PHP

<?php
namespace Gazelle\Collage;
use Gazelle\Intf\CollageEntry;
abstract class AbstractCollage extends \Gazelle\Base {
protected int $id; // hold a local copy of our ID to save time
protected array $artists;
protected array $contributors;
protected array $created;
abstract public function entryTable(): string;
abstract public function entryColumn(): string;
abstract public function entryList(): array;
abstract public function load(): int;
abstract public function rebuildTagList(): array;
abstract protected function flushTarget(CollageEntry $target): void;
public function __construct(protected \Gazelle\Collage $holder) {
$this->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));
}
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 {
$list = $this->parseUrlArgs($series, 'li[]');
if (empty($list)) {
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');
$args = array_merge(
...array_map(
fn(int $sort, int $entryId) => [
$entryId,
($sort + 1) * 10,
$this->id,
$userMap[$entryId]
],
array_keys($list),
$list
)
);
self::$db->prepared_query("
INSERT INTO {$this->entryTable()} ({$this->entryColumn()}, Sort, CollageID, UserID)
VALUES " . implode(', ', array_fill(0, count($list), '(?, ?, ?, ?)')) . "
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;
}
/**
* 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 */
}
public function remove(): int {
// soft delete
self::$db->prepared_query("
UPDATE collages SET
Deleted = '1'
WHERE Deleted = '0'
AND ID = ?
", $this->id
);
return self::$db->affected_rows();
}
}