20 KiB
20 KiB
IMVS - Architektur und Grundprinzipien
Inhaltsverzeichnis
- Projektübersicht
- Framework: MintyPHP
- Verzeichnisstruktur
- Datenbankarchitektur
- PHP-Architektur
- Frontend-Architektur
- Template-System
- Authentifizierung und Autorisierung
- Internationalisierung
- 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
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
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
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
id INT PRIMARY KEY AUTO_INCREMENT
key VARCHAR(100) UNIQUE -- z.B. 'users.view'
description VARCHAR(255)
created DATETIME
departments
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:
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:
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
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
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/)
// 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
// 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
// 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
<!-- 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
// 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
// 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
$_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
// 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
// Einfache Übersetzung
t('Login')
// Mit Parametern
t('At least %d characters', 12)
// Im Template
<?php e(t('Users')); ?>
Sprachauflösung
Priorität:
- URL-Präfix (
/de/admin,/en/admin) - Session (
$_SESSION['user']['locale']) - Cookie (
locale=de) - Default (
APP_LOCALE)
10. Best Practices
Service-Rückgabewerte
// Erfolg
return [
'ok' => true,
'uuid' => $uuid,
'id' => $id,
];
// Fehler
return [
'ok' => false,
'error' => 'validation_error',
'errors' => ['email' => 'Email ist erforderlich'],
'form' => $sanitizedInput,
];
Datenbankzugriff
// 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
// 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
- Services - Geschäftslogik und Validierung
- Repositories - Nur Datenbankzugriff, keine Logik
- Guards - Zugriffskontrolle
- Helpers - Wiederverwendbare Funktionen
- Pages - Koordination, keine Logik