First cut of item-with-feed and list impl
- Add strict types to all files - Convert many queries to document commands
This commit is contained in:
@@ -1,19 +1,25 @@
|
||||
<?php
|
||||
<?php declare(strict_types=1);
|
||||
|
||||
namespace FeedReaderCentral;
|
||||
|
||||
use SQLite3;
|
||||
use SQLite3Result;
|
||||
use SQLite3Stmt;
|
||||
use BitBadger\Documents\Configuration;
|
||||
use BitBadger\Documents\DocumentException;
|
||||
use BitBadger\Documents\DocumentList;
|
||||
use BitBadger\Documents\Field;
|
||||
use BitBadger\Documents\JsonMapper;
|
||||
use BitBadger\Documents\Query;
|
||||
use BitBadger\Documents\SQLite\Custom;
|
||||
use BitBadger\Documents\SQLite\Parameters;
|
||||
|
||||
/**
|
||||
* A list of items to be displayed
|
||||
*
|
||||
* This is a wrapper for retrieval and display of arbitrary lists of items based on a SQLite result.
|
||||
*/
|
||||
class ItemList {
|
||||
|
||||
/** @var SQLite3Result The list of items to be displayed */
|
||||
private SQLite3Result $items;
|
||||
class ItemList
|
||||
{
|
||||
/** @var DocumentList<ItemWithFeed> The items matching the criteria, lazily iterable */
|
||||
private DocumentList $dbList;
|
||||
|
||||
/** @var string The error message generated by executing a query */
|
||||
public string $error = '';
|
||||
@@ -23,7 +29,8 @@ class ItemList {
|
||||
*
|
||||
* @return bool True if there is an error condition associated with this list, false if not
|
||||
*/
|
||||
public function isError(): bool {
|
||||
public function isError(): bool
|
||||
{
|
||||
return $this->error != '';
|
||||
}
|
||||
|
||||
@@ -39,54 +46,34 @@ class ItemList {
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param SQLite3 $db The database connection (used to retrieve error information if the query fails)
|
||||
* @param SQLite3Stmt $query The query to retrieve the items for this list
|
||||
* @param string $itemType The type of item being displayed (unread, bookmark, etc.)
|
||||
* @param string $returnURL The URL to which the item page should return once the item has been viewed
|
||||
* @param array|Field[] $fields The fields to use to restrict the results
|
||||
* @param string $searchWhere Additional WHERE clause to use for searching
|
||||
*/
|
||||
private function __construct(SQLite3 $db, SQLite3Stmt $query, public string $itemType, public string $returnURL = '')
|
||||
private function __construct(public string $itemType, public string $returnURL = '', array $fields = [],
|
||||
string $searchWhere = '')
|
||||
{
|
||||
$result = $query->execute();
|
||||
if (!$result) {
|
||||
$this->error = 'SQLite error: ' . $db->lastErrorMsg();
|
||||
} else {
|
||||
$this->items = $result;
|
||||
$allFields = [Data::userIdField(Table::FEED), ...$fields];
|
||||
try {
|
||||
$this->dbList = Custom::list(
|
||||
ItemWithFeed::SELECT_WITH_FEED . ' WHERE '
|
||||
. Query::whereByFields(array_filter($allFields, fn($it) => $it->paramName <> '@search'))
|
||||
. $searchWhere,
|
||||
Parameters::addFields($allFields, []), new JsonMapper(ItemWithFeed::class));
|
||||
} catch (DocumentException $ex) {
|
||||
$this->error = "$ex";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an item list query
|
||||
*
|
||||
* @param SQLite3 $db The database connection to use to obtain items
|
||||
* @param array $criteria One or more SQL WHERE conditions (will be combined with AND)
|
||||
* @param array $parameters Parameters to be added to the query (key index 0, value index 1; optional)
|
||||
* @return SQLite3Stmt The query, ready to be executed
|
||||
*/
|
||||
private static function makeQuery(SQLite3 $db, array $criteria, array $parameters = []): SQLite3Stmt
|
||||
{
|
||||
$where = empty($criteria) ? '' : 'AND ' . implode(' AND ', $criteria);
|
||||
$sql = <<<SQL
|
||||
SELECT item.id, item.feed_id, item.title AS item_title, coalesce(item.updated_on, item.published_on) AS as_of,
|
||||
item.is_read, item.is_bookmarked, feed.title AS feed_title
|
||||
FROM item INNER JOIN feed ON feed.id = item.feed_id
|
||||
WHERE feed.user_id = :userId $where
|
||||
ORDER BY coalesce(item.updated_on, item.published_on) DESC
|
||||
SQL;
|
||||
$query = $db->prepare($sql);
|
||||
$query->bindValue(':userId', $_SESSION[Key::USER_ID]);
|
||||
foreach ($parameters as $param) $query->bindValue($param[0], $param[1]);
|
||||
return $query;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an item list with all the current user's bookmarked items
|
||||
*
|
||||
* @param SQLite3 $db The database connection to use to obtain items
|
||||
* @return static An item list with all bookmarked items
|
||||
*/
|
||||
public static function allBookmarked(SQLite3 $db): static
|
||||
public static function allBookmarked(): static
|
||||
{
|
||||
$list = new static($db, self::makeQuery($db, ['item.is_bookmarked = 1']), 'Bookmarked', '/?bookmarked');
|
||||
$list = new static('Bookmarked', '/?bookmarked', [Data::bookmarkField(Table::ITEM)]);
|
||||
$list->linkFeed = true;
|
||||
return $list;
|
||||
}
|
||||
@@ -94,12 +81,11 @@ class ItemList {
|
||||
/**
|
||||
* Create an item list with all the current user's unread items
|
||||
*
|
||||
* @param SQLite3 $db The database connection to use to obtain items
|
||||
* @return static An item list with all unread items
|
||||
*/
|
||||
public static function allUnread(SQLite3 $db): static
|
||||
public static function allUnread(): static
|
||||
{
|
||||
$list = new static($db, self::makeQuery($db, ['item.is_read = 0']), 'Unread');
|
||||
$list = new static('Unread', fields: [Data::unreadField(Table::ITEM)]);
|
||||
$list->linkFeed = true;
|
||||
return $list;
|
||||
}
|
||||
@@ -108,13 +94,11 @@ class ItemList {
|
||||
* Create an item list with all items for the given feed
|
||||
*
|
||||
* @param int $feedId The ID of the feed for which items should be retrieved
|
||||
* @param SQLite3 $db The database connection to use to obtain items
|
||||
* @return static An item list with all items for the given feed
|
||||
*/
|
||||
public static function allForFeed(int $feedId, SQLite3 $db): static
|
||||
public static function allForFeed(int $feedId): static
|
||||
{
|
||||
$list = new static($db, self::makeQuery($db, ['feed.id = :feed'], [[':feed', $feedId]]), '',
|
||||
"/feed/items?id=$feedId");
|
||||
$list = new static('', "/feed/items?id=$feedId", [Data::feedField($feedId, Table::FEED)]);
|
||||
$list->showIndicators = true;
|
||||
return $list;
|
||||
}
|
||||
@@ -123,27 +107,24 @@ class ItemList {
|
||||
* Create an item list with unread items for the given feed
|
||||
*
|
||||
* @param int $feedId The ID of the feed for which items should be retrieved
|
||||
* @param SQLite3 $db The database connection to use to obtain items
|
||||
* @return static An item list with unread items for the given feed
|
||||
*/
|
||||
public static function unreadForFeed(int $feedId, SQLite3 $db): static
|
||||
public static function unreadForFeed(int $feedId): static
|
||||
{
|
||||
return new static($db, self::makeQuery($db, ['feed.id = :feed', 'item.is_read = 0'], [[':feed', $feedId]]),
|
||||
'Unread', "/feed/items?id=$feedId&unread");
|
||||
return new static('Unread', "/feed/items?id=$feedId&unread",
|
||||
[Data::feedField($feedId, Table::FEED), Data::unreadField(Table::ITEM)]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an item list with bookmarked items for the given feed
|
||||
*
|
||||
* @param int $feedId The ID of the feed for which items should be retrieved
|
||||
* @param SQLite3 $db The database connection to use to obtain items
|
||||
* @return static An item list with bookmarked items for the given feed
|
||||
*/
|
||||
public static function bookmarkedForFeed(int $feedId, SQLite3 $db): static
|
||||
public static function bookmarkedForFeed(int $feedId): static
|
||||
{
|
||||
return new static($db,
|
||||
self::makeQuery($db, ['feed.id = :feed', 'item.is_bookmarked = 1'], [[':feed', $feedId]]), 'Bookmarked',
|
||||
"/feed/items?id=$feedId&bookmarked");
|
||||
return new static('Bookmarked', "/feed/items?id=$feedId&bookmarked",
|
||||
[Data::feedField($feedId, Table::FEED), Data::bookmarkField(Table::ITEM)]);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -151,16 +132,16 @@ class ItemList {
|
||||
*
|
||||
* @param string $search The item search terms / query
|
||||
* @param bool $isBookmarked Whether to restrict the search to bookmarked items
|
||||
* @param SQLite3 $db The database connection to use to obtain items
|
||||
* @return static An item list match the given search terms
|
||||
*/
|
||||
public static function matchingSearch(string $search, bool $isBookmarked, SQLite3 $db): static
|
||||
public static function matchingSearch(string $search, bool $isBookmarked): static
|
||||
{
|
||||
$where = $isBookmarked ? ['item.is_bookmarked = 1'] : [];
|
||||
$where[] = 'item.id IN (SELECT ROWID FROM item_search WHERE content MATCH :search)';
|
||||
$list = new static($db, self::makeQuery($db, $where, [[':search', $search]]),
|
||||
'Matching' . ($isBookmarked ? ' Bookmarked' : ''),
|
||||
"/search?search=$search&items=" . ($isBookmarked ? 'bookmarked' : 'all'));
|
||||
$fields = [Field::EQ('content', $search, '@search')];
|
||||
if ($isBookmarked) $fields[] = Data::bookmarkField(Table::ITEM);
|
||||
$list = new static('Matching' . ($isBookmarked ? ' Bookmarked' : ''),
|
||||
"/search?search=$search&items=" . ($isBookmarked ? 'bookmarked' : 'all'), $fields,
|
||||
' ' . Table::ITEM . ".data->>'" . Configuration::idField() . "' IN "
|
||||
. '(SELECT ROWID FROM item_search WHERE content MATCH @search)');
|
||||
$list->showIndicators = true;
|
||||
$list->displayFeed = true;
|
||||
return $list;
|
||||
@@ -171,34 +152,33 @@ class ItemList {
|
||||
*/
|
||||
public function render(): void
|
||||
{
|
||||
if ($this->isError()) { ?>
|
||||
<p>Error retrieving list:<br><?=$this->error?><?php
|
||||
if ($this->isError()) {
|
||||
echo "<p>Error retrieving list:<br>$this->error";
|
||||
return;
|
||||
}
|
||||
$item = $this->items->fetchArray(SQLITE3_ASSOC);
|
||||
$return = $this->returnURL == '' ? '' : '&from=' . urlencode($this->returnURL);
|
||||
$return = $this->returnURL == '' ? '' : '&from=' . urlencode($this->returnURL);
|
||||
$hasItems = false;
|
||||
echo '<article>';
|
||||
if ($item) {
|
||||
while ($item) { ?>
|
||||
<p><?=hx_get("/item?id={$item['id']}$return", strip_tags($item['item_title']))?><br>
|
||||
<small><?php
|
||||
if ($this->showIndicators) {
|
||||
if (!$item['is_read']) echo '<strong>Unread</strong> ';
|
||||
if ($item['is_bookmarked']) echo '<strong>Bookmarked</strong> ';
|
||||
}
|
||||
echo '<em>' . date_time($item['as_of']) . '</em>';
|
||||
if ($this->linkFeed) {
|
||||
echo ' • ' .
|
||||
hx_get("/feed/items?id={$item['feed_id']}&" . strtolower($this->itemType),
|
||||
htmlentities($item['feed_title']));
|
||||
} elseif ($this->displayFeed) {
|
||||
echo ' • ' . htmlentities($item['feed_title']);
|
||||
} ?>
|
||||
</small><?php
|
||||
$item = $this->items->fetchArray(SQLITE3_ASSOC);
|
||||
iterator_apply($this->dbList->items(), function (ItemWithFeed $it) use (&$hasItems, $return)
|
||||
{
|
||||
$hasItems = true;
|
||||
echo '<p>' . hx_get("/item?id=$it->id$return", strip_tags($it->title)) . '<br><small>';
|
||||
if ($this->showIndicators) {
|
||||
if (!$it->isRead()) echo '<strong>Unread</strong> ';
|
||||
if ($it->isBookmarked()) echo '<strong>Bookmarked</strong> ';
|
||||
}
|
||||
} else { ?>
|
||||
<p><em>There are no <?=strtolower($this->itemType)?> items</em><?php
|
||||
echo '<em>' . date_time($it->updated_on ?? $it->published_on) . '</em>';
|
||||
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 '</small>';
|
||||
});
|
||||
if (!$hasItems) {
|
||||
echo '<p><em>There are no ' . strtolower($this->itemType) . ' items</em>';
|
||||
}
|
||||
echo '</article>';
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user