diff options
| author | Thomas Vanbesien <tvanbesi@proton.me> | 2026-03-21 20:50:43 +0100 |
|---|---|---|
| committer | Thomas Vanbesien <tvanbesi@proton.me> | 2026-03-21 20:50:43 +0100 |
| commit | d1ef15fa39935bfa0420c5ac2b8c269e294c9a6d (patch) | |
| tree | 618158449863123f6b9527b9db6183f8c3ce5c91 /src/app | |
| download | camagru-d1ef15fa39935bfa0420c5ac2b8c269e294c9a6d.tar.gz camagru-d1ef15fa39935bfa0420c5ac2b8c269e294c9a6d.zip | |
Initial project scaffold
Set up MVC architecture with front controller, router, autoloader,
database singleton, and Docker Compose stack (Nginx + PHP-FPM + MariaDB).
Includes DB schema, responsive layout, dev tooling (php-cs-fixer,
parallel-lint), and documentation.
Diffstat (limited to 'src/app')
| -rw-r--r-- | src/app/Controllers/HomeController.php | 21 | ||||
| -rw-r--r-- | src/app/Database.php | 44 | ||||
| -rw-r--r-- | src/app/Router.php | 56 | ||||
| -rw-r--r-- | src/app/Views/home/index.php | 3 | ||||
| -rw-r--r-- | src/app/Views/layouts/main.php | 35 | ||||
| -rw-r--r-- | src/app/bootstrap.php | 42 |
6 files changed, 201 insertions, 0 deletions
diff --git a/src/app/Controllers/HomeController.php b/src/app/Controllers/HomeController.php new file mode 100644 index 0000000..8c462a0 --- /dev/null +++ b/src/app/Controllers/HomeController.php @@ -0,0 +1,21 @@ +<?php + +declare(strict_types=1); +// Handles the home page and verifies database connectivity. + +namespace App\Controllers; + +use App\Database; + +class HomeController +{ + public function index(): void + { + $db = Database::getInstance()->getPdo(); + $stmt = $db->query('SELECT 1'); + $dbStatus = $stmt ? 'Connected' : 'Failed'; + + $content = __DIR__ . '/../Views/home/index.php'; + include __DIR__ . '/../Views/layouts/main.php'; + } +} diff --git a/src/app/Database.php b/src/app/Database.php new file mode 100644 index 0000000..f4dff79 --- /dev/null +++ b/src/app/Database.php @@ -0,0 +1,44 @@ +<?php + +declare(strict_types=1); +// PDO singleton for MariaDB connections. + +namespace App; + +class Database +{ + private static ?Database $instance = null; + private \PDO $pdo; + + private function __construct() + { + $host = getenv('MYSQL_HOST') ?: 'mariadb'; + $db = getenv('MYSQL_DATABASE'); + $user = getenv('MYSQL_USER'); + $pass = getenv('MYSQL_PASSWORD'); + + $this->pdo = new \PDO( + "mysql:host=$host;dbname=$db;charset=utf8mb4", + $user, + $pass, + [ + \PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION, + \PDO::ATTR_DEFAULT_FETCH_MODE => \PDO::FETCH_ASSOC, + \PDO::ATTR_EMULATE_PREPARES => false, + ] + ); + } + + public static function getInstance(): self + { + if (self::$instance === null) { + self::$instance = new self(); + } + return self::$instance; + } + + public function getPdo(): \PDO + { + return $this->pdo; + } +} diff --git a/src/app/Router.php b/src/app/Router.php new file mode 100644 index 0000000..9bfe09d --- /dev/null +++ b/src/app/Router.php @@ -0,0 +1,56 @@ +<?php + +declare(strict_types=1); +// Regex-based HTTP router that dispatches requests to controller methods. + +namespace App; + +class Router +{ + private array $routes = []; + + public function get(string $path, string $controller, string $method): void + { + $this->routes[] = ['GET', $path, $controller, $method]; + } + + public function post(string $path, string $controller, string $method): void + { + $this->routes[] = ['POST', $path, $controller, $method]; + } + + public function dispatch(): void + { + $requestMethod = $_SERVER['REQUEST_METHOD']; + $uri = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH); + $uri = rtrim($uri, '/') ?: '/'; + + foreach ($this->routes as [$method, $path, $controller, $action]) { + if ($method !== $requestMethod) { + continue; + } + + // Convert route placeholders to named regex capture groups. + // Example with route '/post/{id}' and request '/post/42': + // 1. preg_replace turns '{id}' into '(?P<id>[^/]+)' + // result: '#^/post/(?P<id>[^/]+)$#' + // 2. preg_match against '/post/42' produces: + // [0 => '/post/42', 'id' => '42', 1 => '42'] + // 3. array_filter keeps only string keys: ['id' => '42'] + // 4. PostController->show('42') is called + $pattern = preg_replace('#\{(\w+)\}#', '(?P<$1>[^/]+)', $path); + $pattern = '#^' . $pattern . '$#'; + + if (preg_match($pattern, $uri, $matches)) { + $params = array_filter($matches, 'is_string', ARRAY_FILTER_USE_KEY); + $controllerClass = "App\\Controllers\\$controller"; + $instance = new $controllerClass(); + $instance->$action(...array_values($params)); + return; + } + } + + http_response_code(404); + echo '404 Not Found'; + } +} diff --git a/src/app/Views/home/index.php b/src/app/Views/home/index.php new file mode 100644 index 0000000..f5cde93 --- /dev/null +++ b/src/app/Views/home/index.php @@ -0,0 +1,3 @@ +<?php // Home page: displays a welcome message and database connection status.?> +<h1>Welcome to Camagru</h1> +<p>Database status: <?= htmlspecialchars($dbStatus) ?></p> diff --git a/src/app/Views/layouts/main.php b/src/app/Views/layouts/main.php new file mode 100644 index 0000000..b4c7dad --- /dev/null +++ b/src/app/Views/layouts/main.php @@ -0,0 +1,35 @@ +<?php // Base HTML layout: header with navigation, content slot, and footer.?> +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="UTF-8"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <title>Camagru</title> + <link rel="stylesheet" href="/css/style.css"> +</head> +<body> + <header> + <nav> + <a href="/" class="logo">Camagru</a> + <div class="nav-links"> + <a href="/gallery">Gallery</a> + <?php if (isset($_SESSION['user_id'])): ?> + <a href="/editor">Editor</a> + <a href="/profile">Profile</a> + <a href="/logout">Logout</a> + <?php else: ?> + <a href="/login">Login</a> + <a href="/register">Register</a> + <?php endif; ?> + </div> + </nav> + </header> + <main> + <?php include $content; ?> + </main> + <footer> + <p>Camagru © <?= date('Y') ?></p> + </footer> + <script src="/js/app.js"></script> +</body> +</html> diff --git a/src/app/bootstrap.php b/src/app/bootstrap.php new file mode 100644 index 0000000..835615b --- /dev/null +++ b/src/app/bootstrap.php @@ -0,0 +1,42 @@ +<?php + +declare(strict_types=1); +// Application bootstrap: loads .env, registers the autoloader, and configures error reporting. + +session_start(); + +// Load .env +$envFile = dirname(__DIR__, 2) . '/.env'; +if (file_exists($envFile)) { + $lines = file($envFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES); + foreach ($lines as $line) { + if (str_starts_with(trim($line), '#')) { + continue; + } + $parts = explode('=', $line, 2); + if (count($parts) === 2) { + $key = trim($parts[0]); + $value = trim($parts[1]); + $_ENV[$key] = $value; + putenv("$key=$value"); + } + } +} + +// Autoloader +spl_autoload_register(function (string $class): void { + $prefix = 'App\\'; + if (!str_starts_with($class, $prefix)) { + return; + } + $relative = substr($class, strlen($prefix)); + $file = __DIR__ . '/' . str_replace('\\', '/', $relative) . '.php'; + if (file_exists($file)) { + require $file; + } +}); + +// Error reporting +error_reporting(E_ALL); +ini_set('display_errors', '0'); +ini_set('log_errors', '1'); |
