Documents and Documentation (beta 1) (#23)

- Change to SQLite document store
- Complete documentation on usage of Feed Reader Central
- Update INSTALLING.md for new installation procedures

Reviewed-on: #23
This commit was merged in pull request #23.
This commit is contained in:
2024-06-12 02:07:35 +00:00
parent 819979f2b2
commit 0c87392910
43 changed files with 1652 additions and 1142 deletions

View File

@@ -1,91 +1,130 @@
<?php
<?php declare(strict_types=1);
namespace FeedReaderCentral;
use BitBadger\PDODocument\{Configuration, Custom, Definition, DocumentException, Field};
use BitBadger\PDODocument\Mapper\StringMapper;
use DateTimeImmutable;
use DateTimeInterface;
use Exception;
/**
* A centralized place for data access for the application
*/
class Data {
/**
* Obtain a new connection to the database
* @return SQLite3 A new connection to the database
*/
public static function getConnection(): SQLite3 {
$db = new SQLite3(implode(DIRECTORY_SEPARATOR, [__DIR__, '..', 'data', DATABASE_NAME]));
$db->exec('PRAGMA foreign_keys = ON;');
return $db;
}
class Data
{
/**
* Create the search index and synchronization triggers for the item table
*
* @param SQLite3 $db The database connection on which these will be created
* @throws DocumentException If any is encountered
*/
public static function createSearchIndex(SQLite3 $db): void {
$db->exec("CREATE VIRTUAL TABLE item_search USING fts5(content, content='item', content_rowid='id')");
$db->exec(<<<'SQL'
CREATE TRIGGER item_ai AFTER INSERT ON item BEGIN
INSERT INTO item_search (rowid, content) VALUES (new.id, new.content);
END;
SQL);
$db->exec(<<<'SQL'
CREATE TRIGGER item_au AFTER UPDATE ON item BEGIN
INSERT INTO item_search (item_search, rowid, content) VALUES ('delete', old.id, old.content);
INSERT INTO item_search (rowid, content) VALUES (new.id, new.content);
END;
SQL);
$db->exec(<<<'SQL'
CREATE TRIGGER item_ad AFTER DELETE ON item BEGIN
INSERT INTO item_search (item_search, rowid, content) VALUES ('delete', old.id, old.content);
END;
SQL);
public static function createSearchIndex(): void
{
Custom::nonQuery("CREATE VIRTUAL TABLE item_search USING fts5(content, content='item', content_rowid='id')",
[]);
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, []);
Custom::nonQuery(<<<'SQL'
CREATE TRIGGER item_au AFTER UPDATE ON item BEGIN
INSERT INTO item_search (
item_search, rowid, content
) VALUES (
'delete', old.data->>'id', old.data->>'content'
);
INSERT INTO item_search (rowid, content) VALUES (new.data->>'id', new.data->>'content');
END;
SQL, []);
Custom::nonQuery(<<<'SQL'
CREATE TRIGGER item_ad AFTER DELETE ON item BEGIN
INSERT INTO item_search (
item_search, rowid, content
) VALUES (
'delete', old.data->>'id', old.data->>'content'
);
END;
SQL, []);
}
/**
* Make sure the expected tables exist
*
* @throws DocumentException If any is encountered
*/
public static function ensureDb(): void {
$db = self::getConnection();
$tables = array();
$tableQuery = $db->query("SELECT name FROM sqlite_master WHERE type = 'table'");
while ($table = $tableQuery->fetchArray(SQLITE3_NUM)) $tables[] = $table[0];
if (!in_array('frc_user', $tables)) {
$db->exec(<<<'SQL'
CREATE TABLE frc_user (
id INTEGER NOT NULL PRIMARY KEY,
email TEXT NOT NULL,
password TEXT NOT NULL)
SQL);
$db->exec('CREATE INDEX idx_user_email ON frc_user (email)');
public static function ensureDb(): void
{
$tables = Custom::array("SELECT name FROM sqlite_master WHERE type = 'table'", [], new StringMapper('name'));
if (!in_array(Table::USER, $tables)) {
Definition::ensureTable(Table::USER);
Definition::ensureFieldIndex(Table::USER, 'email', ['email']);
}
if (!in_array('feed', $tables)) {
$db->exec(<<<'SQL'
CREATE TABLE feed (
id INTEGER NOT NULL PRIMARY KEY,
user_id INTEGER NOT NULL,
url TEXT NOT NULL,
title TEXT,
updated_on TEXT,
checked_on TEXT,
FOREIGN KEY (user_id) REFERENCES frc_user (id))
SQL);
if (!in_array(Table::FEED, $tables)) {
Definition::ensureTable(Table::FEED);
Definition::ensureFieldIndex(Table::FEED, 'user', ['user_id']);
}
if (!in_array('item', $tables)) {
$db->exec(<<<'SQL'
CREATE TABLE item (
id INTEGER NOT NULL PRIMARY KEY,
feed_id INTEGER NOT NULL,
title TEXT NOT NULL,
item_guid TEXT NOT NULL,
item_link TEXT NOT NULL,
published_on TEXT NOT NULL,
updated_on TEXT,
content TEXT NOT NULL,
is_read BOOLEAN NOT NULL DEFAULT 0,
is_bookmarked BOOLEAN NOT NULL DEFAULT 0,
FOREIGN KEY (feed_id) REFERENCES feed (id))
SQL);
self::createSearchIndex($db);
if (!in_array(Table::ITEM, $tables)) {
Definition::ensureTable(Table::ITEM);
Definition::ensureFieldIndex(Table::ITEM, 'feed', ['feed_id', 'item_link']);
self::createSearchIndex();
}
$db->close();
$journalMode = Custom::scalar("PRAGMA journal_mode", [], new StringMapper('journal_mode'));
if ($journalMode <> 'wal') Custom::nonQuery("PRAGMA journal_mode = 'wal'", []);
}
/**
* Create a JSON field comparison to find bookmarked items
*
* @param bool $value The flag to set (optional; defaults to true)
* @param string $qualifier The table qualifier to include (optional; defaults to no qualifier)
* @return Field A field that will find bookmarked items
*/
public static function bookmarkField(bool $value = true, string $qualifier = ''): Field
{
$bookField = Field::EQ('is_bookmarked', ($value ? 1 : 0), ':book');
$bookField->qualifier = $qualifier;
return $bookField;
}
/**
* Create a JSON field comparison to find items for a given feed
*
* @param int $feedId The ID of the feed for which items should be retrieved
* @param string $qualifier The table qualifier to include (optional; defaults to no qualifier)
* @return Field A field to find items for the give feed
*/
public static function feedField(int $feedId, string $qualifier = ''): Field
{
$feedField = Field::EQ(Configuration::$idField, $feedId, ':feed');
$feedField->qualifier = $qualifier;
return $feedField;
}
/**
* Create a JSON field comparison to find unread items
*
* @param string $qualifier The table qualifier to include (optional; defaults to no qualifier)
* @return Field A field to find unread items
*/
public static function unreadField(string $qualifier = ''): Field
{
$readField = Field::EQ('is_read', 0, ':read');
$readField->qualifier = $qualifier;
return $readField;
}
/**
* Create a JSON field comparison to find items belonging to feeds to which the given user is subscribed
*
* @param string $qualifier The table qualifier to include (optional; defaults to no qualifier)
* @return Field A field to find feeds belonging to the given user
*/
public static function userIdField(string $qualifier = ''): Field
{
$userField = Field::EQ('user_id', $_SESSION[Key::USER_ID], ':user');
$userField->qualifier = $qualifier;
return $userField;
}
/**
@@ -94,21 +133,12 @@ class Data {
* @param ?string $value The date/time to be parsed and formatted
* @return string|null The date/time in `DateTimeInterface::ATOM` format, or `null` if the input cannot be parsed
*/
public static function formatDate(?string $value): ?string {
public static function formatDate(?string $value): ?string
{
try {
return $value ? (new DateTimeImmutable($value))->format(DateTimeInterface::ATOM) : null;
} catch (Exception) {
return null;
}
}
/**
* Return the last SQLite error message as a result array
*
* @param SQLite3 $db The database connection on which the error has occurred
* @return string[] ['error' => message] for last SQLite error message
*/
public static function error(SQLite3 $db): array {
return ['error' => 'SQLite error: ' . $db->lastErrorMsg()];
}
}