[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 ## Zu erledigen
- sanitizing überprüfen - sanitizing überprüfen
- Persistenz mit SQLite
- Datum für Dokumente ergänzen - 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 ### Voraussetzungen
- [PHP](https://www.php.net/)-Interpreter für Kommandozeile (Debian-Paket-Name: `php-cli`) - [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`) - [FFmpeg](https://ffmpeg.org/) (Debian-Paket-Name: `ffmpeg`)
- Browser - 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; namespace rosavox\helpers\storage;
/** require_once('helpers/storage-interface.php');
*/
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;
}
/** /**
*/ */
class class_jsonfile implements interface_ class class_jsonfile implements interface_/*<int,any>*/
{ {
/**
*/
private string $path; private string $path;
/**
*/
private \Closure $id_encode; private \Closure $id_encode;
/**
*/
private \Closure $id_decode; private \Closure $id_decode;
/**
*/
public function __construct( public function __construct(
string $path, string $path,
\Closure $id_encode, \Closure $id_encode,
@ -39,6 +38,9 @@ class class_jsonfile implements interface_
$this->id_decode = $id_decode; $this->id_decode = $id_decode;
} }
/**
*/
private function get() : array private function get() : array
{ {
$content = (\file_exists($this->path) ? \file_get_contents($this->path) : null); $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 private function put(array $data) : void
{ {
$content = \json_encode($data, \JSON_PRETTY_PRINT); $content = \json_encode($data, \JSON_PRETTY_PRINT);
\file_put_contents($this->path, $content); \file_put_contents($this->path, $content);
} }
/**
*/
public function setup() : void
{
// do nothing
}
/**
*/
public function list_() : array public function list_() : array
{ {
$data = $this->get(); $data = $this->get();
@ -69,7 +85,10 @@ class class_jsonfile implements interface_
); );
} }
public function read(int $id)
/**
*/
public function read($id)
{ {
$data = $this->get(); $data = $this->get();
$id_encoded = ($this->id_encode)($id); $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(); $data = $this->get();
$id = ($data['last_id'] + 1); $id = ($data['last_id'] + 1);
@ -94,7 +113,10 @@ class class_jsonfile implements interface_
return $id; return $id;
} }
public function update(int $id, $value) : void
/**
*/
public function update($id, $value) : void
{ {
$data = $this->get(); $data = $this->get();
$id_encoded = ($this->id_encode)($id); $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(); $data = $this->get();
$id_encoded = ($this->id_encode)($id); $id_encoded = ($this->id_encode)($id);
@ -123,6 +148,7 @@ class class_jsonfile implements interface_
$this->put($data); $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 <?php
require_once('main.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'] ?? ''),
]
);
}
?> ?>
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
@ -213,63 +10,6 @@ function render_edit(?int $id) : string
</head> </head>
<body> <body>
<h1>rosavox</h1> <h1>rosavox</h1>
<?php <?php \rosavox\main($_GET, $_POST); ?>
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;
}
}
?>
</body> </body>
</html> </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; namespace rosavox\services\doc;
require_once('helpers/string.php'); require_once('helpers/string.php');
require_once('helpers/storage.php');
require_once('helpers/misc.php'); require_once('helpers/misc.php');
require_once('entities/doc.php');
/** require_once('repositories/doc.php');
*/
class state
{
public static ?\rosavox\helpers\storage\interface_ $storage = null;
}
/** /**
@ -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( $markdown = \rosavox\helpers\string_\coin(
"# {{value_title}} "# {{value_title}}
@ -58,7 +52,7 @@ function generate_readable(int $id, $doc) : void
{{macro_reasoning}}", {{macro_reasoning}}",
[ [
'label_title' => \rosavox\helpers\misc\translate('domain.doc.title'), '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'), 'label_authors' => \rosavox\helpers\misc\translate('domain.doc.authors'),
'value_authors' => \implode( 'value_authors' => \implode(
"\n", "\n",
@ -69,13 +63,13 @@ function generate_readable(int $id, $doc) : void
'author' => $author, 'author' => $author,
] ]
), ),
$doc['authors'] $doc->authors
) )
), ),
'label_content' => \rosavox\helpers\misc\translate('domain.doc.content'), 'label_content' => \rosavox\helpers\misc\translate('domain.doc.content'),
'value_content' => $doc['content'], 'value_content' => $doc->content,
'macro_reasoning' => ( 'macro_reasoning' => (
($doc['reasoning'] === null) ($doc->reasoning === null)
? ?
'' ''
: :
@ -86,7 +80,7 @@ function generate_readable(int $id, $doc) : void
{{value_reasoning}}", {{value_reasoning}}",
[ [
'label_reasoning' => \rosavox\helpers\misc\translate('domain.doc.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( function remove_readable(int $id) : void
int $id
) : void
{ {
$path = readable_path($id); $path = readable_path($id);
if (! \file_exists($path)) if (! \file_exists($path))
@ -119,10 +111,7 @@ function remove_readable(
/** /**
*/ */
function generate_audible( function generate_audible(int $id, \rosavox\entities\doc\entity $doc) : void
int $id,
$doc
) : void
{ {
$pause = " .\n"; $pause = " .\n";
$text = \rosavox\helpers\string_\coin( $text = \rosavox\helpers\string_\coin(
@ -130,13 +119,13 @@ function generate_audible(
[ [
'pause' => $pause, 'pause' => $pause,
'label_title' => \rosavox\helpers\misc\translate('domain.doc.title'), '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'), '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'), 'label_content' => \rosavox\helpers\misc\translate('domain.doc.content'),
'value_content' => $doc['content'], 'value_content' => $doc->content,
'macro_reasoning' => ( 'macro_reasoning' => (
($doc['reasoning'] === null) ($doc->reasoning === null)
? ?
'' ''
: :
@ -145,7 +134,7 @@ function generate_audible(
[ [
'pause' => $pause, 'pause' => $pause,
'label_reasoning' => \rosavox\helpers\misc\translate('domain.doc.reasoning'), '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( function remove_audible(int $id) : void
int $id
) : void
{ {
$path = audible_path($id); $path = audible_path($id);
if (! \file_exists($path)) if (! \file_exists($path))
@ -179,41 +166,28 @@ function remove_audible(
} }
/** /**
*/ */
function empty_() : array function dump() : array
{ {
return [ return \rosavox\repositories\doc\repo::get_instance()->list_();
'title' => '',
'authors' => [],
'content' => '',
'reasoning' => null,
];
} }
/** /**
*/ */
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); $id = \rosavox\repositories\doc\repo::get_instance()->create($doc);
}
/**
* @todo async generating
*/
function create(array $doc) : int
{
$id = state::$storage->create($doc);
generate_readable($id, $doc); generate_readable($id, $doc);
generate_audible($id, $doc); generate_audible($id, $doc);
return $id; 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_readable($id);
remove_audible($id); remove_audible($id);
generate_readable($id, $doc); 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_readable($id);
remove_audible($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 function init() : void
{ {
state::$storage = new \rosavox\helpers\storage\class_jsonfile( \rosavox\repositories\doc\repo::get_instance()->setup();
'docs.json',
fn ($id) => \sprintf('%u', $id), if (empty(dump()))
fn ($id_encoded) => \intval($id_encoded)
);
if (empty(state::$storage->list_()))
{ {
add_examples(); add_examples();
} }

View file

@ -82,16 +82,6 @@ button
font-size: 1.0em; font-size: 1.0em;
} }
.docs-list-entry
{
margin-bottom: 16px;
}
.docs-list-entry > *
{
vertical-align: middle;
}
.state-waiting .state-waiting
{ {
cursor: progress; cursor: progress;
@ -101,3 +91,37 @@ button
{ {
content: "\21A7 "; 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"> <form method="POST">
<label> <label>
<span>{{label_doc_title}}</span> <span>{{label_doc_title}}</span>

View file

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

View file

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