Skip to content

Lesezeichen-System ​

Ermöglicht es Benutzern, Elemente in benannten Sammlungen zu speichern. Das Setzen von Lesezeichen ist idempotent — dasselbe Element zweimal mit einem Lesezeichen zu versehen gibt das vorhandene Lesezeichen ohne Fehler zurück.

Überblick ​

Ein Lesezeichen-System umfasst:

  • Lesezeichen hinzufügen — ein Element in der Sammlung eines Benutzers speichern (idempotent)
  • Lesezeichen entfernen — ein gespeichertes Lesezeichen löschen (404, wenn nicht gefunden)
  • Lesezeichen auflisten — alle Lesezeichen eines Benutzers, optional nach Sammlung gefiltert
  • Lesezeichen zählen — einfacher Badge-Zähler
  • Lesezeichen abrufen — prüfen, ob ein bestimmtes Element mit einem Lesezeichen versehen ist

Datenbankschema ​

sql
CREATE TABLE bookmarks (
    id         INTEGER PRIMARY KEY AUTOINCREMENT,
    user_id    INTEGER NOT NULL,
    item_id    INTEGER NOT NULL,
    collection TEXT    NOT NULL DEFAULT 'default',
    created_at TEXT    NOT NULL,
    UNIQUE (user_id, item_id),
    FOREIGN KEY (user_id) REFERENCES users(id),
    FOREIGN KEY (item_id) REFERENCES items(id)
);

UNIQUE (user_id, item_id) erzwingt ein Lesezeichen pro Benutzer pro Element. Das collection-Feld gruppiert Lesezeichen in benannte Kategorien, wobei 'default' als Fallback gilt.

Idempotentes Hinzufügen ​

Vor dem Einfügen auf ein vorhandenes Lesezeichen prüfen. Bei Konflikt (Race Condition) DatabaseConstraintException abfangen und den vorhandenen Eintrag zurückgeben:

php
public function add(int $userId, int $itemId, string $collection, string $now): Bookmark
{
    $existing = $this->find($userId, $itemId);

    if ($existing !== null) {
        return $existing;  // bereits als Lesezeichen gespeichert — kein Fehler
    }

    try {
        $this->executor->execute(
            'INSERT INTO bookmarks (user_id, item_id, collection, created_at) VALUES (?, ?, ?, ?)',
            [$userId, $itemId, $collection, $now],
        );
    } catch (DatabaseConstraintException) {
        // Race Condition — eine andere Anfrage war schneller; vorhandenes Lesezeichen zurückgeben
        $found = $this->find($userId, $itemId);
        if ($found !== null) {
            return $found;
        }
    }

    $id = (int) $this->executor->lastInsertId();
    return new Bookmark($id, $userId, $itemId, $collection, $now);
}

Das Prüfen-dann-Einfügen-Muster behandelt den häufigen Fall effizient. Der DatabaseConstraintException-Catch behandelt die Race Condition bei gleichzeitigen Anfragen.

Sammlungsfilterung ​

Optionalen collection-Query-Parameter zum Filtern von Lesezeichen verwenden:

php
// GET /users/{userId}/bookmarks?collection=reading
$collection = isset($query['collection']) && $query['collection'] !== ''
    ? $query['collection'] : null;

$items = $this->repo->listByUser($userId, $collection);

null als Sammlung gibt alle Lesezeichen zurück; ein nicht-leerer String filtert auf diese Sammlung.

Entfernen gibt 204 vs. 404 zurück ​

  • 204 No Content — Lesezeichen existierte und wurde gelöscht
  • 404 Not Found — Lesezeichen existierte nicht
php
$removed = $this->repo->remove($userId, $itemId);

if (!$removed) {
    return $this->responseFactory->create(['error' => 'bookmark not found'], 404);
}

return $this->responseFactory->createEmpty(204);

execute() gibt die Anzahl der betroffenen Zeilen zurück — null bedeutet, dass kein Lesezeichen gefunden wurde.

MySQL-Schema ​

MySQL erfordert explizite ENGINE=InnoDB- und AUTO_INCREMENT-Syntax:

sql
CREATE TABLE bookmarks (
    id         INT          NOT NULL AUTO_INCREMENT,
    user_id    INT          NOT NULL,
    item_id    INT          NOT NULL,
    collection VARCHAR(100) NOT NULL DEFAULT 'default',
    created_at DATETIME     NOT NULL,
    PRIMARY KEY (id),
    UNIQUE KEY uq_user_item (user_id, item_id),
    FOREIGN KEY (user_id) REFERENCES users(id),
    FOREIGN KEY (item_id) REFERENCES items(id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

Für MySQL-Integrationstests SET FOREIGN_KEY_CHECKS = 0 vor dem Löschen von Tabellen ausführen, um FK-Abhängigkeitsreihenfolge-Probleme zu vermeiden.

MySQL-Integrationstestmuster ​

php
protected function setUp(): void
{
    $host = (string) (getenv('MYSQL_HOST') ?: '');
    if ($host === '') {
        self::markTestSkipped('MYSQL_HOST not set — skipping MySQL integration tests');
    }
    ...
    $this->pdo->exec('SET FOREIGN_KEY_CHECKS = 0');
    $this->pdo->exec('DROP TABLE IF EXISTS bookmarks');
    $this->pdo->exec('DROP TABLE IF EXISTS items');
    $this->pdo->exec('DROP TABLE IF EXISTS users');
    $this->pdo->exec('SET FOREIGN_KEY_CHECKS = 1');

    $schema = (string) file_get_contents('.../database/schema.mysql.sql');
    $this->pdo->exec($schema);
}

protected function tearDown(): void
{
    if ($this->mysqlEnabled && $this->pdo !== null) {
        $this->pdo->exec('SET FOREIGN_KEY_CHECKS = 0');
        $this->pdo->exec('DROP TABLE IF EXISTS bookmarks');
        $this->pdo->exec('DROP TABLE IF EXISTS items');
        $this->pdo->exec('DROP TABLE IF EXISTS users');
        $this->pdo->exec('SET FOREIGN_KEY_CHECKS = 1');
    }
}

Sicherheitseigenschaften ​

EigenschaftImplementierung
Ein Lesezeichen pro Benutzer pro ElementUNIQUE (user_id, item_id) DB-Constraint
Race Condition beim HinzufügenDatabaseConstraintException abfangen → vorhandenes zurückgeben
BenutzerisolationAlle Abfragen filtern nach user_id
Nicht vorhandenes entfernenGibt 404 zurück (nicht stillschweigend)

Routenübersicht ​

MethodePfadBeschreibung
POST/usersBenutzer erstellen
POST/itemsElement erstellen
POST/users/{userId}/bookmarksLesezeichen hinzufügen (idempotent)
DELETE/users/{userId}/bookmarks/{itemId}Lesezeichen entfernen (204 oder 404)
GET/users/{userId}/bookmarksLesezeichen auflisten (?collection=-Filter)
GET/users/{userId}/bookmarks/countGesamtzahl der Lesezeichen
GET/users/{userId}/bookmarks/{itemId}Status eines einzelnen Lesezeichens abrufen

Veröffentlicht unter der MIT-Lizenz.