Files
nexus/BASE_FILES.md
Lars Gebhardt-Kusche aea4e9fa5f
Some checks failed
Deploy / deploy-staging (push) Failing after 28s
Deploy / deploy-production (push) Has been skipped
upgrade domain
2026-04-13 01:36:20 +02:00

12 KiB

Basisdateien fuer neue Projekte

Dieses Dokument enthaelt die Basisdateien, die bei einem neuen Projekt direkt angelegt werden sollen. Der Schwerpunkt liegt auf den Config-Dateien, weil diese fuer den ersten lauffaehigen Stand zwingend benoetigt werden.

Dieses Dokument ist dafuer gedacht, zusammen mit GENERAL.md in ein neues GitLab-Projekt kopiert zu werden. Danach kann die Projektstruktur inklusive Basisdateien direkt erstellt werden.

Wichtige Regel vor der Erstellung

Ein neues Projekt darf nur erstellt werden, wenn beide Domains bekannt sind:

  • Live-Domain
  • Staging-Domain

Wenn eine oder beide Angaben fehlen, muss die Erstellung gestoppt werden. Vor dem Anlegen der Dateien ist dann explizit nach beiden Domains zu fragen.

Ohne beide Domains duerfen insbesondere diese Dateien nicht final erzeugt werden:

  • config/prod/domaindata.php
  • config/prod/settings.php
  • config/staging/domaindata.php
  • config/staging/settings.php

Vor der Neuerstellung ist das Repository ausserdem auf den neuen Projektstand zurueckzusetzen:

  • alle bestehenden Dateien und Ordner loeschen
  • .gitlab-ci.yml ausdruecklich behalten
  • .gitlab-ci.yml anschliessend auf alte Domainreferenzen, Umgebungs-URLs und projektspezifische Angaben pruefen
  • gefundene Domainreferenzen in .gitlab-ci.yml auf die neue Live- und Staging-Domain anpassen

Platzhalter fuer neue Projekte

In den folgenden Vorlagen werden diese Platzhalter verwendet:

  • <LIVE_DOMAIN> fuer die Produktiv-Domain
  • <STAGING_DOMAIN> fuer die Staging-Domain
  • <APP_PREFIX> fuer den Cookie- und App-Prefix
  • <APP_NAME> fuer einen allgemeinen Projektnamen oder OIDC-Client-Namen

Datei: config/fileload.php

<?php
declare(strict_types=1);

spl_autoload_register(function ($class) {
    if (str_starts_with($class, 'App\\Repository\\')) {
        $prefix = 'App\\Repository\\';
        $baseDir = __DIR__ . '/../src/Repository/';
    } elseif (str_starts_with($class, 'App\\')) {
        $prefix = 'App\\';
        $baseDir = __DIR__ . '/../src/App/';
    } else {
        return;
    }

    $len = strlen($prefix);
    $relativeClass = substr($class, $len);
    $file = $baseDir . str_replace('\\', '/', $relativeClass) . '.php';

    if (file_exists($file)) {
        require $file;
    }
});

require_once __DIR__ . '/../src/App/functions.php';

$domainFile = __DIR__ . '/domaindata.php';
$settingsFile = __DIR__ . '/settings.php';
$configFile = __DIR__ . '/db.php';
$baseConfigFile = __DIR__ . '/base_db.php';
$fallbackBaseConfigStaging = __DIR__ . '/staging/db_settings_basic.php';
$fallbackBaseConfigProd = __DIR__ . '/prod/db_settings_basic.php';

if (file_exists($domainFile)) {
    require_once $domainFile;
}

if (file_exists($settingsFile)) {
    require_once $settingsFile;
}

$dbConfig = [];
if (file_exists($configFile)) {
    $dbConfig = require $configFile;
}

$baseDbConfig = [];
if (file_exists($baseConfigFile)) {
    $baseDbConfig = require $baseConfigFile;
}
if (empty($baseDbConfig) && file_exists($fallbackBaseConfigStaging)) {
    $baseDbConfig = require $fallbackBaseConfigStaging;
}
if (empty($baseDbConfig) && file_exists($fallbackBaseConfigProd)) {
    $baseDbConfig = require $fallbackBaseConfigProd;
}

global $appConfig;
$dbEnabled = defined('APP_DB_ENABLED') ? APP_DB_ENABLED : true;
$baseDbEnabled = defined('APP_BASE_DB_ENABLED') ? APP_BASE_DB_ENABLED : false;
$appConfig = new \App\Config($dbConfig, $dbEnabled, $baseDbConfig, $baseDbEnabled);

\App\App::init($appConfig);

Datei: config/config.php

<?php
declare(strict_types=1);

ini_set('display_errors', '1');
ini_set('display_startup_errors', '1');
error_reporting(E_ALL);

foreach (['domaindata.php', 'settings.php'] as $cfgFile) {
    $rootPath = __DIR__ . '/' . $cfgFile;

    if (file_exists($rootPath)) {
        require_once $rootPath;
    } else {
        throw new \RuntimeException("Missing required config file: $cfgFile (expected $rootPath)");
    }
}

if (!defined('ASSET_VERSION')) {
    define('ASSET_VERSION', 'dev-' . date('Ymd-His'));
}

if (!defined('APP_DOMAIN_PRIMARY')) {
    define('APP_DOMAIN_PRIMARY', APP_DOMAIN_NAME);
}
if (!defined('APP_URL_PRIMARY')) {
    define('APP_URL_PRIMARY', 'https://' . APP_DOMAIN_PRIMARY);
}
if (!defined('APP_API_BASE')) {
    define('APP_API_BASE', 'https://api.' . APP_DOMAIN_PRIMARY);
}
if (!defined('APP_DB_ENABLED')) {
    define('APP_DB_ENABLED', false);
}

Datei: config/base_db.php

<?php
declare(strict_types=1);

$path = __DIR__ . '/db_settings_basic.php';
if (!file_exists($path)) {
    throw new RuntimeException('Missing base DB config: expected config/db_settings_basic.php');
}

return require $path;

Datei: config/staging/domaindata.php

<?php
declare(strict_types=1);

if (!defined('APP_DOMAIN_NAME')) {
    define('APP_DOMAIN_NAME', '<STAGING_DOMAIN>');
}

if (!defined('APP_PREFIX')) {
    define('APP_PREFIX', '<APP_PREFIX>');
}

Datei: config/prod/domaindata.php

<?php
declare(strict_types=1);

if (!defined('APP_DOMAIN_NAME')) {
    define('APP_DOMAIN_NAME', '<LIVE_DOMAIN>');
}

if (!defined('APP_PREFIX')) {
    define('APP_PREFIX', '<APP_PREFIX>');
}

Datei: config/staging/settings.php

<?php
define('ASSET_VERSION', 'dev-' . date('Ymd-His'));
define('APP_DOMAIN_PRIMARY', APP_DOMAIN_NAME);
define('APP_URL_PRIMARY', 'https://' . APP_DOMAIN_PRIMARY);
define('APP_API_BASE', 'https://api.' . APP_DOMAIN_PRIMARY);

define('APP_DB_ENABLED', false);
define('APP_DB_DEBUG', true);
define('APP_DB_AUTO_INIT', true);
define('APP_BASE_DB_ENABLED', true);

define('APP_AUTH_ENABLED', false);
define('APP_DEBUG_TOOL', true);
define('APP_AUTH_DEBUG', true);

/*
Optional fuer Projekte mit OIDC:

define('APP_OIDC_ISSUER', 'https://auth.example.tld/realms/<APP_NAME>');
define('APP_OIDC_AUTH_ENDPOINT', 'https://auth.example.tld/realms/<APP_NAME>/protocol/openid-connect/auth');
define('APP_OIDC_TOKEN_ENDPOINT', 'https://auth.example.tld/realms/<APP_NAME>/protocol/openid-connect/token');
define('APP_OIDC_USERINFO_ENDPOINT', 'https://auth.example.tld/realms/<APP_NAME>/protocol/openid-connect/userinfo');
define('APP_OIDC_LOGOUT_ENDPOINT', 'https://auth.example.tld/realms/<APP_NAME>/protocol/openid-connect/logout');
define('APP_OIDC_CLIENT_ID', '<APP_NAME>');
define('APP_OIDC_CLIENT_SECRET', 'CHANGE_ME');
define('APP_OIDC_REDIRECT_URI', 'https://<STAGING_DOMAIN>/auth/callback');
define('APP_OIDC_POST_LOGOUT_REDIRECT_URI', 'https://<STAGING_DOMAIN>/');
define('APP_OIDC_GROUP_CLAIM', 'groups');
define('APP_OIDC_ADMIN_GROUP', 'appadmin');
define('APP_OIDC_USER_GROUP', 'user');
*/

Datei: config/prod/settings.php

<?php
define('ASSET_VERSION', 'dev-' . date('Ymd-His'));
define('APP_DOMAIN_PRIMARY', APP_DOMAIN_NAME);
define('APP_URL_PRIMARY', 'https://' . APP_DOMAIN_PRIMARY);
define('APP_API_BASE', 'https://api.' . APP_DOMAIN_PRIMARY);

define('APP_DB_ENABLED', false);
define('APP_DB_DEBUG', false);
define('APP_DB_AUTO_INIT', true);
define('APP_BASE_DB_ENABLED', true);

define('APP_AUTH_ENABLED', false);
define('APP_DEBUG_TOOL', false);
define('APP_AUTH_DEBUG', false);

/*
Optional fuer Projekte mit OIDC:

define('APP_OIDC_ISSUER', 'https://auth.example.tld/realms/<APP_NAME>');
define('APP_OIDC_AUTH_ENDPOINT', 'https://auth.example.tld/realms/<APP_NAME>/protocol/openid-connect/auth');
define('APP_OIDC_TOKEN_ENDPOINT', 'https://auth.example.tld/realms/<APP_NAME>/protocol/openid-connect/token');
define('APP_OIDC_USERINFO_ENDPOINT', 'https://auth.example.tld/realms/<APP_NAME>/protocol/openid-connect/userinfo');
define('APP_OIDC_LOGOUT_ENDPOINT', 'https://auth.example.tld/realms/<APP_NAME>/protocol/openid-connect/logout');
define('APP_OIDC_CLIENT_ID', '<APP_NAME>');
define('APP_OIDC_CLIENT_SECRET', 'CHANGE_ME');
define('APP_OIDC_REDIRECT_URI', 'https://<LIVE_DOMAIN>/auth/callback');
define('APP_OIDC_POST_LOGOUT_REDIRECT_URI', 'https://<LIVE_DOMAIN>/');
define('APP_OIDC_GROUP_CLAIM', 'groups');
define('APP_OIDC_ADMIN_GROUP', 'appadmin');
define('APP_OIDC_USER_GROUP', 'user');
*/

Datei: config/staging/db_settings_basic.php

<?php
declare(strict_types=1);

return [
    'dsn' => 'sqlite:' . __DIR__ . '/../../data/app_staging.sqlite',
    'user' => null,
    'password' => null,
    'options' => [
        PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
        PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
    ],
];

Datei: config/prod/db_settings_basic.php

<?php
declare(strict_types=1);

return [
    'dsn' => 'sqlite:' . __DIR__ . '/../../data/app_prod.sqlite',
    'user' => null,
    'password' => null,
    'options' => [
        PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
        PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
    ],
];

Datei: public/index.php

Minimaler Einstiegspunkt fuer den Webzugriff:

<?php
declare(strict_types=1);

require_once __DIR__ . '/../config/fileload.php';

$uriPath = parse_url($_SERVER['REQUEST_URI'] ?? '/', PHP_URL_PATH) ?: '/';
$uriPath = preg_replace('~/{2,}~', '/', $uriPath);
$uriPath = trim($uriPath, '/');

if (str_contains($uriPath, '..')) {
    http_response_code(400);
    exit('Bad request');
}

$pagesBase = realpath(__DIR__ . '/../partials/landingpages') ?: (__DIR__ . '/../partials/landingpages');
$page404 = $pagesBase . '/404.php';

if (preg_match('~^module/([a-zA-Z0-9_-]+)(?:/(.+))?$~', $uriPath, $m)) {
    $module = $m[1];
    $page = isset($m[2]) && $m[2] !== '' ? trim($m[2], '/') : 'index';
    $modulePage = app()->modules()->resolvePage($module, $page);
    $target = $modulePage ?: $page404;
    if (!$modulePage) {
        http_response_code(404);
    }
} elseif ($uriPath === '' || $uriPath === 'index' || $uriPath === 'index.php') {
    $target = $pagesBase . '/index.php';
} else {
    $base = $pagesBase . '/' . $uriPath;
    if (is_dir($base) && is_file($base . '/index.php')) {
        $target = $base . '/index.php';
    } elseif (is_file($base . '.php')) {
        $target = $base . '.php';
    } else {
        http_response_code(404);
        $target = $page404;
    }
}

ob_start();
require $target;
$content = ob_get_clean();

tpl('layout_start', 'structure');
echo $content;
tpl('layout_end', 'structure');

Datei: partials/landingpages/index.php

<div class="card">
  <h1>Projektstart</h1>
  <p>Die Grundstruktur wurde erfolgreich erstellt.</p>
</div>

Datei: partials/landingpages/404.php

<div class="card">
  <h1>404</h1>
  <p>Die angeforderte Seite wurde nicht gefunden.</p>
</div>

Datei: partials/structure/layout_start.php

<!doctype html>
<html lang="de">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Projektbasis</title>
  <link rel="stylesheet" href="/assets/css/app.css?v=<?= urlencode(app()->config()->assetVersion) ?>">
</head>
<body>
  <main class="main-content">

Datei: partials/structure/layout_end.php

  </main>
  <script src="/assets/js/app.js?v=<?= urlencode(app()->config()->assetVersion) ?>" defer></script>
</body>
</html>

Datei: public/assets/css/app.css

body {
  margin: 0;
  font-family: sans-serif;
  background: #f5f5f5;
  color: #111;
}

.main-content {
  max-width: 960px;
  margin: 0 auto;
  padding: 32px 16px;
}

.card {
  background: #fff;
  border: 1px solid #ddd;
  border-radius: 12px;
  padding: 24px;
}

Datei: public/assets/js/app.js

document.documentElement.classList.add('js');

Datei: public/.htaccess

RewriteEngine On

RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^ index.php [QSA,L]

Empfohlene Startreihenfolge in einem neuen Projekt

  1. Neues GitLab-Projekt anlegen.
  2. Repository lokal mit VS Code verknuepfen.
  3. Alle bestehenden Dateien und Ordner entfernen, .gitlab-ci.yml jedoch behalten.
  4. GENERAL.md und BASE_FILES.md in das neue Repository kopieren.
  5. .gitlab-ci.yml auf alte Domain- und Projektreferenzen pruefen und anpassen.
  6. Pflichtstruktur aus GENERAL.md anlegen.
  7. Basisdateien aus BASE_FILES.md anlegen.
  8. Live- und Staging-Domain einsetzen.
  9. Danach erst die eigentliche Projekterstellung oder Modulentwicklung starten.

Kurzregel

Ohne GENERAL.md, ohne BASE_FILES.md oder ohne beide Domains ist die saubere Erstellung eines neuen Projekts auf dieser Basis nicht vollstaendig und soll nicht ausgefuehrt werden.