diff --git a/src/composer.lock b/src/composer.lock index d929ee6..342b234 100644 --- a/src/composer.lock +++ b/src/composer.lock @@ -12,7 +12,7 @@ "source": { "type": "git", "url": "https://git.bitbadger.solutions/bit-badger/pdo-document", - "reference": "bcca9f5ace2ba32c24412e284ff4789480d9619d" + "reference": "2d8f8b6e8728f8fbe0d801e2bc529a678f65ad9b" }, "require": { "ext-pdo": "*", @@ -32,10 +32,11 @@ "autoload-dev": { "psr-4": { "Test\\Unit\\": "./tests/unit", - "Test\\Integration\\": "./tests/integration" + "Test\\Integration\\": "./tests/integration", + "Test\\Integration\\SQLite\\": "./tests/integration/sqlite" } }, - "time": "2024-06-05T02:36:58+00:00" + "time": "2024-06-08T14:49:52+00:00" }, { "name": "netresearch/jsonmapper", diff --git a/src/lib/Data.php b/src/lib/Data.php index 435c1e8..4174b5d 100644 --- a/src/lib/Data.php +++ b/src/lib/Data.php @@ -2,16 +2,11 @@ namespace FeedReaderCentral; -use BitBadger\PDODocument\Configuration; -use BitBadger\PDODocument\Custom; -use BitBadger\PDODocument\Definition; -use BitBadger\PDODocument\DocumentException; -use BitBadger\PDODocument\Field; +use BitBadger\PDODocument\{Configuration, Custom, Definition, DocumentException, Field}; use BitBadger\PDODocument\Mapper\StringMapper; use DateTimeImmutable; use DateTimeInterface; use Exception; -use PDO; /** * A centralized place for data access for the application @@ -21,18 +16,17 @@ class Data /** * Create the search index and synchronization triggers for the item table * - * @param PDO $pdo The database connection on which these will be created * @throws DocumentException If any is encountered */ - public static function createSearchIndex(PDO $pdo): void + public static function createSearchIndex(): void { Custom::nonQuery("CREATE VIRTUAL TABLE item_search USING fts5(content, content='item', content_rowid='id')", - [], $pdo); + []); Custom::nonQuery(<<<'SQL' CREATE TRIGGER item_ai AFTER INSERT ON item BEGIN INSERT INTO item_search (rowid, content) VALUES (new.data->>'id', new.data->>'content'); END; - SQL, [], $pdo); + SQL, []); Custom::nonQuery(<<<'SQL' CREATE TRIGGER item_au AFTER UPDATE ON item BEGIN INSERT INTO item_search ( @@ -42,7 +36,7 @@ class Data ); INSERT INTO item_search (rowid, content) VALUES (new.data->>'id', new.data->>'content'); END; - SQL, [], $pdo); + SQL, []); Custom::nonQuery(<<<'SQL' CREATE TRIGGER item_ad AFTER DELETE ON item BEGIN INSERT INTO item_search ( @@ -51,7 +45,7 @@ class Data 'delete', old.data->>'id', old.data->>'content' ); END; - SQL, [], $pdo); + SQL, []); } /** @@ -62,19 +56,18 @@ class Data public static function ensureDb(): void { $tables = Custom::array("SELECT name FROM sqlite_master WHERE type = 'table'", [], new StringMapper('name')); - $pdo = Configuration::dbConn(); if (!in_array(Table::USER, $tables)) { - Definition::ensureTable(Table::USER, $pdo); - Definition::ensureFieldIndex(Table::USER, 'email', ['email'], $pdo); + Definition::ensureTable(Table::USER); + Definition::ensureFieldIndex(Table::USER, 'email', ['email']); } if (!in_array(Table::FEED, $tables)) { - Definition::ensureTable(Table::FEED, $pdo); - Definition::ensureFieldIndex(Table::FEED, 'user', ['user_id'], $pdo); + Definition::ensureTable(Table::FEED); + Definition::ensureFieldIndex(Table::FEED, 'user', ['user_id']); } if (!in_array(Table::ITEM, $tables)) { - Definition::ensureTable(Table::ITEM, $pdo); - Definition::ensureFieldIndex(Table::ITEM, 'feed', ['feed_id', 'item_link'], $pdo); - self::createSearchIndex($pdo); + Definition::ensureTable(Table::ITEM); + Definition::ensureFieldIndex(Table::ITEM, 'feed', ['feed_id', 'item_link']); + self::createSearchIndex(); } $journalMode = Custom::scalar("PRAGMA journal_mode", [], new StringMapper('journal_mode')); if ($journalMode <> 'wal') Custom::nonQuery("PRAGMA journal_mode = 'wal'", []); diff --git a/src/lib/Feed.php b/src/lib/Feed.php index ea2aa83..32a0503 100644 --- a/src/lib/Feed.php +++ b/src/lib/Feed.php @@ -2,19 +2,10 @@ namespace FeedReaderCentral; -use BitBadger\PDODocument\Configuration; -use BitBadger\PDODocument\Custom; -use BitBadger\PDODocument\Document; -use BitBadger\PDODocument\DocumentException; -use BitBadger\PDODocument\DocumentList; -use BitBadger\PDODocument\Exists; -use BitBadger\PDODocument\Field; -use BitBadger\PDODocument\Find; -use BitBadger\PDODocument\Parameters; -use BitBadger\PDODocument\Patch; -use BitBadger\PDODocument\Query; +use BitBadger\PDODocument\{ + Configuration, Custom, Document, DocumentException, DocumentList, Exists, Field, Find, Parameters, Patch, Query +}; use DateTimeInterface; -use PDO; /** * An RSS or Atom feed @@ -75,23 +66,22 @@ class Feed * @param int $feedId The ID of the feed to which these items belong * @param ParsedFeed $parsed The extracted Atom or RSS feed items * @param DateTimeInterface $lastChecked When this feed was last checked (only new items will be added) - * @param PDO $pdo The database connection over which items should be updated * @return array ['ok' => true] if successful, ['error' => message] if not */ - public static function updateItems(int $feedId, ParsedFeed $parsed, DateTimeInterface $lastChecked, PDO $pdo): array + public static function updateItems(int $feedId, ParsedFeed $parsed, DateTimeInterface $lastChecked): array { $results = - array_map(function ($item) use ($pdo, $feedId) { + array_map(function ($item) use ($feedId) { try { $existing = Find::firstByFields(Table::ITEM, [Field::EQ('feed_id', $feedId), Field::EQ('item_guid', $item->guid)], Item::class); if ($existing) { if ($existing->published_on != $item->publishedOn || ($existing->updated_on != ($item->updatedOn ?? ''))) { - Patch::byId(Table::ITEM, $existing->id, $item->patchFields(), $pdo); + Patch::byId(Table::ITEM, $existing->id, $item->patchFields()); } } else { - Document::insert(Table::ITEM, Item::fromFeedItem($feedId, $item), $pdo); + Document::insert(Table::ITEM, Item::fromFeedItem($feedId, $item)); } return ['ok' => true]; } catch (DocumentException $ex) { @@ -107,10 +97,9 @@ class Feed * Purge items for a feed * * @param int $feedId The ID of the feed to be purged - * @param PDO $pdo The database connection on which items should be purged * @return array|string[]|true[] ['ok' => true] if purging was successful, ['error' => message] if not */ - private static function purgeItems(int $feedId, PDO $pdo): array + private static function purgeItems(int $feedId): array { if (!array_search(PURGE_TYPE, [self::PURGE_READ, self::PURGE_BY_DAYS, self::PURGE_BY_COUNT])) { return ['error' => 'Unrecognized purge type ' . PURGE_TYPE]; @@ -139,7 +128,7 @@ class Feed SQL; } try { - Custom::nonQuery($sql, Parameters::addFields($fields, []), $pdo); + Custom::nonQuery($sql, Parameters::addFields($fields, [])); return ['ok' => true]; } catch (DocumentException $ex) { return ['error' => "$ex"]; @@ -151,10 +140,9 @@ class Feed * * @param int $feedId The ID of the feed to be refreshed * @param string $url The URL of the feed to be refreshed - * @param PDO $pdo A database connection to use to refresh the feed * @return array|string[]|true[] ['ok' => true] if successful, ['error' => message] if not */ - public static function refreshFeed(int $feedId, string $url, PDO $pdo): array + public static function refreshFeed(int $feedId, string $url): array { $feedRetrieval = ParsedFeed::retrieve($url); if (key_exists('error', $feedRetrieval)) return $feedRetrieval; @@ -165,7 +153,7 @@ class Feed if (!$feedDoc) return ['error' => 'Could not derive date last checked for feed']; $lastChecked = date_create_immutable($feedDoc->checked_on ?? WWW_EPOCH); - $itemUpdate = self::updateItems($feedId, $feed, $lastChecked, $pdo); + $itemUpdate = self::updateItems($feedId, $feed, $lastChecked); if (key_exists('error', $itemUpdate)) return $itemUpdate; $patch = [ @@ -174,12 +162,12 @@ class Feed 'checked_on' => Data::formatDate('now') ]; if ($url == $feed->url) $patch['url'] = $feed->url; - Patch::byId(Table::FEED, $feedId, $patch, $pdo); + Patch::byId(Table::FEED, $feedId, $patch); } catch (DocumentException $ex) { return ['error' => "$ex"]; } - return PURGE_TYPE == self::PURGE_NONE ? ['ok' => true] : self::purgeItems($feedId, $pdo); + return PURGE_TYPE == self::PURGE_NONE ? ['ok' => true] : self::purgeItems($feedId); } /** @@ -196,18 +184,17 @@ class Feed $feed = $feedExtract['ok']; try { - $pdo = Configuration::dbConn(); $fields = [Field::EQ('user_id', $_SESSION[Key::USER_ID]), Field::EQ('url', $feed->url)]; - if (Exists::byFields(Table::FEED, $fields, $pdo)) { + if (Exists::byFields(Table::FEED, $fields)) { return ['error' => "Already subscribed to feed $feed->url"]; } - Document::insert(Table::FEED, self::fromParsed($feed), $pdo); + Document::insert(Table::FEED, self::fromParsed($feed)); $doc = Find::firstByFields(Table::FEED, $fields, static::class); if (!$doc) return ['error' => 'Could not retrieve inserted feed']; - $result = self::updateItems($doc->id, $feed, date_create_immutable(WWW_EPOCH), $pdo); + $result = self::updateItems($doc->id, $feed, date_create_immutable(WWW_EPOCH)); if (key_exists('error', $result)) return $result; return ['ok' => $doc->id]; @@ -226,12 +213,11 @@ class Feed public static function update(Feed $existing, string $url): array { try { - $pdo = Configuration::dbConn(); Patch::byFields(Table::FEED, [Field::EQ(Configuration::$idField, $existing->id), Field::EQ('user_id', $_SESSION[Key::USER_ID])], - ['url' => $url], $pdo); + ['url' => $url]); - return self::refreshFeed($existing->id, $url, $pdo); + return self::refreshFeed($existing->id, $url); } catch (DocumentException $ex) { return ['error' => "$ex"]; } @@ -262,10 +248,9 @@ class Feed try { $feeds = self::retrieveAll($_SESSION[Key::USER_ID]); - $pdo = Configuration::dbConn(); $errors = []; foreach ($feeds->items() as $feed) { - $result = self::refreshFeed($feed->id, $feed->url, $pdo); + $result = self::refreshFeed($feed->id, $feed->url); if (key_exists('error', $result)) $errors[] = $result['error']; } } catch (DocumentException $ex) { @@ -284,7 +269,9 @@ class Feed */ public static function retrieveById(int $feedId): static|false { + define('PDO_DOC_DEBUG_SQL', true); $doc = Find::byId(Table::FEED, $feedId, static::class); + echo "Feed $feedId: " . ($doc ? 'found it' : 'not found'); return $doc && $doc->user_id == $_SESSION[Key::USER_ID] ? $doc : false; } } diff --git a/src/lib/ItemList.php b/src/lib/ItemList.php index 63b226b..5874353 100644 --- a/src/lib/ItemList.php +++ b/src/lib/ItemList.php @@ -2,13 +2,7 @@ namespace FeedReaderCentral; -use BitBadger\PDODocument\Configuration; -use BitBadger\PDODocument\Custom; -use BitBadger\PDODocument\DocumentException; -use BitBadger\PDODocument\DocumentList; -use BitBadger\PDODocument\Field; -use BitBadger\PDODocument\Parameters; -use BitBadger\PDODocument\Query; +use BitBadger\PDODocument\{Configuration, Custom, DocumentException, DocumentList, Field, Parameters, Query}; use BitBadger\PDODocument\Mapper\DocumentMapper; /** @@ -156,27 +150,26 @@ class ItemList echo "

Error retrieving list:
$this->error"; return; } - $return = $this->returnURL == '' ? '' : '&from=' . urlencode($this->returnURL); - $hasItems = false; + $return = $this->returnURL == '' ? '' : '&from=' . urlencode($this->returnURL); echo '

'; - foreach ($this->dbList->items() as $it) { - $hasItems = true; - echo '

' . hx_get("/item?id=$it->id$return", strip_tags($it->title)) . '
'; - if ($this->showIndicators) { - if (!$it->isRead()) echo 'Unread   '; - if ($it->isBookmarked()) echo 'Bookmarked   '; + if ($this->dbList->hasItems()) { + foreach ($this->dbList->items() as $it) { + echo '

' . hx_get("/item?id=$it->id$return", strip_tags($it->title)) . '
'; + if ($this->showIndicators) { + if (!$it->isRead()) echo 'Unread   '; + if ($it->isBookmarked()) echo 'Bookmarked   '; + } + echo '' . date_time($it->updated_on ?? $it->published_on) . ''; + if ($this->linkFeed) { + echo ' • ' . + hx_get("/feed/items?id={$it->feed->id}&" . strtolower($this->itemType), + htmlentities($it->feed->title)); + } elseif ($this->displayFeed) { + echo ' • ' . htmlentities($it->feed->title); + } + echo ''; } - echo '' . date_time($it->updated_on ?? $it->published_on) . ''; - if ($this->linkFeed) { - echo ' • ' . - hx_get("/feed/items?id={$it->feed->id}&" . strtolower($this->itemType), - htmlentities($it->feed->title)); - } elseif ($this->displayFeed) { - echo ' • ' . htmlentities($it->feed->title); - } - echo '
'; - } - if (!$hasItems) { + } else { echo '

There are no ' . strtolower($this->itemType) . ' items'; } echo '

'; diff --git a/src/lib/ItemWithFeed.php b/src/lib/ItemWithFeed.php index 3447225..c7f7ec5 100644 --- a/src/lib/ItemWithFeed.php +++ b/src/lib/ItemWithFeed.php @@ -2,14 +2,8 @@ namespace FeedReaderCentral; -use BitBadger\PDODocument\Configuration; -use BitBadger\PDODocument\Custom; -use BitBadger\PDODocument\DocumentException; -use BitBadger\PDODocument\Field; -use BitBadger\PDODocument\Parameters; -use BitBadger\PDODocument\Query; -use BitBadger\PDODocument\Mapper\DocumentMapper; -use BitBadger\PDODocument\Mapper\ExistsMapper; +use BitBadger\PDODocument\{Configuration, Custom, DocumentException, Field, Parameters, Query}; +use BitBadger\PDODocument\Mapper\{DocumentMapper, ExistsMapper}; /** * A combined item and feed (used for lists) diff --git a/src/lib/User.php b/src/lib/User.php index 7223e5d..4549ce2 100644 --- a/src/lib/User.php +++ b/src/lib/User.php @@ -2,14 +2,8 @@ namespace FeedReaderCentral; -use BitBadger\PDODocument\Custom; -use BitBadger\PDODocument\Document; -use BitBadger\PDODocument\DocumentException; -use BitBadger\PDODocument\Field; -use BitBadger\PDODocument\Find; +use BitBadger\PDODocument\{Custom, Document, DocumentException, Field, Find, Parameters, Query}; use BitBadger\PDODocument\Mapper\ExistsMapper; -use BitBadger\PDODocument\Parameters; -use BitBadger\PDODocument\Query; /** * A user of Feed Reader Central diff --git a/src/util/db-update.php b/src/util/db-update.php index 73917e5..0fa4088 100644 --- a/src/util/db-update.php +++ b/src/util/db-update.php @@ -1,16 +1,8 @@ query('SELECT * FROM old_user'); - if (!$users) throw new DocumentException('Could not retrieve users'); - while ($user = $users->fetch(PDO::FETCH_ASSOC)) { - Document::insert(Table::USER, new User($user['id'], $user['email'], $user['password']), $pdo); + $users = Custom::list('SELECT * FROM old_user', [], new ArrayMapper()); + if (!$users->hasItems()) throw new DocumentException('Could not retrieve users'); + foreach ($users->items() as $user) { + Document::insert(Table::USER, new User($user['id'], $user['email'], $user['password'])); } printfn('Migrating feeds...'); - $feeds = $pdo->query('SELECT * FROM old_feed'); - if (!$feeds) throw new DocumentException('Could not retrieve feeds'); - while ($feed = $feeds->fetch(PDO::FETCH_ASSOC)) { + $feeds = Custom::list('SELECT * FROM old_feed', [], new ArrayMapper()); + if (!$feeds->hasItems()) throw new DocumentException('Could not retrieve feeds'); + foreach ($feeds->items() as $feed) { Document::insert(Table::FEED, new Feed($feed['id'], $feed['user_id'], $feed['url'], $feed['title'], $feed['updated_on'], - $feed['checked_on']), $pdo); + $feed['checked_on'])); } printfn('Migrating items...'); - $items = $pdo->query('SELECT * FROM old_item'); - if (!$items) throw new DocumentException('Could not retrieve items'); - while ($item = $items->fetch(PDO::FETCH_ASSOC)) { + $items = Custom::list('SELECT * FROM old_item', [], new ArrayMapper()); + if (!$items->hasItems()) throw new DocumentException('Could not retrieve items'); + foreach ($items->items() as $item) { Document::insert(Table::ITEM, new Item($item['id'], $item['feed_id'], $item['title'], $item['item_guid'], $item['item_link'], $item['published_on'], $item['updated_on'], $item['content'], $item['is_read'], - $item['is_bookmarked']), $pdo); + $item['is_bookmarked'])); } printfn('Dropping old tables...'); - Custom::nonQuery('DROP TABLE old_item', [], $pdo); - Custom::nonQuery('DROP TABLE old_feed', [], $pdo); - Custom::nonQuery('DROP TABLE old_user', [], $pdo); + Custom::nonQuery('DROP TABLE old_item', []); + Custom::nonQuery('DROP TABLE old_feed', []); + Custom::nonQuery('DROP TABLE old_user', []); printfn(PHP_EOL. 'Migration complete!'); } catch (DocumentException $ex) { printfn("ERR $ex"); diff --git a/src/util/refresh.php b/src/util/refresh.php index 16078d9..49b26ea 100644 --- a/src/util/refresh.php +++ b/src/util/refresh.php @@ -1,11 +1,7 @@ items(), function (Feed $feed) use ($pdo, &$users) { - $result = Feed::refreshFeed($feed->id, $feed->url, $pdo); + iterator_apply(Feed::retrieveAll()->items(), function (Feed $feed) use (&$users) { + $result = Feed::refreshFeed($feed->id, $feed->url); $userKey = "$feed->user_id"; if (!key_exists($userKey, $users)) $users[$userKey] = Find::byId(Table::USER, $feed->user_id, User::class); if (array_key_exists('error', $result)) { diff --git a/src/util/search.php b/src/util/search.php index 526baa8..6c2caab 100644 --- a/src/util/search.php +++ b/src/util/search.php @@ -1,9 +1,7 @@ id)], $db); + $feedCount = Count::byFields(Table::FEED, [Field::EQ('user_id', $user->id)]); } catch (DocumentException $ex) { printfn("$ex"); return; @@ -192,9 +175,9 @@ function delete_user(string $email): void $fields = [Field::EQ('user_id', $user->id, '@user')]; Custom::nonQuery( 'DELETE FROM ' . Table::ITEM . " WHERE data->>'feed_id' IN (SELECT data->>'id' FROM " . Table::FEED - . ' WHERE ' . Query::whereByFields($fields) . ')', Parameters::addFields($fields, []), $db); - Delete::byFields(Table::FEED, $fields, $db); - Delete::byId(Table::USER, $user->id, $db); + . ' WHERE ' . Query::whereByFields($fields) . ')', Parameters::addFields($fields, [])); + Delete::byFields(Table::FEED, $fields); + Delete::byId(Table::USER, $user->id); printfn('%s deleted successfully', init_cap($displayUser)); } catch (DocumentException $ex) { @@ -202,8 +185,6 @@ function delete_user(string $email): void } } catch (DocumentException $ex) { printfn("$ex"); - } finally { - $db->close(); } } @@ -214,13 +195,6 @@ function migrate_single_user(): void { global $argv; - try { - $db = Configuration::dbConn(); - } catch (DocumentException $ex) { - printfn("ERR: Cannot obtain a connection to the database\n $ex"); - return; - } - try { if (!$single = User::findByEmail(Security::SINGLE_USER_EMAIL)) { printfn('There is no single-user mode user to be migrated'); @@ -228,12 +202,10 @@ function migrate_single_user(): void } Patch::byId(Table::USER, $single->id, - ['email' => $argv[2], 'password' => password_hash($argv[3], Security::PW_ALGORITHM)], $db); + ['email' => $argv[2], 'password' => password_hash($argv[3], Security::PW_ALGORITHM)]); printfn('The single user has been moved to "%s", with password "%s"', $argv[2], $argv[3]); } catch (DocumentException $ex) { printfn("$ex"); - } finally { - $db->close(); } }