chore: lots of changes

This commit is contained in:
2025-05-24 07:09:22 +02:00
parent 77ee769d5e
commit 899227b0a4
178 changed files with 5145 additions and 53 deletions

8
.dockerignore Normal file
View File

@@ -0,0 +1,8 @@
.git
vendor
storage/logs/*
storage/app/*
storage/framework/cache/*
storage/framework/sessions/*
storage/framework/views/*
*.log

15
.editorconfig Normal file
View File

@@ -0,0 +1,15 @@
root = true
[*]
charset = utf-8
end_of_line = lf
indent_style = space
indent_size = 4
trim_trailing_whitespace = true
insert_final_newline = true
[*.md]
trim_trailing_whitespace = false
[*.{yml,yaml,json}]
indent_size = 2

18
.gitignore vendored
View File

@@ -1,7 +1,23 @@
# Editor / IDE
.idea/
# System
.DS_Store
Thumbs.db
# Build / Runtime
vendor/
node_modules/
.env
*.log
*.retry
ansible/.vault_pass
*.Zone.Identifier
# Backup Dateien
*~
.php-cs-fixer.php
#ssl/*.pem

111
Makefile
View File

@@ -1,16 +1,107 @@
.PHONY: deploy setup stop build restart
# ----------------------------------
# Projekt: michaelschiemer.de
# Docker & Ansible Makefile
# ----------------------------------
setup:
ansible-playbook -i ansible/inventory.ini ansible/setup.yml
PROJECT_NAME = michaelschiemer
ENV ?= dev
deploy:
ansible-playbook -i ansible/inventory.ini ansible/deploy.yml
# Standart Docker Compose Befehle
stop:
docker compose down
up: ## Startet alle Docker-Container
./bin/up
down: ## Stoppt alle Container
./bin/down
build:
docker compose build --no-cache
docker compose build
restart: stop build
docker compose up -d
restart: ## Neustart aller Container
./bin/restart
logs: ## Zeigt Logs aus Docker
docker compose logs -f
ps: ## Docker PS
docker compose ps
reload: ## Dump Autoload & Restart PHP
docker-compose exec php composer dump-autoload -o
docker-compose restart php
# Wähle dev- oder prod-PHP-Konfig je nach ENV
phpinfo:
@echo "Aktive PHP-Konfiguration: php.$(ENV).ini"
# Ansible Deployment
setup: ## Führt Ansible Setup aus
./bin/setup
deploy: ## Führt Ansible Deploy aus
./bin/deploy
test: ## Führt Tests aus (Platzhalter)
./bin/test
# Cleanup temporärer/metadaten-Dateien
clean: ## Entfernt temporäre Dateien
find . -type f -name "*Zone.Identifier" -delete
find . -type f -name "*.retry" -delete
# Projektstatus
status: ## Zeigt Container-Status
@echo "Aktuelles Projekt: $(PROJECT_NAME)"
@echo "Umgebung: $(ENV)"
doctor: ## Prüft ob Komponenten installiert sind
@echo "🔍 Prüfe Voraussetzungen..."
@which docker > /dev/null || echo "❌ Docker fehlt"
@which ansible-playbook > /dev/null || echo "❌ Ansible fehlt"
@test -f .env || echo "⚠️ .env-Datei fehlt"
# Helfer: Automatische Zielübersicht
help: ## Zeigt diese Hilfe an
@echo ""
@echo "🛠 Verfügbare Make-Befehle:"
@grep -E '^[a-zA-Z_-]+:.*?## ' Makefile | sort | awk 'BEGIN {FS = ":.*?## "}; {printf " \033[36m%-16s\033[0m %s\n", $$1, $$2}'
@echo ""
composer: ## Use Composer
docker compose exec php composer $(ARGS)
fix-perms: ## Fix permissions
sudo chown -R $(USER):$(USER) .
cs:
@$(MAKE) composer ARGS="cs"
cs-fix-file: ## Fix code style for a specific file
docker compose exec -e PHP_CS_FIXER_IGNORE_ENV=1 php ./vendor/bin/php-cs-fixer fix $(subst \,/,$(FILE))
cs-fix: ## Fix code style for all PHP files
docker compose exec -e PHP_CS_FIXER_IGNORE_ENV=1 php ./vendor/bin/php-cs-fixer fix
health:
ansible-playbook ansible/check.yml
# Konfiguration
ANSIBLE_INVENTORY=ansible/inventory/hosts.ini
PLAYBOOK_DIR=ansible/playbooks/deploy
.PHONY: dev staging production
dev:
ansible-playbook -i $(ANSIBLE_INVENTORY) $(PLAYBOOK_DIR)/dev.yml #--ask-become-pass
staging:
ansible-playbook -i $(ANSIBLE_INVENTORY) $(PLAYBOOK_DIR)/staging.yml
production:
ansible-playbook -i $(ANSIBLE_INVENTORY) $(PLAYBOOK_DIR)/production.yml
.PHONY: up down build restart logs ps phpinfo deploy setup clean status

14
README.md Normal file
View File

@@ -0,0 +1,14 @@
## 🚀 Quick Start
```bash
# Starten
make up
# Logs anzeigen
make logs
# Setup-Playbook (Server einmalig vorbereiten)
make setup
# Deployment (Code + Compose auf Server bringen)
make deploy

26
ansible.cfg Normal file
View File

@@ -0,0 +1,26 @@
[defaults]
#inventory = ./ansible/inventory.ini
inventory = ./ansible/inventory/hosts.ini
roles_path = ./ansible/roles
remote_tmp = ~/.ansible/tmp
forks = 5
timeout = 10
retry_files_enabled = False
deprecation_warnings = False
interpreter_python = auto_silent
#stdout_callback = json
host_key_checking = False
command_warnings = False
gathering = smart
fact_caching = jsonfile
fact_caching_connection = .ansible/cache
fact_caching_timeout = 3600
[privilege_escalation]
become = true
become_method = sudo
become_user = root
[ssh_connection]
pipelining = true
ssh_args = -o ControlMaster=auto -o ControlPersist=60s

View File

@@ -0,0 +1,10 @@
[Interface]
PrivateKey = +DcT11ipmMwPXpzEqmCPGwy7cSmseG1YzZWk+tTtM30=
Address = 10.8.0.2/32
DNS = 1.1.1.1
[Peer]
PublicKey = 3qFEUREx6VfqrKoGVtzHt2ojgaly7LvwxjPQPNsFyxM=
Endpoint = 94.16.110.151:51820
AllowedIPs = 10.8.0.0/24, 94.16.110.151/32
PersistentKeepalive = 25

View File

@@ -1,4 +0,0 @@
- hosts: web
become: false
roles:
- deploy

View File

@@ -0,0 +1,39 @@
# Basis-Konfiguration
app_name: michaelschiemer
app_domain: test.michaelschiemer.de
app_email: kontakt@michaelschiemer.de
# Verzeichnisse
project_root: "{{ playbook_dir | dirname }}"
app_root: /var/www/{{ app_name }}
app_public: "{{ app_root }}/public"
# Docker
docker_version: "20.10"
docker_compose_version: "2.24.5"
# Benutzer
deploy_user: deploy
# Let's Encrypt
letsencrypt_enabled: true
letsencrypt_certbot_method: webroot # oder standalone oder nginx
#netcup_customer_id: "218722"
#netcup_api_key: "dmJINUMyNjRmOG1aNDViajZHN2JkOTFRUjU3ckE5ZjJ1Zm1vUz"
#netcup_api_password: "iGWL8Hl4m93DgESsP/MPXmtDd0hEVkZ3480Na0psTlXRALnopl"
#netcup_vserver_id: "v2202309206672239295"
# fallback_ip:
wg_all_clients_private_keys:
michael: "PITbFZ3UfY5vD5dYUCELO37Qo2W8I4R8+r6D9CeMrm4="
wireguard_clients:
- name: michael
address: 10.8.0.2
public_key: DEIN_PUBLIC_KEY

View File

@@ -0,0 +1,4 @@
wg_privkey: "HIER_DEIN_PRIVATER_KEY_ODER_DATEIPFAD"
wg_all_clients_private_keys:
michael: "PITbFZ3UfY5vD5dYUCELO37Qo2W8I4R8+r6D9CeMrm4="

View File

View File

@@ -1,2 +1,8 @@
#[web]
#localhost ansible_connection=local
[web]
localhost ansible_connection=local
94.16.110.151 ansible_user=deploy ansible_ssh_private_key_file=/mnt/c/Users/Mike/.ssh/test.michaelschiemer.de
[vpn]
94.16.110.151 ansible_user=deploy

View File

@@ -0,0 +1,12 @@
[localhost]
127.0.0.1 ansible_connection=local
[staging]
94.16.110.151 ansible_user=deploy ansible_ssh_private_key_file=/mnt/c/Users/Mike/.ssh/test.michaelschiemer.de
[production]
[vpn]
94.16.110.151 ansible_user=deploy

View File

@@ -0,0 +1,40 @@
#- name: Check ob /ping erreichbar ist
# uri:
# url: "http://localhost/ping"
# status_code: 200
# return_content: yes
# register: ping_response
#
#- debug:
# var: ping_response.content
- name: Healthcheck nach dem Deployment
hosts: localhost
connection: local
gather_facts: false
become: false
vars:
healthcheck_url: "http://127.0.0.1:8080/ping"
max_retries: 10
delay_between_retries: 3
tasks:
- name: Warte, bis der Webserver erreichbar ist
uri:
url: "{{ healthcheck_url }}"
status_code: 200
return_content: yes
register: healthcheck_response
retries: "{{ max_retries }}"
delay: "{{ delay_between_retries }}"
until: >
healthcheck_response is defined and
healthcheck_response.status is defined and
healthcheck_response.status == 200
failed_when: healthcheck_response.status != 200
ignore_errors: false
- name: Ausgabe des Healthcheck-Resultats
debug:
msg: "Healthcheck erfolgreich: {{ healthcheck_response.content }}"

View File

@@ -0,0 +1,24 @@
- name: Deployment in jeweilige Umgebung
hosts: all
become: true
gather_facts: false
vars:
docker_compose_project_path: "/var/www/michaelschiemer/"
env_file_path: "/var/www/michaelschiemer/.env"
deploy_root: /var/www/michaelschiemer
deploy_public: "{{ deploy_root }}/public"
deploy_user: deploy
app_domain: "example.com" # Passe ggf. an
project_root: "{{ playbook_dir }}/../.."
roles:
- app
- nginx
- php
- redis

View File

@@ -0,0 +1,26 @@
- name: Deployment für DEV (localhost)
hosts: localhost
become: true
gather_facts: false
vars:
docker_compose_project_path: "/home/michael/dev/michaelschiemer"
env_file_path: "/var/www/michaelschiemer/.env"
deploy_root: /var/www/michaelschiemer
deploy_public: "{{ deploy_root }}/public"
deploy_user: deploy
app_domain: "localhost" # Passe ggf. an
project_root: "/home/michael/dev/michaelschiemer"
roles:
#- app
- nginx
- php
- redis
tasks:
- name: Common Deployment Tasks
import_tasks: ../deploy/includes/deploy_common.yml

View File

@@ -0,0 +1,29 @@
- name: Docker Compose Files & Konfigurationen synchronisieren
ansible.builtin.copy:
src: "{{ item.src }}"
dest: "{{ docker_compose_project_path }}/{{ item.dest }}"
owner: root
group: root
mode: 0644
loop:
- { src: '{{ project_root }}/docker-compose.yml', dest: 'docker-compose.yml' }
- { src: '{{ project_root }}/.env', dest: '.env' }
# Weitere Konfigdateien nach Bedarf (z.B. nginx.conf, redis.conf, ...)
- { src: '{{ project_root }}/docker/nginx/nginx.conf', dest: 'nginx.conf' }
- name: "Docker Compose: Container hochfahren (Build & Start)"
ansible.builtin.command: |
docker-compose -f {{ docker_compose_project_path }}/docker-compose.yml up -d --build
args:
chdir: "{{ docker_compose_project_path }}"
- name: Status prüfen
ansible.builtin.command: |
docker-compose -f {{ docker_compose_project_path }}/docker-compose.yml ps
args:
chdir: "{{ docker_compose_project_path }}"
register: compose_ps
- name: Ergebnis anzeigen
ansible.builtin.debug:
var: compose_ps.stdout_lines

View File

@@ -0,0 +1,23 @@
- name: Deployment für PRODUCTION
hosts: production
become: true
gather_facts: false
vars:
docker_compose_project_path: "/var/www/www.michaelschiemer.de/"
env_file_path: "/var/www/www.michaelschiemer.de/.env"
deploy_root: /var/www/www.michaelschiemer.de
deploy_public: "{{ deploy_root }}/public"
deploy_user: deploy
app_domain: "michaelschiemer.de"
project_root: "{{ playbook_dir }}/../.."
roles:
- app
- nginx
- php
- redis
tasks:
- name: Common Deployment Tasks
import_tasks: ../deploy/includes/deploy_common.yml

View File

@@ -0,0 +1,23 @@
- name: Deployment für STAGING
hosts: staging
become: true
gather_facts: false
vars:
docker_compose_project_path: "/var/www/stage.michaelschiemer/"
env_file_path: "/var/www/stage.michaelschiemer/.env"
deploy_root: /var/www/stage.michaelschiemer
deploy_public: "{{ deploy_root }}/public"
deploy_user: deploy
app_domain: "stage.example.com"
project_root: "{{ playbook_dir }}/../.."
roles:
- app
- nginx
- php
- redis
tasks:
- name: Common Deployment Tasks
import_tasks: ../deploy/includes/deploy_common.yml

View File

@@ -0,0 +1,11 @@
---
- name: Basis Setup für alle Zielsysteme
hosts: all
become: true
#gather_facts: true
roles:
#- common
- docker
#- webserver
#- app

View File

@@ -0,0 +1,6 @@
- hosts: web
become: true
gather_facts: true
roles:
- console

View File

@@ -0,0 +1,7 @@
# ansible/wireguard.yml
- hosts: vpn
become: false
gather_facts: false
roles:
- wireguard

View File

@@ -0,0 +1,134 @@
- name: Zielverzeichnis erstellen
file:
path: "{{ deploy_root }}"
state: directory
owner: "{{ deploy_user }}"
group: "{{ deploy_user }}"
mode: '0755'
- name: SSL-Verzeichnis sicherstellen
file:
path: "{{ deploy_root }}/ssl"
state: directory
owner: "{{ deploy_user }}"
group: "{{ deploy_user }}"
mode: '0755'
- name: SSL-Zertifikate prüfen
stat:
path: "/etc/letsencrypt/live/{{ app_domain }}/fullchain.pem"
register: ssl_certs
- name: SSL-Zertifikate kopieren (falls vorhanden)
copy:
src: "{{ item.src }}"
dest: "{{ item.dest }}"
remote_src: yes
owner: "{{ deploy_user }}"
group: "{{ deploy_user }}"
mode: '0644'
loop:
- { src: "/etc/letsencrypt/live/{{ app_domain }}/fullchain.pem", dest: "{{ deploy_root }}/ssl/fullchain.pem" }
- { src: "/etc/letsencrypt/live/{{ app_domain }}/privkey.pem", dest: "{{ deploy_root }}/ssl/privkey.pem" }
when: ssl_certs.stat.exists
- name: public-Verzeichnis synchronisieren
synchronize:
src: "{{ playbook_dir }}/../../public/"
dest: "{{ deploy_public }}/"
delete: yes
recursive: yes
- name: Projekt-Stammdaten kopieren
copy:
src: "{{ playbook_dir }}/../../docker-compose.yml"
dest: "{{ deploy_root }}/docker-compose.yml"
owner: "{{ deploy_user }}"
group: "{{ deploy_user }}"
mode: '0644'
- name: .env-Datei prüfen
stat:
path: "{{ project_root }}/.env"
register: env_file
- name: .env kopieren (falls vorhanden)
copy:
src: "{{ project_root }}/.env"
dest: "{{ deploy_root }}/.env"
mode: '0644'
when: env_file.stat.exists
- name: Quellcode synchronisieren
synchronize:
src: "{{ playbook_dir }}/../../src/"
dest: "{{ deploy_root }}/src/"
delete: yes
recursive: yes
- name: Docker-Verzeichnis prüfen
stat:
path: "{{ project_root }}/docker"
register: docker_dir
delegate_to: localhost
become: false
- name: Docker-Configs synchronisieren (falls vorhanden)
synchronize:
src: "{{ project_root }}/docker/"
dest: "{{ deploy_root }}/docker/"
delete: yes
recursive: yes
when: docker_dir.stat.exists
- name: Rechte im Zielverzeichnis korrigieren
file:
path: "{{ deploy_root }}"
state: directory
owner: "{{ deploy_user }}"
group: "{{ deploy_user }}"
mode: '0755'
recurse: yes
# Cache-Verzeichnis für UID/GID 1000 (z.B. appuser im Container)
- name: Stelle Schreibrechte für Cache-Verzeichnis her
file:
path: "{{ deploy_root }}/cache"
state: directory
owner: 1000
group: 1000
mode: '0775'
recurse: yes
- name: Docker Compose neu bauen und starten
shell: |
docker compose down
docker compose up -d --build
args:
chdir: "{{ deploy_root }}"
- name: PHP-Container für Composer starten
shell: docker compose up -d php
args:
chdir: "{{ deploy_root }}"
- name: Kurze Wartezeit bis PHP-Container bereit
wait_for:
timeout: 5
- name: Composer Abhängigkeiten installieren
shell: docker compose exec -T php composer install --no-interaction
args:
chdir: "{{ deploy_root }}"
register: composer_result
ignore_errors: yes
- name: Composer-Ergebnis anzeigen
debug:
var: composer_result.stdout_lines
when: composer_result.stdout is defined
- name: Composer-Fehler anzeigen
debug:
var: composer_result.stderr_lines
when: composer_result.stderr is defined

View File

@@ -0,0 +1,26 @@
# Grundlegende Systemkonfiguration
- name: Basis-Pakete aktualisieren und installieren
apt:
name:
- sudo
- vim
- htop
- git
- zip
- unzip
- curl
- wget
state: present
update_cache: yes
become: true
# Passwordless sudo für den deploy-Benutzer einrichten
- name: Konfiguriere passwordless sudo für deploy-Benutzer
lineinfile:
path: "/etc/sudoers.d/{{ deploy_user }}"
line: "{{ deploy_user }} ALL=(ALL) NOPASSWD: ALL"
state: present
create: yes
mode: '0440'
validate: 'visudo -cf %s'
become: true

View File

@@ -0,0 +1,9 @@
- name: Füge Funktion für ms (mit Argumenten) hinzu
blockinfile:
path: "/home/{{ ansible_user }}/.bashrc"
marker: "# {mark} ms docker alias"
block: |
ms() {
docker compose exec php php ms "$@"
}
become: false

View File

@@ -0,0 +1,9 @@
# Rolle: Docker
Diese Rolle installiert Docker Engine, CLI, Compose-Plugin sowie (optional) Docker Compose V1 als Fallback.
- Fügt den gewünschten User zur Docker-Gruppe hinzu.
- Startet und aktiviert den Docker-Dienst.
## Variablen
- `docker_compose_version`: Version von Docker Compose V1 für Fallback (Standard: 1.29.2).
- `docker_user`: Benutzer, der in die Gruppe `docker` aufgenommen werden soll (Standard: aktueller Ansible-User).

View File

@@ -0,0 +1,3 @@
docker_compose_version: "v2.29.2"
docker_install_compose: true
docker_user: "{{ ansible_user || default('michael' }}"

View File

@@ -0,0 +1,4 @@
- name: restart docker
ansible.builtin.service:
name: docker
state: restarted

View File

@@ -0,0 +1,58 @@
- name: Docker-Abhängigkeiten installieren
apt:
name:
- apt-transport-https
- ca-certificates
- curl
- gnupg
- lsb-release
state: present
update_cache: yes
- name: Docker GPG-Schlüssel hinzufügen
apt_key:
url: https://download.docker.com/linux/{{ ansible_distribution | lower }}/gpg
state: present
- name: Docker Repository hinzufügen
apt_repository:
repo: "deb [arch=amd64] https://download.docker.com/linux/{{ ansible_distribution | lower }} {{ ansible_distribution_release }} stable"
state: present
- name: Docker Engine installieren
apt:
name:
- docker-ce
- docker-ce-cli
- containerd.io
- docker-compose-plugin
state: present
update_cache: yes
- name: Docker Compose installieren (V1 als Fallback)
get_url:
url: "https://github.com/docker/compose/releases/download/v{{ docker_compose_version }}/docker-compose-linux-x86_64"
dest: /usr/local/bin/docker-compose
mode: '0755'
- name: Benutzer zur Docker-Gruppe hinzufügen
user:
name: "{{ ansible_user }}"
groups: docker
append: yes
- name: Docker-Service starten und aktivieren
service:
name: docker
state: started
enabled: yes
notify: restart docker
- name: Starte Docker-Container via Compose
community.docker.docker_compose_v2:
#project_src: "{{ playbook_dir | dirname }}/../" # ggf. anpassen auf deinen Compose-Pfad!
project_src: "{{ app_root }}"
build: always
recreate: always

View File

@@ -0,0 +1,5 @@
nginx_conf_template: nginx.conf.j2
nginx_default_site_template: default.conf.j2
nginx_ssl_src_dir: "{{ app_root }}/ssl"
nginx_ssl_dest_dir: "/var/www/michaelschiemer/ssl"
nginx_target_dir: "/var/www/michaelschiemer/docker/nginx"

View File

@@ -0,0 +1,2 @@
- name: reload nginx
ansible.builtin.command: docker exec <nginx_container_name> nginx -s reload

View File

@@ -0,0 +1,37 @@
- name: Stelle das nginx-Verzeichnis sicher
ansible.builtin.file:
path: "{{ nginx_target_dir }}"
state: directory
recurse: yes
mode: '0755'
- name: Kopiere nginx-Konfiguration (nginx.conf)
ansible.builtin.template:
src: "{{ nginx_conf_template }}"
dest: "{{ nginx_target_dir }}/nginx.conf"
mode: '0644'
- name: Kopiere default site conf
ansible.builtin.template:
src: "{{ nginx_default_site_template }}"
dest: "{{ nginx_target_dir }}/default.conf"
mode: '0644'
- name: Kopiere docker-entrypoint Skript
ansible.builtin.copy:
src: docker-entrypoint.sh
dest: "{{ nginx_target_dir }}/docker-entrypoint.sh"
mode: '0755'
- name: Baue und starte Nginx-Container (optional, wenn Compose separat genutzt wird, dann hier nicht nötig)
ansible.builtin.command: docker-compose up -d --build web
args:
chdir: "{{ docker_compose_project_path }}"
when: nginx_target_dir is defined
register: nginx_compose_result
ignore_errors: true
- name: Zeige Compose-Resultat
ansible.builtin.debug:
var: nginx_compose_result.stdout_lines
when: nginx_compose_result is defined

View File

@@ -0,0 +1,4 @@
- name: Reload nginx
service:
name: nginx
state: reloaded

View File

@@ -1,3 +1,47 @@
- name: Test-Task Setup-Rolle lokal
debug:
msg: "Setup-Rolle ist vorbereitet echte Installation folgt auf Server."
- name: Docker installieren
apt:
name:
- docker.io
- docker-compose
state: present
update_cache: yes
- name: Certbot + Plugin installieren
apt:
name:
- certbot
- python3-certbot-nginx
state: present
update_cache: yes
- name: Challenge-Verzeichnis für Let's Encrypt anlegen
file:
path: /var/www/html/.well-known/acme-challenge
state: directory
owner: www-data
group: www-data
mode: '0755'
recurse: yes
- name: Füge Let's Encrypt Challenge-Pfad in den Nginx-Vhost ein
blockinfile:
path: /etc/nginx/sites-available/default
marker: "# {mark} ANSIBLE LETSENCRYPT"
insertafter: "^\\s*server\\s*{"
block: |
location ^~ /.well-known/acme-challenge/ {
root /var/www/html;
allow all;
default_type "text/plain";
}
notify: Reload nginx
- name: Let's Encrypt Zertifikat anfordern
command: >
certbot --nginx -n --agree-tos --redirect
-m kontakt@michaelschiemer.de
-d test.michaelschiemer.de
args:
creates: /etc/letsencrypt/live/test.michaelschiemer.de/fullchain.pem

View File

@@ -0,0 +1,18 @@
- name: Systempakete aktualisieren
apt:
update_cache: yes
upgrade: safe
autoremove: yes
autoclean: yes
register: upgrade_result
become: true
- name: Zeige ggf. Anzahl aktualisierter Pakete
debug:
msg: "Anzahl aktualisierter Pakete: {{ upgrade_result.stdout_lines | default([]) | length }}"
- name: Reboot durchführen, wenn notwendig
reboot:
msg: "Reboot wegen Kernel-/System-Update erforderlich"
pre_reboot_delay: 30
when: upgrade_result.changed

View File

@@ -0,0 +1,50 @@
- name: Certbot + Plugin installieren
apt:
name:
- certbot
- python3-certbot-nginx
state: present
update_cache: yes
when: letsencrypt_enabled
- name: Challenge-Verzeichnis für Let's Encrypt anlegen
file:
path: "{{ app_public }}/.well-known/acme-challenge"
state: directory
owner: www-data
group: www-data
mode: '0755'
recurse: yes
when: letsencrypt_enabled and letsencrypt_certbot_method == 'webroot'
- name: Stoppe Nginx für Standalone-Methode
service:
name: nginx
state: stopped
when: letsencrypt_enabled and letsencrypt_certbot_method == 'standalone'
- name: Let's Encrypt Zertifikat anfordern (Standalone)
command: >
certbot certonly --standalone -n --agree-tos
-m {{ app_email }}
-d {{ app_domain }}
args:
creates: /etc/letsencrypt/live/{{ app_domain }}/fullchain.pem
when: letsencrypt_enabled and letsencrypt_certbot_method == 'standalone'
- name: Let's Encrypt Zertifikat anfordern (Webroot)
command: >
certbot certonly --webroot -w {{ app_public }} -n --agree-tos
-m {{ app_email }}
-d {{ app_domain }}
args:
creates: /etc/letsencrypt/live/{{ app_domain }}/fullchain.pem
when: letsencrypt_enabled and letsencrypt_certbot_method == 'webroot'
- name: Kopiere SSL-Zertifikate für Docker
copy:
src: "/etc/letsencrypt/live/{{ app_domain }}/"
dest: "{{ app_root }}/ssl/"
remote_src: yes
mode: '0644'
when: letsencrypt_enabled

View File

@@ -0,0 +1,6 @@
wireguard_interface: wg0
wireguard_port: 51820
wireguard_address: 10.8.0.1/24
wireguard_server_ip: 94.16.110.151 # oder deine Domain
wireguard_network: "10.8.0.0/24"

View File

@@ -0,0 +1,133 @@
# --------------------------------------------------------
# WireGuard installieren
# --------------------------------------------------------
- name: Stelle sicher, dass WireGuard installiert ist
apt:
name: wireguard
state: present
update_cache: yes
become: true
when: ansible_connection != "local"
# --------------------------------------------------------
# Server-Schlüssel erzeugen und speichern
# --------------------------------------------------------
- name: Prüfe ob privater Server-Schlüssel existiert
stat:
path: /etc/wireguard/privatekey
register: privkey_file
become: true
when: ansible_connection != "local"
- name: Erstelle Schlüsselpaar für Server (wenn nicht vorhanden)
command: wg genkey
register: server_private_key
when: ansible_connection != "local" and (not privkey_file.stat.exists | default(true))
- name: Speichere privaten Schlüssel
copy:
content: "{{ server_private_key.stdout }}"
dest: /etc/wireguard/privatekey
mode: "0600"
when: server_private_key.stdout is defined and server_private_key.stdout is defined
- name: Lies privaten Schlüssel ein
slurp:
src: /etc/wireguard/privatekey
become: true
when: ansible_connection != "local"
- name: Erzeuge öffentlichen Server-Schlüssel
command: "echo '{{ wg_privkey }}' | wg pubkey"
register: wg_pubkey
when: ansible_connection != "local"
- name: Privaten Server-Schlüssel anzeigen
debug:
msg: "{{ server_private_key }}"
when: ansible_connection != "local"
# --------------------------------------------------------
# Client-Key-Erzeugung lokal (einmalig pro Client)
# --------------------------------------------------------
- name: Generiere privaten Schlüssel für Clients (auf dem Server)
command: wg genkey
args:
creates: "/etc/wireguard/client-{{ item.name }}.key"
loop: "{{ wireguard_clients }}"
loop_control:
label: "{{ item.name }}"
register: client_private_keys
when: ansible_connection != "local"
- name: Erzeuge öffentlichen Schlüssel für Clients
command: "echo '{{ client_privkey_result.stdout }}' | wg pubkey"
register: client_pubkey_result
when:
- ansible_connection != "local"
- client_privkey_result is defined
- client_privkey_result.stdout is defined
- name: wireguard_clients mit public_key anreichern
set_fact:
wireguard_clients: "{{ wireguard_clients_with_pubkey | default([]) + [ item.0 | combine({'public_key': item.1.stdout|trim }) ] }}"
loop: "{{ wireguard_clients | zip(client_public_keys.results) | list }}"
when: client_public_keys is defined
- name: Aktuelles wireguard_clients-Set überschreiben
set_fact:
wireguard_clients: "{{ wireguard_clients_with_pubkey }}"
when: wireguard_clients_with_pubkey is defined
# --------------------------------------------------------
# Konfigurationsdatei erzeugen
# --------------------------------------------------------
#- debug:
# var: wireguard_clients
- name: Render wg0.conf
template:
src: wg0.conf.j2
dest: /etc/wireguard/wg0.conf
when: wg_privkey is defined and wg_privkey != ""
# --------------------------------------------------------
# IP Forwarding & WireGuard aktivieren
# --------------------------------------------------------
- name: Aktiviere IP-Forwarding
sysctl:
name: net.ipv4.ip_forward
value: 1
state: present
sysctl_set: yes
reload: yes
become: true
when: ansible_connection != "local"
- name: Starte und aktiviere WireGuard
systemd:
name: wg-quick@wg0
enabled: true
state: started
daemon_reload: yes
become: true
when: ansible_connection != "local"
- name: Verteilt für jeden Client die Client-Config
template:
src: client.conf.j2
dest: "/etc/wireguard/clients/{{ item.name }}.conf"
owner: root
group: root
mode: 0600
loop: "{{ wireguard_clients }}"
#delegate_to: localhost
run_once: true
become: true
when: ansible_connection != "local"

View File

@@ -0,0 +1,54 @@
---
# roles/wireguard/tasks/failsafe.yml
# Sicherstellt, dass SSH über VPN funktioniert und ein Fallback vorhanden ist
- name: Stelle sicher, dass wireguard_network gesetzt ist
assert:
that:
- wireguard_network is defined
fail_msg: "wireguard_network muss gesetzt sein (z.B. 10.8.0.0/24)"
- name: Automatisch externe IP als fallback_ip setzen (nur wenn nicht gesetzt)
shell: curl -s ifconfig.me
register: detected_fallback_ip
when: fallback_ip is not defined
changed_when: false
- name: Setze fallback_ip dynamisch als Ansible-Fact (wenn nicht gesetzt)
set_fact:
fallback_ip: "{{ detected_fallback_ip.stdout }}"
when: fallback_ip is not defined
- name: (Optional) Erlaube temporär Fallback-SSH von aktueller IP
ufw:
rule: allow
port: 22
proto: tcp
from_ip: "{{ fallback_ip }}"
- name: Erlaube SSH-Zugriff über VPN
ufw:
rule: allow
port: 22
proto: tcp
from_ip: "{{ wireguard_network }}"
- name: (Warnung) Prüfe ob VPN-Interface aktiv ist
shell: ip a show dev wg0
register: vpn_interface_check
failed_when: false
- name: Hinweis, wenn VPN-Interface nicht aktiv ist
debug:
msg: "⚠️ VPN-Interface wg0 scheint nicht aktiv zu sein. SSH über VPN wird nicht funktionieren."
when: vpn_interface_check.rc != 0
- name: (Optional) SSH von überall blockieren nur wenn VPN aktiv
when:
- ssh_lockdown | default(false)
- vpn_interface_check.rc == 0
ufw:
rule: deny
port: 22
proto: tcp
from_ip: 0.0.0.0/0

View File

@@ -0,0 +1,83 @@
# Beispiel: Passe jeden Task in dieser Datei so an:
- name: Aktiviere Firewall-Regeln für WireGuard
ufw:
rule: allow
port: "{{ wireguard_port }}"
proto: udp
become: true
when: ansible_connection != "local"
- name: Prüfe, ob UFW installiert ist
command: which ufw
register: ufw_installed
ignore_errors: true
changed_when: false
- name: Installiere UFW (falls nicht vorhanden)
apt:
name: ufw
state: present
update_cache: yes
when: ufw_installed.rc != 0
# Setze Standardrichtlinien (erst Konfiguration, dann am Ende aktivieren)
- name: Setze Policy für eingehenden Traffic auf "deny"
ufw:
direction: incoming
policy: deny
- name: Setze Policy für ausgehenden Traffic auf "allow"
ufw:
direction: outgoing
policy: allow
# WireGuard-Port freigeben (UDP)
- name: WireGuard-Port erlauben
ufw:
rule: allow
port: "{{ wireguard_port | default(51820) }}"
proto: udp
# SSH von bestimmter IP erlauben
- name: SSH von deiner IP erlauben (empfohlen)
ufw:
rule: allow
port: 22
proto: tcp
from_ip: "{{ fallback_ip }}"
when: fallback_ip is defined and fallback_ip | length > 0
# Temporär für Tests: SSH für alle erlauben (nur bei Bedarf!)
- name: SSH von überall erlauben (fail-safe, NUR während Setup/Test)
ufw:
rule: allow
port: 22
proto: tcp
when: (not (fallback_ip is defined and fallback_ip | length > 0)) or (enable_ssh_from_anywhere | default(false))
# Masquerading für WireGuard
- name: NAT für WireGuard aktivieren
iptables:
table: nat
chain: POSTROUTING
out_interface: "{{ wireguard_exit_interface | default('eth0') }}"
source: "{{ wireguard_network }}"
jump: MASQUERADE
- name: WireGuard Kernel-Modul laden
modprobe:
name: wireguard
state: present
# UFW ganz am Schluss aktivieren
- name: UFW aktivieren
ufw:
state: enabled
- name: Aktive UFW-Regeln anzeigen (zum Debuggen)
command: ufw status verbose
register: ufw_status
changed_when: false
- name: Zeige UFW-Regeln im Ansible-Output
debug:
var: ufw_status.stdout

View File

@@ -0,0 +1,60 @@
- name: Key-Verzeichnis für Client anlegen
file:
path: "{{ role_path }}/client-keys/{{ client.name }}"
state: directory
mode: "0700"
become: true
- name: Existenz des privaten Schlüssels prüfen
stat:
path: "{{ role_path }}/client-keys/{{ client.name }}/private.key"
register: client_private_key_stat
- name: Privaten Schlüssel generieren (nur falls nicht vorhanden)
command: wg genkey
register: genpriv
args:
chdir: "{{ role_path }}/client-keys/{{ client.name }}"
when: not client_private_key_stat.stat.exists
- name: Privaten Schlüssel speichern (nur falls nicht vorhanden)
copy:
content: "{{ genpriv.stdout }}"
dest: "{{ role_path }}/client-keys/{{ client.name }}/private.key"
mode: "0600"
when: not client_private_key_stat.stat.exists
- name: Public Key aus privaten Schlüssel generieren (bei Neuerstellung)
command: wg pubkey
args:
stdin: "{{ genpriv.stdout }}"
chdir: "{{ role_path }}/client-keys/{{ client.name }}"
register: genpub
when: not client_private_key_stat.stat.exists
- name: Bestehenden privaten Schlüssel laden (falls vorhanden)
slurp:
src: "{{ role_path }}/client-keys/{{ client.name }}/private.key"
register: loaded_private
when: client_private_key_stat.stat.exists
- name: Public Key aus gespeichertem Private Key erzeugen (falls vorhanden)
command: wg pubkey
args:
stdin: "{{ loaded_private.content | b64decode }}"
chdir: "{{ role_path }}/client-keys/{{ client.name }}"
register: genpub_existing
when: client_private_key_stat.stat.exists
- name: Public Key für Client in Datei schreiben
copy:
content: >
{{ (genpub.stdout if not client_private_key_stat.stat.exists else genpub_existing.stdout) }}
dest: "{{ role_path }}/client-keys/{{ client.name }}/public.key"
mode: "0644"
- name: Variablen für Client setzen (private/public key, Adresse)
set_fact:
"wg_{{ client.name }}_private_key": "{{ (genpriv.stdout if not client_private_key_stat.stat.exists else loaded_private.content | b64decode) }}"
"wg_{{ client.name }}_public_key": "{{ (genpub.stdout if not client_private_key_stat.stat.exists else genpub_existing.stdout) }}"
"wg_{{ client.name }}_address": "{{ client.address }}"

View File

@@ -0,0 +1,39 @@
- name: Schleife über alle WireGuard-Clients
include_tasks: generate_client_single.yml
loop: "{{ wireguard_clients }}"
loop_control:
loop_var: client
- name: Generiere privaten Schlüssel für jeden Client
shell: "wg genkey"
register: wg_client_private_keys
loop: "{{ wireguard_clients }}"
loop_control:
label: "{{ item.name }}"
# kein delegate_to mehr!
run_once: true # ggf. auch entfernen, siehe Anmerkung unten
- name: Setze globale Client-Key-Facts für alle Clients
set_fact:
wg_all_clients_private_keys: >-
{{
wg_all_clients_private_keys | default({}) | combine({
item.1.name: item.0.stdout
})
}}
loop: "{{ wg_client_private_keys.results | zip(wireguard_clients) | list }}"
delegate_to: localhost
run_once: true
- name: Generiere Private Keys für Clients
command: "wg genkey"
register: client_keys_raw
loop: "{{ wireguard_clients }}"
loop_control:
loop_var: client
changed_when: false
- name: Mappe Keys nach Namen
set_fact:
wg_all_clients_private_keys: "{{ dict(wireguard_clients | map(attribute='name') | list | zip(client_keys_raw.results | map(attribute='stdout') | list)) }}"

View File

@@ -0,0 +1,7 @@
- name: Stelle sicher, dass WireGuard installiert ist
apt:
name: wireguard
state: present
update_cache: yes
become: true
when: ansible_connection != "local"

View File

@@ -0,0 +1,22 @@
#- include_tasks: install.yml
#- include_tasks: configure.yml
#- include_tasks: generate_clients.yml
#- include_tasks: firewall.yml
- name: Installiere WireGuard
import_tasks: install.yml
when: ansible_connection != "local"
- name: Konfiguriere WireGuard
import_tasks: configure.yml
- name: Generiert .conf Dateien
import_tasks: generate_clients.yml
- name: Setze Firewall-Regeln
import_tasks: firewall.yml
when: ansible_connection != "local"
- name: Wende VPN-Failsafe-Regeln an
import_tasks: failsafe.yml

View File

@@ -0,0 +1,10 @@
[Interface]
PrivateKey = {{ wg_all_clients_private_keys[item.name] }}
Address = {{ item.address }}/32
DNS = 1.1.1.1
[Peer]
PublicKey = {{ item.public_key }}
Endpoint = {{ wireguard_server_ip }}:{{ wireguard_port }}
AllowedIPs = {{ wireguard_network }}, {{ wireguard_server_ip }}/32
PersistentKeepalive = 25

View File

@@ -0,0 +1,12 @@
[Interface]
Address = {{ wireguard_address }}
PrivateKey = {{ wg_privkey | b64decode | trim }}
ListenPort = {{ wireguard_port }}
PostUp = iptables -A FORWARD -i {{ wireguard_interface }} -j ACCEPT; iptables -A FORWARD -o {{ wireguard_interface }} -j ACCEPT; iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE
PostDown = iptables -D FORWARD -i {{ wireguard_interface }} -j ACCEPT; iptables -D FORWARD -o {{ wireguard_interface }} -j ACCEPT; iptables -t nat -D POSTROUTING -o eth0 -j MASQUERADE
{% for client in wireguard_clients %}
[Peer]
PublicKey = {{ client.public_key }}
AllowedIPs = {{ client.address }}/32
{% endfor %}

View File

@@ -1,4 +0,0 @@
- hosts: web
become: false
roles:
- setup

View File

@@ -0,0 +1,7 @@
# ansible/wireguard-create-config.yml
- hosts: vpn
gather_facts: false
roles:
- role: wireguard
tasks_from: generate_clients # Zum Beispiel, je nach Task
# tasks_from: generate_client_single # Oder für einzelne Clients

View File

@@ -0,0 +1,9 @@
# ansible/wireguard-install-server.yml
- hosts: vpn
become: true
gather_facts: true
roles:
- role: wireguard
tasks_from: install # z.B., je nach Namensschema deiner Rolle
- role: wireguard
tasks_from: configure # Für Config/Firewall usw.

View File

@@ -1,3 +0,0 @@
FROM nginx:stable-alpine
COPY nginx/default.conf /etc/nginx/conf.d/default.conf

View File

@@ -1,2 +0,0 @@
<?php
phpinfo();

View File

@@ -1,17 +0,0 @@
server {
listen 80;
server_name localhost;
root /var/www/html;
index index.php index.html;
location / {
try_files $uri $uri/ =404;
}
location ~ \.php$ {
include fastcgi_params;
fastcgi_pass php:9000;
fastcgi_param SCRIPT_FILENAME /var/www/html$fastcgi_script_name;
}
}

11
bin/check-env Normal file
View File

@@ -0,0 +1,11 @@
#!/bin/sh
if [ ! -f .env ]; then
echo "❌ .env fehlt!"
exit 1
fi
if ! grep -q "APP_PORT=" .env; then
echo "⚠️ APP_PORT nicht gesetzt"
fi
# TODO In make up oder make deploy einbauen.

4
bin/deploy Executable file
View File

@@ -0,0 +1,4 @@
#!/bin/sh -l
# Führt das Ansible-Deploy-Playbook aus
/home/michael/.local/bin/ansible-playbook -i ansible/inventory.ini ansible/playbooks/deploy.yml

3
bin/down Executable file
View File

@@ -0,0 +1,3 @@
#!/bin/sh
# Stoppt alle laufenden Container
docker compose down

3
bin/logs Executable file
View File

@@ -0,0 +1,3 @@
#!/bin/sh
# Zeigt die Live-Logs aller Container
docker compose logs -f

3
bin/restart Executable file
View File

@@ -0,0 +1,3 @@
#!/bin/sh
# Stoppt und startet alle Container neu
docker compose down && docker compose up -d

3
bin/setup Executable file
View File

@@ -0,0 +1,3 @@
#!/bin/sh
# Führt das Ansible-Deploy-Playbook aus
/home/michael/.local/bin/ansible-playbook -i ansible/inventory.ini ansible/playbooks/setup.yml

3
bin/test Executable file
View File

@@ -0,0 +1,3 @@
#!/bin/sh
# Platzhalter für Tests kann später durch phpunit, etc. ersetzt werden
docker compose exec php ./vendor/bin/pest # --coverage

3
bin/up Executable file
View File

@@ -0,0 +1,3 @@
#!/bin/sh
# Startet Docker-Container im Hintergrund
docker compose up -d

39
composer.json Normal file
View File

@@ -0,0 +1,39 @@
{
"name": "michaelschiemer/website",
"description": "michaelschiemer.de website",
"type": "project",
"license": "proprietary",
"autoload": {
"psr-4": {
"App\\": "src/"
}
},
"authors": [
{
"name": "Michael Schiemer",
"email": "kontakt@michaelschiemer.de"
}
],
"minimum-stability": "stable",
"require-dev": {
"pestphp/pest": "^3.8",
"friendsofphp/php-cs-fixer": "^3.75"
},
"config": {
"allow-plugins": {
"pestphp/pest-plugin": true
}
},
"require": {
"predis/predis": "^3.0",
"ext-dom": "*",
"ext-libxml": "*",
"ext-curl": "*"
},
"scripts": {
"cs": "php-cs-fixer fix --dry-run --diff",
"cs-fix": "php-cs-fixer fix --allow-risky=yes || true",
"reload": "composer dump-autoload -o"
}
}

1
config/app.php Normal file
View File

@@ -0,0 +1 @@
<?php

View File

@@ -1,16 +1,99 @@
version: "3.9"
x-docker-settings: &docker-settings
build:
context: .
args:
- BUILDKIT_INLINE_CACHE=1
services:
web:
build: ./app
build:
context: ./docker/nginx
dockerfile: Dockerfile
ports:
- "${APP_PORT}:80"
- "${APP_PORT:-8000}:80"
- "127.0.0.1:8080:80"
- "${APP_SSL_PORT:-443}:443"
environment:
- APP_ENV=${APP_ENV:-development}
volumes:
- ./app/html:/var/www/html
- ./:/var/www/html:cached
- ./ssl:/etc/nginx/ssl:ro # SSL-Zertifikate mounten
depends_on:
- php
php:
condition: service_started
restart: unless-stopped
networks:
- frontend
- backend
env_file:
- .env
php:
image: php:${PHP_VERSION}-fpm
container_name: php
build:
context: .
dockerfile: docker/php/Dockerfile
args:
- ENV=${APP_ENV:-dev}
- COMPOSER_INSTALL_FLAGS=${COMPOSER_INSTALL_FLAGS:---no-scripts --no-autoloader}
volumes:
- ./app/html:/var/www/html
# Shared Volume für Composer-Cache über Container-Neustarts hinweg
- composer-cache:/root/.composer/cache
# Bindet das Projektverzeichnis für Live-Änderungen ein
- ./:/var/www/html:cached
# Verhindert Überschreiben der Vendor-Verzeichnisse
#- /var/www/html/vendor
#- cache-volume:/var/www/html/cache:rw
environment:
PHP_IDE_CONFIG: "serverName=docker"
APP_ENV: ${APP_ENV:-development}
healthcheck:
test: [ "CMD", "php", "-v" ]
interval: 10s
timeout: 5s
retries: 5
restart: unless-stopped
networks:
- backend
#- cache
# backend:
# aliases:
# - php
# cache:
env_file:
- .env
redis:
image: redis:8-alpine
volumes:
- ./docker/redis/redis.conf:/usr/local/etc/redis/redis.conf
- redis_data:/data
command: ["redis-server", "/usr/local/etc/redis/redis.conf"]
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 30s
timeout: 5s
retries: 3
restart: unless-stopped
networks:
- cache
env_file:
- .env
networks:
frontend:
driver: bridge
backend:
driver: bridge
cache:
driver: bridge
volumes:
redis_data:
composer-cache:
#cache-volume:

33
docker/nginx/Dockerfile Normal file
View File

@@ -0,0 +1,33 @@
FROM nginx:alpine
# Standard-Konfiguration entfernen
RUN rm /etc/nginx/conf.d/default.conf
# Verzeichnisse erstellen mit korrekten Berechtigungen
RUN mkdir -p /var/cache/nginx /var/log/nginx /etc/nginx/template && \
chmod -R 777 /var/cache/nginx /var/log/nginx
# Kopiere die Template-Konfiguration
COPY ./nginx.conf /etc/nginx/nginx.conf
COPY ./default.conf /etc/nginx/conf.d/default.conf
# Kopiere die SSL-Zertifikate
COPY ./ssl/ /etc/nginx/ssl/
# Startup-Skript zum Ersetzen der Variablen
COPY ./docker-entrypoint.sh /
RUN chmod +x /docker-entrypoint.sh
#Install Netcat
RUN apk add --no-cache netcat-openbsd
# Als user www-data laufen lassen
RUN addgroup -g 1000 www && adduser -D -G www -u 1000 www-data \
&& chown -R www-data:www /var/cache/nginx /var/log/nginx /etc/nginx
USER www-data
EXPOSE 80 443
ENTRYPOINT ["/docker-entrypoint.sh"]
CMD ["nginx", "-g", "daemon off;"]

228
docker/nginx/default.conf Normal file
View File

@@ -0,0 +1,228 @@
# FastCGI-Cache-Einstellungen
fastcgi_cache_path /var/cache/nginx levels=1:2 keys_zone=PHPCACHE:100m inactive=60m;
fastcgi_cache_key "$scheme$request_method$host$request_uri";
fastcgi_cache_use_stale error timeout invalid_header http_500;
fastcgi_ignore_headers Cache-Control Expires Set-Cookie;
# Hardcoded Umgebungsmodus basierend auf Template-Ersetzung
map $http_host $env_mode {
default "${APP_ENV}";
}
# Dynamische Cache-Kontrolle basierend auf Umgebungsvariable
map $env_mode $should_skip_cache {
default 0; # Standard (Produktion): Cache aktivieren
development 1; # Entwicklung: Cache deaktivieren
testing 1; # Testing: Cache deaktivieren
}
# Skip-Cache für Sessions und basierend auf Umgebung
map $http_cookie$should_skip_cache $skip_cache {
"~PHPSESSID" 1; # Sessions nie cachen
"1" 1; # Cache überspringen, wenn should_skip_cache = 1
default 0; # Ansonsten cachen
}
map $host $block_health {
default 1; # Blockiere alles
localhost 0; # Erlaube nur Host "localhost"
}
upstream php-upstream {
server php:9000; # „php“ ist durch Network-Alias immer erreichbar
}
server {
listen 80;
server_name localhost;
return 301 https://$host$request_uri;
}
server {
# Korrigierte HTTP/2 Syntax
listen 443 ssl;
http2 on; # Neue Syntax für HTTP/2
server_name localhost;
#ssl_certificate /etc/nginx/ssl/localhost+2.pem;
#ssl_certificate_key /etc/nginx/ssl/localhost+2-key.pem;
ssl_certificate /etc/nginx/ssl/fullchain.pem;
ssl_certificate_key /etc/nginx/ssl/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers on;
ssl_ciphers ECDHE-RSA-AES256-GCM-SHA512:DHE-RSA-AES256-GCM-SHA512:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES256-GCM-SHA384;
# Verbesserte SSL-Konfiguration
ssl_session_timeout 1d;
ssl_session_cache shared:SSL:10m;
ssl_session_tickets off;
# OCSP Stapling (auskommentiert, wenn Zertifikate fehlen)
# ssl_stapling on;
# ssl_stapling_verify on;
resolver 1.1.1.1 1.0.0.1 valid=300s;
resolver_timeout 5s;
root /var/www/html/public;
index index.php index.html;
# Debug-Header für die Entwicklung
add_header X-Environment $env_mode always;
# Sicherheits-Header
add_header X-Content-Type-Options nosniff;
add_header X-Frame-Options DENY always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "no-referrer-when-downgrade" always;
add_header Permissions-Policy "geolocation=(), microphone=()" always;
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains" always;
# CSP Header
add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self'; object-src 'none'" always;
# Buffer-Größen anpassen
client_body_buffer_size 10K;
client_header_buffer_size 1k;
client_max_body_size 10m;
large_client_header_buffers 2 1k;
# Verbesserte Gzip-Kompression
gzip on;
gzip_vary on;
gzip_proxied any;
gzip_comp_level 6;
gzip_buffers 16 8k;
gzip_http_version 1.1;
gzip_min_length 256;
gzip_types
application/atom+xml
application/javascript
application/json
application/ld+json
application/manifest+json
application/rss+xml
application/vnd.geo+json
application/vnd.ms-fontobject
application/x-font-ttf
application/x-web-app-manifest+json
application/xhtml+xml
application/xml
font/opentype
image/bmp
image/svg+xml
image/x-icon
text/cache-manifest
text/css
text/plain
text/vcard
text/vnd.rim.location.xloc
text/vtt
text/x-component
text/x-cross-domain-policy;
# Logs
access_log /var/log/nginx/access.log combined;
error_log /var/log/nginx/error.log error;
location / {
try_files $uri $uri/ /index.php?$query_string;
autoindex off;
}
# Caching Header für statische Dateien
location ~* \.(jpg|jpeg|png|gif|ico|svg|woff|woff2|ttf|eot)$ {
expires 1y;
add_header Cache-Control "public, immutable, max-age=31536000";
}
location ~* \.(css|js)$ {
expires 1w;
add_header Cache-Control "public, max-age=604800";
}
location ~* \.(json|xml)$ {
expires 1d;
add_header Cache-Control "public, max-age=86400";
}
location ~ \.php$ {
try_files $uri =404;
include fastcgi_params;
fastcgi_pass php-upstream;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_param PATH_INFO $fastcgi_path_info;
fastcgi_split_path_info ^(.+\.php)(/.+)$;
# Wichtig: APP_ENV an PHP weitergeben
fastcgi_param APP_ENV $env_mode;
# Timeout-Einstellungen
fastcgi_read_timeout 60s;
fastcgi_connect_timeout 60s;
fastcgi_send_timeout 60s;
# Caching-Einstellungen
fastcgi_buffer_size 128k;
fastcgi_buffers 4 256k;
fastcgi_busy_buffers_size 256k;
# Cache FastCGI-Antworten
fastcgi_cache_bypass $skip_cache;
fastcgi_no_cache $skip_cache;
fastcgi_cache PHPCACHE;
fastcgi_cache_valid 200 60m;
# Debug-Header hinzufügen
add_header X-Cache-Status $upstream_cache_status;
add_header X-Cache-Environment $env_mode;
add_header X-Cache-Skip $skip_cache;
# Für bessere Performance
fastcgi_keep_conn on;
}
# Sicherheitseinstellungen
location ~ /\.(?!well-known).* {
deny all;
}
server_tokens off;
limit_req zone=mylimit burst=20 nodelay;
location ~* /(?:uploads|files)/.*\.php$ {
deny all;
}
# Healthcheck-Endpunkt
location = /ping {
access_log off;
add_header Content-Type text/plain;
return 200 'pong';
}
location = /health {
if ($block_health) {
return 404;
}
try_files /health.php =404;
allow 127.0.0.1; # Lokal erlaubt (Ansible, Docker, Monitoring intern)
allow ::1;
allow 192.168.0.0/16; # Optional: internes Netz (z.B. für internen Loadbalancer)
deny all;
error_page 403 =404;
}
error_page 404 /errors/404.html;
error_page 403 /errors/403.html;
error_page 500 502 503 504 /errors/50x.html;
location /errors/ {
internal; # Verhindert direkten Zugriff
}
}

View File

@@ -0,0 +1,20 @@
#!/bin/sh
set -e
until nc -z -w 2 php 9000; do
echo "Warte auf PHP-FPM..."
sleep 1
done
# Optional: eigene Umgebungsvariable mit Default setzen
export APP_ENV="${APP_ENV:-production}"
echo "Starte Nginx mit APP_ENV=$APP_ENV"
# Ersetze Umgebungsvariablen wie ${APP_ENV} in der Nginx-Config per envsubst
envsubst '${APP_ENV}' < /etc/nginx/conf.d/default.conf > /etc/nginx/conf.d/default.conf.tmp
mv /etc/nginx/conf.d/default.conf.tmp /etc/nginx/conf.d/default.conf
# Starte Nginx (Foreground)
exec nginx -g 'daemon off;'

37
docker/nginx/nginx.conf Normal file
View File

@@ -0,0 +1,37 @@
worker_processes auto;
pid /tmp/nginx.pid;
events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
server_tokens off;
# Rate-Limiting für besseren DDoS-Schutz
limit_req_zone $binary_remote_addr zone=mylimit:10m rate=10r/s;
# Logging-Einstellungen
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log /var/log/nginx/access.log main;
error_log /var/log/nginx/error.log warn;
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
types_hash_max_size 2048;
# TLS-Einstellungen
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 10m;
# Include server configs
include /etc/nginx/conf.d/*.conf;
}

View File

@@ -0,0 +1,28 @@
-----BEGIN PRIVATE KEY-----
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDAzwS8FGSCDwDg
7QX8OpGkX1SbSwbUyzXNjEta319BvAH2OfcFFCj6u/iqfL7gKOM83t8u71VBFsCx
ZlxX2Ilyu2+r72sCdGBXcK6riTHrkjTs4uV6YV98eJuYhvAzSijpsRQjwnwQ587c
axtCXZhOzee3Tnbtzq4plqmOKR10D+cvrOZxuoKI914blXpGe8ds3vWEixewrex0
CYhzPj/zEF3yfCoSXeTmFBUbmmH/JwcCK8uO5t6XR1Dyo3M4GOMrmGtO7U4nuL6e
7JsbZfPaEW9wKtDjEwFDJSLy0ALEpiNWvbW4OaZWNkJk0jfKYwyBunNSs62B4307
oF8lqVo1AgMBAAECggEAbPlU0ryv5fZ256nvlRTBVmbvGep4zPKh0TA3MwBHBY8u
iK1QWVWAp95v+GQTOfzCGphZCl0JEYW7mUiibqAbZ3Za8pGaKMP/48vzXU5ooZ18
PlsrmlTItEAyqS2zOznyD8se9+snViK+f0QmHwdpWzjze15kx5nmQ+k8ofXJCNwq
q3dJIMI/WNuc0e/mMHYjZBsIwuoUi6YJHCE6RkWhGcnvlyXdKUV73/n8Loy6DUtW
VmshXag7+GfbVZIesMCjfnJ0gr9OG+XrFl6AcggzFA1ZHRoQliraVYGB2duQlIpW
o1wJMhFSGFPZxvl67hwXHJeo7ghHHfqNYXS1OuhV7QKBgQDBrvyzLtav51LzqOUY
2HPvaH86arbARc4Fy6ZJ0TaSlmKQ5GzRG0lG2CR03oZz+OcMV/BU8xUMM7CX0zUq
9RAmbE7rvXYOvqTe8pcdHeKKflzsr5p0HNROaeZdpMu8xoK1KLelAo6UCEBUGEny
oMtQWapuYvmdlHR2el2ICRGNzwKBgQD+1/iM1LcF9CYvEc8Sly9XuoRsdUCxavQa
sssv7eG5kkL8HroNs1pGZU8lNuZaT1V0ekWVOFk+X3+dGgCXg5/e/CluK9K7qOHX
3IkyUnZLEH5sDXGMGBzYA9AQTaB1PMTQYku6GNWYab6LFQTvpvvLcIILaFHokq8p
D/dGVJH8uwKBgQCBOxDBPe9hTye6DGdQPJyekUrS34EwqWLd2xQJDN8sz8rUgpVY
sKwj6PPqRs/PcbQ4ODTTeZ4BljuuEe7XyswL1xiRksjC7dF0MMlDVD1jywyVoFWe
Q94ks+RRdzO5sXplBdYC88HOY/MIKWytxzvhUPK21LNYwUU0CFGAAw0DYQKBgQD4
mT/qSdscoLXa9tl0fiz9vIJPtvXb3MSxgra5U6n9t9NGVMcUdGBdCZjyaaK+eGOZ
U2mrjiNouAop++KV6x26jWvxACj7TVy6kXT4tP6WbUmWKGsaya7hfp6qOL+NfjFU
Qn8y0+URYB4zWNbO3asFIwSJEkPMx8K9IMkMP5WF3wKBgCYiqAhPDF4WxA3fAqP7
95px8Clrety0mwOtE/rMQRf1nKJ78oA4pr+/VXRbyghAxtD4psbmBQofX3iwnn3B
o1DV3FLpNw004mvcKGScUcNwHQtWAtWX2nVDcxes5R2DgN+lpmWmf5Tq47p0r5ZP
nRb92drrnf8FoBv78CxLjIu+
-----END PRIVATE KEY-----

View File

@@ -0,0 +1,25 @@
-----BEGIN CERTIFICATE-----
MIIEHjCCAoagAwIBAgIQLqhFNHvvWJKUpuypArU2CjANBgkqhkiG9w0BAQsFADBb
MR4wHAYDVQQKExVta2NlcnQgZGV2ZWxvcG1lbnQgQ0ExGDAWBgNVBAsMD21pY2hh
ZWxATWlrZS1QQzEfMB0GA1UEAwwWbWtjZXJ0IG1pY2hhZWxATWlrZS1QQzAeFw0y
NTA1MTgxOTUyMDlaFw0yNzA4MTgxOTUyMDlaMEMxJzAlBgNVBAoTHm1rY2VydCBk
ZXZlbG9wbWVudCBjZXJ0aWZpY2F0ZTEYMBYGA1UECwwPbWljaGFlbEBNaWtlLVBD
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwM8EvBRkgg8A4O0F/DqR
pF9Um0sG1Ms1zYxLWt9fQbwB9jn3BRQo+rv4qny+4CjjPN7fLu9VQRbAsWZcV9iJ
crtvq+9rAnRgV3Cuq4kx65I07OLlemFffHibmIbwM0oo6bEUI8J8EOfO3GsbQl2Y
Ts3nt0527c6uKZapjikddA/nL6zmcbqCiPdeG5V6RnvHbN71hIsXsK3sdAmIcz4/
8xBd8nwqEl3k5hQVG5ph/ycHAivLjubel0dQ8qNzOBjjK5hrTu1OJ7i+nuybG2Xz
2hFvcCrQ4xMBQyUi8tACxKYjVr21uDmmVjZCZNI3ymMMgbpzUrOtgeN9O6BfJala
NQIDAQABo3YwdDAOBgNVHQ8BAf8EBAMCBaAwEwYDVR0lBAwwCgYIKwYBBQUHAwEw
HwYDVR0jBBgwFoAUhhzxUvThIGRX4MSoX91Vzm1zZ9AwLAYDVR0RBCUwI4IJbG9j
YWxob3N0hwR/AAABhxAAAAAAAAAAAAAAAAAAAAABMA0GCSqGSIb3DQEBCwUAA4IB
gQDUFLYZPo8RrfZh/vwT15LcIce8brdVegms6DvPK9lMZX6C4sGf4+rTJCwPuqHW
dqVZAhHdvcsyGI15xvVPT4qSh89RN1JB9uIHCk+weIzp+Rn06MMrB49m4abAvWp2
hB8bCo80hMVIsCb3Wr9sHg7CsJItsdGz8jHYCvHpvPLR7gWhYjm1g0meglT3tZqd
TsKDMb3Vj/vsivEueM6Oj/of8xbamVSSkqljWbRls7Ti7xqXMbmf7nl0WvG9IXg3
5Ucv1AWJIFEeLnMM5V0nEbO3sAhbNMLXieGPBWHXOgHuvVnQyu1mBESjgc5bjwfN
UjYBHluFkF9aYw3mGcFqAlb1FpGoMtHwTw0uGZzHzj5FY8oZix5edq/upriV6cU2
t0tidlfhvkJNSSO4zjAPjU1wd+/QRZwY2PcB5kBxs5MzSmiMlEjTkGgHWqMWMBf1
NPbyaxtjL69xBVonxpqD6BLJ2qLatgCs6fkZZF7AT38OFXr8Cv5vxt1rR5fs1P6X
mI0=
-----END CERTIFICATE-----

69
docker/php/Dockerfile Normal file
View File

@@ -0,0 +1,69 @@
# Dockerfile für PHP-FPM
FROM php:8.4-fpm AS base
# System-Abhängigkeiten: Werden selten geändert, daher ein eigener Layer
RUN apt-get update && apt-get install -y \
git \
unzip \
libzip-dev \
zip \
&& docker-php-ext-install zip pdo pdo_mysql \
&& docker-php-ext-install opcache \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*
# Composer installieren
RUN curl -sS https://getcomposer.org/installer | php \
&& mv composer.phar /usr/local/bin/composer
# Installiere Xdebug nur im Entwicklungsmodus
ARG ENV=prod
RUN if [ "$ENV" = "dev" ]; then \
pecl install xdebug \
&& docker-php-ext-enable xdebug; \
fi
WORKDIR /var/www/html
# Kopiere zuerst nur composer.json/lock für besseres Layer-Caching
COPY composer.json composer.lock ./
# Installiere Abhängigkeiten - variiert je nach Umgebung
RUN if [ "$ENV" = "prod" ]; then \
composer install --no-dev --no-scripts --no-autoloader --optimize-autoloader; \
else \
composer install --no-scripts --no-autoloader; \
fi
# Kopiere PHP-Konfigurationen
COPY docker/php/php.common.ini /usr/local/etc/php/php.common.ini
COPY docker/php/php.${ENV}.ini /usr/local/etc/php/php.ini
# Wenn dev, kopiere auch xdebug-Konfiguration
RUN if [ "$ENV" = "dev" ]; then \
mkdir -p /usr/local/etc/php/conf.d/; \
fi
COPY docker/php/xdebug.ini /usr/local/etc/php/conf.d/docker-php-ext-xdebug.ini
# Kopiere den Rest des Projekts
COPY . .
# Optimiere Autoloader
RUN composer dump-autoload --optimize
# <<--- ALLE zusätzlichen System-Dateien und chmod noch als root!
COPY docker/php/docker-entrypoint.sh /usr/local/bin/docker-entrypoint.sh
RUN chmod +x /usr/local/bin/docker-entrypoint.sh
# Danach erst den Nutzer wechseln!
RUN groupadd -g 1000 appuser && useradd -u 1000 -g appuser -m appuser
RUN chown -R appuser:appuser /var/www/html
USER appuser
RUN mkdir -p /var/www/html/cache && \
chown -R 1000:1000 /var/www/html/cache && \
chmod -R 775 /var/www/html/cache
ENTRYPOINT ["/usr/local/bin/docker-entrypoint.sh"]
CMD ["php-fpm"]

View File

@@ -0,0 +1,4 @@
#!/bin/bash
chown -R www-data:www-data /var/www/html/cache
chmod -R 775 /var/www/html/cache
exec "$@"

View File

@@ -0,0 +1,8 @@
expose_php = Off
session.cookie_secure = 1
session.cookie_httponly = 1
session.cookie_samesite = Lax
date.timezone = Europe/Berlin

View File

@@ -0,0 +1,29 @@
; php.ini für Entwicklung
include = php.common.ini
[opcache]
opcache.enable=1
opcache.enable_cli=0
opcache.memory_consumption=128
opcache.max_accelerated_files=10000
; Häufigere Validierung im Dev-Modus
opcache.revalidate_freq=0
; Timestamps-Validierung einschalten für Entwicklung
opcache.validate_timestamps=1
opcache.file_cache=
realpath_cache_ttl=0
opcache.interned_strings_buffer=16
display_errors = On
display_startup_errors = On
error_reporting = E_ALL
memory_limit = 512M
upload_max_filesize = 20M
post_max_size = 25M
max_execution_time = 60
; Xdebug-Einstellungen können auch hier hinzugefügt werden, falls gewünscht

31
docker/php/php.prod.ini Normal file
View File

@@ -0,0 +1,31 @@
; php.ini für Produktion
include = php.common.ini
[opcache]
; Aktiviere OPcache
opcache.enable=1
; Aktiviere OPcache für CLI-Anwendungen (optional)
opcache.enable_cli=0
; Maximale Speichernutzung für Cache in MB
opcache.memory_consumption=128
; Maximale Anzahl an gecachten Dateien
opcache.max_accelerated_files=10000
; Wie oft wird der Cache validiert (0 = bei jedem Request, empfohlen für Entwicklung)
; In Produktion höher setzen für bessere Performance
opcache.revalidate_freq=60
; Cache-Zeitstempel prüfen (0 für Produktionsumgebungen)
opcache.validate_timestamps=0
; Performance-Optimierungen
opcache.interned_strings_buffer=16
; JIT (Just-In-Time Compilation) - Optional für PHP 8.0+
opcache.jit_buffer_size=100M
opcache.jit=1255
display_errors = Off
display_startup_errors = Off
error_reporting = E_ALL & ~E_DEPRECATED & ~E_STRICT
memory_limit = 256M
upload_max_filesize = 10M
post_max_size = 12M
max_execution_time = 30

7
docker/php/xdebug.ini Normal file
View File

@@ -0,0 +1,7 @@
; Xdebug 3 Konfiguration
xdebug.mode=${XDEBUG_MODE:-off}
xdebug.client_host=host.docker.internal
xdebug.client_port=9003
xdebug.start_with_request=yes
xdebug.log=/var/log/xdebug.log
xdebug.idekey=PHPSTORM

7
docker/redis/redis.conf Normal file
View File

@@ -0,0 +1,7 @@
bind 0.0.0.0
#protected-mode yes
dir /data
save 900 1
save 300 10
save 60 10000
appendonly yes

25
docs/ARCHITECURE.md Normal file
View File

@@ -0,0 +1,25 @@
# Architektur-Prinzipien
Dieses Dokument beschreibt die grundlegenden Architekturprinzipien unseres Frameworks.
## 1. Immutabilität und Unveränderlichkeit
Wo immer möglich, sollten Objekte unveränderlich (immutable) sein. Dies verbessert die Voraussagbarkeit und Testbarkeit.
## 2. Final by Default
Alle Klassen sollten standardmäßig als `final` deklariert werden, es sei denn, es gibt einen konkreten Grund für Vererbung.
Begründung:
- Vermeidet unbeabsichtigte Vererbungshierarchien
- Verbessert die Kapselung
- Ermöglicht interne Änderungen, ohne Kinderklassen zu beeinflussen
## 3. Explizite über Implizite
- Alle Abhängigkeiten sollten explizit injiziert werden
- Keine globalen Zustände oder Singletons
- Typen immer explizit deklarieren
## 4. Modularität
Jedes Modul sollte in sich geschlossen sein und minimale Abhängigkeiten nach außen haben.

View File

@@ -8,7 +8,7 @@ Dieses Projekt verwendet `.env`-Dateien zur Konfiguration von Docker Compose und
```env
COMPOSE_PROJECT_NAME=michaelschiemer
APP_PORT=8080
APP_PORT=8000
PHP_VERSION=8.2
```

42
docs/features.md Normal file
View File

@@ -0,0 +1,42 @@
# Framework Features
## Core Features
- [ ] Routing mit Unterstützung für PHP-Attribute
- [ ] Dependency Injection Container
- [ ] Request/Response Abstraktion
- [ ] Template-Engine
- [ ] Error/Exception Handling
- [ ] Konfigurationssystem
## Database Features
- [ ] PDO-Wrapper
- [ ] Query Builder
- [ ] Migrations-System
- [ ] Schema Manager
- [ ] Entity-Mapping (optional)
## Security Features
- [ ] CSRF-Schutz
- [ ] XSS-Filtierung
- [ ] Input-Validierung
- [ ] Authentifizierung
- [ ] Autorisierung/Rechtemanagement
## Module: Music
- [ ] Album-Verwaltung
- [ ] Track-Management
- [ ] Playlists
- [ ] Integrationsmöglichkeit mit Spotify/SoundCloud
## Module: Content
- [ ] Blog-System
- [ ] Markdown-Support
- [ ] Medienbibliothek
- [ ] SEO-Optimierung
- [ ] Kommentarsystem
## Admin Interface
- [ ] Dashboard
- [ ] Content-Editor
- [ ] Benutzer-/Rechteverwaltung
- [ ] Statistiken

13
package.json Normal file
View File

@@ -0,0 +1,13 @@
{
"devDependencies": {
"@types/jest": "^29.2.5",
"jest": "^29.3.1",
"vite": "^6.3.5"
},
"scripts": {
"test": "jest",
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
}
}

18
phpunit.xml Normal file
View File

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd"
bootstrap="vendor/autoload.php"
colors="true"
>
<testsuites>
<testsuite name="Test Suite">
<directory suffix="Test.php">./tests</directory>
</testsuite>
</testsuites>
<source>
<include>
<directory>app</directory>
<directory>src</directory>
</include>
</source>
</phpunit>

View File

@@ -0,0 +1 @@

View File

@@ -0,0 +1 @@
p{color:red}html{background:#00f}

View File

@@ -0,0 +1,12 @@
* {
}
footer > nav {
display: flex;
}
footer > nav > li {
flex-direction: row;
}

View File

@@ -0,0 +1 @@
import"./css-CKd28aW2.js";

0
public/favico.ico Normal file
View File

7
public/health.php Normal file
View File

@@ -0,0 +1,7 @@
<?php
header('Content-Type: application/json');
$status = ['status' => 'ok'];
echo json_encode($status);

84
public/index.php Normal file
View File

@@ -0,0 +1,84 @@
<?php
declare(strict_types=1);
use App\Framework\Core\Discovery;
use App\Framework\Core\DynamicRoute;
use App\Framework\Core\PhpObjectExporter;
use App\Framework\Core\RouteCache;
use App\Framework\Core\RouteMapper;
use App\Framework\Core\StaticRoute;
use App\Framework\ErrorHandling\ErrorHandler;
use App\Framework\Http\HttpMethod;
use App\Framework\Router\RouteCollection;
require __DIR__ . '/../vendor/autoload.php';
// Fehleranzeige für die Entwicklung aktivieren
error_reporting(E_ALL);
ini_set('display_errors', 1);
$rfl = new ReflectionClass(Discovery::class);;
$ghost = $rfl->newLazyGhost(function (Discovery $object) {
// Initialize object in-place
$object->__construct();
});
/*$clientrequest = new \App\Framework\HttpClient\ClientRequest(HttpMethod::GET, 'https://jsonplaceholder.typicode.com/posts');
$client = new \App\Framework\HttpClient\CurlHttpClient();
var_dump($client->send($clientrequest));*/
$emitter = new \App\Framework\Http\ResponseEmitter();
ErrorHandler::register($emitter);
#echo dirname(__DIR__) . '/cache/routes.cache.php';
$discovery = new Discovery(new \App\Framework\Core\RouteMapper());
$results = $discovery->discover(__DIR__ . '/../src/Application/');
$rc = new \App\Framework\Core\RouteCompiler();
$routes = $rc->compile($results[\App\Framework\Attributes\Route::class]);
$cacheFile = dirname(__DIR__) . '/cache/routes.cache.php';
$routeCache = new \App\Framework\Core\RouteCache($cacheFile);
$routeCache->save($routes);
$request = new \App\Framework\Http\HttpRequest(
method: \App\Framework\Http\HttpMethod::tryFrom($_SERVER['REQUEST_METHOD'] ?? 'GET'),
path:parse_url( $_SERVER['REQUEST_URI'] ?? '/', PHP_URL_PATH),
);
#var_dump("<pre>", $routeCache->load());
$router = new \App\Framework\Router\HttpRouter(new RouteCollection($routeCache->load()));
$match = $router->match($request->method->value, $request->path);
$dispatcher = new \App\Framework\Router\RouteDispatcher();
$return = $dispatcher->dispatch($match);
$responder = new \App\Framework\Router\RouteResponder();
$response = $responder->respond($return);
$emitter = new \App\Framework\Http\ResponseEmitter();
$emitter->emit($response);
/*$redis = new Predis\Client([
'scheme' => 'tcp',
'host' => 'redis', // Service-Name aus docker-compose
'port' => 6379,
]);
$redis->set('hello', 'world');
echo $redis->get('hello'); // Gibt: world aus*/
exit;

7
resources/css/styles.css Normal file
View File

@@ -0,0 +1,7 @@
p {
color: red;
}
html {
background: blue;
}

1
resources/js/main.js Normal file
View File

@@ -0,0 +1 @@
import '../css/styles.css';

View File

@@ -0,0 +1,21 @@
<?php
namespace App\Application\Api;
use App\Framework\Attributes\Route;
use App\Framework\Router\ActionResult;
class IrkEndpoint
{
#[Route(method: 'GET', path: '/irk-impressum')]
public function impressum()
{
return;
}
#[Route(method: 'GET', path: '/irk-datenschutz')]
public function datenschutz()
{
return;
}
}

View File

@@ -0,0 +1,16 @@
<?php
namespace App\Application\EPK;
use App\Framework\Attributes\Route;
use App\Framework\Router\ActionResult;
use App\Framework\Router\ResultType;
class ShowEpk
{
#[Route(path: '/epk')]
public function epk(): ActionResult
{
return new ActionResult(ResultType::Html, 'epk', ['text' => 'EPK!']);
}
}

View File

@@ -0,0 +1,33 @@
<?php
declare(strict_types=1);
namespace App\Application\Website;
use App\Framework\Attributes\Route;
use App\Framework\Http\Request;
use App\Framework\Router\ActionResult;
use App\Framework\Router\ResultType;
class ShowHome
{
#[Route(method: 'GET', path: '/')]
public function __invoke(): ActionResult
{
return new ActionResult(
ResultType::Html,
'test',
['name' => 'Michael','title' => 'HalloWeltTitel'],
);
}
#[Route(method: 'GET', path: '/epk')]
public function impressum(string $test = 'hallo'): ActionResult
{
return new ActionResult(
ResultType::Plain,
'test',
['text' => 'EPK!'],
);
}
}

View File

@@ -0,0 +1,32 @@
<?php
declare(strict_types=1);
namespace App\Application\Website;
use App\Framework\Attributes\Route;
use App\Framework\Router\ActionResult;
use App\Framework\Router\ResultType;
class ShowImpressum
{
#[Route(method: 'GET', path: '/impressum')]
public function impressum(string $test = 'hallo'): ActionResult
{
return new ActionResult(
ResultType::Html,
'impressum',
['text' => 'Hallo Welt!'],
);
}
#[Route(method: 'GET', path: '/datenschutz')]
public function datenschutz(string $test = 'hallo'): ActionResult
{
return new ActionResult(
ResultType::Html,
'impressum',
['title' => 'Datenschutz!'],
);
}
}

View File

@@ -0,0 +1,18 @@
<?php
declare(strict_types=1);
namespace App\Framework\Attributes;
use Attribute;
#[Attribute(\Attribute::TARGET_METHOD, \Attribute::IS_REPEATABLE)]
class Route
{
public function __construct(
public string $path,
public string $method = 'GET',
) {
}
}

View File

@@ -0,0 +1,9 @@
<?php
namespace App\Framework\Attributes;
#[\Attribute(\Attribute::TARGET_CLASS)]
final class Singleton
{
}

View File

@@ -0,0 +1,17 @@
<?php
declare(strict_types=1);
namespace App\Framework\Core;
interface AttributeMapper
{
public function getAttributeClass(): string;
/**
* @param object $reflectionTarget ReflectionClass|ReflectionMethod
* @param object $attributeInstance
* @return array|null
*/
public function map(object $reflectionTarget, object $attributeInstance): ?array;
}

Some files were not shown because too many files have changed in this diff Show More