Initial setup for framework
This commit is contained in:
parent
3ddce5e41f
commit
3cc233de3a
16 changed files with 720 additions and 0 deletions
6
DatabaseMigrations.php
Normal file
6
DatabaseMigrations.php
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
<?php
|
||||||
|
require_once("Settings.php");
|
||||||
|
|
||||||
|
$db = new Database(!file_exists(SQLITE_DB));
|
||||||
|
$db->runMigrations();
|
||||||
|
$db->close();
|
19
LastLog.php
Normal file
19
LastLog.php
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
<?php
|
||||||
|
require_once("Settings.php");
|
||||||
|
|
||||||
|
$limit = 100;
|
||||||
|
if (isset($argv[1]) && is_numeric($argv[1])) {
|
||||||
|
$limit = $argv[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
$db = new Database();
|
||||||
|
$stmt = $db::$handle->prepare("SELECT * FROM (SELECT * FROM `logs` ORDER BY datetime(`created`) DESC, `id` DESC LIMIT " . $limit . ") ORDER BY `created` ASC, `id` ASC");
|
||||||
|
$result = $stmt->execute();
|
||||||
|
|
||||||
|
while ($row = $result->fetchArray(SQLITE3_ASSOC)) {
|
||||||
|
$levelColor = Logger::getColor($row["level"]);
|
||||||
|
|
||||||
|
echo "\033[0;37m[" . $row["created"] . "] " . $levelColor . $row["level"] . "\033[0;37m: " . $row["message"] . "\033[0m" . PHP_EOL;
|
||||||
|
}
|
||||||
|
|
||||||
|
$db->close();
|
63
Loader.php
Normal file
63
Loader.php
Normal file
|
@ -0,0 +1,63 @@
|
||||||
|
<?php
|
||||||
|
class Loader {
|
||||||
|
protected static array $dirs = [
|
||||||
|
"LIB_MIGRATIONS_DIR",
|
||||||
|
"LIB_TASKS_DIR",
|
||||||
|
"LIB_TEMPLATE_DIR",
|
||||||
|
"LIB_LIB_DIR",
|
||||||
|
"TASKS_DIR",
|
||||||
|
"TEMPLATE_DIR",
|
||||||
|
"LIB_DIR"
|
||||||
|
];
|
||||||
|
|
||||||
|
protected static $classmap = [];
|
||||||
|
|
||||||
|
public static function _autoload($class) {
|
||||||
|
|
||||||
|
if (count(self::$classmap) === 0) {
|
||||||
|
self::buildRecursiveClassMap();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isset(self::$classmap[$class])) {
|
||||||
|
require_once(self::$classmap[$class]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static function buildRecursiveClassMap() {
|
||||||
|
foreach (self::$dirs as $dir) {
|
||||||
|
if (!defined($dir)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$dir = constant($dir);
|
||||||
|
|
||||||
|
if (!is_dir($dir) || !is_readable($dir)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
self::buildClassMap($dir);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static function buildClassMap($dir) {
|
||||||
|
$files = scandir($dir);
|
||||||
|
foreach ($files as $file) {
|
||||||
|
if ($file === "." || $file === "..") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (is_dir($file)) {
|
||||||
|
self::buildClassMap($file);
|
||||||
|
} else {
|
||||||
|
$parts = explode(".", $file);
|
||||||
|
$ext = array_pop($parts);
|
||||||
|
if ($ext === "php") {
|
||||||
|
$classname = array_shift($parts);
|
||||||
|
self::$classmap[$classname] = $dir . "/" . $file;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
spl_autoload_register("Loader::_autoload");
|
17
RunTask.php
Normal file
17
RunTask.php
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
<?php
|
||||||
|
require_once("Settings.php");
|
||||||
|
|
||||||
|
if (!isset($argv[1]) || empty($argv[1])) {
|
||||||
|
Logger::log("No task specified", Logger::ERROR, "RunTask");
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
$task = $argv[1] . "Task";
|
||||||
|
if (!file_exists(LIB_DIR . "/tasks/" . $task . ".php")) {
|
||||||
|
Logger::log("Task " . $task . " does not exist", Logger::ERROR, "RunTask");
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
require_once(LIB_DIR . "/tasks/" . $task . ".php");
|
||||||
|
$task = new $task();
|
||||||
|
$task->run();
|
24
Settings.php
Normal file
24
Settings.php
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
<?php
|
||||||
|
set_error_handler(function ($errno, $errstr, $errfile, $errline) {
|
||||||
|
throw new ErrorException($errstr, 0, $errno, $errfile, $errline);
|
||||||
|
});
|
||||||
|
|
||||||
|
set_exception_handler(function ($exception) {
|
||||||
|
echo $exception->getMessage() . " on line " . $exception->getLine() . " in " . $exception->getFile() . PHP_EOL;
|
||||||
|
exit;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!defined("PROJECT_DIR") ) {
|
||||||
|
define("PROJECT_DIR", rtrim( dirname(__FILE__ . "/../.."), "/"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!defined("SQLITE_DB")) {
|
||||||
|
throw new Exception("SQLITE_DB not defined");
|
||||||
|
}
|
||||||
|
|
||||||
|
define("LIB_MIGRATIONS_DIR", dirname(__FILE__) . "/schema");
|
||||||
|
define("LIB_TASKS_DIR", dirname(__FILE__) . "/tasks");
|
||||||
|
define("LIB_TEMPLATE_DIR", dirname(__FILE__) . "/templates");
|
||||||
|
define("LIB_LIB_DIR", dirname(__FILE__) . "/lib");
|
||||||
|
|
||||||
|
require_once(dirname(__FILE__) . "/Loader.php");
|
120
lib/Cache.php
Normal file
120
lib/Cache.php
Normal file
|
@ -0,0 +1,120 @@
|
||||||
|
<?php
|
||||||
|
class Cache {
|
||||||
|
public static function getCacheFile(string $name, $maxAge = 3600) {
|
||||||
|
$cacheFile = CACHE_DIR . "/" . $name;
|
||||||
|
|
||||||
|
if (file_exists($cacheFile) && filemtime($cacheFile) > time() - $maxAge) {
|
||||||
|
Logger::log("Cache hit for " . $name, Logger::IOREAD, "Cache");
|
||||||
|
return COMPRESS_LOCAL_CACHE ? gzuncompress(file_get_contents($cacheFile)) : file_get_contents($cacheFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function saveCacheFile(string $name, string $data) {
|
||||||
|
$cacheFile = CACHE_DIR . "/" . $name;
|
||||||
|
file_put_contents($cacheFile, COMPRESS_LOCAL_CACHE ? gzcompress($data,9) : $data );
|
||||||
|
Logger::log("Saved cache file " . $name, Logger::IOWRITE, "Cache");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function publicCacheExists(string $name, bool $skipExtensionCheck = false) {
|
||||||
|
// For now cache is not reliable since we use a generated name.
|
||||||
|
return false;
|
||||||
|
/*if (!$skipExtensionCheck) {
|
||||||
|
$ext = substr($name, strrpos($name, ".") + 1);
|
||||||
|
if (strtolower($ext) == "jpg" || strtolower($ext) == "jpeg" || strtolower($ext) == "png") {
|
||||||
|
$name = str_replace("." . $ext, ".webp", strtolower($name));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$cacheFile = PUBLIC_CACHE_DIR . "/" . $name;
|
||||||
|
|
||||||
|
try {
|
||||||
|
$fe = file_exists($cacheFile);
|
||||||
|
} catch (Exception $e) {
|
||||||
|
$fe = false;
|
||||||
|
Logger::log("Could not check if public cache file " . $name . " exists: " . $e->getMessage(), Logger::ERROR, "Cache");
|
||||||
|
}
|
||||||
|
|
||||||
|
return $fe;*/
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function savePublicCacheFile(string $name, string $data, int $cacheLevel = 70, bool $skipExtensionCheck = false) {
|
||||||
|
$name = strtolower($name);
|
||||||
|
|
||||||
|
$ext = substr($name, strrpos($name, ".") + 1);
|
||||||
|
if ($skipExtensionCheck || strtolower($ext) == "jpg" || strtolower($ext) == "jpeg" || strtolower($ext) == "png") {
|
||||||
|
$size = strlen($data);
|
||||||
|
$img = new Imagick();
|
||||||
|
$img->readImageBlob($data);
|
||||||
|
$img->setImageFormat('webp');
|
||||||
|
$img->setImageCompressionQuality($cacheLevel);
|
||||||
|
$img->stripImage();
|
||||||
|
$data = $img->getImageBlob();
|
||||||
|
$name = str_replace("." . $ext, ".webp", strtolower($name));
|
||||||
|
|
||||||
|
$cacheFile = PUBLIC_CACHE_DIR . "/" . $name;
|
||||||
|
$blurhash = self::generateBlurHash($cacheFile, $img, $skipExtensionCheck);
|
||||||
|
if (!empty($blurhash)) {
|
||||||
|
$name = "BH-" . bin2hex($blurhash) . "-W" . $img->getImageWidth() . "xH" . $img->getImageHeight() . ".webp";
|
||||||
|
$cacheFile = PUBLIC_CACHE_DIR . "/" . $name;
|
||||||
|
}
|
||||||
|
|
||||||
|
// For now don't transfer file if its generated name exists
|
||||||
|
if (file_exists($cacheFile)) {
|
||||||
|
Logger::log("Public cache file " . $name . " already exists", Logger::INFO, "Cache");
|
||||||
|
return PUBLIC_CACHE_URL . "/" . $name;
|
||||||
|
}
|
||||||
|
|
||||||
|
Logger::log("Compressed image from " . $size . " to " . strlen($data) . " bytes", Logger::METRICS, "Cache");
|
||||||
|
}
|
||||||
|
|
||||||
|
$context = stream_context_create([
|
||||||
|
's3' => [
|
||||||
|
'ACL' => 'public-read'
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
|
||||||
|
file_put_contents($cacheFile, $data, 0, $context);
|
||||||
|
|
||||||
|
Logger::log("Saved public cache file " . $name, Logger::IOWRITE, "Cache");
|
||||||
|
|
||||||
|
return PUBLIC_CACHE_URL . "/" . $name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function generateBlurHash(string $cacheFile, ?IMagick $img = null, bool $skipExtensionCheck = false):?string {
|
||||||
|
$ext = substr($cacheFile, strrpos($cacheFile, ".") + 1);
|
||||||
|
if ($skipExtensionCheck || strtolower($ext) == "jpg" || strtolower($ext) == "jpeg" || strtolower($ext) == "png" || strtolower($ext) == "webp") {
|
||||||
|
|
||||||
|
$hashFile = strtolower($cacheFile);
|
||||||
|
$hashFileName = substr($hashFile, strrpos($hashFile, "/") + 1);
|
||||||
|
if (file_exists($hashFile)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$pixels = [];
|
||||||
|
|
||||||
|
if ($img === null) {
|
||||||
|
$img = new Imagick();
|
||||||
|
$img->readImage($cacheFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
$width = $img->getImageWidth();
|
||||||
|
$height = $img->getImageHeight();
|
||||||
|
|
||||||
|
for ($y = 0; $y < $height; $y++) {
|
||||||
|
$row = [];
|
||||||
|
for ($x = 0; $x < $width; $x++) {
|
||||||
|
$pixel = $img->getImagePixelColor($x, $y);
|
||||||
|
$color = $pixel->getColor();
|
||||||
|
$row[] = [$color["r"], $color["g"], $color["b"]];
|
||||||
|
}
|
||||||
|
$pixels[] = $row;
|
||||||
|
}
|
||||||
|
|
||||||
|
$hash = \kornrunner\Blurhash\Blurhash::encode($pixels, BLURHASH_X, BLURHASH_Y);
|
||||||
|
|
||||||
|
return $hash;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
95
lib/Database.php
Normal file
95
lib/Database.php
Normal file
|
@ -0,0 +1,95 @@
|
||||||
|
<?php
|
||||||
|
class Database
|
||||||
|
{
|
||||||
|
public static $handle = null;
|
||||||
|
|
||||||
|
public function __construct($createDatabase = false)
|
||||||
|
{
|
||||||
|
if (self::$handle === null) {
|
||||||
|
self::$handle = new SQLite3(SQLITE_DB);
|
||||||
|
self::$handle->enableExceptions(true);
|
||||||
|
|
||||||
|
|
||||||
|
if ($createDatabase === true) {
|
||||||
|
$this->createDatabase();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function createDatabase()
|
||||||
|
{
|
||||||
|
$schemaFiles = [
|
||||||
|
LIB_MIGRATIONS_DIR . "/_schema.sql",
|
||||||
|
MIGRATIONS_DIR . "/_schema.sql"
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($schemaFiles as $schemaFile) {
|
||||||
|
if (file_exists($schemaFile)) {
|
||||||
|
$schema = file_get_contents($schemaFile);
|
||||||
|
self::$handle->exec($schema);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function runMigrations()
|
||||||
|
{
|
||||||
|
$currentTableVersions = [];
|
||||||
|
|
||||||
|
$result = self::$handle->query("SELECT `table`,`version` FROM `migrations`");
|
||||||
|
while ($row = $result->fetchArray(SQLITE3_ASSOC)) {
|
||||||
|
$currentTableVersions[$row["table"]] = $row["version"];
|
||||||
|
}
|
||||||
|
|
||||||
|
$migrations = [];
|
||||||
|
$migrationPaths = [LIB_MIGRATIONS_DIR, MIGRATIONS_DIR];
|
||||||
|
foreach ($migrationPaths as $migrationPath) {
|
||||||
|
if (is_dir($migrationPath)) {
|
||||||
|
$migrations = array_merge($migrations, scandir($migrationPath));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$migrationCount = 0;
|
||||||
|
|
||||||
|
foreach ($migrations as $migration) {
|
||||||
|
if ($migration == "." || $migration == ".." || $migration == "_schema.sql") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$migrationParts = explode("-", str_replace(".sql", "", $migration));
|
||||||
|
$migrationTable = $migrationParts[0];
|
||||||
|
$migrationVersion = $migrationParts[1];
|
||||||
|
|
||||||
|
$run = false;
|
||||||
|
|
||||||
|
if (!isset($currentTableVersions[$migrationTable])) {
|
||||||
|
$run = true;
|
||||||
|
} else {
|
||||||
|
if ($migrationVersion > $currentTableVersions[$migrationTable]) {
|
||||||
|
$run = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($run === true) {
|
||||||
|
Logger::log("Found migration for table " . $migrationTable . " version " . $migrationVersion, LOGGER::INFO, "Database");
|
||||||
|
$migrationContents = file_get_contents(MIGRATIONS_DIR . "/" . $migration);
|
||||||
|
if (@self::$handle->exec($migrationContents)) {
|
||||||
|
self::$handle->exec("INSERT INTO `migrations` (`table`,`version`, `schemafile`, `created`) VALUES ('" . $migrationTable . "'," . $migrationVersion . ",'" . $migration . "', strftime('%Y-%m-%d %H:%M:%S','now'))");
|
||||||
|
$migrationCount++;
|
||||||
|
} else {
|
||||||
|
Logger::log("Failed to run migration for table " . $migrationTable . " version " . $migrationVersion . " with error: " . self::$handle->lastErrorMsg(), LOGGER::ERROR, "Database");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($migrationCount > 0) {
|
||||||
|
Logger::log("Ran " . $migrationCount . " migrations", LOGGER::DEBUG, "Database");
|
||||||
|
} else {
|
||||||
|
Logger::log("No migrations to run", LOGGER::DEBUG, "Database");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function close()
|
||||||
|
{
|
||||||
|
self::$handle->close();
|
||||||
|
}
|
||||||
|
}
|
28
lib/EmptyTemplate.php
Normal file
28
lib/EmptyTemplate.php
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
<?php
|
||||||
|
class EmptyTemplate implements ITemplate
|
||||||
|
{
|
||||||
|
protected Template $template;
|
||||||
|
protected ?object $options = null;
|
||||||
|
|
||||||
|
public function __construct(Template &$template, ?object $options = null)
|
||||||
|
{
|
||||||
|
$this->template = $template;
|
||||||
|
$this->options = $options;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function load()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public function pretransform()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public function transform()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public function save()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
8
lib/ITemplate.php
Normal file
8
lib/ITemplate.php
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
<?php
|
||||||
|
interface ITemplate {
|
||||||
|
public function __construct(Template &$template, ?object $options = null);
|
||||||
|
public function load();
|
||||||
|
public function pretransform();
|
||||||
|
public function transform();
|
||||||
|
public function save();
|
||||||
|
}
|
53
lib/KeyValue.php
Normal file
53
lib/KeyValue.php
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
<?php
|
||||||
|
class KeyValue {
|
||||||
|
protected static $kv = null;
|
||||||
|
|
||||||
|
public static function get(string $keyname, ?string $channel = null) {
|
||||||
|
if (self::$kv === null) {
|
||||||
|
self::load();
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($channel === null) {
|
||||||
|
$channel = "all";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isset(self::$kv[$keyname][$channel])) {
|
||||||
|
return self::$kv[$keyname][$channel];
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function save(string $keyname, string $value, ?string $channel = null) {
|
||||||
|
if (self::$kv === null) {
|
||||||
|
self::load();
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($channel === null) {
|
||||||
|
$channel = "all";
|
||||||
|
}
|
||||||
|
|
||||||
|
$db = new Database();
|
||||||
|
$stmt = $db::$handle->prepare("INSERT OR REPLACE INTO `keyvalue` (`key`, `value`, `channel`, `created`) VALUES (:key, :value, :channel, strftime('%Y-%m-%d %H:%M:%S','now'))");
|
||||||
|
$stmt->bindValue(":key", $keyname, SQLITE3_TEXT);
|
||||||
|
$stmt->bindValue(":value", $value, SQLITE3_TEXT);
|
||||||
|
$stmt->bindValue(":channel", $channel, SQLITE3_TEXT);
|
||||||
|
$stmt->execute();
|
||||||
|
|
||||||
|
self::$kv[$keyname][$channel] = $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static function load() {
|
||||||
|
$db = new Database();
|
||||||
|
$stmt = $db::$handle->prepare("SELECT * FROM `keyvalue`");
|
||||||
|
$result = $stmt->execute();
|
||||||
|
while ($row = $result->fetchArray(SQLITE3_ASSOC)) {
|
||||||
|
$channel = $row["channel"];
|
||||||
|
if ($channel === null) {
|
||||||
|
$channel = "all";
|
||||||
|
}
|
||||||
|
|
||||||
|
self::$kv[$row["key"]][$channel] = $row["value"];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
40
lib/Logger.php
Normal file
40
lib/Logger.php
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
<?php
|
||||||
|
class Logger {
|
||||||
|
const INFO = "INFO";
|
||||||
|
const WARNING = "WARNING";
|
||||||
|
const ERROR = "ERROR";
|
||||||
|
const DEBUG = "DEBUG";
|
||||||
|
const IOREAD = "IOREAD";
|
||||||
|
const IOWRITE = "IOWRITE";
|
||||||
|
const METRICS = "METRICS";
|
||||||
|
|
||||||
|
public static function getColor(string $level):string {
|
||||||
|
match ($level) {
|
||||||
|
self::INFO => $levelColor = "\033[0;32m",
|
||||||
|
self::WARNING => $levelColor = "\033[0;33m",
|
||||||
|
self::ERROR => $levelColor = "\033[0;31m",
|
||||||
|
self::DEBUG => $levelColor = "\033[0;34m",
|
||||||
|
self::IOREAD => $levelColor = "\033[0;35m",
|
||||||
|
self::IOWRITE => $levelColor = "\033[0;36m",
|
||||||
|
self::METRICS => $levelColor = "\033[0;41m",
|
||||||
|
default => $levelColor = "\033[0;37m"
|
||||||
|
};
|
||||||
|
|
||||||
|
return $levelColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function log(string $message, string $level = "INFO", string $context = "") {
|
||||||
|
|
||||||
|
$db = new Database();
|
||||||
|
$stmt = $db::$handle->prepare("INSERT INTO `logs` (`level`, `message`, `context`, `created`) VALUES (:level, :message, :context, strftime('%Y-%m-%d %H:%M:%S','now'))");
|
||||||
|
$stmt->bindValue(":level", $level, SQLITE3_TEXT);
|
||||||
|
$stmt->bindValue(":message", $message, SQLITE3_TEXT);
|
||||||
|
$stmt->bindValue(":context", $context, SQLITE3_TEXT);
|
||||||
|
|
||||||
|
@$stmt->execute();
|
||||||
|
|
||||||
|
$levelColor = self::getColor($level);
|
||||||
|
|
||||||
|
echo "\033[0;37m[" . date("Y-m-d H:i:s") . "] " . $levelColor . $level . "\033[0;37m: " . $message . "\033[0m" . PHP_EOL;
|
||||||
|
}
|
||||||
|
}
|
10
lib/Task.php
Normal file
10
lib/Task.php
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
<?php
|
||||||
|
abstract class Task {
|
||||||
|
public function run() {
|
||||||
|
Logger::log("Running task " . get_class($this), Logger::DEBUG, "Task");
|
||||||
|
$this->execute();
|
||||||
|
Logger::log("Finished task " . get_class($this), Logger::DEBUG, "Task");
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract protected function execute();
|
||||||
|
}
|
207
lib/Template.php
Normal file
207
lib/Template.php
Normal file
|
@ -0,0 +1,207 @@
|
||||||
|
<?php
|
||||||
|
class Template {
|
||||||
|
protected $template = null;
|
||||||
|
protected $data = array();
|
||||||
|
|
||||||
|
public function __construct(string $template, ?array $parentData = null) {
|
||||||
|
$this->template = $template;
|
||||||
|
|
||||||
|
if (isset($parentData)) {
|
||||||
|
$this->data = $parentData;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function set(string $key, $value) {
|
||||||
|
$this->data[$key] = $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function render($options = null): string {
|
||||||
|
Logger::log("Rendering template " . $this->template, Logger::IOREAD, "Template");
|
||||||
|
|
||||||
|
$class = is_object($options) && isset($options->class) ? new $options->class($this, $options) : new EmptyTemplate($this, $options);
|
||||||
|
$class->load();
|
||||||
|
$contents = file_get_contents(TEMPLATE_DIR . "/" . $this->template);
|
||||||
|
|
||||||
|
$class->pretransform();
|
||||||
|
// Replace includes of templates
|
||||||
|
$matches = array();
|
||||||
|
preg_match_all("/{{include (.*\.html)(.*)}}/", $contents, $matches);
|
||||||
|
for ($i = 0; $i < count($matches[1]); $i++) {
|
||||||
|
$matchString = $matches[0][$i];
|
||||||
|
$match = $matches[1][$i];
|
||||||
|
$options = trim($matches[2][$i]);
|
||||||
|
|
||||||
|
$include = new Template(dirname($this->template) . "/" . $match, $this->data);
|
||||||
|
$contents = str_replace($matchString, $include->render(
|
||||||
|
!empty($options) ? json_decode($options, false, 512, JSON_THROW_ON_ERROR) : null
|
||||||
|
), $contents);
|
||||||
|
}
|
||||||
|
|
||||||
|
$class->transform();
|
||||||
|
|
||||||
|
// Replace variables
|
||||||
|
foreach ($this->data as $key => $value) {
|
||||||
|
if (is_string($value) || is_numeric($value)) {
|
||||||
|
$contents = str_replace("{{" . $key . "}}", $value, $contents);
|
||||||
|
} else {
|
||||||
|
$contents = str_replace("{{" . $key . "}}", "", $contents);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Conditional tags are written as {{?<variablename>}}some content{{/<variablename>}}, find conditional tags and evaluate them if they exist in $this->data
|
||||||
|
$matches = array();
|
||||||
|
preg_match_all("/{{\?(.*)}}(.*){{\/(.*)}}/sU", $contents, $matches);
|
||||||
|
for ($i = 0; $i < count($matches[1]); $i++) {
|
||||||
|
$matchString = $matches[0][$i];
|
||||||
|
$match = $matches[1][$i];
|
||||||
|
$content = $matches[2][$i];
|
||||||
|
|
||||||
|
if (isset($this->data[$match])) {
|
||||||
|
$contents = str_replace($matchString, $content, $contents);
|
||||||
|
} else {
|
||||||
|
$contents = str_replace($matchString, "", $contents);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$class->save();
|
||||||
|
|
||||||
|
return $contents;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function save(string $filename, string $contents, bool $minify = true) {
|
||||||
|
Logger::log("Saving template to " . $filename, Logger::IOWRITE, "Template");
|
||||||
|
|
||||||
|
if ($minify) {
|
||||||
|
$size = strlen($contents);
|
||||||
|
|
||||||
|
$doc = new DOMDocument();
|
||||||
|
$doc->preserveWhiteSpace = false;
|
||||||
|
$doc->formatOutput = false;
|
||||||
|
$doc->normalizeDocument();
|
||||||
|
|
||||||
|
libxml_use_internal_errors(true);
|
||||||
|
|
||||||
|
$doc->loadHTML($contents);
|
||||||
|
|
||||||
|
libxml_use_internal_errors(false);
|
||||||
|
|
||||||
|
$options = [
|
||||||
|
'keep_whitespace_around' => [
|
||||||
|
// keep whitespace around inline elements
|
||||||
|
'b', 'big', 'i', 'small', 'tt',
|
||||||
|
'abbr', 'acronym', 'cite', 'code', 'dfn', 'em', 'kbd', 'strong', 'samp', 'var',
|
||||||
|
'a', 'bdo', 'br', 'img', 'map', 'object', 'q', 'span', 'sub', 'sup',
|
||||||
|
'button', 'input', 'label', 'select', 'textarea'
|
||||||
|
],
|
||||||
|
'keep_whitespace_in' => [/*'script', 'style',*/ 'pre'],
|
||||||
|
'remove_empty_attributes' => ['style', 'class'],
|
||||||
|
'indent_characters' => "\t"
|
||||||
|
];
|
||||||
|
|
||||||
|
//Comments,empty,whitespace
|
||||||
|
$xpath = new \DOMXPath($doc);
|
||||||
|
foreach ($xpath->query('//comment()') as $comment) {
|
||||||
|
$comment->parentNode->removeChild($comment);
|
||||||
|
}
|
||||||
|
|
||||||
|
$xpath = new \DOMXPath($doc);
|
||||||
|
foreach ($options['remove_empty_attributes'] as $attr) {
|
||||||
|
foreach ($xpath->query('//*[@' . $attr . ']') as $el) {
|
||||||
|
if (trim($el->getAttribute($attr)) == '') {
|
||||||
|
$el->removeAttribute($attr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$x = new \DOMXPath($doc);
|
||||||
|
$nodeList = $x->query("//text()");
|
||||||
|
foreach ($nodeList as $node) {
|
||||||
|
/** @var \DOMNode $node */
|
||||||
|
|
||||||
|
if (in_array($node->parentNode->nodeName, $options['keep_whitespace_in'])) {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
$node->nodeValue = str_replace(["\r", "\n", "\t"], ' ', $node->nodeValue);
|
||||||
|
|
||||||
|
while (strpos($node->nodeValue, ' ') !== false) {
|
||||||
|
$node->nodeValue = str_replace(' ', ' ', $node->nodeValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!in_array($node->parentNode->nodeName, $options['keep_whitespace_around'])) {
|
||||||
|
if (!($node->previousSibling && in_array(
|
||||||
|
$node->previousSibling->nodeName,
|
||||||
|
$options['keep_whitespace_around']
|
||||||
|
))) {
|
||||||
|
$node->nodeValue = ltrim($node->nodeValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!($node->nextSibling && in_array(
|
||||||
|
$node->nextSibling->nodeName,
|
||||||
|
$options['keep_whitespace_around']
|
||||||
|
))) {
|
||||||
|
$node->nodeValue = rtrim($node->nodeValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((strlen($node->nodeValue) == 0)) {
|
||||||
|
$node->parentNode->removeChild($node);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$contents = $doc->saveHTML();
|
||||||
|
|
||||||
|
Logger::log("Minified template from " . $size . " to " . strlen($contents) . " bytes", Logger::METRICS, "Template");
|
||||||
|
}
|
||||||
|
|
||||||
|
file_put_contents(PUBLIC_DIR . "/" . $filename, $contents);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function saveCss(string $filename, string $contents, bool $minify = true)
|
||||||
|
{
|
||||||
|
Logger::log("Saving css to " . $filename, Logger::IOWRITE, "Template");
|
||||||
|
|
||||||
|
if ($minify) {
|
||||||
|
$size = strlen($contents);
|
||||||
|
|
||||||
|
$contents = preg_replace('!/\*[^*]*\*+([^/][^*]*\*+)*/!', '', $contents);
|
||||||
|
$contents = str_replace(': ', ':', $contents);
|
||||||
|
$contents = str_replace(array("\r\n", "\r", "\n", "\t", ' ', ' ', ' '), '', $contents);
|
||||||
|
|
||||||
|
Logger::log("Minified " . $filename . " from " . $size . " to " . strlen($contents) . " bytes", Logger::METRICS, "Template");
|
||||||
|
}
|
||||||
|
|
||||||
|
file_put_contents(PUBLIC_DIR . "/" . $filename, $contents);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function saveScript(string $filename, string $contents, bool $minify = true)
|
||||||
|
{
|
||||||
|
Logger::log("Saving script to " . $filename, Logger::IOWRITE, "Template");
|
||||||
|
|
||||||
|
if ($minify) {
|
||||||
|
$size = strlen($contents);
|
||||||
|
|
||||||
|
$contents = preg_replace('/([-\+])\s+\+([^\s;]*)/', '$1 (+$2)', $contents);
|
||||||
|
$contents = preg_replace("/\s*\n\s*/", "\n", $contents);
|
||||||
|
$contents = preg_replace("/\h+/", " ", $contents);
|
||||||
|
$contents = preg_replace("/\h([^A-Za-z0-9\_\$])/", '$1', $contents);
|
||||||
|
$contents = preg_replace("/([^A-Za-z0-9\_\$])\h/", '$1', $contents);
|
||||||
|
$contents = preg_replace("/\s?([\(\[{])\s?/", '$1', $contents);
|
||||||
|
$contents = preg_replace("/\s([\)\]}])/", '$1', $contents);
|
||||||
|
$contents = preg_replace("/\s?([\.=:\-+,])\s?/", '$1', $contents);
|
||||||
|
$contents = preg_replace("/;\n/", ";", $contents);
|
||||||
|
$contents = preg_replace('/;}/', '}', $contents);
|
||||||
|
$contents = preg_replace('/\s+/', ' ', $contents);
|
||||||
|
$contents = preg_replace('/\s*([;{}])\s*/', '$1', $contents);
|
||||||
|
|
||||||
|
$contents = preg_replace('/\s*(;{})\s*/', '$1', $contents);
|
||||||
|
|
||||||
|
$contents = str_replace(array("\r\n", "\r", "\n", "\t", ' ', ' ', ' '), '', $contents);
|
||||||
|
|
||||||
|
Logger::log("Minified " . $filename . " from " . $size . " to " . strlen($contents) . " bytes", Logger::METRICS, "Template");
|
||||||
|
}
|
||||||
|
|
||||||
|
file_put_contents(PUBLIC_DIR . "/" . $filename, $contents);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
30
schema/_schema.sql
Normal file
30
schema/_schema.sql
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
CREATE TABLE IF NOT EXISTS `migrations` (
|
||||||
|
`id` INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
`table` TEXT NOT NULL,
|
||||||
|
`version` INTEGER NOT NULL,
|
||||||
|
`schemafile` TEXT NOT NULL,
|
||||||
|
`created` TEXT NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS `migrations_table` ON `migrations` (`table`, `version`);
|
||||||
|
CREATE INDEX IF NOT EXISTS `migrations_created` ON `migrations` (`created`);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS `logs` (
|
||||||
|
`id` INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
`level` TEXT NOT NULL,
|
||||||
|
`message` TEXT NOT NULL,
|
||||||
|
`context` TEXT,
|
||||||
|
`created` TEXT
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS `logs_level` ON `logs` (`level`, `created`);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS `keyvalue` (
|
||||||
|
`id` INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
`key` TEXT NOT NULL,
|
||||||
|
`channel` TEXT DEFAULT NULL,
|
||||||
|
`value` TEXT NOT NULL,
|
||||||
|
`created` TEXT NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS `keyvalue_key` ON `keyvalue` (`key`, `channel`);
|
0
schema/logs-000.sql
Normal file
0
schema/logs-000.sql
Normal file
0
schema/migrations-000.sql
Normal file
0
schema/migrations-000.sql
Normal file
Loading…
Reference in a new issue