Compare commits

...

5 Commits

19 changed files with 1847 additions and 19 deletions
+1
View File
@@ -0,0 +1 @@
node_modules/
+15 -4
View File
@@ -1,11 +1,22 @@
FROM node:22-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY assets/ ./assets/
COPY templates/ ./templates/
# JS bauen
RUN ./node_modules/.bin/esbuild ./assets/javascript/test.js --bundle --outfile=./public/build/app.js --minify
# CSS bauen
RUN ./node_modules/.bin/tailwindcss -i ./assets/css/app.css -o ./public/build/app.css --minify
FROM php:8.3-apache FROM php:8.3-apache
RUN a2enmod rewrite RUN a2enmod rewrite
RUN docker-php-ext-install pdo_mysql
COPY . /var/www/html COPY . /var/www/html
COPY --from=builder /app/public/build /var/www/html/public/build
RUN sed -i 's|/var/www/html|/var/www/html/public|g' \ RUN sed -i 's|/var/www/html|/var/www/html/public|g' \
/etc/apache2/sites-available/000-default.conf /etc/apache2/sites-available/000-default.conf
RUN sed -i 's|AllowOverride None|AllowOverride All|g' \ RUN sed -i 's|AllowOverride None|AllowOverride All|g' \
/etc/apache2/apache2.conf /etc/apache2/apache2.conf
+1
View File
@@ -0,0 +1 @@
@import "tailwindcss";
+11
View File
@@ -0,0 +1,11 @@
const colors = ['#ff6b6b', '#4ecdc4', '#45b7d1', '#96ceb4', '#ffeaa7', '#dda0dd'];
let current = 0;
setInterval(() => {
current = (current + 1) % colors.length;
document.body.style.backgroundColor = colors[current];
}, 1000);
window.test = function(world) {
console.log('Hello, ' + world + '!');
}
+40
View File
@@ -0,0 +1,40 @@
services:
app:
build:
context: .
dockerfile: Dockerfile
container_name: pawra-app
ports:
- "8080:80"
environment:
DB_HOST: db
DB_PORT: 3306
DB_NAME: pawra
DB_USER: pawra
DB_PASS: pawra_pw
depends_on:
db:
condition: service_healthy
db:
image: mysql:8.4
container_name: pawra-db
restart: unless-stopped
environment:
MYSQL_ROOT_PASSWORD: root_pw
MYSQL_DATABASE: pawra
MYSQL_USER: pawra
MYSQL_PASSWORD: pawra_pw
ports:
- "3306:3306"
volumes:
- db_data:/var/lib/mysql
- ./migrations/schema.sql:/docker-entrypoint-initdb.d/01-schema.sql:ro
healthcheck:
test: ["CMD-SHELL", "mysqladmin ping -h localhost -uroot -proot_pw || exit 1"]
interval: 5s
timeout: 3s
retries: 20
volumes:
db_data:
+1 -1
View File
@@ -1,4 +1,4 @@
<?php <?php
use src\Controller\HomeController; use src\Controller\HomeController;
$router->get('/', [new HomeController(), 'index']); $router->get('/', [HomeController::class, 'index']);
+23
View File
@@ -0,0 +1,23 @@
CREATE DATABASE IF NOT EXISTS auth_system
CHARACTER SET utf8mb4
COLLATE utf8mb4_unicode_ci;
USE auth_system;
CREATE TABLE IF NOT EXISTS users (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(30) NOT NULL UNIQUE,
email VARCHAR(255) NOT NULL UNIQUE,
password_hash VARCHAR(255) NOT NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
last_login DATETIME NULL,
is_active TINYINT(1) NOT NULL DEFAULT 1
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS remember_tokens (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
user_id INT UNSIGNED NOT NULL,
token_hash VARCHAR(255) NOT NULL,
expires_at DATETIME NOT NULL,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+1540
View File
File diff suppressed because it is too large Load Diff
+22
View File
@@ -0,0 +1,22 @@
{
"name": "pawra",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"repository": {
"type": "git",
"url": "git@git.pawly.dev:cheeseburger/pawra.git"
},
"keywords": [],
"author": "",
"license": "ISC",
"type": "commonjs",
"devDependencies": {
"@tailwindcss/cli": "^4.2.4",
"esbuild": "0.28.0",
"tailwindcss": "^4.2.4"
}
}
+21 -3
View File
@@ -1,13 +1,31 @@
<?php <?php
require_once __DIR__ . '/../src/Autoloader.php'; require_once __DIR__ . '/../src/Autoloader.php';
use src\Router;
$autoloader = new Autoloader(); $autoloader = new Autoloader();
$autoloader->addNamespace('src', __DIR__ . '/../src'); $autoloader->addNamespace('src', __DIR__ . '/../src');
$autoloader->register(); $autoloader->register();
$router = new Router(); use src\Container;
use src\Router;
use src\Middleware\TestMiddleware;
use src\Database;
$container = new Container();
$container->bind(Database::class, function() {
return new Database(
$_ENV['DB_HOST'] ?? '127.0.0.1',
$_ENV['DB_PORT'] ?? '3306',
$_ENV['DB_NAME'] ?? 'pawra',
$_ENV['DB_USER'] ?? 'root',
$_ENV['DB_PASS'] ?? ''
);
});
$router = new Router($container);
$router->addGlobalMiddleware(TestMiddleware::class);
require_once __DIR__ . '/../config/routes.php'; require_once __DIR__ . '/../config/routes.php';
$router->dispatch($_SERVER['REQUEST_METHOD'], $_SERVER['REQUEST_URI']); $router->dispatch($_SERVER['REQUEST_METHOD'], $_SERVER['REQUEST_URI']);
+50
View File
@@ -0,0 +1,50 @@
<?php
namespace src;
class Container
{
private array $bindings = [];
public function bind(string $abstract, callable $factory): void
{
$this->bindings[$abstract] = $factory;
}
public function make(string $class): object
{
if (isset($this->bindings[$class])) {
return ($this->bindings[$class])($this);
}
return $this->resolve($class);
}
private function resolve(string $class): object
{
$reflector = new \ReflectionClass($class);
$constructor = $reflector->getConstructor();
if ($constructor === null) {
return new $class();
}
$dependencies = array_map(function($param) {
$type = $param->getType();
if ($type === null) {
throw new \Exception("Cannot resolve parameter \${$param->getName()} — no type hint.");
}
if ($type->isBuiltin()) {
if ($param->isDefaultValueAvailable()) {
return $param->getDefaultValue();
}
throw new \Exception("Cannot resolve primitive parameter \${$param->getName()} — no default value.");
}
return $this->make($type->getName());
}, $constructor->getParameters());
return $reflector->newInstanceArgs($dependencies);
}
}
+5 -1
View File
@@ -3,11 +3,15 @@ namespace src\Controller;
abstract class BaseController abstract class BaseController
{ {
public function __construct(
protected string $templatePath = __DIR__ . '/../../templates/'
) {}
protected function render(string $view, array $data = []): void protected function render(string $view, array $data = []): void
{ {
extract($data); extract($data);
$viewPath = __DIR__ . '/../../templates/' . $view . '.php'; $viewPath = $this->templatePath . '/' . $view . '.php';
if (!file_exists($viewPath)) { if (!file_exists($viewPath)) {
http_response_code(500); http_response_code(500);
+12 -1
View File
@@ -1,12 +1,23 @@
<?php <?php
namespace src\Controller; namespace src\Controller;
use src\Database;
class HomeController extends BaseController class HomeController extends BaseController
{ {
public function __construct(private Database $db)
{
parent::__construct();
}
public function index(): void public function index(): void
{ {
$stmt = $this->db->pdo()->query('SELECT NOW() as now');
$row = $stmt->fetch();
$this->render('home', [ $this->render('home', [
'title' => 'meow :3' 'title' => 'meow :3',
'now' => $row['now'] ?? null,
]); ]);
} }
} }
+30
View File
@@ -0,0 +1,30 @@
<?php
namespace src;
use PDO;
use PDOException;
class Database
{
private PDO $pdo;
public function __construct(
string $host,
string $port,
string $name,
string $user,
string $password
) {
$dsn = "mysql:host={$host};port={$port};dbname={$name};charset=utf8mb4";
$this->pdo = new PDO($dsn, $user, $password, [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
]);
}
public function pdo(): PDO
{
return $this->pdo;
}
}
+7
View File
@@ -0,0 +1,7 @@
<?php
namespace src\Middleware;
interface MiddlewareInterface
{
public function handle(callable $next): void;
}
+11
View File
@@ -0,0 +1,11 @@
<?php
namespace src\Middleware;
class TestMiddleware implements MiddlewareInterface
{
public function handle(callable $next): void
{
error_log('TestMiddleware: Handling request...');
$next();
}
}
+49 -9
View File
@@ -4,28 +4,68 @@ namespace src;
class Router class Router
{ {
private array $routes = []; private array $routes = [];
private array $globalMiddleware = [];
private Container $container;
public function get(string $path, callable $handler): void public function __construct(Container $container)
{ {
$this->routes['GET'][$path] = $handler; $this->container = $container;
} }
public function post(string $path, callable $handler): void public function get(string $path, array $handler, array $middleware = []): void
{ {
$this->routes['POST'][$path] = $handler; $this->routes['GET'][$path] = compact('handler', 'middleware');
}
public function post(string $path, array $handler, array $middleware = []): void
{
$this->routes['POST'][$path] = compact('handler', 'middleware');
}
public function addGlobalMiddleware(string $middleware): void
{
$this->globalMiddleware[] = $middleware;
} }
public function dispatch(string $method, string $uri): void public function dispatch(string $method, string $uri): void
{ {
$path = parse_url($uri, PHP_URL_PATH) ?? '/'; $route = $this->routes[$method][$uri] ?? null;
$handler = $this->routes[$method][$path] ?? null;
if ($handler === null) { if ($route === null) {
http_response_code(404); http_response_code(404);
echo '404 Not Found'; require __DIR__ . '/../views/404.php';
return; return;
} }
call_user_func($handler); [$controllerClass, $action] = $route['handler'];
$final = function() use ($controllerClass, $action) {
$controller = $this->container->make($controllerClass);
$controller->$action();
};
$middleware = array_merge($this->globalMiddleware, $route['middleware']);
$this->run($middleware, $final);
}
private function run(array $middleware, callable $final): void
{
if (empty($middleware)) {
$final();
return;
}
$chain = array_reduce(
array_reverse($middleware),
function(callable $carry, string $middlewareClass) {
return function() use ($carry, $middlewareClass) {
$instance = $this->container->make($middlewareClass);
$instance->handle($carry);
};
},
$final
);
$chain();
} }
} }
+6
View File
@@ -5,6 +5,12 @@ ob_start();
<h1><?= htmlspecialchars($title) ?></h1> <h1><?= htmlspecialchars($title) ?></h1>
<p>Willkommen auf meiner Seite!</p> <p>Willkommen auf meiner Seite!</p>
<p class="text-red-500">I am red.</p>
<button onclick="test('World')">Test</button>
<p><?= htmlspecialchars($now) ?></p>
<?php <?php
$content = ob_get_clean(); $content = ob_get_clean();
require __DIR__ . '/layout.php'; require __DIR__ . '/layout.php';
+2
View File
@@ -3,6 +3,8 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<title><?= htmlspecialchars($title) ?></title> <title><?= htmlspecialchars($title) ?></title>
<link rel="stylesheet" href="/build/app.css">
<script src="/build/app.js" defer></script>
</head> </head>
<body> <body>
<?= $content ?> <?= $content ?>