feat: Basic project setup

This commit is contained in:
overflowerror 2023-11-29 22:17:08 +01:00
parent 9c6b129914
commit f5d5166b4b
24 changed files with 514 additions and 0 deletions

49
.github/workflows/upload.yml vendored Normal file
View file

@ -0,0 +1,49 @@
name: 'Publish to prod'
on:
push:
branches:
- "main"
permissions:
id-token: write
contents: read
jobs:
upload-prod:
runs-on: ubuntu-latest
permissions:
contents: 'read'
id-token: 'write'
steps:
- uses: 'actions/checkout@v3'
- name: Upload
env:
FTP_SERVER: ${{ secrets.FTP_SERVER }}
FTP_USERNAME: ${{ secrets.FTP_USERNAME }}
FTP_PASSWORD: ${{ secrets.FTP_PASSWORD }}
DB_DSN: ${{ secrets.DB_DSN }}
DB_SCHEMA: ${{ secrets.DB_SCHEMA }}
DB_USERNAME: ${{ secrets.DB_USERNAME }}
DB_PASSWORD: ${{ secrets.DB_PASSWORD }}
run: |
sudo apt install lftp
rm -rf .git .github
cp credentials.templ.php credentials.php
sed -i -e "s/{DB_DSN}/${DB_DSN}/g" credentials.php
sed -i -e "s/{DB_SCHEMA}/${DB_SCHEMA}/g" credentials.php
sed -i -e "s/{DB_USERNAME}/${DB_USERNAME}/g" credentials.php
sed -i -e "s/{DB_PASSWORD}/${DB_PASSWORD}/g" credentials.php
mv maintenance.php maintenance-real.php
sed -e 's/false/true/g' maintenance-real.php > maintenance.php
lftp -e "
set sftp:auto-confirm yes;
set ssl:verify-certificate no;
open -u ${FTP_USERNAME},${FTP_PASSWORD} sftp://${FTP_SERVER};
put maintenance.php -o maintenance.php;
mirror --exclude=maintenance-real.php -e -R ./ ./;
put maintenance-real.php -o maintenance.php;
quit;
"

5
.gitignore vendored Normal file
View file

@ -0,0 +1,5 @@
.idea
credentials.php
notes.txt

4
context.php Normal file
View file

@ -0,0 +1,4 @@
<?php
const DB_CONNECTION = "DB_CONNECTION";
const REPOSITORIES = "REPOSITORIES";

5
controllers/GET.php Normal file
View file

@ -0,0 +1,5 @@
<?php
return function (array &$context) {
echo "Hello World";
};

18
controllers/routes.php Normal file
View file

@ -0,0 +1,18 @@
<?php
require_once(ROOT . "/router/Router.php");
function fromController(string $path, string $endpoint = null) {
return function(array &$context) use ($path, $endpoint) {
if ($endpoint)
$context[ENDPOINT] = $endpoint;
return (require(ROOT . "/controllers/" . $path . ".php"))($context);
};
}
return function(Router $router) {
$router->addRoute(GET, "/", fromController("/GET"));
$router->addRoute(GET, "/.*", fromController("/slug/GET"));
};

5
controllers/slug/GET.php Normal file
View file

@ -0,0 +1,5 @@
<?php
return function (array &$context) {
echo "Hello World 2";
};

25
core.php Normal file
View file

@ -0,0 +1,25 @@
<?php
const ROOT = __DIR__;
define("MAINTENANCE_MODE", require(ROOT . "/maintenance.php"));
if (MAINTENANCE_MODE) {
require(ROOT . "/templates/maintenance.php");
} else {
$connection = require_once(ROOT . "/persistence/connection.php");
(require(ROOT . "/persistence/migrate.php"))($connection);
$repositories = (require_once(ROOT . "/persistence/Repositories.php"))($connection);
$router = require(ROOT . "/router/Router.php");
(require(ROOT . "/controllers/routes.php"))($router);
require_once(ROOT . "/context.php");
$context = [
DB_CONNECTION => $connection,
REPOSITORIES => $repositories,
];
$router->execute($context);
}

7
credentials.templ.php Normal file
View file

@ -0,0 +1,7 @@
<?php
// not constants so we can minimize their scope
$DB_DSN = "{DB_DSN}";
$DB_SCHEMA = "{DB_SCHEMA}";
$DB_USERNAME = "{DB_USERNAME}";
$DB_PASSWORD = "{DB_PASSWORD}";

13
html/.htaccess Normal file
View file

@ -0,0 +1,13 @@
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteBase /
RewriteRule ^index\.php$ - [L]
RewriteCond %{REQUEST_URI} !^/static/
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule . /index.php [L]
</IfModule>
php_flag log_errors on
php_value error_log /usr/home/usefula/php_dm.log

3
html/index.php Normal file
View file

@ -0,0 +1,3 @@
<?php
require("../core.php");

3
maintenance.php Normal file
View file

@ -0,0 +1,3 @@
<?php
return false;

View file

@ -0,0 +1,33 @@
<?php
require_once(__DIR__ . "/repositories/URLs.php");
class Repositories {
private PDO $connection;
private array $repositoryClasses = [
"urls" => URLs::class,
];
private array $repositoryCache = [];
public function __construct(PDO $connection) {
$this->connection = $connection;
}
public function __get(string $name) {
$repository = $this->repositoryCache[$name] ?? null;
if (!$repository) {
$repository = new ($this->repositoryClasses[$name])($this->connection);
$this->repositoryCache[$name] = $repository;
}
return $repository;
}
}
return function(PDO $connection): Repositories {
return new Repositories($connection);
};

View file

@ -0,0 +1,7 @@
<?php
return (function() {
require(ROOT . "/credentials.php");
return new PDO($DB_DSN, $DB_USERNAME, $DB_PASSWORD);
})();

116
persistence/migrate.php Normal file
View file

@ -0,0 +1,116 @@
<?php
// has to be variable instead of const because PHP sucks
$MIGRATION_TABLE = "dm_migrations";
function ensureDbStructureForMigrations(PDO $connection) {
global $MIGRATION_TABLE;
require(ROOT . "/credentials.php");
if ($connection->query(<<<EOF
SELECT
TABLE_NAME
FROM
information_schema.TABLES
WHERE
TABLE_SCHEMA LIKE '${DB_SCHEMA}' AND
TABLE_TYPE LIKE 'BASE TABLE' AND
TABLE_NAME = '${MIGRATION_TABLE}'
EOF)->rowCount() == 0
) {
if ($connection->exec(<<<EOF
CREATE TABLE `${MIGRATION_TABLE}` (
`id` INTEGER PRIMARY KEY,
`file` VARCHAR(255),
`applied` DATETIME DEFAULT CURRENT_TIMESTAMP
);
EOF) === false
) {
die("unable to bootstrap migrations: " . $connection->errorCode());
}
}
}
function getAllMigrations() {
$files = scandir(ROOT . "/persistence/migrations/");
$files = array_values(array_filter($files, fn($f) => $f[0] != "."));
sort($files);
$migrations = [];
foreach ($files as $file) {
$delimiterPos = strpos($file, "_");
$migrations[intval(substr($file, 0, $delimiterPos))] = $file;
}
return $migrations;
}
function getAppliedMigrations(PDO $connection) {
global $MIGRATION_TABLE;
$result = $connection->query(<<<EOF
SELECT * FROM `${MIGRATION_TABLE}`
EOF);
$migrations = [];
foreach ($result->fetchAll() as $row) {
$migrations[$row["id"]] = $row["file"];
}
return $migrations;
}
function getMigrationsToApply(PDO $connection) {
$all = getAllMigrations();
$applied = getAppliedMigrations($connection);
foreach ($applied as $id => $file) {
unset($all[$id]);
}
return $all;
}
function executeSqlScript(PDO $connection, string $sql, string $file) {
if ($connection->exec($sql) === false) {
die("failed to apply migration " . $file . ": " . $connection->errorCode());
}
}
function applyMigration(PDO $connection, int $id, string $file) {
global $MIGRATION_TABLE;
$connection->beginTransaction();
$sql = file_get_contents(ROOT . "/persistence/migrations/" . $file);
if (!$sql) {
die("Unable to read migration file: " . $file);
}
executeSqlScript($connection, $sql, $file);
$statement = $connection->prepare(<<<EOF
INSERT INTO `${MIGRATION_TABLE}`
(`id`, `file`) VALUES
(?, ?)
EOF);
$statement->execute([$id, $file]);
try {
$connection->commit();
} catch (PDOException $e) {
// this might happen if the migration script contains a DDL statement
// -> ignore
}
}
return function(PDO $connection) {
ensureDbStructureForMigrations($connection);
$migrations = getMigrationsToApply($connection);
foreach ($migrations as $id => $file) {
applyMigration($connection, $id, $file);
}
};

View file

View file

@ -0,0 +1,12 @@
CREATE TABLE `dm_urls` (
`id` BIGINT AUTO_INCREMENT PRIMARY KEY,
`created` DATETIME DEFAULT CURRENT_TIMESTAMP,
`updated` DATETIME DEFAULT CURRENT_TIMESTAMP,
`deleted` DATETIME,
`slug` VARCHAR(100) NOT NULL,
`url` VARCHAR(255) NOT NULL,
`access_key` VARCHAR(255),
INDEX (`slug`)
);

View file

@ -0,0 +1,21 @@
<?php
class URL {
public int $id;
public DateTime $created;
public DateTime $updated;
public DateTime $deleted;
public string $slug;
public string $url;
public string $accessKey;
public function __construct(
string $slug,
string $url,
string $accessKey
) {
$this->slug = $slug;
$this->url = $url;
$this->accessKey = $accessKey;
}
}

View file

@ -0,0 +1,40 @@
<?php
require_once(__DIR__ . "/../models/URL.php");
class URLs {
private string $table = "dm_urls";
private PDO $connection;
public function __construct(PDO $connection) {
$this->connection = $connection;
}
public function add(URL $entry) {
$statement = $this->connection->prepare(<<<EOF
INSERT INTO `$this->table`
(`slug`, `url`, `access_key`) VALUES
(?, ?, ?)
EOF);
$statement->execute([
$entry->slug,
$entry->url,
$entry->accessKey,
]);
}
public function getBySlug(string $slug) {
$statement = $this->connection->prepare(<<<EOF
SELECT * FROM `$this->table`
WHERE `slug` = ?
EOF);
$statement->execute([$slug]);
if ($statement->rowCount() == 0) {
return null;
} else {
return $statement->fetch();
}
}
}

78
router/Router.php Normal file
View file

@ -0,0 +1,78 @@
<?php
require_once(ROOT . "/utils/arrays.php");
require_once(ROOT . "/utils/error.php");
const GET = "GET";
const POST = "POST";
const PUT = "PUT";
const DELETE = "DELETE";
const HEAD = "HEAD";
const CONNECT = "CONNECT";
const OPTIONS = "OPTIONS";
const TRACE = "TRACE";
const PATCH = "PATCH";
class Router {
private $routes = [];
private $prefix = "";
public $notFoundHandler;
function __construct(string $prefix = "") {
$this->notFoundHandler = function(array &$context) {
setStatusCode(404);
require(ROOT . "/templates/404.php");
};
$this->prefix = $prefix;
}
private function findRoute(string $method, string $url) {
if (!key_exists($method, $this->routes)) {
return null;
}
$paths = $this->routes[$method];
foreach ($paths as $path => $handler) {
if (preg_match("/^" . str_replace("/", "\/", $path, ) . "$/", $url)) {
return $handler;
}
}
return null;
}
private function getPath($uri) {
if (($i = strpos($uri, "?")) !== false) {
return substr($uri, 0, $i);
} else {
return $uri;
}
}
public function addRoute(string $method, string $path, $handler) {
array_get_or_add($method, $this->routes, [])[$path] = $handler;
}
public function execute(array &$context = []) {
$path = $this->getPath($_SERVER["REQUEST_URI"]);
$path = substr($path, strlen($this->prefix));
$route = $this->findRoute($_SERVER["REQUEST_METHOD"], $path);
if (!$route) {
$route = $this->notFoundHandler;
}
$context["REQUEST_PATH"] = $path;
return $route($context);
}
// calling magic to make the router a handler and thus cascade-able
public function __invoke(array &$context = []) {
return $this->execute($context);
}
}
return new Router();

1
templates/404.php Normal file
View file

@ -0,0 +1 @@
404 Not found

View file

@ -0,0 +1 @@
We are currently in maintenance mode.

10
utils/arrays.php Normal file
View file

@ -0,0 +1,10 @@
<?php
function &array_get_or_add($needle, array &$haystack, $default=null) {
if (key_exists($needle, $haystack)) {
return $haystack[$needle];
} else {
$haystack[$needle] = $default;
return $haystack[$needle];
}
}

13
utils/error.php Normal file
View file

@ -0,0 +1,13 @@
<?php
function setStatusCode(int $status) {
header("HTTP/", true, $status);
}
function errorResponse(string $error, string $description) {
return [
"error" => $error,
"description" => $description,
"timestamp" => (new DateTime())->format("c")
];
}

45
utils/http.php Normal file
View file

@ -0,0 +1,45 @@
<?php
function request(string $method, string $url, string $body = null, array $headers = []) {
$curlResource = curl_init();
curl_setopt($curlResource, CURLOPT_URL, $url);
curl_setopt($curlResource, CURLOPT_RETURNTRANSFER, true);
curl_setopt($curlResource, CURLOPT_CUSTOMREQUEST, $method);
curl_setopt($curlResource, CURLOPT_FOLLOWLOCATION, true);
curl_setopt($curlResource, CURLOPT_TIMEOUT, 10);
curl_setopt($curlResource, CURLOPT_FAILONERROR, true);
curl_setopt($curlResource, CURLOPT_VERBOSE, true);
if ($body)
curl_setopt($curlResource, CURLOPT_POSTFIELDS, $body);
if ($headers) {
if (!array_is_list($headers))
$headers = array_map(fn($k, $v) => $k . ": " . $v, array_keys($headers), array_values($headers));
curl_setopt($curlResource, CURLOPT_HTTPHEADER, $headers);
}
$body = curl_exec($curlResource);
$result = [
"body" => $body,
"isError" => curl_errno($curlResource),
"error" => curl_error($curlResource),
"status" => curl_getinfo($curlResource, CURLINFO_RESPONSE_CODE),
];
curl_close($curlResource);
return $result;
}
function getRequest(string $url, array $headers = []) {
return request("GET", $url, null, $headers);
}
function postRequest(string $url, string $body = null, $headers = []) {
return request("POST", $url, $body, $headers);
}