718 lines
20 KiB
Markdown
718 lines
20 KiB
Markdown
# IMVS - Architektur und Grundprinzipien
|
|
|
|
## Inhaltsverzeichnis
|
|
|
|
1. [Projektübersicht](#1-projektübersicht)
|
|
2. [Framework: MintyPHP](#2-framework-mintyphp)
|
|
3. [Verzeichnisstruktur](#3-verzeichnisstruktur)
|
|
4. [Datenbankarchitektur](#4-datenbankarchitektur)
|
|
5. [PHP-Architektur](#5-php-architektur)
|
|
6. [Frontend-Architektur](#6-frontend-architektur)
|
|
7. [Template-System](#7-template-system)
|
|
8. [Authentifizierung und Autorisierung](#8-authentifizierung-und-autorisierung)
|
|
9. [Internationalisierung](#9-internationalisierung)
|
|
10. [Best Practices](#10-best-practices)
|
|
|
|
---
|
|
|
|
## 1. Projektübersicht
|
|
|
|
**Projektname:** IMVS
|
|
**Framework:** MintyPHP v3+
|
|
**PHP-Version:** 8.5+
|
|
**Datenbank:** MySQL 5.7+
|
|
**Lizenz:** MIT
|
|
|
|
### Kernfeatures
|
|
|
|
- Multi-Tenant-Architektur
|
|
- Rollen- und Berechtigungssystem
|
|
- Mehrsprachigkeit (DE/EN)
|
|
- Dark/Light Theme
|
|
- E-Mail-Integration mit Templates
|
|
|
|
---
|
|
|
|
## 2. Framework: MintyPHP
|
|
|
|
### Philosophie
|
|
|
|
MintyPHP ist ein leichtgewichtiges PHP-Framework mit folgenden Prinzipien:
|
|
|
|
- **Easy to learn** - Einfach für PHP-Entwickler zu erlernen
|
|
- **Secure by design** - Sicherheit als Grundprinzip
|
|
- **Lightweight** - Minimaler Overhead
|
|
- **Single variable scope** - Eine Variable-Scope für alle Layer
|
|
- **SQL required** - Direktes SQL statt ORM
|
|
- **PHP as templating** - PHP selbst als Template-Sprache
|
|
|
|
### Kern-Komponenten
|
|
|
|
| Komponente | Beschreibung |
|
|
|------------|--------------|
|
|
| `Router` | URL-Routing zu Pages |
|
|
| `DB` | Datenbankzugriff mit Prepared Statements |
|
|
| `Auth` | Basis-Authentifizierung |
|
|
| `Session` | Session-Management |
|
|
| `Buffer` | Output-Buffering für Templates |
|
|
| `I18n` | Internationalisierung |
|
|
| `Cache` | Memcached-Integration |
|
|
| `Firewall` | DDoS-Schutz |
|
|
|
|
### Router-Konvention
|
|
|
|
```
|
|
URL: /admin/users/edit/abc-123
|
|
Datei: pages/admin/users/edit($id).php
|
|
Variable: $id = 'abc-123'
|
|
```
|
|
|
|
---
|
|
|
|
## 3. Verzeichnisstruktur
|
|
|
|
```
|
|
ImmobilienMaklerVerkaufssoftware/
|
|
├── config/ # Konfigurationsdateien
|
|
│ ├── config.php # Hauptkonfiguration
|
|
│ ├── router.php # Route-Definitionen
|
|
│ └── settings.php # App-Einstellungen (generiert)
|
|
├── db/ # Datenbank
|
|
│ └── init/init.sql # Schema und Seeds
|
|
├── docker/ # Docker-Konfiguration
|
|
├── docs/ # Dokumentation
|
|
├── i18n/ # Sprachdateien
|
|
│ ├── default_de.json
|
|
│ └── default_en.json
|
|
├── lib/ # Geschäftslogik
|
|
│ ├── Http/ # HTTP-Utilities
|
|
│ ├── Repository/ # Datenbankschicht
|
|
│ ├── Service/ # Business-Logic
|
|
│ └── Support/ # Helper und Guards
|
|
├── pages/ # Seiten (Controller)
|
|
│ ├── admin/ # Geschützte Admin-Seiten
|
|
│ ├── auth/ # Authentifizierung
|
|
│ ├── error/ # Fehlerseiten
|
|
│ └── page/ # CMS-Seiten
|
|
├── storage/ # Dateiablage
|
|
│ ├── branding/ # Logos, Favicons
|
|
│ ├── tenants/ # Tenant-Dateien
|
|
│ └── users/ # User-Avatare
|
|
├── templates/ # View-Templates
|
|
│ ├── partials/ # Wiederverwendbare Teile
|
|
│ └── emails/ # E-Mail-Templates
|
|
├── tests/ # PHPUnit Tests
|
|
├── vendor/ # Composer Dependencies
|
|
└── web/ # Public Root
|
|
├── css/ # Stylesheets
|
|
├── js/ # JavaScript-Module
|
|
├── vendor/ # Frontend-Libraries
|
|
└── index.php # Entry Point
|
|
```
|
|
|
|
---
|
|
|
|
## 4. Datenbankarchitektur
|
|
|
|
### Entity-Relationship-Diagramm
|
|
|
|
```
|
|
┌─────────────┐ ┌─────────────────┐ ┌─────────────┐
|
|
│ USERS │────<│ USER_TENANTS │>────│ TENANTS │
|
|
└─────────────┘ └─────────────────┘ └─────────────┘
|
|
│ │
|
|
│ ┌─────────────────┐ │
|
|
└────────────<│ USER_ROLES │ │
|
|
│ └─────────────────┘ │
|
|
│ │ │
|
|
│ ▼ │
|
|
│ ┌─────────────┐ │
|
|
│ │ ROLES │ │
|
|
│ └─────────────┘ │
|
|
│ │ │
|
|
│ ┌─────────────────┐ │
|
|
│ │ROLE_PERMISSIONS │ │
|
|
│ └─────────────────┘ │
|
|
│ │ │
|
|
│ ▼ │
|
|
│ ┌─────────────┐ │
|
|
│ │ PERMISSIONS │ │
|
|
│ └─────────────┘ │
|
|
│ │
|
|
│ ┌─────────────────┐ ┌──────┴──────┐
|
|
└────────────<│USER_DEPARTMENTS │>────│ DEPARTMENTS │
|
|
└─────────────────┘ └─────────────┘
|
|
│
|
|
┌──────┴──────┐
|
|
│TENANT_DEPTS │
|
|
└─────────────┘
|
|
```
|
|
|
|
### Haupttabellen
|
|
|
|
#### users
|
|
```sql
|
|
id INT PRIMARY KEY AUTO_INCREMENT
|
|
uuid VARCHAR(36) UNIQUE
|
|
first_name VARCHAR(100)
|
|
last_name VARCHAR(100)
|
|
email VARCHAR(255) UNIQUE
|
|
password VARCHAR(255) -- bcrypt hash
|
|
locale VARCHAR(10) -- 'de', 'en'
|
|
theme VARCHAR(20) -- 'light', 'dark'
|
|
active TINYINT DEFAULT 1
|
|
primary_tenant_id INT FK -> tenants.id
|
|
current_tenant_id INT FK -> tenants.id
|
|
created_by INT FK -> users.id
|
|
modified_by INT FK -> users.id
|
|
created DATETIME
|
|
modified DATETIME
|
|
```
|
|
|
|
#### tenants
|
|
```sql
|
|
id INT PRIMARY KEY AUTO_INCREMENT
|
|
uuid VARCHAR(36) UNIQUE
|
|
description VARCHAR(255) -- Name/Firma
|
|
address VARCHAR(255)
|
|
postal_code VARCHAR(20)
|
|
city VARCHAR(100)
|
|
country VARCHAR(100)
|
|
vat_id VARCHAR(50) -- MwSt-ID
|
|
tax_number VARCHAR(50)
|
|
phone VARCHAR(50)
|
|
email VARCHAR(255)
|
|
website VARCHAR(255)
|
|
created_by INT FK -> users.id
|
|
modified_by INT FK -> users.id
|
|
created DATETIME
|
|
modified DATETIME
|
|
```
|
|
|
|
#### roles
|
|
```sql
|
|
id INT PRIMARY KEY AUTO_INCREMENT
|
|
uuid VARCHAR(36) UNIQUE
|
|
description VARCHAR(255) -- Rollenname
|
|
created_by INT FK -> users.id
|
|
modified_by INT FK -> users.id
|
|
created DATETIME
|
|
modified DATETIME
|
|
```
|
|
|
|
#### permissions
|
|
```sql
|
|
id INT PRIMARY KEY AUTO_INCREMENT
|
|
key VARCHAR(100) UNIQUE -- z.B. 'users.view'
|
|
description VARCHAR(255)
|
|
created DATETIME
|
|
```
|
|
|
|
#### departments
|
|
```sql
|
|
id INT PRIMARY KEY AUTO_INCREMENT
|
|
uuid VARCHAR(36) UNIQUE
|
|
description VARCHAR(255) -- Abteilungsname
|
|
created_by INT FK -> users.id
|
|
modified_by INT FK -> users.id
|
|
created DATETIME
|
|
modified DATETIME
|
|
```
|
|
|
|
### Verknüpfungstabellen
|
|
|
|
| Tabelle | Beschreibung |
|
|
|---------|--------------|
|
|
| `user_tenants` | User ↔ Tenant (M:N) |
|
|
| `user_roles` | User ↔ Role (M:N) |
|
|
| `user_departments` | User ↔ Department (M:N) |
|
|
| `role_permissions` | Role ↔ Permission (M:N) |
|
|
| `tenant_departments` | Tenant ↔ Department (M:N) |
|
|
|
|
### Vordefinierte Permissions
|
|
|
|
```
|
|
users.view, users.create, users.update, users.delete
|
|
users.self_update, users.update_assignments
|
|
tenants.view, tenants.create, tenants.update, tenants.delete
|
|
departments.view, departments.create, departments.update, departments.delete
|
|
roles.view, roles.create, roles.update, roles.delete
|
|
permissions.view, permissions.create, permissions.update, permissions.delete
|
|
settings.view, settings.update
|
|
mail_log.view
|
|
stats.view
|
|
```
|
|
|
|
---
|
|
|
|
## 5. PHP-Architektur
|
|
|
|
### Schichten-Architektur
|
|
|
|
```
|
|
┌─────────────────────────────────────────────┐
|
|
│ Pages │
|
|
│ (Controller/Actions) │
|
|
├─────────────────────────────────────────────┤
|
|
│ Services │
|
|
│ (Business Logic, Validation) │
|
|
├─────────────────────────────────────────────┤
|
|
│ Repositories │
|
|
│ (Data Access, SQL) │
|
|
├─────────────────────────────────────────────┤
|
|
│ DB │
|
|
│ (MintyPHP Database) │
|
|
└─────────────────────────────────────────────┘
|
|
```
|
|
|
|
### Service-Layer (`lib/Service/`)
|
|
|
|
Services implementieren die Geschäftslogik:
|
|
|
|
```php
|
|
namespace MintyPHP\Service;
|
|
|
|
class UserService
|
|
{
|
|
// Listen
|
|
public static function list(): array
|
|
public static function listPaged(array $options): array
|
|
|
|
// Finder
|
|
public static function findByUuid(string $uuid): ?array
|
|
public static function findById(int $id): ?array
|
|
public static function findByEmail(string $email): ?array
|
|
|
|
// CRUD
|
|
public static function createFromAdmin(array $input, int $currentUserId): array
|
|
public static function updateFromAdmin(int $userId, array $input, int $currentUserId): array
|
|
public static function deleteByUuid(string $uuid, int $currentUserId): array
|
|
|
|
// Bulk-Operationen
|
|
public static function deleteByUuids(array $uuids, int $currentUserId): array
|
|
public static function setActiveByUuids(array $uuids, bool $active, int $currentUserId): array
|
|
}
|
|
```
|
|
|
|
**Wichtige Services:**
|
|
|
|
| Service | Verantwortung |
|
|
|---------|---------------|
|
|
| `AuthService` | Login, Logout, Session-Management |
|
|
| `UserService` | Benutzerverwaltung |
|
|
| `TenantService` | Mandantenverwaltung |
|
|
| `RoleService` | Rollenverwaltung |
|
|
| `PermissionService` | Berechtigungsprüfung und -verwaltung |
|
|
| `DepartmentService` | Abteilungsverwaltung |
|
|
| `MailService` | E-Mail-Versand |
|
|
| `SettingService` | App-Einstellungen |
|
|
|
|
### Repository-Layer (`lib/Repository/`)
|
|
|
|
Repositories kapseln Datenbankzugriffe:
|
|
|
|
```php
|
|
namespace MintyPHP\Repository;
|
|
|
|
class UserRepository
|
|
{
|
|
public static function list(): array
|
|
{
|
|
return DB::select('SELECT * FROM users ORDER BY last_name, first_name');
|
|
}
|
|
|
|
public static function find(int $id): ?array
|
|
{
|
|
return DB::selectOne('SELECT * FROM users WHERE id = ?', [$id]);
|
|
}
|
|
|
|
public static function create(array $data): int
|
|
{
|
|
return DB::insert('users', $data);
|
|
}
|
|
|
|
public static function update(int $id, array $data): bool
|
|
{
|
|
return DB::update('users', $data, ['id' => $id]) > 0;
|
|
}
|
|
}
|
|
```
|
|
|
|
### Support-Layer (`lib/Support/`)
|
|
|
|
#### Guard - Zugriffskontrolle
|
|
|
|
```php
|
|
namespace MintyPHP\Support;
|
|
|
|
class Guard
|
|
{
|
|
// Nur für angemeldete Benutzer
|
|
public static function requireLogin(string $redirect = 'login'): void
|
|
|
|
// Spezifische Berechtigung erforderlich
|
|
public static function requirePermission(string $permissionKey, string $redirect = 'admin'): void
|
|
|
|
// Berechtigung oder 403 (für AJAX)
|
|
public static function requirePermissionOrForbidden(string $permissionKey): void
|
|
}
|
|
```
|
|
|
|
#### Flash - Session-Nachrichten
|
|
|
|
```php
|
|
namespace MintyPHP\Support;
|
|
|
|
class Flash
|
|
{
|
|
public static function success(string $message, ?string $scope = null, ?string $key = null): string
|
|
public static function error(string $message, ?string $scope = null, ?string $key = null): string
|
|
public static function info(string $message, ?string $scope = null, ?string $key = null): string
|
|
public static function peek(?string $scope = null): array
|
|
public static function has(): bool
|
|
}
|
|
```
|
|
|
|
### Helper-Funktionen (`lib/Support/helpers/`)
|
|
|
|
```php
|
|
// HTML-Escape
|
|
e($string);
|
|
|
|
// Asset-URL
|
|
asset('css/style.css');
|
|
|
|
// Lokalisierte URL
|
|
lurl('admin/users');
|
|
|
|
// Übersetzung
|
|
t('Hello');
|
|
t('Welcome %s', $name);
|
|
|
|
// App-Einstellung
|
|
appSetting('app_title');
|
|
|
|
// Berechtigung prüfen
|
|
can('users.view');
|
|
```
|
|
|
|
---
|
|
|
|
## 6. Frontend-Architektur
|
|
|
|
### CSS-Struktur (`web/css/`)
|
|
|
|
```
|
|
web/css/
|
|
├── base/
|
|
│ └── variables.css # CSS-Variablen
|
|
├── layout/
|
|
│ ├── app-shell.css # Haupt-Layout
|
|
│ ├── app-topbar.css # Header
|
|
│ ├── app-sidebar.css # Navigation
|
|
│ └── app-aside-icon-bar.css
|
|
├── components/
|
|
│ ├── app-buttons.css
|
|
│ ├── app-forms.css
|
|
│ ├── app-list-table.css
|
|
│ ├── app-badges.css
|
|
│ ├── app-tabs.css
|
|
│ ├── app-flash.css
|
|
│ └── vendor-*.css # Third-Party
|
|
└── pages/
|
|
└── app-login.css
|
|
```
|
|
|
|
### JavaScript-Module (`web/js/`)
|
|
|
|
ES-Module-Architektur:
|
|
|
|
```
|
|
web/js/
|
|
├── app-init.js # Haupt-Initialisierung
|
|
├── core/
|
|
│ └── app-grid-factory.js # Grid/Tabellen-Generator
|
|
├── components/
|
|
│ ├── app-tenant-switcher.js
|
|
│ ├── app-theme-toggle.js
|
|
│ ├── app-bulk-selection.js
|
|
│ ├── app-flash-auto-dismiss.js
|
|
│ └── ...
|
|
└── pages/
|
|
├── app-users-list.js
|
|
└── app-list-utils.js
|
|
```
|
|
|
|
### Modul-Pattern
|
|
|
|
```javascript
|
|
// web/js/components/app-tenant-switcher.js
|
|
const switchTenant = async (link) => {
|
|
const tenantId = link.dataset.switchTenant;
|
|
// ...
|
|
};
|
|
|
|
const initTenantSwitcher = () => {
|
|
const tenantLinks = document.querySelectorAll('[data-switch-tenant]');
|
|
tenantLinks.forEach(link => {
|
|
link.addEventListener('click', (e) => {
|
|
e.preventDefault();
|
|
switchTenant(link);
|
|
});
|
|
});
|
|
};
|
|
|
|
// Auto-Init bei DOM Ready
|
|
if (document.readyState === 'loading') {
|
|
document.addEventListener('DOMContentLoaded', initTenantSwitcher);
|
|
} else {
|
|
initTenantSwitcher();
|
|
}
|
|
```
|
|
|
|
### Import in app-init.js
|
|
|
|
```javascript
|
|
// web/js/app-init.js
|
|
import './components/app-tenant-switcher.js';
|
|
import './components/app-theme-toggle.js';
|
|
import './components/app-flash-auto-dismiss.js';
|
|
// ...
|
|
```
|
|
|
|
---
|
|
|
|
## 7. Template-System
|
|
|
|
### Struktur
|
|
|
|
```
|
|
templates/
|
|
├── default.phtml # Haupt-Layout
|
|
├── login.phtml # Login-Layout
|
|
├── error.phtml # Fehler-Layout
|
|
├── partials/
|
|
│ ├── app-topbar.phtml
|
|
│ ├── app-sidebar.phtml
|
|
│ ├── app-aside.phtml
|
|
│ ├── app-flash.phtml
|
|
│ └── app-footer.phtml
|
|
└── emails/
|
|
├── de/
|
|
│ ├── access_info.html
|
|
│ └── reset_code.html
|
|
└── en/
|
|
└── ...
|
|
```
|
|
|
|
### Template-Rendering
|
|
|
|
```php
|
|
<!-- templates/default.phtml -->
|
|
<!DOCTYPE html>
|
|
<html lang="<?php e(I18n::$locale); ?>">
|
|
<head>
|
|
<title><?php e(Buffer::get('title') ?? appTitle()); ?></title>
|
|
<link rel="stylesheet" href="<?php e(asset('css/app.css')); ?>">
|
|
</head>
|
|
<body>
|
|
<?php require 'partials/app-topbar.phtml'; ?>
|
|
|
|
<main>
|
|
<?php require 'partials/app-flash.phtml'; ?>
|
|
<?php echo Buffer::get('html'); ?>
|
|
</main>
|
|
|
|
<script type="module" src="<?php e(asset('js/app-init.js')); ?>"></script>
|
|
</body>
|
|
</html>
|
|
```
|
|
|
|
### Page mit View
|
|
|
|
```php
|
|
// pages/admin/users/index().php
|
|
use MintyPHP\Buffer;
|
|
use MintyPHP\Support\Guard;
|
|
|
|
Guard::requirePermissionOrForbidden('users.view');
|
|
|
|
$users = UserService::list();
|
|
|
|
Buffer::set('title', t('Users'));
|
|
|
|
// View wird automatisch geladen: pages/admin/users/index(default).phtml
|
|
```
|
|
|
|
---
|
|
|
|
## 8. Authentifizierung und Autorisierung
|
|
|
|
### Login-Ablauf
|
|
|
|
```
|
|
1. POST /login
|
|
│
|
|
├─> AuthService::login(email, password)
|
|
│ ├─> Auth::login() - Passwort-Verifikation
|
|
│ ├─> $_SESSION['user'] setzen
|
|
│ ├─> PermissionService::getUserPermissions()
|
|
│ │ └─> $_SESSION['permissions'] setzen
|
|
│ └─> AuthService::loadTenantDataIntoSession()
|
|
│ ├─> $_SESSION['available_tenants']
|
|
│ └─> $_SESSION['current_tenant']
|
|
│
|
|
└─> Router::redirect('admin')
|
|
```
|
|
|
|
### Berechtigungsprüfung
|
|
|
|
```php
|
|
// In Page
|
|
Guard::requirePermissionOrForbidden(PermissionService::USERS_VIEW);
|
|
|
|
// Im Template
|
|
<?php if (can('users.create')): ?>
|
|
<a href="admin/users/create">Neuer Benutzer</a>
|
|
<?php endif; ?>
|
|
```
|
|
|
|
### Session-Struktur
|
|
|
|
```php
|
|
$_SESSION = [
|
|
'user' => [
|
|
'id' => 1,
|
|
'uuid' => 'abc-123',
|
|
'email' => 'user@example.com',
|
|
'first_name' => 'Max',
|
|
'last_name' => 'Mustermann',
|
|
'locale' => 'de',
|
|
'theme' => 'dark',
|
|
],
|
|
'permissions' => [
|
|
'user_id' => 1,
|
|
'keys' => ['users.view', 'users.create', ...],
|
|
],
|
|
'available_tenants' => [
|
|
['id' => 1, 'uuid' => '...', 'description' => 'Tenant A'],
|
|
['id' => 2, 'uuid' => '...', 'description' => 'Tenant B'],
|
|
],
|
|
'current_tenant' => [
|
|
'id' => 1,
|
|
'uuid' => '...',
|
|
'description' => 'Tenant A',
|
|
],
|
|
];
|
|
```
|
|
|
|
---
|
|
|
|
## 9. Internationalisierung
|
|
|
|
### Sprachdateien
|
|
|
|
```json
|
|
// i18n/default_de.json
|
|
{
|
|
"Login": "Anmelden",
|
|
"Users": "Benutzer",
|
|
"At least %d characters": "Mindestens %d Zeichen"
|
|
}
|
|
|
|
// i18n/default_en.json
|
|
{
|
|
"Login": "Login",
|
|
"Users": "Users",
|
|
"At least %d characters": "At least %d characters"
|
|
}
|
|
```
|
|
|
|
### Verwendung
|
|
|
|
```php
|
|
// Einfache Übersetzung
|
|
t('Login')
|
|
|
|
// Mit Parametern
|
|
t('At least %d characters', 12)
|
|
|
|
// Im Template
|
|
<?php e(t('Users')); ?>
|
|
```
|
|
|
|
### Sprachauflösung
|
|
|
|
Priorität:
|
|
1. URL-Präfix (`/de/admin`, `/en/admin`)
|
|
2. Session (`$_SESSION['user']['locale']`)
|
|
3. Cookie (`locale=de`)
|
|
4. Default (`APP_LOCALE`)
|
|
|
|
---
|
|
|
|
## 10. Best Practices
|
|
|
|
### Service-Rückgabewerte
|
|
|
|
```php
|
|
// Erfolg
|
|
return [
|
|
'ok' => true,
|
|
'uuid' => $uuid,
|
|
'id' => $id,
|
|
];
|
|
|
|
// Fehler
|
|
return [
|
|
'ok' => false,
|
|
'error' => 'validation_error',
|
|
'errors' => ['email' => 'Email ist erforderlich'],
|
|
'form' => $sanitizedInput,
|
|
];
|
|
```
|
|
|
|
### Datenbankzugriff
|
|
|
|
```php
|
|
// RICHTIG - Prepared Statements
|
|
$user = DB::selectOne('SELECT * FROM users WHERE email = ?', [$email]);
|
|
|
|
// FALSCH - SQL-Injection
|
|
$user = DB::selectOne("SELECT * FROM users WHERE email = '$email'");
|
|
```
|
|
|
|
### Template-Ausgabe
|
|
|
|
```php
|
|
// RICHTIG - Escape
|
|
<?php e($user['email']); ?>
|
|
|
|
// FALSCH - XSS-Risiko
|
|
<?php echo $user['email']; ?>
|
|
```
|
|
|
|
### Passwort-Anforderungen
|
|
|
|
- Mindestens 12 Zeichen
|
|
- Mindestens 1 Großbuchstabe
|
|
- Mindestens 1 Kleinbuchstabe
|
|
- Mindestens 1 Ziffer
|
|
- Mindestens 1 Sonderzeichen
|
|
|
|
### Code-Organisation
|
|
|
|
1. **Services** - Geschäftslogik und Validierung
|
|
2. **Repositories** - Nur Datenbankzugriff, keine Logik
|
|
3. **Guards** - Zugriffskontrolle
|
|
4. **Helpers** - Wiederverwendbare Funktionen
|
|
5. **Pages** - Koordination, keine Logik
|
|
|
|
---
|
|
|
|
## Weiterführende Dokumentation
|
|
|
|
- [MintyPHP Framework](https://github.com/mevdschee/php-crud-api)
|
|
- [Docker Setup](../docker-compose.yml)
|
|
- [Database Schema](../db/init/init.sql)
|