[mod] alles mögliche verbessert [mod] SQLite-Anbindung

This commit is contained in:
roydfalk 2025-05-22 23:57:03 +00:00
parent 3497430fa4
commit 42e6b13597
18 changed files with 1151 additions and 389 deletions

View file

@ -3,6 +3,4 @@
## Zu erledigen
- sanitizing überprüfen
- Persistenz mit SQLite
- Datum für Dokumente ergänzen
- für jedes bestehende Dokument soll es Funktionen zum Aufbereiten und Herunderladen geben in den Formaten pdf/odt (pandoc nutzen) und ogg

View file

@ -34,6 +34,7 @@ proof-of-concept für Partei-Arbeits-Dokumenten-Verwaltung, welche hörbare Vers
### Voraussetzungen
- [PHP](https://www.php.net/)-Interpreter für Kommandozeile (Debian-Paket-Name: `php-cli`)
- SQLite3-Modul für PHP (Debian-Paket-Name: `php-sqlite3`)
- [FFmpeg](https://ffmpeg.org/) (Debian-Paket-Name: `ffmpeg`)
- Browser

43
source/entities/doc.php Normal file
View file

@ -0,0 +1,43 @@
<?php
namespace rosavox\entities\doc;
/**
*/
class entity
{
public string $title;
public array $authors;
public string $content;
public ?string $reasoning;
public function __construct(
string $title,
array $authors,
string $content,
?string $reasoning
)
{
$this->title = $title;
$this->authors = $authors;
$this->content = $content;
$this->reasoning = $reasoning;
}
}
/**
*/
function empty_() : entity
{
return (new entity(
'',
[],
'',
null,
));
}

39
source/helpers/list.php Normal file
View file

@ -0,0 +1,39 @@
<?php
namespace rosavox\helpers\list_;
/**
*/
function sequence(int $length) : array
{
return (($length <= 0) ? [] : \array_merge(sequence($length-1), [$length-1]));
}
/**
*/
function map(array $list, \Closure $function) : array
{
$result = [];
foreach ($list as $element)
{
\array_push($result, ($function)($element));
}
return $result;
}
/**
*/
function to_map(array $pairs) : array
{
$result = [];
foreach ($pairs as $pair)
{
$result[$pair['key']] = $pair['value'];
}
return $result;
}
?>

112
source/helpers/sqlite.php Normal file
View file

@ -0,0 +1,112 @@
<?php
namespace rosavox\helpers\sqlite;
/**
*/
function placeholder(
string $name
) : string
{
return (':' . $name);
}
/**
*/
function map_type(string $type) : string
{
switch ($type)
{
case 'bool':
{
return 'INTEGER';
break;
}
case 'integer':
{
return 'INTEGER';
break;
}
case 'string':
{
return 'TEXT';
break;
}
default:
{
throw (new \Exception('unhandled type: ' . $type));
break;
}
}
}
/**
*/
function format_value($value) : string
{
if ($value === null)
{
return 'NULL';
}
else
{
$type = \gettype($value);
switch ($type)
{
case 'bool':
{
return ($value ? '1' : '0');
break;
}
case 'int':
{
return \sprintf('%u', $value);
break;
}
case 'string':
{
return \sprintf("'%s'", $value);
break;
}
default:
{
throw (new \Exception('unhandled type: ' . $type));
break;
}
}
}
}
/**
*/
function query(string $path, string $template, array $arguments) : array
{
/*
\error_log(
\json_encode(
[
'template' => $template,
'arguments' => $arguments,
]
)
);
*/
$connection = new \SQLite3($path);
$statement = $connection->prepare($template);
foreach ($arguments as $key => $value)
{
$statement->bindValue(placeholder($key), $value);
}
$result = $statement->execute();
$last_insert_id = $connection->lastInsertRowID();
return [
'result' => $result,
'last_insert_id' => $last_insert_id,
];
}
?>

View file

@ -0,0 +1,25 @@
<?php
namespace rosavox\helpers\storage;
/**
* @template Key
* @template Value
*/
interface interface_
{
public function setup() : void;
public function list_() : array;
public function read(/*Key */$id)/* : Value*/;
public function create(/*Value */$value)/* : Key*/;
public function update(/*Key */$id, /*Value */$value) : void;
public function delete(/*Key */$id) : void;
}
?>

View file

@ -2,32 +2,31 @@
namespace rosavox\helpers\storage;
/**
*/
interface interface_
{
public function list_() : array;
public function read(int $id);
public function create($value) : int;
public function update(int $id, $value) : void;
public function delete(int $id) : void;
}
require_once('helpers/storage-interface.php');
/**
*/
class class_jsonfile implements interface_
class class_jsonfile implements interface_/*<int,any>*/
{
/**
*/
private string $path;
/**
*/
private \Closure $id_encode;
/**
*/
private \Closure $id_decode;
/**
*/
public function __construct(
string $path,
\Closure $id_encode,
@ -39,6 +38,9 @@ class class_jsonfile implements interface_
$this->id_decode = $id_decode;
}
/**
*/
private function get() : array
{
$content = (\file_exists($this->path) ? \file_get_contents($this->path) : null);
@ -51,12 +53,26 @@ class class_jsonfile implements interface_
);
}
/**
*/
private function put(array $data) : void
{
$content = \json_encode($data, \JSON_PRETTY_PRINT);
\file_put_contents($this->path, $content);
}
/**
*/
public function setup() : void
{
// do nothing
}
/**
*/
public function list_() : array
{
$data = $this->get();
@ -69,7 +85,10 @@ class class_jsonfile implements interface_
);
}
public function read(int $id)
/**
*/
public function read($id)
{
$data = $this->get();
$id_encoded = ($this->id_encode)($id);
@ -83,7 +102,7 @@ class class_jsonfile implements interface_
}
}
public function create($value) : int
public function create($value)
{
$data = $this->get();
$id = ($data['last_id'] + 1);
@ -94,7 +113,10 @@ class class_jsonfile implements interface_
return $id;
}
public function update(int $id, $value) : void
/**
*/
public function update($id, $value) : void
{
$data = $this->get();
$id_encoded = ($this->id_encode)($id);
@ -109,7 +131,10 @@ class class_jsonfile implements interface_
}
}
public function delete(int $id) : void
/**
*/
public function delete($id) : void
{
$data = $this->get();
$id_encoded = ($this->id_encode)($id);
@ -123,6 +148,7 @@ class class_jsonfile implements interface_
$this->put($data);
}
}
}
?>

View file

@ -0,0 +1,311 @@
<?php
namespace rosavox\helpers\storage;
require_once('helpers/list.php');
require_once('helpers/sqlite.php');
require_once('helpers/storage-interface.php');
/**
*/
class class_sqlitetable implements interface_/*<int,list<any>>*/
{
/**
*/
public static function field_name(
array $field
) : string
{
return \rosavox\helpers\string_\coin(
'field_{{name}}',
[
'name' => $field['name'],
]
);
}
/**
*/
private string $path;
/**
*/
private string $name;
/**
* @param $fields {
* list<
* record<
* name:string,
* type:string,
* nullable:boolean,
* >
* >
* }
*/
private array $fields;
/**
*/
public function __construct(
string $path,
string $name,
array $fields
)
{
$this->path = $path;
$this->name = $name;
$this->fields = $fields;
}
/**
*/
public function setup() : void
{
$result = \rosavox\helpers\sqlite\query(
$this->path,
\rosavox\helpers\string_\coin(
'CREATE TABLE IF NOT EXISTS {{name}}(id INTEGER PRIMARY KEY AUTOINCREMENT, {{fields}});',
[
'name' => $this->name,
'fields' => \implode(
', ',
\array_map(
fn ($field) => \rosavox\helpers\string_\coin(
'{{name}} {{type}}{{macro_nullable}}',
[
'name' => self::field_name($field),
'type' => \rosavox\helpers\sqlite\map_type($field['type']),
'macro_nullable' => ($field['nullable'] ? '' : ' NOT NULL'),
]
),
$this->fields
)
),
]
),
[
]
);
}
/**
*/
public function list_() : array
{
$result = \rosavox\helpers\sqlite\query(
$this->path,
\rosavox\helpers\string_\coin(
'SELECT id,{{fields}} FROM {{name}}',
[
'fields' => \implode(
', ',
\rosavox\helpers\list_\map(
$this->fields,
fn ($field) => self::field_name($field)
)
),
'name' => $this->name,
]
),
[
]
);
$entries = [];
while (true)
{
$row = $result['result']->fetchArray(\SQLITE3_NUM);
if ($row !== false)
{
\array_push(
$entries,
[
'id' => $row[0],
'value' => \array_slice($row, 1),
]
);
}
else
{
break;
}
}
return $entries;
}
/**
*/
public function read($id)
{
$result = \rosavox\helpers\sqlite\query(
$this->path,
\rosavox\helpers\string_\coin(
'SELECT {{fields}} FROM {{name}} WHERE (id = {{id}});',
[
'fields' => \implode(
', ',
\rosavox\helpers\list_\map(
$this->fields,
fn ($field) => self::field_name($field)
)
),
'name' => $this->name,
'id' => \rosavox\helpers\sqlite\placeholder('id'),
]
),
[
'id' => $id,
]
);
$rows = [];
while (true)
{
$row = $result['result']->fetchArray(\SQLITE3_NUM);
if ($row !== false)
{
\array_push($rows, $row);
}
else
{
break;
}
}
$count = \count($rows);
if ($count === 0)
{
throw (new \Exception('not found'));
}
else
{
if ($count > 1)
{
throw (new \Exception('ambiguous'));
}
else
{
return $rows[0];
}
}
}
/**
*/
public function create($row)
{
$result = \rosavox\helpers\sqlite\query(
$this->path,
\rosavox\helpers\string_\coin(
'INSERT INTO {{name}}({{schema}}) VALUES ({{values}});',
[
'name' => $this->name,
'schema' => \implode(
', ',
\array_map(
fn ($field) => self::field_name($field),
$this->fields
)
),
'values' => \implode(
', ',
\array_map(
fn ($field) => \rosavox\helpers\sqlite\placeholder(
self::field_name($field)
),
$this->fields
)
),
]
),
\rosavox\helpers\list_\to_map(
\rosavox\helpers\list_\map(
\rosavox\helpers\list_\sequence(
\count($this->fields)
),
fn ($index) => [
'key' => self::field_name($this->fields[$index]),
'value' => $row[$index],
]
)
)
);
return $result['last_insert_id'];
}
/**
*/
public function update($id, $row) : void
{
$result = \rosavox\helpers\sqlite\query(
$this->path,
\rosavox\helpers\string_\coin(
'UPDATE {{name}} SET {{sets}} WHERE (id = :id);',
[
'name' => $this->name,
'sets' => \implode(
', ',
\array_map(
fn ($field) => \rosavox\helpers\string_\coin(
'{{name}} = {{placeholder}}',
[
'name' => self::field_name($field),
'placeholder' => \rosavox\helpers\sqlite\placeholder(self::field_name($field)),
]
),
$this->fields
)
),
]
),
\array_merge(
[
'id' => $id,
],
\rosavox\helpers\list_\to_map(
\rosavox\helpers\list_\map(
\rosavox\helpers\list_\sequence(
\count($this->fields)
),
fn ($index) => [
'key' => self::field_name($this->fields[$index]),
'value' => $row[$index],
]
)
)
)
);
}
/**
*/
public function delete($id) : void
{
$result = \rosavox\helpers\sqlite\query(
$this->path,
\rosavox\helpers\string_\coin(
'DELETE * FROM {{name}} WHERE (id = {{id}});',
[
'name' => $this->name,
'id' => \rosavox\helpers\sqlite\placeholder('id'),
]
),
[
'id' => $id,
]
);
}
}
?>

View file

@ -1,208 +1,5 @@
<?php
require_once('helpers/string.php');
require_once('helpers/misc.php');
require_once('services/doc.php');
\rosavox\services\doc\init();
$mode = ($_GET['mode'] ?? 'list');
$id_encoded = (! empty($_GET['id']) ? $_GET['id'] : null);
$id = (($id_encoded === null) ? null : \intval($id_encoded));
/**
*/
function make_link(string $mode, array $args) : string
{
$a = \array_merge($args, ['mode' => $mode]);
return (
'?'
.
\implode(
'&',
\array_map(
fn ($key) => \rosavox\helpers\string_\coin(
'{{key}}={{value}}',
[
'key' => $key,
'value' => $a[$key],
]
),
\array_keys($a)
)
)
);
}
/**
*/
function nav(string $mode, array $args) : void
{
\rosavox\helpers\misc\navigate(make_link($mode, $args));
}
/**
*/
function render_list() : string
{
return \rosavox\helpers\misc\render(
'docs-list',
[
'label_make' => \rosavox\helpers\misc\translate('action.make'),
'link_make' => make_link('make', []),
'entries' => \implode(
"\n",
\array_map(
fn ($entry) => \rosavox\helpers\misc\render(
'docs-list-entry',
[
'label_read' => \rosavox\helpers\misc\translate('action.read'),
'value_text' => $entry['value']['title'],
'value_link_open' => make_link('edit', ['id' => \sprintf('%u', $entry['id'])]),
'value_link_read' => \rosavox\services\doc\readable_path($entry['id']),
'area_download_readable' => (
(fn ($name, $path) => (
(! \file_exists($path))
?
\rosavox\helpers\misc\render(
'state-waiting',
[
'info' => \rosavox\helpers\misc\translate('state.generating'),
]
)
:
\rosavox\helpers\misc\render(
'download',
[
'text' => \rosavox\helpers\misc\translate('item.readable.short'),
'tooltip' => \rosavox\helpers\string_\coin(
'{{item}} {{action}}',
[
'action' => \rosavox\helpers\misc\translate('action.download'),
'item' => \rosavox\helpers\misc\translate('item.readable.long'),
]
),
'link' => $path,
'name' => \rosavox\helpers\string_\coin(
'{{name}}.md',
[
'name' => $name,
]
),
'type' => 'text/markdown',
]
)
)) (
\rosavox\services\doc\name($entry['id']),
\rosavox\services\doc\readable_path($entry['id'])
)
),
'area_download_audible' => (
(fn ($name, $path) => (
(! \file_exists($path))
?
\rosavox\helpers\misc\render(
'state-waiting',
[
'info' => \rosavox\helpers\misc\translate('state.generating'),
]
)
:
\rosavox\helpers\misc\render(
'download',
[
'text' => \rosavox\helpers\misc\translate('item.audible.short'),
'tooltip' => \rosavox\helpers\string_\coin(
'{{item}} {{action}}',
[
'action' => \rosavox\helpers\misc\translate('action.download'),
'item' => \rosavox\helpers\misc\translate('item.audible.long'),
]
),
'link' => $path,
'name' => \rosavox\helpers\string_\coin(
'{{name}}.oga',
[
'name' => $name,
]
),
'type' => 'audio/ogg',
]
)
)) (
\rosavox\services\doc\name($entry['id']),
\rosavox\services\doc\audible_path($entry['id'])
)
),
'area_hear' => (
(fn ($name, $path) => (
(! \file_exists($path))
?
\rosavox\helpers\misc\render(
'state-waiting',
[
'info' => \rosavox\helpers\misc\translate('state.generating'),
]
)
:
\rosavox\helpers\misc\render(
'player',
[
'source_path' => $path,
]
)
)) (
\rosavox\services\doc\name($entry['id']),
\rosavox\services\doc\audible_path($entry['id'])
)
),
]
),
\rosavox\services\doc\list_()
)
),
]
);
}
/**
*/
function render_edit(?int $id) : string
{
$doc = (
($id === null)
?
\rosavox\services\doc\empty_()
:
\rosavox\services\doc\read($id)
);
return \rosavox\helpers\misc\render(
'docs-edit',
[
'label_action_back' => \rosavox\helpers\misc\translate('action.back'),
'label_action_save' => \rosavox\helpers\misc\translate('action.save'),
'label_action_delete' => \rosavox\helpers\misc\translate('action.delete'),
'label_doc_title' => \rosavox\helpers\misc\translate('domain.doc.title'),
'label_doc_authors' => \rosavox\helpers\misc\translate('domain.doc.authors'),
'label_doc_content' => \rosavox\helpers\misc\translate('domain.doc.content'),
'label_doc_reasoning' => \rosavox\helpers\misc\translate('domain.doc.reasoning'),
'value_action_back' => make_link('list', []),
'value_action_save' => make_link('save', (($id === null) ? [] : ['id' => \strval($id)])),
'value_action_delete' => make_link('delete', (($id === null) ? [] : ['id' => \strval($id)])),
'value_doc_title' => $doc['title'],
'value_doc_authors' => \implode(', ', $doc['authors']),
'value_doc_content' => $doc['content'],
'value_doc_reasoning' => ($doc['reasoning'] ?? ''),
]
);
}
require_once('main.php');
?>
<!DOCTYPE html>
<html>
@ -213,63 +10,6 @@ function render_edit(?int $id) : string
</head>
<body>
<h1>rosavox</h1>
<?php
switch ($mode)
{
case 'delete':
{
if ($id === null)
{
// do nothing
}
else
{
\rosavox\services\doc\delete($id);
}
nav('list', []);
break;
}
case 'save':
{
$doc = [
'title' => $_POST['title'],
'authors' => \explode(',', $_POST['authors']),
'content' => $_POST['content'],
'reasoning' => (empty($_POST['reasoning']) ? null : $_GET['reasoning']),
];
if ($id === null)
{
$id = \rosavox\services\doc\create($doc);
}
else
{
\rosavox\services\doc\update($id, $doc);
}
// nav('edit', ['id' => \rosavox\services\doc\id_encode($id)]);
nav('list', []);
break;
}
case 'list':
{
echo(render_list());
break;
}
case 'make':
{
echo(render_edit(null));
break;
}
case 'edit':
{
echo(render_edit($id));
break;
}
default:
{
throw (new \Exception(\sprintf('invalid mode: %s', $mode)));
break;
}
}
?>
<?php \rosavox\main($_GET, $_POST); ?>
</body>
</html>

90
source/main.php Normal file
View file

@ -0,0 +1,90 @@
<?php
namespace rosavox;
require_once('entities/doc.php');
require_once('services/doc.php');
require_once('nav.php');
require_once('renderings.php');
/**
*/
function main(array $get, array $post) : void
{
// init
\rosavox\services\doc\init();
// args
$mode = (
$get['mode']
??
'list'
);
$id = (
(! empty($get['id']))
?
\intval($get['id'])
:
null
);
// exec
switch ($mode)
{
case 'delete':
{
if ($id === null)
{
// do nothing
}
else
{
\rosavox\services\doc\remove($id);
}
nav('list', []);
break;
}
case 'save':
{
$doc = new \rosavox\entities\doc\entity(
$post['title'],
\explode(',', $post['authors']),
$post['content'],
(empty($post['reasoning']) ? null : $post['reasoning'])
);
if ($id === null)
{
$id = \rosavox\services\doc\add($doc);
}
else
{
\rosavox\services\doc\change($id, $doc);
}
// \rosavox\nav('edit', ['id' => \strval($id)]);
\rosavox\nav('list', []);
break;
}
case 'list':
{
echo(\rosavox\renderings\doc_list());
break;
}
case 'make':
{
echo(\rosavox\renderings\doc_edit(null));
break;
}
case 'edit':
{
echo(\rosavox\renderings\doc_edit($id));
break;
}
default:
{
throw (new \Exception(\sprintf('invalid mode: %s', $mode)));
break;
}
}
}
?>

42
source/nav.php Normal file
View file

@ -0,0 +1,42 @@
<?php
namespace rosavox;
require_once('helpers/list.php');
require_once('helpers/string.php');
require_once('helpers/misc.php');
/**
*/
function make_link(string $mode, array $args) : string
{
$a = \array_merge($args, ['mode' => $mode]);
return (
'?'
.
\implode(
'&',
\rosavox\helpers\list_\map(
\array_keys($a),
fn ($key) => \rosavox\helpers\string_\coin(
'{{key}}={{value}}',
[
'key' => $key,
'value' => $a[$key],
]
)
)
)
);
}
/**
*/
function nav(string $mode, array $args) : void
{
\rosavox\helpers\misc\navigate(make_link($mode, $args));
}
?>

170
source/renderings.php Normal file
View file

@ -0,0 +1,170 @@
<?php
namespace rosavox\renderings;
require_once('helpers/list.php');
require_once('helpers/string.php');
require_once('helpers/misc.php');
require_once('entities/doc.php');
require_once('services/doc.php');
require_once('nav.php');
/**
*/
function doc_list() : string
{
return \rosavox\helpers\misc\render(
'docs-list',
[
'label_make' => \rosavox\helpers\misc\translate('action.make'),
'link_make' => \rosavox\make_link('make', []),
'entries' => \implode(
"\n",
\rosavox\helpers\list_\map(
\rosavox\services\doc\dump(),
fn ($entry) => \rosavox\helpers\misc\render(
'docs-list-entry',
[
'label_read' => \rosavox\helpers\misc\translate('action.read'),
'value_text' => $entry['value']->title,
'value_link_open' => \rosavox\make_link('edit', ['id' => \sprintf('%u', $entry['id'])]),
'value_link_read' => \rosavox\services\doc\readable_path($entry['id']),
'area_download_readable' => (
(fn ($name, $path) => (
(! \file_exists($path))
?
\rosavox\helpers\misc\render(
'state-waiting',
[
'info' => \rosavox\helpers\misc\translate('state.generating'),
]
)
:
\rosavox\helpers\misc\render(
'download',
[
'text' => \rosavox\helpers\misc\translate('item.readable.short'),
'tooltip' => \rosavox\helpers\string_\coin(
'{{item}} {{action}}',
[
'action' => \rosavox\helpers\misc\translate('action.download'),
'item' => \rosavox\helpers\misc\translate('item.readable.long'),
]
),
'link' => $path,
'name' => \rosavox\helpers\string_\coin(
'{{name}}.md',
[
'name' => $name,
]
),
'type' => 'text/markdown',
]
)
)) (
\rosavox\services\doc\name($entry['id']),
\rosavox\services\doc\readable_path($entry['id'])
)
),
'area_download_audible' => (
(fn ($name, $path) => (
(! \file_exists($path))
?
\rosavox\helpers\misc\render(
'state-waiting',
[
'info' => \rosavox\helpers\misc\translate('state.generating'),
]
)
:
\rosavox\helpers\misc\render(
'download',
[
'text' => \rosavox\helpers\misc\translate('item.audible.short'),
'tooltip' => \rosavox\helpers\string_\coin(
'{{item}} {{action}}',
[
'action' => \rosavox\helpers\misc\translate('action.download'),
'item' => \rosavox\helpers\misc\translate('item.audible.long'),
]
),
'link' => $path,
'name' => \rosavox\helpers\string_\coin(
'{{name}}.oga',
[
'name' => $name,
]
),
'type' => 'audio/ogg',
]
)
)) (
\rosavox\services\doc\name($entry['id']),
\rosavox\services\doc\audible_path($entry['id'])
)
),
'area_hear' => (
(fn ($name, $path) => (
(! \file_exists($path))
?
\rosavox\helpers\misc\render(
'state-waiting',
[
'info' => \rosavox\helpers\misc\translate('state.generating'),
]
)
:
\rosavox\helpers\misc\render(
'player',
[
'source_path' => $path,
]
)
)) (
\rosavox\services\doc\name($entry['id']),
\rosavox\services\doc\audible_path($entry['id'])
)
),
]
)
)
),
]
);
}
/**
*/
function doc_edit(?int $id) : string
{
$doc = (
($id === null)
?
\rosavox\entities\doc\empty_()
:
\rosavox\services\doc\get($id)
);
return \rosavox\helpers\misc\render(
'docs-edit',
[
'label_action_back' => \rosavox\helpers\misc\translate('action.back'),
'label_action_save' => \rosavox\helpers\misc\translate('action.save'),
'label_action_delete' => \rosavox\helpers\misc\translate('action.delete'),
'label_doc_title' => \rosavox\helpers\misc\translate('domain.doc.title'),
'label_doc_authors' => \rosavox\helpers\misc\translate('domain.doc.authors'),
'label_doc_content' => \rosavox\helpers\misc\translate('domain.doc.content'),
'label_doc_reasoning' => \rosavox\helpers\misc\translate('domain.doc.reasoning'),
'value_action_back' => \rosavox\make_link('list', []),
'value_action_save' => \rosavox\make_link('save', (($id === null) ? [] : ['id' => \strval($id)])),
'value_action_delete' => \rosavox\make_link('delete', (($id === null) ? [] : ['id' => \strval($id)])),
'value_doc_title' => $doc->title,
'value_doc_authors' => \implode(', ', $doc->authors),
'value_doc_content' => $doc->content,
'value_doc_reasoning' => ($doc->reasoning ?? ''),
]
);
}
?>

164
source/repositories/doc.php Normal file
View file

@ -0,0 +1,164 @@
<?php
namespace rosavox\repositories\doc;
require_once('helpers/storage-interface.php');
require_once('helpers/storage-jsonfile.php');
require_once('helpers/storage-sqlitetable.php');
require_once('entities/doc.php');
/**
*/
class repo implements \rosavox\helpers\storage\interface_/*<int,\rosavox\entities\doc\entity>*/
{
/**
*/
private \rosavox\helpers\storage\interface_ $core;
/**
*/
private function __construct()
{
/*
$this->core = new \rosavox\helpers\storage\class_jsonfile(
'docs.json',
fn ($id) => \sprintf('%u', $id),
fn ($id_encoded) => \intval($id_encoded)
);
*/
$this->core = new \rosavox\helpers\storage\class_sqlitetable(
'data.sqlite',
'docs',
[
[
'name' => 'title',
'type' => 'string',
'nullable' => false,
],
[
'name' => 'authors',
'type' => 'string',
'nullable' => false,
],
[
'name' => 'content',
'type' => 'string',
'nullable' => false,
],
[
'name' => 'reasoning',
'type' => 'string',
'nullable' => true,
],
]
);
}
/**
*/
private static ?repo $instance = null;
/**
*/
public static function get_instance()
{
if (self::$instance === null)
{
self::$instance = new self();
}
return self::$instance;
}
/**
*/
private static function encode(\rosavox\entities\doc\entity $doc) : array
{
return [
$doc->title,
\implode(',', $doc->authors),
$doc->content,
$doc->reasoning,
];
}
/**
*/
private static function decode(array $row) : \rosavox\entities\doc\entity
{
return (new \rosavox\entities\doc\entity(
$row[0],
\explode(',', $row[1]),
$row[2],
$row[3],
));
}
/**
* [implementation]
*/
public function setup() : void
{
$this->core->setup();
}
/**
* [implementation]
*/
public function list_() : array
{
return \rosavox\helpers\list_\map(
$this->core->list_(),
fn ($entry) => [
'id' => $entry['id'],
'value' => self::decode($entry['value']),
]
);
}
/**
* [implementation]
*/
public function read($id)
{
return self::decode($this->core->read($id));
}
/**
* [implementation]
*/
public function create($doc)
{
return $this->core->create(self::encode($doc));
}
/**
* [implementation]
*/
public function update($id, $doc) : void
{
$this->core->update($id, self::encode($doc));
}
/**
* [implementation]
*/
public function delete($id) : void
{
$this->core->delete($id);
}
}

View file

@ -3,16 +3,10 @@
namespace rosavox\services\doc;
require_once('helpers/string.php');
require_once('helpers/storage.php');
require_once('helpers/misc.php');
/**
*/
class state
{
public static ?\rosavox\helpers\storage\interface_ $storage = null;
}
require_once('entities/doc.php');
require_once('repositories/doc.php');
/**
@ -41,7 +35,7 @@ function audible_path(int $id) : string
/**
*/
function generate_readable(int $id, $doc) : void
function generate_readable(int $id, \rosavox\entities\doc\entity $doc) : void
{
$markdown = \rosavox\helpers\string_\coin(
"# {{value_title}}
@ -58,7 +52,7 @@ function generate_readable(int $id, $doc) : void
{{macro_reasoning}}",
[
'label_title' => \rosavox\helpers\misc\translate('domain.doc.title'),
'value_title' => $doc['title'],
'value_title' => $doc->title,
'label_authors' => \rosavox\helpers\misc\translate('domain.doc.authors'),
'value_authors' => \implode(
"\n",
@ -69,13 +63,13 @@ function generate_readable(int $id, $doc) : void
'author' => $author,
]
),
$doc['authors']
$doc->authors
)
),
'label_content' => \rosavox\helpers\misc\translate('domain.doc.content'),
'value_content' => $doc['content'],
'value_content' => $doc->content,
'macro_reasoning' => (
($doc['reasoning'] === null)
($doc->reasoning === null)
?
''
:
@ -86,7 +80,7 @@ function generate_readable(int $id, $doc) : void
{{value_reasoning}}",
[
'label_reasoning' => \rosavox\helpers\misc\translate('domain.doc.reasoning'),
'value_reasoning' => $doc['reasoning'],
'value_reasoning' => $doc->reasoning,
]
)
)
@ -101,9 +95,7 @@ function generate_readable(int $id, $doc) : void
/**
*/
function remove_readable(
int $id
) : void
function remove_readable(int $id) : void
{
$path = readable_path($id);
if (! \file_exists($path))
@ -119,10 +111,7 @@ function remove_readable(
/**
*/
function generate_audible(
int $id,
$doc
) : void
function generate_audible(int $id, \rosavox\entities\doc\entity $doc) : void
{
$pause = " .\n";
$text = \rosavox\helpers\string_\coin(
@ -130,13 +119,13 @@ function generate_audible(
[
'pause' => $pause,
'label_title' => \rosavox\helpers\misc\translate('domain.doc.title'),
'value_title' => $doc['title'],
'value_title' => $doc->title,
'label_authors' => \rosavox\helpers\misc\translate('domain.doc.authors'),
'value_authors' => \implode($pause, $doc['authors']),
'value_authors' => \implode($pause, $doc->authors),
'label_content' => \rosavox\helpers\misc\translate('domain.doc.content'),
'value_content' => $doc['content'],
'value_content' => $doc->content,
'macro_reasoning' => (
($doc['reasoning'] === null)
($doc->reasoning === null)
?
''
:
@ -145,7 +134,7 @@ function generate_audible(
[
'pause' => $pause,
'label_reasoning' => \rosavox\helpers\misc\translate('domain.doc.reasoning'),
'value_reasoning' => $doc['reasoning'],
'value_reasoning' => $doc->reasoning,
]
)
)
@ -163,9 +152,7 @@ function generate_audible(
/**
*/
function remove_audible(
int $id
) : void
function remove_audible(int $id) : void
{
$path = audible_path($id);
if (! \file_exists($path))
@ -179,41 +166,28 @@ function remove_audible(
}
/**
*/
function empty_() : array
function dump() : array
{
return [
'title' => '',
'authors' => [],
'content' => '',
'reasoning' => null,
];
return \rosavox\repositories\doc\repo::get_instance()->list_();
}
/**
*/
function list_() : array
function get(int $id) : \rosavox\entities\doc\entity
{
return state::$storage->list_();
return \rosavox\repositories\doc\repo::get_instance()->read($id);
}
/**
*/
function read(int $id) : array
function add(\rosavox\entities\doc\entity $doc) : int
{
return state::$storage->read($id);
}
/**
* @todo async generating
*/
function create(array $doc) : int
{
$id = state::$storage->create($doc);
$id = \rosavox\repositories\doc\repo::get_instance()->create($doc);
generate_readable($id, $doc);
generate_audible($id, $doc);
return $id;
@ -222,9 +196,40 @@ function create(array $doc) : int
/**
*/
function update(int $id, array $doc) : void
function add_examples() : void
{
state::$storage->update($id, $doc);
$entities = [
new \rosavox\entities\doc\entity(
'Freibier bei Parteitagen',
[
'Björn Biernot',
'Doreen Dauerdurst',
],
'Der Landesverband möge beschließen, dass zu Beginn eines jeden Parteitags für jeden Deligierten mindestens zwei Flaschen Bier auf den zugehörigen Platz zu stellen sind.',
'Wir haben Durst!',
),
new \rosavox\entities\doc\entity(
'Götterdämmerung',
[
'Fenriswolf',
],
'Der Allvater hat mich betrogen. Ich werde ihn und seine elende Asenbrut verschlingen. Diese Welt wird enden.',
null,
),
];
foreach ($entities as $entity)
{
add($entity);
}
}
/**
*/
function change(int $id, \rosavox\entities\doc\entity $doc) : void
{
\rosavox\repositories\doc\repo::get_instance()->update($id, $doc);
remove_readable($id);
remove_audible($id);
generate_readable($id, $doc);
@ -234,52 +239,21 @@ function update(int $id, array $doc) : void
/**
*/
function delete(int $id) : void
function remove(int $id) : void
{
state::$storage->delete($id);
\rosavox\repositories\doc\repo::get_instance()->delete($id);
remove_readable($id);
remove_audible($id);
}
/**
*/
function add_examples() : void
{
create(
[
'title' => 'Freibier bei Parteitagen',
'authors' => [
'Björn Biernot',
'Doreen Dauerdurst',
],
'content' => 'Der Landesverband möge beschließen, dass zu Beginn eines jeden Parteitags für jeden Deligierten mindestens zwei Flaschen Bier auf den zugehörigen Platz zu stellen sind.',
'reasoning' => 'Wir haben Durst!',
]
);
create(
[
'title' => 'Götterdämmerung',
'authors' => [
'Fenriswolf',
],
'content' => 'Der Allvater hat mich betrogen. Ich werde ihn und seine elende Asenbrut verschlingen. Diese Welt wird enden.',
'reasoning' => null,
]
);
}
/**
*/
function init() : void
{
state::$storage = new \rosavox\helpers\storage\class_jsonfile(
'docs.json',
fn ($id) => \sprintf('%u', $id),
fn ($id_encoded) => \intval($id_encoded)
);
if (empty(state::$storage->list_()))
\rosavox\repositories\doc\repo::get_instance()->setup();
if (empty(dump()))
{
add_examples();
}

View file

@ -82,16 +82,6 @@ button
font-size: 1.0em;
}
.docs-list-entry
{
margin-bottom: 16px;
}
.docs-list-entry > *
{
vertical-align: middle;
}
.state-waiting
{
cursor: progress;
@ -101,3 +91,37 @@ button
{
content: "\21A7 ";
}
.docs-list > ul
{
padding: 0;
margin: 0 0 0 16px;
}
.docs-list-entry
{
padding-top: 24px;
padding-bottom: 24px;
list-style-type: none;
}
.docs-list-entry:not(:first-child)
{
border-top: 2px solid hsl(var(--hue), 0%, 50%);
}
.docs-list-entry-main
{
}
.docs-list-entry-actions
{
margin-top: 8px;
display: block;
}
.docs-list-entry-actions > *
{
vertical-align: middle;
}

View file

@ -1,4 +1,4 @@
<div id="docs-edit">
<div class="docs-edit">
<form method="POST">
<label>
<span>{{label_doc_title}}</span>

View file

@ -1,9 +1,12 @@
<li class="docs-list-entry">
<a href="{{value_link_open}}">{{value_text}}</a>
|
{{area_download_readable}}
|
{{area_download_audible}}
|
{{area_hear}}
<div class="docs-list-entry-main">
<a href="{{value_link_open}}">{{value_text}}</a>
</div>
<div class="docs-list-entry-actions">
{{area_download_readable}}
|
{{area_download_audible}}
|
{{area_hear}}
</div>
</li>

View file

@ -1,4 +1,4 @@
<div id="docs-list">
<div class="docs-list">
<ul>
{{entries}}
</ul>