aboutsummaryrefslogtreecommitdiffhomepage
path: root/WpfTest
diff options
context:
space:
mode:
Diffstat (limited to 'WpfTest')
-rw-r--r--WpfTest/Db.php41
-rw-r--r--WpfTest/Db/Migrator.php52
-rw-r--r--WpfTest/GlobalConfig.php21
-rw-r--r--WpfTest/HttpResponseStatus.php (renamed from WpfTest/HttpReturnStatus.php)2
-rw-r--r--WpfTest/HttpResponseStatus/BadRequest.php (renamed from WpfTest/HttpReturnStatus/BadRequest.php)6
-rw-r--r--WpfTest/HttpResponseStatus/InternalServerError.php23
-rw-r--r--WpfTest/HttpResponseStatus/NoHandlerFound.php (renamed from WpfTest/HttpReturnStatus/NoHandlerFound.php)6
-rw-r--r--WpfTest/HttpResponseStatus/NotFound.php (renamed from WpfTest/HttpReturnStatus/NotFound.php)6
-rw-r--r--WpfTest/HttpReturnStatus/InternalServerError.php16
-rw-r--r--WpfTest/Model.php73
-rw-r--r--WpfTest/Model/Actor.php26
-rw-r--r--WpfTest/Model/ActorEndpoints.php15
-rw-r--r--WpfTest/Router.php29
-rw-r--r--WpfTest/Singleton.php18
14 files changed, 277 insertions, 57 deletions
diff --git a/WpfTest/Db.php b/WpfTest/Db.php
new file mode 100644
index 0000000..3de1011
--- /dev/null
+++ b/WpfTest/Db.php
@@ -0,0 +1,41 @@
+<?php
+namespace WpfTest;
+
+use WpfTest\Model\Actor;
+
+class Db extends Singleton {
+ private const INIT_SCHEMA_PATH = __DIR__ . '/../schema.sql';
+
+ private \PDO $conn;
+
+ protected function __construct() {
+ $this->conn = new \PDO('pgsql:host=localhost;dbname=wpftest', 'wpftest', 'wpftest', [
+ \PDO::ATTR_PERSISTENT => true,
+ ]);
+ }
+
+ public function runInitialSchema() {
+ $this->conn->exec(file_get_contents(self::INIT_SCHEMA_PATH));
+ }
+
+ public function getDbVersion(): int {
+ $result = $this->conn->query('SELECT db_version FROM db_version');
+ return $result->fetch()['db_version'];
+ }
+
+ /**
+ * Changes the database schema version
+ *
+ * CAREFUL! this shouldn't be used unless you absolutely know why
+ * @param int $newVersion the new version number
+ * @return void
+ */
+ public function setDbVersion(int $newVersion) {
+ $stmt = $this->conn->prepare('UPDATE db_version SET db_version = ?');
+ $stmt->execute([$newVersion]);
+ }
+
+ public function getPdo(): \PDO {
+ return $this->conn;
+ }
+} \ No newline at end of file
diff --git a/WpfTest/Db/Migrator.php b/WpfTest/Db/Migrator.php
new file mode 100644
index 0000000..ea7f92a
--- /dev/null
+++ b/WpfTest/Db/Migrator.php
@@ -0,0 +1,52 @@
+<?php
+namespace WpfTest\Db;
+
+use WpfTest\Db;
+use WpfTest\Singleton;
+
+class Migrator extends Singleton {
+ private array $migrations = [];
+
+ public function register(int $newVersion, callable $fn) {
+ $this->migrations[$newVersion] = $fn;
+ }
+
+ /**
+ * Runs all available migrations
+ * @return void
+ */
+ public function migrate() {
+ $db = Db::getInstance();
+ $currentVersion = null;
+ try {
+ $currentVersion = $db->getDbVersion();
+ error_log('starting on version ' . $currentVersion);
+ } catch (\PDOException $e) {
+ if ($e->getCode() == '42P01') { // undefined table
+ error_log("initialising database because db version table doesn't exist");
+ $db->runInitialSchema();
+ $currentVersion = $db->getDbVersion();
+ error_log('starting on version ' . $currentVersion);
+ } else
+ throw $e;
+ }
+
+ // find migrations that haven't been run yet
+ $available = array_filter($this->migrations, function (int $key) use ($currentVersion) {
+ return $key > $currentVersion;
+ }, ARRAY_FILTER_USE_KEY);
+
+ try {
+ // run them in order (at least they should be in order?)
+ foreach (array_keys($available) as $version) {
+ $fn = $available[$version];
+ $filename = basename((new \ReflectionFunction($fn))->getFileName());
+ error_log("running migration $filename");
+ $available[$version]($db->getPdo());
+ $db->setDbVersion($version);
+ }
+ } finally {
+ error_log('now on version ' . $db->getDbVersion());
+ }
+ }
+} \ No newline at end of file
diff --git a/WpfTest/GlobalConfig.php b/WpfTest/GlobalConfig.php
index 4bbfe68..03cfe6f 100644
--- a/WpfTest/GlobalConfig.php
+++ b/WpfTest/GlobalConfig.php
@@ -1,25 +1,16 @@
<?php
namespace WpfTest;
-class GlobalConfig {
- private static GlobalConfig $instance;
-
- private function __construct() {
-
- }
-
- public static function getInstance() {
- if (!isset(self::$instance)) {
- self::$instance = new self();
- }
- return self::$instance;
- }
-
+class GlobalConfig extends Singleton {
public function getCanonicalHost(): string {
return $_SERVER['HTTP_HOST']; // for now this is fine
}
public function getBasePath(): string {
- return 'http://' . $this->getCanonicalHost(); // fix this?
+ return 'http://' . $this->getCanonicalHost(); // FIXME: don't hardcode http
+ }
+
+ public function shouldReportTraces(): bool {
+ return true; // FIXME: don't hardcode
}
} \ No newline at end of file
diff --git a/WpfTest/HttpReturnStatus.php b/WpfTest/HttpResponseStatus.php
index 60622bb..c81e000 100644
--- a/WpfTest/HttpReturnStatus.php
+++ b/WpfTest/HttpResponseStatus.php
@@ -1,7 +1,7 @@
<?php
namespace WpfTest;
-interface HttpReturnStatus extends \Throwable {
+interface HttpResponseStatus extends \Throwable {
public function httpCode(): int;
public function httpReason(): string;
} \ No newline at end of file
diff --git a/WpfTest/HttpReturnStatus/BadRequest.php b/WpfTest/HttpResponseStatus/BadRequest.php
index 83b5011..74fab3c 100644
--- a/WpfTest/HttpReturnStatus/BadRequest.php
+++ b/WpfTest/HttpResponseStatus/BadRequest.php
@@ -1,9 +1,9 @@
<?php
-namespace WpfTest\HttpReturnStatus;
+namespace WpfTest\HttpResponseStatus;
-use WpfTest\HttpReturnStatus;
+use WpfTest\HttpResponseStatus;
-class BadRequest extends \Exception implements HttpReturnStatus {
+class BadRequest extends \Exception implements HttpResponseStatus {
public function __construct(?string $reason) {
$this->message = $reason ?? 'Request does not conform to expectations';
}
diff --git a/WpfTest/HttpResponseStatus/InternalServerError.php b/WpfTest/HttpResponseStatus/InternalServerError.php
new file mode 100644
index 0000000..11b67f3
--- /dev/null
+++ b/WpfTest/HttpResponseStatus/InternalServerError.php
@@ -0,0 +1,23 @@
+<?php
+namespace WpfTest\HttpResponseStatus;
+
+use WpfTest\GlobalConfig;
+use WpfTest\HttpResponseStatus;
+
+class InternalServerError extends \Exception implements HttpResponseStatus {
+ public function __construct(\Throwable $reason, ?string $context = null) {
+ $this->message = $reason::class . ': ' . $reason->getMessage();
+ if (isset($context)) {
+ $this->message .= " [$context]";
+ }
+ if (GlobalConfig::getInstance()->shouldReportTraces()) {
+ $this->message .= "\n" . $reason->getTraceAsString();
+ }
+ }
+ public function httpCode(): int {
+ return 500;
+ }
+ public function httpReason(): string {
+ return "Internal Server Error";
+ }
+} \ No newline at end of file
diff --git a/WpfTest/HttpReturnStatus/NoHandlerFound.php b/WpfTest/HttpResponseStatus/NoHandlerFound.php
index d914ca2..9ceeb0b 100644
--- a/WpfTest/HttpReturnStatus/NoHandlerFound.php
+++ b/WpfTest/HttpResponseStatus/NoHandlerFound.php
@@ -1,9 +1,9 @@
<?php
-namespace WpfTest\HttpReturnStatus;
+namespace WpfTest\HttpResponseStatus;
-use WpfTest\HttpReturnStatus;
+use WpfTest\HttpResponseStatus;
-class NoHandlerFound extends \Exception implements HttpReturnStatus {
+class NoHandlerFound extends \Exception implements HttpResponseStatus {
public function __construct(string $path) {
$this->message = "No handler found for path $path";
}
diff --git a/WpfTest/HttpReturnStatus/NotFound.php b/WpfTest/HttpResponseStatus/NotFound.php
index eef23b3..b36d0a3 100644
--- a/WpfTest/HttpReturnStatus/NotFound.php
+++ b/WpfTest/HttpResponseStatus/NotFound.php
@@ -1,9 +1,9 @@
<?php
-namespace WpfTest\HttpReturnStatus;
+namespace WpfTest\HttpResponseStatus;
-use WpfTest\HttpReturnStatus;
+use WpfTest\HttpResponseStatus;
-class NotFound extends \Exception implements HttpReturnStatus {
+class NotFound extends \Exception implements HttpResponseStatus {
public function __construct(?string $reason) {
$this->message = $reason ?? 'Request was handled but no appropriate resource found';
}
diff --git a/WpfTest/HttpReturnStatus/InternalServerError.php b/WpfTest/HttpReturnStatus/InternalServerError.php
deleted file mode 100644
index 0141e26..0000000
--- a/WpfTest/HttpReturnStatus/InternalServerError.php
+++ /dev/null
@@ -1,16 +0,0 @@
-<?php
-namespace WpfTest\HttpReturnStatus;
-
-use WpfTest\HttpReturnStatus;
-
-class InternalServerError extends \Exception implements HttpReturnStatus {
- public function __construct(\Exception $reason) {
- $this->message = $reason->getMessage();
- }
- public function httpCode(): int {
- return 500;
- }
- public function httpReason(): string {
- return "Internal Server Error";
- }
-} \ No newline at end of file
diff --git a/WpfTest/Model.php b/WpfTest/Model.php
new file mode 100644
index 0000000..26a7b49
--- /dev/null
+++ b/WpfTest/Model.php
@@ -0,0 +1,73 @@
+<?php
+namespace WpfTest;
+
+abstract class Model {
+ private static function mangleName(string $name): string {
+ if ($name == 'self')
+ return 'uri';
+ $chars = str_split($name);
+ $chars[0] = strtolower($chars[0]);
+ for ($i = 0; $i < count($chars); $i++) {
+ $c = $chars[$i];
+ if (ctype_upper($c)) {
+ array_splice($chars, $i, 1, ['_', strtolower($c)]);
+ }
+ }
+ return implode($chars);
+ }
+
+ private static function unmangleName(string $name): string {
+ if ($name == 'uri')
+ return 'self';
+ $chars = str_split($name);
+ for ($i = 0; $i < count($chars); $i++) {
+ if ($chars[$i] == '_') {
+ array_splice($chars, $i, 2, [strtoupper($chars[$i + 1])]);
+ }
+ }
+ return implode($chars);
+ }
+
+ public function toDbRow(): array {
+ $refl = new \ReflectionObject($this);
+ $props = $refl->getProperties();
+ $result = [];
+ foreach ($props as $p) {
+ $result[self::mangleName($p->getName())] = $p->getValue();
+ }
+ return $result;
+ }
+
+ public static function fromDbRow(array|false $columns): ?static {
+ if ($columns === false)
+ return null;
+ $obj = new static();
+ $refl = new \ReflectionObject($obj);
+ foreach (array_keys($columns) as $name) {
+ $value = $columns[$name];
+ $name = self::unmangleName($name);
+ try {
+ $obj->{$name} = $value;
+ } catch (\TypeError $e) {
+ $type = $refl->getProperty($name)->getType();
+ if ($type == null || !is_a($type, \ReflectionNamedType::class))
+ throw $e;
+ $obj->{$name} = new ($type->getName())($value);
+ }
+ }
+ return $obj;
+ }
+
+ public static function findWhere(string $whereClause, array $parameters): ?static {
+ $classNameParts = explode('\\', static::class);
+ $className = $classNameParts[count($classNameParts) - 1];
+ $tableName = self::mangleName($className);
+ $stmt = Db::getInstance()->getPdo()->prepare("SELECT * FROM $tableName WHERE $whereClause");
+ $stmt->execute($parameters);
+ return static::fromDbRow($stmt->fetch(\PDO::FETCH_ASSOC));
+ }
+
+ public static function find(int $id): ?static {
+ return static::findWhere('id = ?', [$id]);
+ }
+} \ No newline at end of file
diff --git a/WpfTest/Model/Actor.php b/WpfTest/Model/Actor.php
new file mode 100644
index 0000000..cf8d7ca
--- /dev/null
+++ b/WpfTest/Model/Actor.php
@@ -0,0 +1,26 @@
+<?php
+namespace WpfTest\Model;
+
+use WpfTest\Model;
+
+class Actor extends Model {
+ public int $id;
+ public string $self;
+ public bool $isLocal = false;
+ public \DateTimeImmutable $created;
+ public ?\DateTimeImmutable $modified;
+ public ?string $homepage;
+ public string $handle;
+ public string $displayName;
+ public ?string $avatar;
+ public ?string $bio;
+ public ?string $pronouns;
+ public bool $automated = false;
+ public bool $requestToFollow = false;
+ public ActorEndpoints $endpoints;
+ public array $extensions = [];
+
+ public static function findLocalByHandle(string $handle): ?self {
+ return self::findWhere('is_local = true AND handle = ?', [$handle]);
+ }
+} \ No newline at end of file
diff --git a/WpfTest/Model/ActorEndpoints.php b/WpfTest/Model/ActorEndpoints.php
new file mode 100644
index 0000000..03dab9e
--- /dev/null
+++ b/WpfTest/Model/ActorEndpoints.php
@@ -0,0 +1,15 @@
+<?php
+namespace WpfTest\Model;
+
+use WpfTest\Model;
+
+class ActorEndpoints extends Model {
+ public string $basicFeed;
+ public ?string $fullFeed;
+ public ?string $follow;
+ public ?string $unfollow;
+ public ?string $block;
+ public ?string $unblock;
+ public ?string $subscribe;
+ public ?string $unsubscribe;
+} \ No newline at end of file
diff --git a/WpfTest/Router.php b/WpfTest/Router.php
index ea71657..c975723 100644
--- a/WpfTest/Router.php
+++ b/WpfTest/Router.php
@@ -1,51 +1,48 @@
<?php
namespace WpfTest;
-use WpfTest\HttpReturnStatus\InternalServerError;
-use WpfTest\HttpReturnStatus\NoHandlerFound;
+use WpfTest\HttpResponseStatus\InternalServerError;
+use WpfTest\HttpResponseStatus\NoHandlerFound;
-class Router {
- private static Router $instance;
+class Router extends Singleton {
private readonly string $requestPath;
private array $routes;
- private function __construct() {
+ protected function __construct() {
$this->requestPath = $_GET['requestPath'] ?? $_SERVER['PATH_INFO'] ?? explode('?', $_SERVER['REQUEST_URI'], 2)[0] ?? '/';
$this->routes = [];
}
/**
- * @return Router the singleton instance
+ * Processes and sends a response to the current request
+ * @return void
*/
- public static function getInstance() {
- if (!isset(self::$instance)) {
- self::$instance = new self();
- }
- return self::$instance;
- }
-
public function processRequest() {
try {
$this->processRequestInner();
- } catch (HttpReturnStatus $e) {
+ } catch (HttpResponseStatus $e) {
http_response_code($e->httpCode());
echo '<h1>' . $e->httpReason() . '</h1><pre>' . $e->getMessage() . '</pre>';
}
}
private function processRequestInner() {
+ $context = null;
try {
$route = $this->matchPath($this->requestPath);
if ($route == null) {
throw new NoHandlerFound($this->requestPath);
}
+ $context = 'handler for /' . implode('/', $route[0]);
$route[1]($route[2]); // call handler with args
} catch (\Exception $e) {
- if (is_a($e, HttpReturnStatus::class)) {
+ if (is_a($e, HttpResponseStatus::class)) {
throw $e;
}
- throw new InternalServerError($e);
+ throw new InternalServerError($e, $context);
+ } catch (\Error $e) {
+ throw new InternalServerError($e, $context);
}
}
diff --git a/WpfTest/Singleton.php b/WpfTest/Singleton.php
new file mode 100644
index 0000000..b365e1e
--- /dev/null
+++ b/WpfTest/Singleton.php
@@ -0,0 +1,18 @@
+<?php
+namespace WpfTest;
+
+abstract class Singleton {
+ private static array $instances;
+ protected function __construct() {
+ }
+ /**
+ * @return static the singleton instance
+ */
+ public static function getInstance() {
+ if (!isset(self::$instances[static::class])) {
+ //error_log('making a new ' . static::class);
+ self::$instances[static::class] = new static();
+ }
+ return self::$instances[static::class];
+ }
+} \ No newline at end of file