Browse Source

i can't believe it's getting updates

master
Julian van de Groep 10 months ago
parent
commit
2fbc29f79a
8 changed files with 409 additions and 6 deletions
  1. 2
    0
      .gitignore
  2. 6
    1
      README.md
  3. 8
    0
      composer.json
  4. 46
    3
      index.php
  5. 2
    2
      public/index.php
  6. 237
    0
      src/DatabaseMigrationManager.php
  7. 53
    0
      src/Router.php
  8. 55
    0
      src/Templates.php

+ 2
- 0
.gitignore View File

@@ -4,3 +4,5 @@ $RECYCLE.BIN/
vendor/
img/
thumb/
config.ini.php
.debug

+ 6
- 1
README.md View File

@@ -1,3 +1,8 @@
# Satoko

it's not dead!
cool image board software that exists because i am bored

## Requirements

- PHP 7.3
- MariaDB 10.3+ / MySQL 5.7+

+ 8
- 0
composer.json View File

@@ -1,4 +1,12 @@
{
"autoload": {
"psr-4": {
"Satoko\\": "src/"
},
"classmap": [
"database"
]
},
"require": {
"twig/twig": "^2.10",
"phroute/phroute": "^2.1"

+ 46
- 3
index.php View File

@@ -1,5 +1,48 @@
<?php
if(!defined('SATOKO_ROOT'))
define('SATOKO_ROOT', __DIR__ . '/');
namespace Satoko;

require_once SATOKO_ROOT . 'vendor/autoload.php';
if(!defined('STK_ROOT'))
define('STK_ROOT', __DIR__ . '/');

if(!defined('STK_DEBUG'))
define('STK_DEBUG', is_file(STK_ROOT . '/.debug'));

define('STK_PHP_MIN_VER', '7.3.0');

if (version_compare(PHP_VERSION, STK_PHP_MIN_VER, '<')) {
die('Satoko requires PHP <b>' . STK_PHP_MIN_VER . '</b> or newer to run.');
}

error_reporting(STK_DEBUG ? -1 : 0);
ini_set('display_errors', STK_DEBUG ? 'On' : 'Off');

define('STK_CONFIG', STK_ROOT . '/config.ini.php');

if(!is_file(STK_CONFIG))
die('Missing configuration.');

$config = parse_ini_file(STK_ROOT . '/config.ini.php', true, INI_SCANNER_TYPED);

if(!defined('STK_DB_PFX'))
define('STK_DB_PFX', $config['Database']['table_prefix'] ?? 'stk_');

require_once STK_ROOT . 'vendor/autoload.php';

$templates = new Templates([
'debug' => STK_DEBUG,
'auto_reload' => STK_DEBUG,
'cache' => false,
]);

$router = new Router(
$config['Satoko']['base_path'] ?? '/',
!empty($config['Satoko']['use_path_info'])
);
$router->get(['/', 'index'], function() {
return 'whoa ! images !<br>';
});
$router->get(['/{board}', 'board-index'], function($board) {
return "Viewing {$board}.<br>";
});

$router->dispatch();

+ 2
- 2
public/index.php View File

@@ -1,4 +1,4 @@
<?php
define('SATOKO_ROOT', __DIR__ . '/../');
define('STK_ROOT', __DIR__ . '/../');

require_once SATOKO_ROOT . 'index.php';
require_once STK_ROOT . 'index.php';

+ 237
- 0
src/DatabaseMigrationManager.php View File

@@ -0,0 +1,237 @@
<?php
namespace Satoko;

use Exception;
use PDO;
use PDOException;

final class DatabaseMigrationManager
{
private $targetConnection;
private $migrationStorage;

private const MIGRATION_NAMESPACE = '\\Satoko\\DatabaseMigrations\\%s\\%s';

private $errors = [];

private $logFunction;

public function __construct(PDO $conn, string $path)
{
$this->targetConnection = $conn;
$this->migrationStorage = realpath($path);
}

private function addError(Exception $exception): void
{
$this->errors[] = $exception;
$this->writeLog($exception->getMessage());
}

public function setLogger(callable $logger): void
{
$this->logFunction = $logger;
}

private function writeLog(string $log): void
{
if (!is_callable($this->logFunction)) {
return;
}

call_user_func($this->logFunction, $log);
}

public function getErrors(): array
{
return $this->errors;
}

private function getMigrationScripts(): array
{
if (!file_exists($this->migrationStorage) || !is_dir($this->migrationStorage)) {
$this->addError(new Exception('Migrations script directory does not exist.'));
return [];
}

$files = glob(rtrim($this->migrationStorage, '/\\') . '/*.php');
return $files;
}

private function createMigrationRepository(): bool
{
try {
$this->targetConnection->exec('
CREATE TABLE IF NOT EXISTS `' . STK_DB_PFX . 'migrations` (
`migration_id` INT(10) UNSIGNED NOT NULL AUTO_INCREMENT,
`migration_name` VARCHAR(255) NOT NULL,
`migration_batch` INT(11) UNSIGNED NOT NULL,
PRIMARY KEY (`migration_id`),
UNIQUE INDEX (`migration_id`)
)
');
} catch (PDOException $ex) {
$this->addError($ex);
return false;
}

return true;
}

public function migrate(): bool
{
$this->writeLog('Running migrations...');

if (!$this->createMigrationRepository()) {
return false;
}

$migrationScripts = $this->getMigrationScripts();

if (count($migrationScripts) < 1) {
if (count($this->errors) > 0) {
return false;
}

$this->writeLog('Nothing to migrate!');
return true;
}

try {
$this->writeLog('Fetching completed migration...');
$fetchStatus = $this->targetConnection->prepare("
SELECT *, CONCAT(:basepath, '/', `migration_name`, '.php') as `migration_path`
FROM `" . STK_DB_PFX . "migrations`
");
$fetchStatus->bindValue('basepath', $this->migrationStorage);
$migrationStatus = $fetchStatus->execute() ? $fetchStatus->fetchAll() : [];
} catch (PDOException $ex) {
$this->addError($ex);
return false;
}

if (count($migrationStatus) < 1 && count($this->errors) > 0) {
return false;
}

$remainingMigrations = array_diff($migrationScripts, array_column($migrationStatus, 'migration_path'));

if (count($remainingMigrations) < 1) {
$this->writeLog('Nothing to migrate!');
return true;
}

$batchNumber = $this->targetConnection->query('
SELECT COALESCE(MAX(`migration_batch`), 0) + 1
FROM `' . STK_DB_PFX . 'migrations`
')->fetchColumn();

$recordMigration = $this->targetConnection->prepare('
INSERT INTO `' . STK_DB_PFX . 'migrations`
(`migration_name`, `migration_batch`)
VALUES
(:name, :batch)
');
$recordMigration->bindValue('batch', $batchNumber);

foreach ($remainingMigrations as $migration) {
$filename = pathinfo($migration, PATHINFO_FILENAME);
$filenameSplit = explode('_', $filename);
$recordMigration->bindValue('name', $filename);
$migrationName = '';

if (count($filenameSplit) < 5) {
$this->addError(new Exception("Invalid migration name: '{$filename}'"));
return false;
}

for ($i = 4; $i < count($filenameSplit); $i++) {
$migrationName .= ucfirst(mb_strtolower($filenameSplit[$i]));
}

include_once $migration;

$this->writeLog("Running migration '{$filename}'...");
$migrationFunction = sprintf(self::MIGRATION_NAMESPACE, $migrationName, 'migrate_up');
$migrationFunction($this->targetConnection);
$recordMigration->execute();
}

$this->writeLog('Successfully completed all migrations!');

return true;
}

public function rollback(): bool
{
$this->writeLog('Rolling back last migration batch...');

if (!$this->createMigrationRepository()) {
return false;
}

try {
$fetchStatus = $this->targetConnection->prepare("
SELECT *, CONCAT(:basepath, '/', `migration_name`, '.php') as `migration_path`
FROM `" . STK_DB_PFX . "migrations`
WHERE `migration_batch` = (
SELECT MAX(`migration_batch`)
FROM `" . STK_DB_PFX . "migrations`
)
");
$fetchStatus->bindValue('basepath', $this->migrationStorage);
$migrations = $fetchStatus->execute() ? $fetchStatus->fetchAll() : [];
} catch (PDOException $ex) {
$this->addError($ex);
return false;
}

if (count($migrations) < 1) {
if (count($this->errors) > 0) {
return false;
}

$this->writeLog('Nothing to roll back!');
return true;
}

$migrationScripts = $this->getMigrationScripts();

if (count($migrationScripts) < count($migrations)) {
$this->addError(new Exception('There are missing migration scripts!'));
return false;
}

$removeRecord = $this->targetConnection->prepare('
DELETE FROM `' . STK_DB_PFX . 'migrations`
WHERE `migration_id` = :id
');

foreach ($migrations as $migration) {
if (!file_exists($migration['migration_path'])) {
$this->addError(new Exception("Migration '{$migration['migration_name']}' does not exist."));
return false;
}

$nameSplit = explode('_', $migration['migration_name']);
$migrationName = '';

for ($i = 4; $i < count($nameSplit); $i++) {
$migrationName .= ucfirst(mb_strtolower($nameSplit[$i]));
}

include_once $migration['migration_path'];

$this->writeLog("Rolling '{$migration['migration_name']}' back...");
$migrationFunction = sprintf(self::MIGRATION_NAMESPACE, $migrationName, 'migrate_down');
$migrationFunction($this->targetConnection);

$removeRecord->bindValue('id', $migration['migration_id']);
$removeRecord->execute();
}

$this->writeLog('Successfully completed all rollbacks');

return true;
}
}

+ 53
- 0
src/Router.php View File

@@ -0,0 +1,53 @@
<?php
namespace Satoko;

use Phroute\Phroute\RouteCollector;
use Phroute\Phroute\Dispatcher;

final class Router extends RouteCollector {
private static $instance = null;
private $usePathInfo;
private $basePath = '/';

public function __construct(string $basePath = '/', bool $usePathInfo = false) {
if(is_null(self::$instance))
self::$instance = $this;

$this->basePath = $basePath;
$this->usePathInfo = $usePathInfo;
parent::__construct();
}

public static function instance(): Router {
return self::$instance ?? new static;
}

public function url(string $name, ...$params): string {
$path = $this->basePath;

if($this->hasRoute($name)) {
if($this->usePathInfo)
$path .= 'index.php/';

$path .= $this->route($name, $params);
} else
$path .= $name;

return $path;
}

public function dispatch(): void
{
$dispatcher = new Dispatcher($this->getData());
$response = $dispatcher->dispatch(
$_SERVER['REQUEST_METHOD'],
$this->usePathInfo
? $_SERVER['PATH_INFO']
: parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH)
);

// just always assume html for now
header('Content-Type: text/html; charset=utf-8');
echo $response;
}
}

+ 55
- 0
src/Templates.php View File

@@ -0,0 +1,55 @@
<?php
namespace Satoko;

use Twig_Environment;
use Twig_Loader_Filesystem;
use UnexpectedValueException;

final class Templates extends Twig_Environment
{
private static $instance = null;
private $vars = [];

public static function instance(): Twig_Environment {
return self::$instance;
}

public function __construct(array $options = []) {
if (self::$instance !== null) {
throw new UnexpectedValueException('Instance of Twig already present, use the static instance() function.');
}

$options = array_merge([
'cache' => false,
'strict_variables' => true,
'auto_reload' => false,
'debug' => false,
], $options);

parent::__construct(new Twig_Loader_Filesystem, $options);
self::$instance = $this;
}

public function var(string $key, $value): void {
$this->vars([$key => $value]);
}

public function vars(array $vars): void {
$this->vars = array_merge_recursive($this->vars, $vars);
}

public function addPath(string $path): void {
$this->getLoader()->addPath($path);
}

public function exists(string $path): void {
$this->getLoader()->exists($path);
}

public function render($path, $vars = []): string {
if(!empty($vars))
$this->vars($vars);

return parent::render($path, $this->vars);
}
}

Loading…
Cancel
Save