WIP on document conversion
This commit is contained in:
274
src/lib/Feed.php
274
src/lib/Feed.php
@@ -1,24 +1,25 @@
|
||||
<?php
|
||||
namespace FeedReaderCentral;
|
||||
|
||||
use BitBadger\Documents\DocumentException;
|
||||
use BitBadger\Documents\DocumentList;
|
||||
use BitBadger\Documents\Field;
|
||||
use BitBadger\Documents\Query;
|
||||
use BitBadger\Documents\SQLite\Configuration;
|
||||
use BitBadger\Documents\SQLite\Custom;
|
||||
use BitBadger\Documents\SQLite\Document;
|
||||
use BitBadger\Documents\SQLite\Exists;
|
||||
use BitBadger\Documents\SQLite\Find;
|
||||
use BitBadger\Documents\SQLite\Patch;
|
||||
use BitBadger\Documents\SQLite\Results;
|
||||
use DateTimeInterface;
|
||||
use DOMDocument;
|
||||
use DOMElement;
|
||||
use DOMException;
|
||||
use DOMNode;
|
||||
use Exception;
|
||||
use FeedReaderCentral\Domain\Feed as FeedDocument;
|
||||
use FeedReaderCentral\Domain\Item;
|
||||
use FeedReaderCentral\Domain\Table;
|
||||
use SQLite3;
|
||||
use SQLite3Result;
|
||||
|
||||
/**
|
||||
* Feed retrieval, parsing, and manipulation
|
||||
@@ -278,62 +279,6 @@ class Feed {
|
||||
return $extract($parsed['ok'], $doc['url']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a feed item
|
||||
*
|
||||
* @param int $itemId The ID of the item to be updated
|
||||
* @param FeedItem $item The item to be updated
|
||||
* @param SQLite3 $db A database connection to use for the update
|
||||
* @return bool|SQLite3Result The result if the update is successful, false if it failed
|
||||
*/
|
||||
private static function updateItem(int $itemId, FeedItem $item, SQLite3 $db): bool|SQLite3Result {
|
||||
Patch::byId(Table::ITEM, $itemId, $item->patchFields(), $db);
|
||||
// $query = $db->prepare(<<<'SQL'
|
||||
// UPDATE item
|
||||
// SET title = :title,
|
||||
// published_on = :published,
|
||||
// updated_on = :updated,
|
||||
// content = :content,
|
||||
// is_read = 0
|
||||
// WHERE id = :id
|
||||
// SQL);
|
||||
// $query->bindValue(':title', $item->title);
|
||||
// $query->bindValue(':published', $item->publishedOn);
|
||||
// $query->bindValue(':updated', $item->updatedOn);
|
||||
// $query->bindValue(':content', $item->content);
|
||||
// $query->bindValue(':id', $itemId);
|
||||
// return $query->execute();
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a feed item
|
||||
*
|
||||
* @param int $feedId The ID of the feed to which the item should be added
|
||||
* @param FeedItem $item The item to be added
|
||||
* @param SQLite3 $db A database connection to use for the addition
|
||||
* @return bool|SQLite3Result The result if the update is successful, false if it failed
|
||||
*/
|
||||
private static function addItem(int $feedId, FeedItem $item, SQLite3 $db): bool|SQLite3Result {
|
||||
Document::insert(Table::ITEM, Item::fromFeedItem($feedId, $item), $db);
|
||||
// $query = $db->prepare(<<<'SQL'
|
||||
// INSERT INTO item (
|
||||
// feed_id, item_guid, item_link, title, published_on, updated_on, content
|
||||
// ) VALUES (
|
||||
// :feed, :guid, :link, :title, :published, :updated, :content
|
||||
// )
|
||||
// SQL);
|
||||
// $query->bindValue(':feed', $feedId);
|
||||
// $query->bindValue(':guid', $item->guid);
|
||||
// $query->bindValue(':link', $item->link);
|
||||
// $query->bindValue(':title', $item->title);
|
||||
// $query->bindValue(':published', $item->publishedOn);
|
||||
// $query->bindValue(':updated', $item->updatedOn);
|
||||
// $query->bindValue(':content', $item->content);
|
||||
// return $query->execute();
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a feed's items
|
||||
*
|
||||
@@ -345,24 +290,21 @@ class Feed {
|
||||
public static function updateItems(int $feedId, Feed $feed, DateTimeInterface $lastChecked, SQLite3 $db): array {
|
||||
$results =
|
||||
array_map(function ($item) use ($db, $feedId) {
|
||||
// TODO: convert this query
|
||||
$existsQuery = $db->prepare(
|
||||
'SELECT id, published_on, updated_on FROM item WHERE feed_id = :feed AND item_guid = :guid');
|
||||
$existsQuery->bindValue(':feed', $feedId);
|
||||
$existsQuery->bindValue(':guid', $item->guid);
|
||||
if ($exists = $existsQuery->execute()) {
|
||||
if ($existing = $exists->fetchArray(SQLITE3_ASSOC)) {
|
||||
if ( $existing['published_on'] != $item->publishedOn
|
||||
|| ($existing['updated_on'] ?? '') != ($item->updatedOn ?? '')) {
|
||||
if (!self::updateItem($existing['id'], $item, $db)) return Data::error($db);
|
||||
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(), $db);
|
||||
}
|
||||
} else {
|
||||
if (!self::addItem($feedId, $item, $db)) return Data::error($db);
|
||||
Document::insert(Table::ITEM, Item::fromFeedItem($feedId, $item), $db);
|
||||
}
|
||||
} else {
|
||||
return Data::error($db);
|
||||
return ['ok' => true];
|
||||
} catch (DocumentException $ex) {
|
||||
return ['error' => "$ex"];
|
||||
}
|
||||
return ['ok' => true];
|
||||
}, array_filter($feed->items,
|
||||
fn($it) => date_create_immutable($it->updatedOn ?? $it->publishedOn) >= $lastChecked));
|
||||
$errors = array_map(fn($it) => $it['error'], array_filter($results, fn($it) => array_key_exists('error', $it)));
|
||||
@@ -381,26 +323,33 @@ class Feed {
|
||||
return ['error' => 'Unrecognized purge type ' . PURGE_TYPE];
|
||||
}
|
||||
|
||||
$fields = [Field::EQ('feed_id', $feedId, '@feed'), Field::EQ('is_bookmarked', 0, '@book')];
|
||||
$sql = Query\Delete::byFields(Table::ITEM, $fields);
|
||||
if (PURGE_TYPE == self::PURGE_READ) {
|
||||
$readField = Field::EQ('is_read', 1, '@read');
|
||||
$fields[] = $readField;
|
||||
$sql .= ' AND ' . Query::whereByFields([$readField]);
|
||||
} elseif (PURGE_TYPE == self::PURGE_BY_DAYS) {
|
||||
$fields[] = Field::EQ('', Data::formatDate('-' . PURGE_NUMBER . ' day'), '@oldest');
|
||||
$sql .= " AND date(coalesce(data->>'updated_on', data->>'published_on)) < date(@oldest)";
|
||||
} elseif (PURGE_TYPE == self::PURGE_BY_COUNT) {
|
||||
$fields[] = Field::EQ('', PURGE_NUMBER, '@keep');
|
||||
$id = Configuration::idField();
|
||||
$table = Table::ITEM;
|
||||
$sql .= ' ' . <<<SQL
|
||||
AND data->>'$id' IN (
|
||||
SELECT data->>'$id' FROM $table
|
||||
WHERE data->>'feed_id' = @feed
|
||||
ORDER BY date(coalesce(data->>'updated_on', data->>'published_on')) DESC
|
||||
LIMIT -1 OFFSET @keep
|
||||
)
|
||||
SQL;
|
||||
}
|
||||
try {
|
||||
// TODO: convert this query
|
||||
$sql = match (PURGE_TYPE) {
|
||||
self::PURGE_READ => 'AND is_read = 1',
|
||||
self::PURGE_BY_DAYS => 'AND date(coalesce(updated_on, published_on)) < date(:oldest)',
|
||||
self::PURGE_BY_COUNT => 'AND id IN (SELECT id FROM item WHERE feed_id = :feed
|
||||
ORDER BY date(coalesce(updated_on, published_on)) DESC
|
||||
LIMIT -1 OFFSET :keep)'
|
||||
};
|
||||
|
||||
$purge = $db->prepare("DELETE FROM item WHERE feed_id = :feed AND is_bookmarked = 0 $sql");
|
||||
$purge->bindValue(':feed', $feedId);
|
||||
if (PURGE_TYPE == self::PURGE_BY_DAYS) {
|
||||
$purge->bindValue(':oldest', Data::formatDate('-' . PURGE_NUMBER . ' day'));
|
||||
} elseif (PURGE_TYPE == self::PURGE_BY_COUNT) {
|
||||
$purge->bindValue(':keep', PURGE_NUMBER);
|
||||
}
|
||||
return $purge->execute() ? ['ok' => true] : Data::error($db);
|
||||
} catch (Exception $ex) {
|
||||
return ['error' => $ex->getMessage()];
|
||||
Custom::nonQuery($sql, array_merge(array_map($it -> $it->asParameter(), $fields)), $db);
|
||||
return ['ok' => true];
|
||||
} catch (DocumentException $ex) {
|
||||
return ['error' => "$ex"];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -417,35 +366,25 @@ class Feed {
|
||||
if (key_exists('error', $feedRetrieval)) return $feedRetrieval;
|
||||
$feed = $feedRetrieval['ok'];
|
||||
|
||||
$lastCheckedQuery = $db->prepare('SELECT checked_on FROM feed where id = :id');
|
||||
$lastCheckedQuery->bindValue(':id', $feedId);
|
||||
if (!($lastCheckedResult = $lastCheckedQuery->execute())) return Data::error($db);
|
||||
if (!($lastChecked = date_create_immutable($lastCheckedResult->fetchArray(SQLITE3_NUM)[0] ?? WWW_EPOCH))) {
|
||||
return ['error' => 'Could not derive date last checked for feed'];
|
||||
try {
|
||||
$feedDoc = Find::byId(Table::FEED, $feedId, FeedDocument::class);
|
||||
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, $db);
|
||||
if (key_exists('error', $itemUpdate)) return $itemUpdate;
|
||||
|
||||
$patch = [
|
||||
'title' => $feed->title,
|
||||
'updated_on' => $feed->updatedOn,
|
||||
'checked_on' => Data::formatDate('now')
|
||||
];
|
||||
if ($url == $feed->url) $patch['url'] = $feed->url;
|
||||
Patch::byId(Table::FEED, $feedId, $patch, $db);
|
||||
} catch (DocumentException $ex) {
|
||||
return ['error' => "$ex"];
|
||||
}
|
||||
|
||||
$itemUpdate = self::updateItems($feedId, $feed, $lastChecked, $db);
|
||||
if (key_exists('error', $itemUpdate)) return $itemUpdate;
|
||||
|
||||
$patch = ['title' => $feed->title, 'updated_on' => $feed->updatedOn, 'checked_on' => Data::formatDate('now')];
|
||||
if ($url == $feed->url) $patch['url'] = $feed->url;
|
||||
Patch::byId(Table::FEED, $feedId, $patch, $db);
|
||||
// $urlUpdate = $url == $feed->url ? '' : ', url = :url';
|
||||
// $feedUpdate = $db->prepare(<<<SQL
|
||||
// UPDATE feed
|
||||
// SET title = :title,
|
||||
// updated_on = :updated,
|
||||
// checked_on = :checked
|
||||
// $urlUpdate
|
||||
// WHERE id = :id
|
||||
// SQL);
|
||||
// $feedUpdate->bindValue(':title', $feed->title);
|
||||
// $feedUpdate->bindValue(':updated', $feed->updatedOn);
|
||||
// $feedUpdate->bindValue(':checked', Data::formatDate('now'));
|
||||
// $feedUpdate->bindValue(':id', $feedId);
|
||||
// if ($urlUpdate != '') $feedUpdate->bindValue(':url', $feed->url);
|
||||
// if (!$feedUpdate->execute()) return Data::error($db);
|
||||
|
||||
return PURGE_TYPE == self::PURGE_NONE ? ['ok' => true] : self::purgeItems($feedId, $db);
|
||||
}
|
||||
|
||||
@@ -461,31 +400,19 @@ class Feed {
|
||||
|
||||
$feed = $feedExtract['ok'];
|
||||
|
||||
$whereUserAndUrl = ' WHERE ' . Query::whereByField(Field::EQ('user_id', ''), '@user')
|
||||
. ' AND ' . Query::whereByField(Field::EQ('url', ''), '@url');
|
||||
$userAndUrlParams = ['@user' => $_SESSION[Key::USER_ID], '@url' => $feed->url];
|
||||
if (Custom::scalar('SELECT EXISTS (SELECT 1 FROM ' . Table::FEED . $whereUserAndUrl . ')', $userAndUrlParams,
|
||||
Results::toExists(...), $db)) {
|
||||
return ['error' => "Already subscribed to feed $feed->url"];
|
||||
}
|
||||
try {
|
||||
$fields = [Field::EQ('user_id', $_SESSION[Key::USER_ID]), Field::EQ('url', $feed->url)];
|
||||
if (Exists::byFields(Table::FEED, $fields, $db)) {
|
||||
return ['error' => "Already subscribed to feed $feed->url"];
|
||||
}
|
||||
|
||||
Document::insert(Table::FEED, FeedDocument::fromParsed($feed), $db);
|
||||
// $query = $db->prepare(<<<'SQL'
|
||||
// INSERT INTO feed (
|
||||
// user_id, url, title, updated_on, checked_on
|
||||
// ) VALUES (
|
||||
// :user, :url, :title, :updated, :checked
|
||||
// )
|
||||
// SQL);
|
||||
// $query->bindValue(':user', $_SESSION[Key::USER_ID]);
|
||||
// $query->bindValue(':url', $feed->url);
|
||||
// $query->bindValue(':title', $feed->title);
|
||||
// $query->bindValue(':updated', $feed->updatedOn);
|
||||
// $query->bindValue(':checked', Data::formatDate('now'));
|
||||
// if (!$query->execute()) return Data::error($db);
|
||||
$doc = Custom::single(Query::selectFromTable(Table::FEED) . $whereUserAndUrl, $userAndUrlParams,
|
||||
Results::fromData(...), Domain\Feed::class, $db);
|
||||
if (!$doc) return ['error' => 'Could not retrieve inserted feed'];
|
||||
Document::insert(Table::FEED, FeedDocument::fromParsed($feed), $db);
|
||||
|
||||
$doc = Find::firstByFields(Table::FEED, $fields, FeedDocument::class);
|
||||
if (!$doc) return ['error' => 'Could not retrieve inserted feed'];
|
||||
} catch (DocumentException $ex) {
|
||||
return ['error' => "$ex"];
|
||||
}
|
||||
|
||||
$result = self::updateItems($doc->id, $feed, date_create_immutable(WWW_EPOCH), $db);
|
||||
if (key_exists('error', $result)) return $result;
|
||||
@@ -502,12 +429,13 @@ class Feed {
|
||||
* @return bool[]|string[] [ 'ok' => true ] if successful, [ 'error' => message ] if not
|
||||
*/
|
||||
public static function update(FeedDocument $existing, string $url, SQLite3 $db): array {
|
||||
// TODO: convert this query (need to make Query\Patch::update visible)
|
||||
$query = $db->prepare('UPDATE feed SET url = :url WHERE id = :id AND user_id = :user');
|
||||
$query->bindValue(':url', $url);
|
||||
$query->bindValue(':id', $existing->id);
|
||||
$query->bindValue(':user', $_SESSION[Key::USER_ID]);
|
||||
if (!$query->execute()) return Data::error($db);
|
||||
try {
|
||||
Patch::byFields(Table::FEED,
|
||||
[Field::EQ(Configuration::idField(), $existing->id), Field::EQ('user_id', $_SESSION[Key::USER_ID])],
|
||||
['url' => $url], $db);
|
||||
} catch (DocumentException $ex) {
|
||||
return ['error' => "$ex"];
|
||||
}
|
||||
|
||||
return self::refreshFeed($existing->id, $url, $db);
|
||||
}
|
||||
@@ -515,19 +443,15 @@ class Feed {
|
||||
/**
|
||||
* Retrieve all feeds, optionally for a specific user
|
||||
*
|
||||
* @param SQLite3 $db The database connection to use to retrieve the feeds
|
||||
* @param int $user The ID of the user whose feeds should be retrieved (optional, defaults to all feeds)
|
||||
* @return array An array of arrays with ['id', 'url', 'email'] keys
|
||||
* @return DocumentList<FeedDocument> A list of feeds
|
||||
* @throws DocumentException If any is encountered
|
||||
*/
|
||||
public static function retrieveAll(SQLite3 $db, int $user = 0): array {
|
||||
$extraSQL = $user > 0 ? ' WHERE u.id = :user' : '';
|
||||
$query = $db->prepare(
|
||||
"SELECT f.id, f.url, u.email FROM feed f INNER JOIN frc_user u ON u.id = f.user_id$extraSQL");
|
||||
if ($user > 0) $query->bindValue(':user', $user);
|
||||
if (!($result = $query->execute())) return Data::error($db);
|
||||
$feeds = [];
|
||||
while ($feed = $result->fetchArray(SQLITE3_ASSOC)) $feeds[] = $feed;
|
||||
return $feeds;
|
||||
public static function retrieveAll(int $user = 0): DocumentList
|
||||
{
|
||||
return $user == 0
|
||||
? Find::all(Table::FEED, FeedDocument::class)
|
||||
: Find::byFields(Table::FEED, [Field::EQ('user_id', $user)], FeedDocument::class);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -537,15 +461,19 @@ class Feed {
|
||||
* @return array|true[]|string[] ['ok' => true] if successful,
|
||||
* ['error' => message] if not (may have multiple error lines)
|
||||
*/
|
||||
public static function refreshAll(SQLite3 $db): array {
|
||||
$feeds = self::retrieveAll($db, $_SESSION[Key::USER_ID]);
|
||||
if (key_exists('error', $feeds)) return $feeds;
|
||||
public static function refreshAll(SQLite3 $db): array
|
||||
{
|
||||
try {
|
||||
$feeds = self::retrieveAll($_SESSION[Key::USER_ID]);
|
||||
|
||||
$errors = [];
|
||||
array_walk($feeds, function ($feed) use ($db, &$errors) {
|
||||
$result = self::refreshFeed($feed['id'], $feed['url'], $db);
|
||||
if (key_exists('error', $result)) $errors[] = $result['error'];
|
||||
});
|
||||
$errors = [];
|
||||
foreach ($feeds->items() as $feed) {
|
||||
$result = self::refreshFeed($feed->id, $feed->url, $db);
|
||||
if (key_exists('error', $result)) $errors[] = $result['error'];
|
||||
}
|
||||
} catch (DocumentException $ex) {
|
||||
return ['error' => "$ex"];
|
||||
}
|
||||
|
||||
return sizeof($errors) == 0 ? ['ok' => true] : ['error' => implode("\n", $errors)];
|
||||
}
|
||||
@@ -554,15 +482,11 @@ class Feed {
|
||||
* Retrieve a feed by its ID for the current user
|
||||
*
|
||||
* @param int $feedId The ID of the feed to retrieve
|
||||
* @param SQLite3 $db A database connection to use to retrieve the feed
|
||||
* @return FeedDocument|false The data for the feed if found, false if not found
|
||||
* @throws DocumentException If any is encountered
|
||||
*/
|
||||
public static function retrieveById(int $feedId, SQLite3 $db): FeedDocument|false {
|
||||
$doc = Find::byId(Table::FEED, $feedId, FeedDocument::class, $db);
|
||||
public static function retrieveById(int $feedId): FeedDocument|false {
|
||||
$doc = Find::byId(Table::FEED, $feedId, FeedDocument::class);
|
||||
return $doc && $doc->user_id == $_SESSION[Key::USER_ID] ? $doc : false;
|
||||
// $query = $db->prepare('SELECT * FROM feed WHERE id = :id AND user_id = :user');
|
||||
// $query->bindValue(':id', $feedId);
|
||||
// $query->bindValue(':user', $_SESSION[Key::USER_ID]);
|
||||
// return ($result = $query->execute()) ? $result->fetchArray(SQLITE3_ASSOC) : false;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user