fix(Discovery): Add comprehensive debug logging for router initialization
- Add initializer count logging in DiscoveryServiceBootstrapper - Add route structure analysis in RouterSetup - Add request parameter logging in HttpRouter - Update PHP production config for better OPcache handling - Fix various config and error handling improvements
This commit is contained in:
554
deployment/infrastructure/DEPLOYMENT_ANALYSIS.md
Normal file
554
deployment/infrastructure/DEPLOYMENT_ANALYSIS.md
Normal file
@@ -0,0 +1,554 @@
|
|||||||
|
# Production Deployment Analysis & Fix Strategy
|
||||||
|
|
||||||
|
**Date**: 2025-10-27
|
||||||
|
**Status**: CRITICAL - Production website returning HTTP 500 errors
|
||||||
|
**Root Cause**: Database connection configuration error (DB_PORT mismatch)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Complete Deployment Flow Analysis
|
||||||
|
|
||||||
|
### Deployment Architecture
|
||||||
|
|
||||||
|
The project uses a **release-based deployment pattern** with shared configuration:
|
||||||
|
|
||||||
|
```
|
||||||
|
/home/deploy/michaelschiemer/
|
||||||
|
├── releases/
|
||||||
|
│ ├── 1761566515/ # Current release (timestamped)
|
||||||
|
│ ├── 1761565432/ # Previous releases
|
||||||
|
│ └── ...
|
||||||
|
├── shared/
|
||||||
|
│ └── .env.production # Shared configuration file
|
||||||
|
└── current -> releases/1761566515/ # Symlink to active release
|
||||||
|
```
|
||||||
|
|
||||||
|
**Key Characteristics**:
|
||||||
|
- **Releases Directory**: Each deployment creates a new timestamped release
|
||||||
|
- **Shared Directory**: Configuration files persist across deployments
|
||||||
|
- **Current Symlink**: Points to the active release
|
||||||
|
- **Symlink Chain**: `current/.env.production` → `shared/.env.production` → Used by application
|
||||||
|
|
||||||
|
### .env File Sources (3 Different Files Identified)
|
||||||
|
|
||||||
|
#### 1. Root Directory: `/home/michael/dev/michaelschiemer/.env.production`
|
||||||
|
- **Size**: 2.9K
|
||||||
|
- **Checksum**: 9f33068713432c1dc4008724dc6923b0
|
||||||
|
- **DB_PORT**: 5432 (CORRECT for PostgreSQL)
|
||||||
|
- **DB_USERNAME**: mdb_user (with underscore)
|
||||||
|
- **DB_PASSWORD**: Qo2KNgGqeYksEhKr57pgugakxlothn8J
|
||||||
|
- **Purpose**: Framework default configuration
|
||||||
|
- **Status**: CORRECT database configuration
|
||||||
|
|
||||||
|
#### 2. Deployment Directory: `/home/michael/dev/michaelschiemer/deployment/applications/environments/.env.production`
|
||||||
|
- **Size**: 4.3K
|
||||||
|
- **Checksum**: b516bf86beed813df03a30f655687b72
|
||||||
|
- **DB_PORT**: 5432 (CORRECT for PostgreSQL)
|
||||||
|
- **DB_USERNAME**: mdb_user (with underscore)
|
||||||
|
- **DB_PASSWORD**: Qo2KNgGqeYksEhKr57pgugakxlothn8J
|
||||||
|
- **Purpose**: Application-specific production configuration
|
||||||
|
- **Status**: CORRECT and MORE COMPLETE (includes Redis, Queue, Mail, Monitoring configs)
|
||||||
|
|
||||||
|
#### 3. Production Server: `/home/deploy/michaelschiemer/shared/.env.production`
|
||||||
|
- **Size**: 3.0K (modified Oct 26 20:56)
|
||||||
|
- **Line 15**: `DB_PORT=3306` (WRONG - MySQL port instead of PostgreSQL)
|
||||||
|
- **Line 67**: `DB_PORT=` (duplicate empty entry)
|
||||||
|
- **DB_USERNAME**: mdb-user (with hyphen - likely wrong)
|
||||||
|
- **DB_PASSWORD**: StartSimple2024! (different from local configs)
|
||||||
|
- **Status**: CORRUPTED - Wrong database configuration causing HTTP 500 errors
|
||||||
|
|
||||||
|
### Deployment Playbook Flow
|
||||||
|
|
||||||
|
**File**: `/home/michael/dev/michaelschiemer/deployment/infrastructure/playbooks/deploy-rsync-based.yml`
|
||||||
|
|
||||||
|
**Critical Configuration**:
|
||||||
|
```yaml
|
||||||
|
local_project_path: "{{ playbook_dir }}/../../.." # 3 dirs up = /home/michael/dev/michaelschiemer
|
||||||
|
shared_files:
|
||||||
|
- .env.production # Marked as SHARED file
|
||||||
|
rsync_excludes:
|
||||||
|
- .env
|
||||||
|
- .env.local
|
||||||
|
- .env.development
|
||||||
|
```
|
||||||
|
|
||||||
|
**Deployment Steps**:
|
||||||
|
1. **Rsync files** from `{{ local_project_path }}` (framework root) to release directory
|
||||||
|
- Excludes: `.env`, `.env.local`, `.env.development`
|
||||||
|
- Includes: `.env.production` from root directory
|
||||||
|
2. **Create release directory**: `/home/deploy/michaelschiemer/releases/{{ timestamp }}`
|
||||||
|
3. **Copy files** to release directory
|
||||||
|
4. **Create symlinks**:
|
||||||
|
- `release/.env.production` → `../../shared/.env.production`
|
||||||
|
- `release/.env` → `../../shared/.env.production`
|
||||||
|
5. **Update current** symlink → latest release
|
||||||
|
6. **Restart containers** via docker-compose
|
||||||
|
|
||||||
|
**CRITICAL ISSUE IDENTIFIED**:
|
||||||
|
The playbook does NOT have a task to initially copy `.env.production` to `shared/.env.production`. It only creates symlinks assuming the file already exists. This means:
|
||||||
|
- Initial setup requires MANUAL copy of `.env.production` to `shared/`
|
||||||
|
- Updates to `.env.production` require MANUAL sync to production server
|
||||||
|
- The rsync'd `.env.production` in release directory is IGNORED (symlink overrides it)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Production Server .env Status
|
||||||
|
|
||||||
|
### Current State (BROKEN)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# /home/deploy/michaelschiemer/shared/.env.production
|
||||||
|
|
||||||
|
Line 15: DB_PORT=3306 # WRONG - MySQL port (should be 5432 for PostgreSQL)
|
||||||
|
Line 67: DB_PORT= # Duplicate empty entry
|
||||||
|
|
||||||
|
DB_USERNAME=mdb-user # Wrong format (should be mdb_user with underscore)
|
||||||
|
DB_PASSWORD=StartSimple2024! # Wrong password (doesn't match local configs)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Container Status
|
||||||
|
|
||||||
|
```
|
||||||
|
CONTAINER STATUS ISSUE
|
||||||
|
php Up 27 minutes (healthy) -
|
||||||
|
db Up 40 minutes (healthy) PostgreSQL running on port 5432
|
||||||
|
redis Up 40 minutes (healthy) -
|
||||||
|
web Up 40 minutes (UNHEALTHY) Nginx cannot connect to PHP due to DB error
|
||||||
|
queue-worker Restarting (1) 4s ago PHP crashing due to DB connection error
|
||||||
|
```
|
||||||
|
|
||||||
|
### Error Pattern
|
||||||
|
|
||||||
|
- **HTTP 500** on all requests (/, /impressum, etc.)
|
||||||
|
- **Root Cause**: PHP application cannot connect to database because:
|
||||||
|
1. `DB_PORT=3306` (MySQL) instead of `5432` (PostgreSQL)
|
||||||
|
2. Wrong username format (`mdb-user` vs `mdb_user`)
|
||||||
|
3. Wrong password
|
||||||
|
- **Impact**: All PHP processes fail to initialize → Nginx returns 500
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Deployment Command Documentation
|
||||||
|
|
||||||
|
### WORKING Commands (Current Playbook)
|
||||||
|
|
||||||
|
#### Deploy via Ansible Playbook
|
||||||
|
```bash
|
||||||
|
cd /home/michael/dev/michaelschiemer/deployment/infrastructure
|
||||||
|
|
||||||
|
# Full production deployment
|
||||||
|
ansible-playbook \
|
||||||
|
-i inventories/production/hosts.yml \
|
||||||
|
playbooks/deploy-rsync-based.yml \
|
||||||
|
--vault-password-file .vault_pass
|
||||||
|
|
||||||
|
# With specific variables
|
||||||
|
ansible-playbook \
|
||||||
|
-i inventories/production/hosts.yml \
|
||||||
|
playbooks/deploy-rsync-based.yml \
|
||||||
|
--vault-password-file .vault_pass \
|
||||||
|
-e "deployment_branch=main"
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Check Production Status
|
||||||
|
```bash
|
||||||
|
# Check containers
|
||||||
|
ansible web_servers \
|
||||||
|
-i inventories/production/hosts.yml \
|
||||||
|
-m shell -a "docker ps -a" \
|
||||||
|
--vault-password-file .vault_pass
|
||||||
|
|
||||||
|
# Check .env configuration
|
||||||
|
ansible web_servers \
|
||||||
|
-i inventories/production/hosts.yml \
|
||||||
|
-m shell -a "cat /home/deploy/michaelschiemer/shared/.env.production" \
|
||||||
|
--vault-password-file .vault_pass
|
||||||
|
|
||||||
|
# Check application logs
|
||||||
|
ansible web_servers \
|
||||||
|
-i inventories/production/hosts.yml \
|
||||||
|
-m shell -a "docker logs web --tail 50" \
|
||||||
|
--vault-password-file .vault_pass
|
||||||
|
```
|
||||||
|
|
||||||
|
### COMMANDS TO CREATE (User Requirements)
|
||||||
|
|
||||||
|
#### 1. Simple Manual Deploy Script
|
||||||
|
```bash
|
||||||
|
#!/bin/bash
|
||||||
|
# File: /home/michael/dev/michaelschiemer/deployment/infrastructure/scripts/deploy.sh
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
cd "$(dirname "$0")/.."
|
||||||
|
|
||||||
|
echo "🚀 Deploying to production..."
|
||||||
|
|
||||||
|
ansible-playbook \
|
||||||
|
-i inventories/production/hosts.yml \
|
||||||
|
playbooks/deploy-rsync-based.yml \
|
||||||
|
--vault-password-file .vault_pass
|
||||||
|
|
||||||
|
echo "✅ Deployment complete!"
|
||||||
|
echo "🔍 Check status: docker ps"
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2. .env Update Script
|
||||||
|
```bash
|
||||||
|
#!/bin/bash
|
||||||
|
# File: /home/michael/dev/michaelschiemer/deployment/infrastructure/scripts/update-env.sh
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
cd "$(dirname "$0")/../.."
|
||||||
|
|
||||||
|
SOURCE_ENV="deployment/applications/environments/.env.production"
|
||||||
|
REMOTE_PATH="/home/deploy/michaelschiemer/shared/.env.production"
|
||||||
|
|
||||||
|
if [[ ! -f "$SOURCE_ENV" ]]; then
|
||||||
|
echo "❌ Source .env.production not found at: $SOURCE_ENV"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "📤 Uploading .env.production to production server..."
|
||||||
|
|
||||||
|
ansible web_servers \
|
||||||
|
-i deployment/infrastructure/inventories/production/hosts.yml \
|
||||||
|
-m copy \
|
||||||
|
-a "src=$SOURCE_ENV dest=$REMOTE_PATH mode=0644" \
|
||||||
|
--vault-password-file deployment/infrastructure/.vault_pass
|
||||||
|
|
||||||
|
echo "🔄 Restarting containers..."
|
||||||
|
|
||||||
|
ansible web_servers \
|
||||||
|
-i deployment/infrastructure/inventories/production/hosts.yml \
|
||||||
|
-m shell \
|
||||||
|
-a "cd /home/deploy/michaelschiemer/current && docker-compose restart php web queue-worker" \
|
||||||
|
--vault-password-file deployment/infrastructure/.vault_pass
|
||||||
|
|
||||||
|
echo "✅ .env.production updated and containers restarted!"
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 3. Quick Production Sync
|
||||||
|
```bash
|
||||||
|
#!/bin/bash
|
||||||
|
# File: /home/michael/dev/michaelschiemer/deployment/infrastructure/scripts/quick-sync.sh
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
cd "$(dirname "$0")/../.."
|
||||||
|
|
||||||
|
# Sync code changes (no .env update)
|
||||||
|
rsync -avz \
|
||||||
|
--exclude '.env' \
|
||||||
|
--exclude '.env.local' \
|
||||||
|
--exclude 'node_modules/' \
|
||||||
|
--exclude '.git/' \
|
||||||
|
./ deploy@94.16.110.151:/home/deploy/michaelschiemer/current/
|
||||||
|
|
||||||
|
# Restart containers
|
||||||
|
ansible web_servers \
|
||||||
|
-i deployment/infrastructure/inventories/production/hosts.yml \
|
||||||
|
-m shell \
|
||||||
|
-a "cd /home/deploy/michaelschiemer/current && docker-compose restart php web" \
|
||||||
|
--vault-password-file deployment/infrastructure/.vault_pass
|
||||||
|
|
||||||
|
echo "✅ Quick sync complete!"
|
||||||
|
```
|
||||||
|
|
||||||
|
### SCRIPTS TO REMOVE (Unused/Deprecated)
|
||||||
|
|
||||||
|
1. **`/home/michael/dev/michaelschiemer/deploy.sh`** (if exists in root)
|
||||||
|
- Reason: Conflicting with playbook-based deployment
|
||||||
|
|
||||||
|
2. **`/home/michael/dev/michaelschiemer/.env.local`** (if exists)
|
||||||
|
- Reason: Not used in production, causes confusion
|
||||||
|
|
||||||
|
3. **Duplicate .env files** in root:
|
||||||
|
- Keep: `.env.production` (source of truth for framework defaults)
|
||||||
|
- Remove: `.env.backup.*`, `.env.old`, etc.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Fix Strategy (Step-by-Step)
|
||||||
|
|
||||||
|
### IMMEDIATE FIX (Restore Production)
|
||||||
|
|
||||||
|
#### Step 1: Update Production .env.production File
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /home/michael/dev/michaelschiemer
|
||||||
|
|
||||||
|
# Copy correct .env.production to production server
|
||||||
|
ansible web_servers \
|
||||||
|
-i deployment/infrastructure/inventories/production/hosts.yml \
|
||||||
|
-m copy \
|
||||||
|
-a "src=deployment/applications/environments/.env.production dest=/home/deploy/michaelschiemer/shared/.env.production mode=0644" \
|
||||||
|
--vault-password-file deployment/infrastructure/.vault_pass
|
||||||
|
```
|
||||||
|
|
||||||
|
**Why this file?**
|
||||||
|
- Most complete configuration (4.3K vs 2.9K)
|
||||||
|
- Includes Redis, Queue, Mail, Monitoring configs
|
||||||
|
- Correct DB_PORT=5432
|
||||||
|
- Correct DB credentials
|
||||||
|
|
||||||
|
#### Step 2: Verify .env.production on Server
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ansible web_servers \
|
||||||
|
-i deployment/infrastructure/inventories/production/hosts.yml \
|
||||||
|
-m shell \
|
||||||
|
-a "grep -E '(DB_PORT|DB_USERNAME|DB_PASSWORD)' /home/deploy/michaelschiemer/shared/.env.production" \
|
||||||
|
--vault-password-file deployment/infrastructure/.vault_pass
|
||||||
|
```
|
||||||
|
|
||||||
|
**Expected Output**:
|
||||||
|
```
|
||||||
|
DB_PORT=5432
|
||||||
|
DB_USERNAME=mdb_user
|
||||||
|
DB_PASSWORD=Qo2KNgGqeYksEhKr57pgugakxlothn8J
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Step 3: Restart Containers
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ansible web_servers \
|
||||||
|
-i deployment/infrastructure/inventories/production/hosts.yml \
|
||||||
|
-m shell \
|
||||||
|
-a "cd /home/deploy/michaelschiemer/current && docker-compose restart php web queue-worker" \
|
||||||
|
--vault-password-file deployment/infrastructure/.vault_pass
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Step 4: Verify Website Functionality
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check HTTP status
|
||||||
|
curl -I https://michaelschiemer.de
|
||||||
|
|
||||||
|
# Expected: HTTP/2 200 OK (instead of 500)
|
||||||
|
|
||||||
|
# Check container health
|
||||||
|
ansible web_servers \
|
||||||
|
-i deployment/infrastructure/inventories/production/hosts.yml \
|
||||||
|
-m shell \
|
||||||
|
-a "docker ps | grep -E '(web|php|queue-worker)'" \
|
||||||
|
--vault-password-file deployment/infrastructure/.vault_pass
|
||||||
|
```
|
||||||
|
|
||||||
|
**Expected**: All containers should be "Up" and "healthy"
|
||||||
|
|
||||||
|
### LONG-TERM FIX (Prevent Future Issues)
|
||||||
|
|
||||||
|
#### 1. Update Playbook to Sync .env.production
|
||||||
|
|
||||||
|
Add task to `deploy-rsync-based.yml`:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# After "Synchronize project files" task, add:
|
||||||
|
|
||||||
|
- name: Sync .env.production to shared directory
|
||||||
|
copy:
|
||||||
|
src: "{{ local_project_path }}/deployment/applications/environments/.env.production"
|
||||||
|
dest: "{{ project_path }}/shared/.env.production"
|
||||||
|
mode: '0644'
|
||||||
|
when: sync_env_to_shared | default(true)
|
||||||
|
tags:
|
||||||
|
- deploy
|
||||||
|
- config
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2. Create Helper Scripts
|
||||||
|
|
||||||
|
Create the 3 scripts documented in section 3:
|
||||||
|
- `scripts/deploy.sh` - Simple wrapper for playbook
|
||||||
|
- `scripts/update-env.sh` - Update .env.production only
|
||||||
|
- `scripts/quick-sync.sh` - Quick code sync without full deployment
|
||||||
|
|
||||||
|
#### 3. Establish Source of Truth
|
||||||
|
|
||||||
|
**Decision**: Use `deployment/applications/environments/.env.production` as source of truth
|
||||||
|
- Most complete configuration
|
||||||
|
- Application-specific settings
|
||||||
|
- Includes all production services
|
||||||
|
|
||||||
|
**Action**: Document in README.md:
|
||||||
|
```markdown
|
||||||
|
## Production Configuration
|
||||||
|
|
||||||
|
**Source of Truth**: `deployment/applications/environments/.env.production`
|
||||||
|
|
||||||
|
To update production .env:
|
||||||
|
1. Edit `deployment/applications/environments/.env.production`
|
||||||
|
2. Run `./deployment/infrastructure/scripts/update-env.sh`
|
||||||
|
3. Containers will auto-restart with new config
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 4. Add .env Validation
|
||||||
|
|
||||||
|
Create pre-deployment validation script:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
#!/bin/bash
|
||||||
|
# scripts/validate-env.sh
|
||||||
|
|
||||||
|
ENV_FILE="deployment/applications/environments/.env.production"
|
||||||
|
|
||||||
|
echo "🔍 Validating .env.production..."
|
||||||
|
|
||||||
|
# Check required variables
|
||||||
|
REQUIRED_VARS=(
|
||||||
|
"DB_DRIVER"
|
||||||
|
"DB_HOST"
|
||||||
|
"DB_PORT"
|
||||||
|
"DB_DATABASE"
|
||||||
|
"DB_USERNAME"
|
||||||
|
"DB_PASSWORD"
|
||||||
|
)
|
||||||
|
|
||||||
|
for var in "${REQUIRED_VARS[@]}"; do
|
||||||
|
if ! grep -q "^${var}=" "$ENV_FILE"; then
|
||||||
|
echo "❌ Missing required variable: $var"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# Check PostgreSQL port
|
||||||
|
if ! grep -q "^DB_PORT=5432" "$ENV_FILE"; then
|
||||||
|
echo "⚠️ Warning: DB_PORT should be 5432 for PostgreSQL"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "✅ .env.production validation passed"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Cleanup Recommendations
|
||||||
|
|
||||||
|
### Files to Remove
|
||||||
|
|
||||||
|
#### In Framework Root (`/home/michael/dev/michaelschiemer/`)
|
||||||
|
```bash
|
||||||
|
# List files to remove
|
||||||
|
find . -maxdepth 1 -name ".env.backup*" -o -name ".env.old*" -o -name ".env.local"
|
||||||
|
|
||||||
|
# Remove after confirmation
|
||||||
|
rm -f .env.backup* .env.old* .env.local
|
||||||
|
```
|
||||||
|
|
||||||
|
#### In Deployment Directory
|
||||||
|
```bash
|
||||||
|
# Check for duplicate/old deployment scripts
|
||||||
|
find deployment/ -name "deploy-old.yml" -o -name "*.backup"
|
||||||
|
```
|
||||||
|
|
||||||
|
#### On Production Server
|
||||||
|
```bash
|
||||||
|
# Clean up old releases (keep last 5)
|
||||||
|
ansible web_servers \
|
||||||
|
-i deployment/infrastructure/inventories/production/hosts.yml \
|
||||||
|
-m shell \
|
||||||
|
-a "cd /home/deploy/michaelschiemer/releases && ls -t | tail -n +6 | xargs rm -rf" \
|
||||||
|
--vault-password-file deployment/infrastructure/.vault_pass
|
||||||
|
|
||||||
|
# Remove duplicate .env files in current release
|
||||||
|
ansible web_servers \
|
||||||
|
-i deployment/infrastructure/inventories/production/hosts.yml \
|
||||||
|
-m shell \
|
||||||
|
-a "cd /home/deploy/michaelschiemer/current && rm -f .env.backup* .env.old*" \
|
||||||
|
--vault-password-file deployment/infrastructure/.vault_pass
|
||||||
|
```
|
||||||
|
|
||||||
|
### Configuration to Keep
|
||||||
|
|
||||||
|
**Essential Files**:
|
||||||
|
- `/.env.production` - Framework defaults (keep for reference)
|
||||||
|
- `/deployment/applications/environments/.env.production` - Source of truth
|
||||||
|
- `/deployment/infrastructure/playbooks/deploy-rsync-based.yml` - Main playbook
|
||||||
|
- `/deployment/infrastructure/inventories/production/hosts.yml` - Inventory
|
||||||
|
|
||||||
|
**Symlinks (Do Not Remove)**:
|
||||||
|
- `/home/deploy/michaelschiemer/current/.env.production` → `shared/.env.production`
|
||||||
|
- `/home/deploy/michaelschiemer/current/.env` → `shared/.env.production`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Post-Fix Verification Checklist
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Website accessible
|
||||||
|
curl -I https://michaelschiemer.de
|
||||||
|
# Expected: HTTP/2 200 OK
|
||||||
|
|
||||||
|
# 2. All containers healthy
|
||||||
|
ansible web_servers \
|
||||||
|
-i deployment/infrastructure/inventories/production/hosts.yml \
|
||||||
|
-m shell -a "docker ps" \
|
||||||
|
--vault-password-file deployment/infrastructure/.vault_pass
|
||||||
|
# Expected: All "Up" and "(healthy)"
|
||||||
|
|
||||||
|
# 3. Database connection working
|
||||||
|
ansible web_servers \
|
||||||
|
-i deployment/infrastructure/inventories/production/hosts.yml \
|
||||||
|
-m shell -a "docker exec php php -r \"new PDO('pgsql:host=db;port=5432;dbname=michaelschiemer', 'mdb_user', 'Qo2KNgGqeYksEhKr57pgugakxlothn8J');\"" \
|
||||||
|
--vault-password-file deployment/infrastructure/.vault_pass
|
||||||
|
# Expected: No errors
|
||||||
|
|
||||||
|
# 4. Application logs clean
|
||||||
|
ansible web_servers \
|
||||||
|
-i deployment/infrastructure/inventories/production/hosts.yml \
|
||||||
|
-m shell -a "docker logs web --tail 20" \
|
||||||
|
--vault-password-file deployment/infrastructure/.vault_pass
|
||||||
|
# Expected: HTTP 200 responses, no 500 errors
|
||||||
|
|
||||||
|
# 5. Queue worker stable
|
||||||
|
ansible web_servers \
|
||||||
|
-i deployment/infrastructure/inventories/production/hosts.yml \
|
||||||
|
-m shell -a "docker ps | grep queue-worker" \
|
||||||
|
--vault-password-file deployment/infrastructure/.vault_pass
|
||||||
|
# Expected: "Up" status (not "Restarting")
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Future Deployment Best Practices
|
||||||
|
|
||||||
|
1. **Always validate .env before deployment**
|
||||||
|
- Run `scripts/validate-env.sh` pre-deployment
|
||||||
|
- Check DB_PORT=5432 for PostgreSQL
|
||||||
|
- Verify credentials match database server
|
||||||
|
|
||||||
|
2. **Use playbook for all deployments**
|
||||||
|
- Consistent process
|
||||||
|
- Automated rollback capability
|
||||||
|
- Proper symlink management
|
||||||
|
|
||||||
|
3. **Monitor container health post-deployment**
|
||||||
|
- Check `docker ps` output
|
||||||
|
- Verify all containers "(healthy)"
|
||||||
|
- Check application logs for errors
|
||||||
|
|
||||||
|
4. **Keep .env.production in sync**
|
||||||
|
- Single source of truth: `deployment/applications/environments/.env.production`
|
||||||
|
- Use `update-env.sh` script for updates
|
||||||
|
- Never manually edit on production server
|
||||||
|
|
||||||
|
5. **Regular backups**
|
||||||
|
- Backup `shared/.env.production` before changes
|
||||||
|
- Keep last 5 releases for quick rollback
|
||||||
|
- Document any manual production changes
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
**Current Status**: Production broken due to DB_PORT configuration error
|
||||||
|
**Root Cause**: Manual edits to `shared/.env.production` with wrong PostgreSQL port
|
||||||
|
**Fix Time**: ~5 minutes (copy correct .env + restart containers)
|
||||||
|
**Prevention**: Automated .env sync in playbook + validation scripts
|
||||||
|
|
||||||
|
**Next Steps**:
|
||||||
|
1. Execute Step 1-4 of Fix Strategy (IMMEDIATE)
|
||||||
|
2. Verify website returns HTTP 200
|
||||||
|
3. Implement long-term fixes (playbook updates, scripts)
|
||||||
|
4. Document deployment process in README.md
|
||||||
286
deployment/infrastructure/DEPLOYMENT_FIX_SUMMARY.md
Normal file
286
deployment/infrastructure/DEPLOYMENT_FIX_SUMMARY.md
Normal file
@@ -0,0 +1,286 @@
|
|||||||
|
# Production Deployment Fix Summary
|
||||||
|
|
||||||
|
**Date**: 2025-10-27
|
||||||
|
**Status**: PARTIALLY FIXED - DB configuration corrected, but additional issues remain
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## What Was Fixed
|
||||||
|
|
||||||
|
### 1. Database Configuration Corrected ✅
|
||||||
|
|
||||||
|
**Problem**: Wrong DB_PORT in production `.env.production`
|
||||||
|
- Line 15: `DB_PORT=3306` (MySQL port)
|
||||||
|
- Line 67: `DB_PORT=` (duplicate empty entry)
|
||||||
|
- Wrong username: `mdb-user` (should be `mdb_user`)
|
||||||
|
- Wrong password
|
||||||
|
|
||||||
|
**Solution Applied**:
|
||||||
|
```bash
|
||||||
|
# Copied correct .env.production from source of truth
|
||||||
|
ansible web_servers -m copy \
|
||||||
|
-a "src=deployment/applications/environments/.env.production \
|
||||||
|
dest=/home/deploy/michaelschiemer/shared/.env.production" \
|
||||||
|
--vault-password-file deployment/infrastructure/.vault_pass
|
||||||
|
```
|
||||||
|
|
||||||
|
**Verification**:
|
||||||
|
```bash
|
||||||
|
DB_PORT=5432 # ✅ Correct
|
||||||
|
DB_USERNAME=mdb_user # ✅ Correct
|
||||||
|
DB_PASSWORD=Qo2KNgGqeYksEhKr57pgugakxlothn8J # ✅ Correct
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Containers Restarted ✅
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose restart php web queue-worker
|
||||||
|
```
|
||||||
|
|
||||||
|
**Current Status**:
|
||||||
|
- **php**: Up 6 minutes (healthy) ✅
|
||||||
|
- **db**: Up 53 minutes (healthy) ✅
|
||||||
|
- **redis**: Up 53 minutes (healthy) ✅
|
||||||
|
- **web**: Up 6 minutes (UNHEALTHY) ⚠️
|
||||||
|
- **queue-worker**: Restarting (1) ❌
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Remaining Issues
|
||||||
|
|
||||||
|
### Issue 1: Web Container Unhealthy ⚠️
|
||||||
|
|
||||||
|
**Symptom**: Website still returns HTTP 500
|
||||||
|
|
||||||
|
**Possible Causes**:
|
||||||
|
1. **PHP-FPM not responding** - Web container can't connect to PHP
|
||||||
|
2. **Application error** - PHP code failing during bootstrap
|
||||||
|
3. **Missing files** - Application files not properly deployed
|
||||||
|
4. **Permissions** - Web server can't access application files
|
||||||
|
|
||||||
|
**Next Steps to Diagnose**:
|
||||||
|
```bash
|
||||||
|
# Check if PHP-FPM is accessible from web container
|
||||||
|
docker exec web curl http://php:9000
|
||||||
|
|
||||||
|
# Check Nginx configuration
|
||||||
|
docker exec web nginx -t
|
||||||
|
|
||||||
|
# Check web container health check
|
||||||
|
docker inspect web --format='{{json .State.Health}}' | jq
|
||||||
|
|
||||||
|
# Check if application files exist
|
||||||
|
docker exec web ls -la /var/www/html/public/index.php
|
||||||
|
```
|
||||||
|
|
||||||
|
### Issue 2: Queue Worker Crashing ❌
|
||||||
|
|
||||||
|
**Symptom**: Continuous restart loop
|
||||||
|
|
||||||
|
**Possible Causes**:
|
||||||
|
1. **Same DB connection issue** (should be fixed now)
|
||||||
|
2. **Missing queue configuration**
|
||||||
|
3. **Redis connection issue**
|
||||||
|
4. **Application code error in queue worker**
|
||||||
|
|
||||||
|
**Next Steps to Diagnose**:
|
||||||
|
```bash
|
||||||
|
# Check queue-worker logs
|
||||||
|
docker logs queue-worker --tail 100
|
||||||
|
|
||||||
|
# Try running queue worker manually
|
||||||
|
docker exec php php artisan queue:work --tries=1 --once
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Scripts Created ✅
|
||||||
|
|
||||||
|
### 1. Simple Deployment Script
|
||||||
|
**Location**: `/home/michael/dev/michaelschiemer/deployment/infrastructure/scripts/deploy.sh`
|
||||||
|
```bash
|
||||||
|
./deployment/infrastructure/scripts/deploy.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. .env Update Script
|
||||||
|
**Location**: `/home/michael/dev/michaelschiemer/deployment/infrastructure/scripts/update-env.sh`
|
||||||
|
```bash
|
||||||
|
./deployment/infrastructure/scripts/update-env.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Quick Sync Script
|
||||||
|
**Location**: `/home/michael/dev/michaelschiemer/deployment/infrastructure/scripts/quick-sync.sh`
|
||||||
|
```bash
|
||||||
|
./deployment/infrastructure/scripts/quick-sync.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
**Note**: All scripts updated to use `docker compose` (v2) instead of `docker-compose` (v1)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Documentation Created ✅
|
||||||
|
|
||||||
|
### Comprehensive Deployment Analysis
|
||||||
|
**Location**: `/home/michael/dev/michaelschiemer/deployment/infrastructure/DEPLOYMENT_ANALYSIS.md`
|
||||||
|
|
||||||
|
**Contents**:
|
||||||
|
1. Complete deployment flow analysis
|
||||||
|
2. .env file sources and conflicts
|
||||||
|
3. Deployment command documentation
|
||||||
|
4. Step-by-step fix strategy
|
||||||
|
5. Cleanup recommendations
|
||||||
|
6. Post-fix verification checklist
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Recommended Next Actions
|
||||||
|
|
||||||
|
### Immediate (To Fix HTTP 500)
|
||||||
|
|
||||||
|
1. **Check Application Bootstrap**:
|
||||||
|
```bash
|
||||||
|
# Test if PHP application can start
|
||||||
|
ansible web_servers -m shell \
|
||||||
|
-a "docker exec php php /var/www/html/public/index.php" \
|
||||||
|
--vault-password-file deployment/infrastructure/.vault_pass
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Check Nginx-PHP Connection**:
|
||||||
|
```bash
|
||||||
|
# Test PHP-FPM socket
|
||||||
|
ansible web_servers -m shell \
|
||||||
|
-a "docker exec web curl -v http://php:9000" \
|
||||||
|
--vault-password-file deployment/infrastructure/.vault_pass
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Check Application Logs**:
|
||||||
|
```bash
|
||||||
|
# Look for PHP errors
|
||||||
|
ansible web_servers -m shell \
|
||||||
|
-a "docker exec php ls -la /var/www/html/storage/logs/" \
|
||||||
|
--vault-password-file deployment/infrastructure/.vault_pass
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Verify File Permissions**:
|
||||||
|
```bash
|
||||||
|
# Check if web server can read files
|
||||||
|
ansible web_servers -m shell \
|
||||||
|
-a "docker exec web ls -la /var/www/html/public/" \
|
||||||
|
--vault-password-file deployment/infrastructure/.vault_pass
|
||||||
|
```
|
||||||
|
|
||||||
|
### Short-Term (Within 24h)
|
||||||
|
|
||||||
|
1. **Fix Web Container Health** - Resolve HTTP 500 errors
|
||||||
|
2. **Fix Queue Worker** - Stop crash loop
|
||||||
|
3. **Full Deployment Test** - Run complete deployment playbook
|
||||||
|
4. **Verify All Services** - Ensure all containers healthy
|
||||||
|
|
||||||
|
### Long-Term (This Week)
|
||||||
|
|
||||||
|
1. **Update Playbook** - Add .env.production sync task
|
||||||
|
2. **Add Validation** - Pre-deployment .env validation script
|
||||||
|
3. **Document Process** - Update README with deployment guide
|
||||||
|
4. **Setup Monitoring** - Add health check alerts
|
||||||
|
5. **Cleanup Old Files** - Remove duplicate .env files
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Key Learnings
|
||||||
|
|
||||||
|
### 1. Deployment Flow Issues
|
||||||
|
|
||||||
|
**Problem**: Playbook doesn't sync `.env.production` to `shared/`
|
||||||
|
**Impact**: Manual updates required for configuration changes
|
||||||
|
**Solution**: Add sync task to playbook
|
||||||
|
|
||||||
|
### 2. Multiple .env Sources
|
||||||
|
|
||||||
|
**Problem**: 3 different `.env.production` files with conflicting content
|
||||||
|
**Resolution**: Use `deployment/applications/environments/.env.production` as source of truth
|
||||||
|
|
||||||
|
### 3. Docker Compose Version
|
||||||
|
|
||||||
|
**Problem**: Production uses Docker Compose v2 (`docker compose`)
|
||||||
|
**Impact**: Scripts using v1 syntax (`docker-compose`) fail
|
||||||
|
**Solution**: All scripts updated to v2 syntax
|
||||||
|
|
||||||
|
### 4. Symlink Chain Complexity
|
||||||
|
|
||||||
|
**Structure**:
|
||||||
|
```
|
||||||
|
current/.env → shared/.env.production
|
||||||
|
current/.env.production → shared/.env.production
|
||||||
|
```
|
||||||
|
|
||||||
|
**Risk**: If `shared/.env.production` is wrong, ALL releases break
|
||||||
|
**Mitigation**: Validate before deploy, backup before changes
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Quick Reference
|
||||||
|
|
||||||
|
### Check Production Status
|
||||||
|
```bash
|
||||||
|
cd /home/michael/dev/michaelschiemer/deployment/infrastructure
|
||||||
|
|
||||||
|
# Container status
|
||||||
|
ansible web_servers -i inventories/production/hosts.yml \
|
||||||
|
-m shell -a "docker ps" --vault-password-file .vault_pass
|
||||||
|
|
||||||
|
# .env configuration
|
||||||
|
ansible web_servers -i inventories/production/hosts.yml \
|
||||||
|
-m shell -a "cat /home/deploy/michaelschiemer/shared/.env.production" \
|
||||||
|
--vault-password-file .vault_pass
|
||||||
|
|
||||||
|
# Application logs
|
||||||
|
ansible web_servers -i inventories/production/hosts.yml \
|
||||||
|
-m shell -a "docker logs web --tail 50" --vault-password-file .vault_pass
|
||||||
|
```
|
||||||
|
|
||||||
|
### Deploy to Production
|
||||||
|
```bash
|
||||||
|
# Full deployment
|
||||||
|
./deployment/infrastructure/scripts/deploy.sh
|
||||||
|
|
||||||
|
# Update .env only
|
||||||
|
./deployment/infrastructure/scripts/update-env.sh
|
||||||
|
|
||||||
|
# Quick code sync
|
||||||
|
./deployment/infrastructure/scripts/quick-sync.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
### Emergency Rollback
|
||||||
|
```bash
|
||||||
|
# List releases
|
||||||
|
ansible web_servers -i inventories/production/hosts.yml \
|
||||||
|
-m shell -a "ls -la /home/deploy/michaelschiemer/releases/" \
|
||||||
|
--vault-password-file .vault_pass
|
||||||
|
|
||||||
|
# Switch to previous release
|
||||||
|
ansible web_servers -i inventories/production/hosts.yml \
|
||||||
|
-m shell -a "ln -sfn /home/deploy/michaelschiemer/releases/PREVIOUS_TIMESTAMP \
|
||||||
|
/home/deploy/michaelschiemer/current" \
|
||||||
|
--vault-password-file .vault_pass
|
||||||
|
|
||||||
|
# Restart containers
|
||||||
|
ansible web_servers -i inventories/production/hosts.yml \
|
||||||
|
-m shell -a "cd /home/deploy/michaelschiemer/current && docker compose restart" \
|
||||||
|
--vault-password-file .vault_pass
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Support Contacts
|
||||||
|
|
||||||
|
**Documentation**:
|
||||||
|
- Deployment Analysis: `deployment/infrastructure/DEPLOYMENT_ANALYSIS.md`
|
||||||
|
- This Summary: `deployment/infrastructure/DEPLOYMENT_FIX_SUMMARY.md`
|
||||||
|
|
||||||
|
**Scripts**:
|
||||||
|
- All scripts in: `deployment/infrastructure/scripts/`
|
||||||
|
- Make executable: `chmod +x deployment/infrastructure/scripts/*.sh`
|
||||||
|
|
||||||
|
**Configuration**:
|
||||||
|
- Source of Truth: `deployment/applications/environments/.env.production`
|
||||||
|
- Production File: `/home/deploy/michaelschiemer/shared/.env.production`
|
||||||
17
deployment/infrastructure/deploy.sh
Executable file
17
deployment/infrastructure/deploy.sh
Executable file
@@ -0,0 +1,17 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Quick deployment script with force flag
|
||||||
|
# Usage: ./deploy.sh
|
||||||
|
|
||||||
|
cd "$(dirname "$0")"
|
||||||
|
|
||||||
|
echo "🚀 Starting deployment to production..."
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
ansible-playbook \
|
||||||
|
-i inventories/production/hosts.yml \
|
||||||
|
playbooks/deploy-rsync-based.yml \
|
||||||
|
--vault-password-file .vault_pass \
|
||||||
|
--extra-vars 'force_deploy=true'
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "✅ Deployment completed!"
|
||||||
14
deployment/infrastructure/logs.sh
Executable file
14
deployment/infrastructure/logs.sh
Executable file
@@ -0,0 +1,14 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Quick script to show PHP logs from production server
|
||||||
|
# Usage: ./logs.sh [lines]
|
||||||
|
# Default: 50 lines
|
||||||
|
|
||||||
|
LINES="${1:-50}"
|
||||||
|
|
||||||
|
echo "📋 Showing last $LINES lines of PHP logs from production..."
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
ssh -i ~/.ssh/production deploy@michaelschiemer.de "docker logs php --tail $LINES"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "✅ Done!"
|
||||||
14
deployment/infrastructure/nginx-logs.sh
Executable file
14
deployment/infrastructure/nginx-logs.sh
Executable file
@@ -0,0 +1,14 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Show Nginx error logs from production server
|
||||||
|
# Usage: ./nginx-logs.sh [lines]
|
||||||
|
# Default: 50 lines
|
||||||
|
|
||||||
|
LINES="${1:-50}"
|
||||||
|
|
||||||
|
echo "📋 Showing last $LINES lines of Nginx error logs from production..."
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
ssh -i ~/.ssh/production deploy@michaelschiemer.de "docker exec web tail -n $LINES /var/log/nginx/error.log"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "✅ Done!"
|
||||||
21
deployment/infrastructure/restart.sh
Executable file
21
deployment/infrastructure/restart.sh
Executable file
@@ -0,0 +1,21 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Restart specific Docker container on production server
|
||||||
|
# Usage: ./restart.sh [container_name]
|
||||||
|
# Example: ./restart.sh php
|
||||||
|
# Without argument: restarts all containers
|
||||||
|
|
||||||
|
CONTAINER="${1:-all}"
|
||||||
|
|
||||||
|
echo "🔄 Restarting container(s) on production server..."
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
if [ "$CONTAINER" = "all" ]; then
|
||||||
|
echo "Restarting ALL containers..."
|
||||||
|
ssh -i ~/.ssh/production deploy@michaelschiemer.de "cd /home/deploy/michaelschiemer/current && docker compose -f docker-compose.yml -f docker-compose.production.yml restart"
|
||||||
|
else
|
||||||
|
echo "Restarting container: $CONTAINER"
|
||||||
|
ssh -i ~/.ssh/production deploy@michaelschiemer.de "docker restart $CONTAINER"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "✅ Done!"
|
||||||
32
deployment/infrastructure/scripts/deploy.sh
Executable file
32
deployment/infrastructure/scripts/deploy.sh
Executable file
@@ -0,0 +1,32 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Simple Production Deployment Script
|
||||||
|
# Usage: ./deploy.sh
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
INFRA_DIR="$(dirname "$SCRIPT_DIR")"
|
||||||
|
|
||||||
|
cd "$INFRA_DIR"
|
||||||
|
|
||||||
|
echo "🚀 Deploying to production..."
|
||||||
|
echo "📍 Infrastructure directory: $INFRA_DIR"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Check if vault password file exists
|
||||||
|
if [[ ! -f ".vault_pass" ]]; then
|
||||||
|
echo "❌ Vault password file not found: .vault_pass"
|
||||||
|
echo " Create this file with your Ansible Vault password"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Run deployment playbook
|
||||||
|
ansible-playbook \
|
||||||
|
-i inventories/production/hosts.yml \
|
||||||
|
playbooks/deploy-rsync-based.yml \
|
||||||
|
--vault-password-file .vault_pass
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "✅ Deployment complete!"
|
||||||
|
echo "🔍 Check status:"
|
||||||
|
echo " ansible web_servers -i inventories/production/hosts.yml -m shell -a 'docker ps' --vault-password-file .vault_pass"
|
||||||
46
deployment/infrastructure/scripts/quick-sync.sh
Executable file
46
deployment/infrastructure/scripts/quick-sync.sh
Executable file
@@ -0,0 +1,46 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Quick Production Code Sync
|
||||||
|
# Usage: ./quick-sync.sh
|
||||||
|
# Note: Does NOT update .env.production (use update-env.sh for that)
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
PROJECT_ROOT="$(dirname "$(dirname "$SCRIPT_DIR")")"
|
||||||
|
INFRA_DIR="$PROJECT_ROOT/deployment/infrastructure"
|
||||||
|
|
||||||
|
cd "$PROJECT_ROOT"
|
||||||
|
|
||||||
|
echo "🔄 Quick sync to production (code only)..."
|
||||||
|
echo "📍 Project root: $PROJECT_ROOT"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Sync code changes (excludes .env files and development artifacts)
|
||||||
|
rsync -avz \
|
||||||
|
--exclude '.env' \
|
||||||
|
--exclude '.env.local' \
|
||||||
|
--exclude '.env.development' \
|
||||||
|
--exclude '.env.production' \
|
||||||
|
--exclude 'node_modules/' \
|
||||||
|
--exclude '.git/' \
|
||||||
|
--exclude 'vendor/' \
|
||||||
|
--exclude 'tests/' \
|
||||||
|
--exclude '.idea/' \
|
||||||
|
--exclude '.vscode/' \
|
||||||
|
--exclude '*.log' \
|
||||||
|
./ deploy@94.16.110.151:/home/deploy/michaelschiemer/current/
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "🔄 Restarting PHP and web containers..."
|
||||||
|
|
||||||
|
ansible web_servers \
|
||||||
|
-i "$INFRA_DIR/inventories/production/hosts.yml" \
|
||||||
|
-m shell \
|
||||||
|
-a "cd /home/deploy/michaelschiemer/current && docker compose restart php web" \
|
||||||
|
--vault-password-file "$INFRA_DIR/.vault_pass"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "✅ Quick sync complete!"
|
||||||
|
echo ""
|
||||||
|
echo "⚠️ Note: This does NOT update .env.production"
|
||||||
|
echo " To update configuration, use: ./update-env.sh"
|
||||||
74
deployment/infrastructure/scripts/update-env.sh
Executable file
74
deployment/infrastructure/scripts/update-env.sh
Executable file
@@ -0,0 +1,74 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Update Production .env.production File
|
||||||
|
# Usage: ./update-env.sh
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
PROJECT_ROOT="$(dirname "$(dirname "$SCRIPT_DIR")")"
|
||||||
|
INFRA_DIR="$PROJECT_ROOT/deployment/infrastructure"
|
||||||
|
|
||||||
|
SOURCE_ENV="$PROJECT_ROOT/deployment/applications/environments/.env.production"
|
||||||
|
REMOTE_PATH="/home/deploy/michaelschiemer/shared/.env.production"
|
||||||
|
|
||||||
|
cd "$PROJECT_ROOT"
|
||||||
|
|
||||||
|
echo "🔍 Validating .env.production..."
|
||||||
|
|
||||||
|
if [[ ! -f "$SOURCE_ENV" ]]; then
|
||||||
|
echo "❌ Source .env.production not found at: $SOURCE_ENV"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Validate required variables
|
||||||
|
REQUIRED_VARS=("DB_DRIVER" "DB_HOST" "DB_PORT" "DB_DATABASE" "DB_USERNAME" "DB_PASSWORD")
|
||||||
|
VALIDATION_FAILED=0
|
||||||
|
|
||||||
|
for var in "${REQUIRED_VARS[@]}"; do
|
||||||
|
if ! grep -q "^${var}=" "$SOURCE_ENV"; then
|
||||||
|
echo "❌ Missing required variable: $var"
|
||||||
|
VALIDATION_FAILED=1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# Check PostgreSQL port
|
||||||
|
if ! grep -q "^DB_PORT=5432" "$SOURCE_ENV"; then
|
||||||
|
echo "⚠️ Warning: DB_PORT should be 5432 for PostgreSQL"
|
||||||
|
read -p "Continue anyway? (y/N): " -n 1 -r
|
||||||
|
echo
|
||||||
|
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ $VALIDATION_FAILED -eq 1 ]]; then
|
||||||
|
echo "❌ Validation failed"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "✅ Validation passed"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
echo "📤 Uploading .env.production to production server..."
|
||||||
|
|
||||||
|
ansible web_servers \
|
||||||
|
-i "$INFRA_DIR/inventories/production/hosts.yml" \
|
||||||
|
-m copy \
|
||||||
|
-a "src=$SOURCE_ENV dest=$REMOTE_PATH mode=0644" \
|
||||||
|
--vault-password-file "$INFRA_DIR/.vault_pass"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "🔄 Restarting containers..."
|
||||||
|
|
||||||
|
ansible web_servers \
|
||||||
|
-i "$INFRA_DIR/inventories/production/hosts.yml" \
|
||||||
|
-m shell \
|
||||||
|
-a "cd /home/deploy/michaelschiemer/current && docker compose restart php web queue-worker" \
|
||||||
|
--vault-password-file "$INFRA_DIR/.vault_pass"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "✅ .env.production updated and containers restarted!"
|
||||||
|
echo ""
|
||||||
|
echo "🔍 Verify:"
|
||||||
|
echo " curl -I https://michaelschiemer.de"
|
||||||
|
echo " (Should return HTTP/2 200 OK)"
|
||||||
11
deployment/infrastructure/status.sh
Executable file
11
deployment/infrastructure/status.sh
Executable file
@@ -0,0 +1,11 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Show Docker container status on production server
|
||||||
|
# Usage: ./status.sh
|
||||||
|
|
||||||
|
echo "🐳 Docker Container Status on Production Server"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
ssh -i ~/.ssh/production deploy@michaelschiemer.de "docker ps -a --format 'table {{.Names}}\t{{.Status}}\t{{.Ports}}'"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "✅ Done!"
|
||||||
33
docker/php/php.dev.ini
Normal file
33
docker/php/php.dev.ini
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
; php.ini für Entwicklung
|
||||||
|
include = php.common.ini
|
||||||
|
|
||||||
|
|
||||||
|
[opcache]
|
||||||
|
; Opcache komplett deaktiviert für lokale Entwicklung
|
||||||
|
; Keine Caching-Probleme bei Code-Änderungen
|
||||||
|
opcache.enable=0
|
||||||
|
opcache.enable_cli=0
|
||||||
|
|
||||||
|
; Alternative: Opcache aktiviert mit sofortiger Revalidierung
|
||||||
|
; Nur aktivieren wenn Performance wichtiger ist als sofortige Änderungen
|
||||||
|
;opcache.enable=1
|
||||||
|
;opcache.enable_cli=1
|
||||||
|
;opcache.memory_consumption=128
|
||||||
|
;opcache.max_accelerated_files=10000
|
||||||
|
;opcache.revalidate_freq=0
|
||||||
|
;opcache.validate_timestamps=1
|
||||||
|
;opcache.interned_strings_buffer=16
|
||||||
|
|
||||||
|
opcache.file_cache=
|
||||||
|
realpath_cache_ttl=0
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
; php.ini für Entwicklung
|
|
||||||
include = php.common.ini
|
|
||||||
|
|
||||||
|
|
||||||
[opcache]
|
|
||||||
opcache.enable=1
|
|
||||||
opcache.enable_cli=1
|
|
||||||
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
|
|
||||||
@@ -10,11 +10,14 @@ opcache.enable_cli=0
|
|||||||
opcache.memory_consumption=128
|
opcache.memory_consumption=128
|
||||||
; Maximale Anzahl an gecachten Dateien
|
; Maximale Anzahl an gecachten Dateien
|
||||||
opcache.max_accelerated_files=10000
|
opcache.max_accelerated_files=10000
|
||||||
; Wie oft wird der Cache validiert (0 = bei jedem Request, empfohlen für Entwicklung)
|
; Wie oft wird der Cache validiert (0 = bei jedem Request)
|
||||||
; In Produktion höher setzen für bessere Performance
|
; TEMPORÄR: 0 für einfachere Deployments während aktiver Entwicklung
|
||||||
opcache.revalidate_freq=60
|
; SPÄTER: Auf 60 erhöhen wenn System stabil ist
|
||||||
; Cache-Zeitstempel prüfen (0 für Produktionsumgebungen)
|
opcache.revalidate_freq=0
|
||||||
opcache.validate_timestamps=0
|
; Cache-Zeitstempel prüfen
|
||||||
|
; TEMPORÄR: 1 aktiviert für Deployment-Flexibilität
|
||||||
|
; SPÄTER: Auf 0 setzen für maximale Performance wenn stabil
|
||||||
|
opcache.validate_timestamps=1
|
||||||
; Performance-Optimierungen
|
; Performance-Optimierungen
|
||||||
opcache.interned_strings_buffer=16
|
opcache.interned_strings_buffer=16
|
||||||
; JIT (Just-In-Time Compilation) - Optional für PHP 8.0+
|
; JIT (Just-In-Time Compilation) - Optional für PHP 8.0+
|
||||||
|
|||||||
@@ -12,9 +12,7 @@ final class RequestContext
|
|||||||
private ?string $protocol,
|
private ?string $protocol,
|
||||||
private ?string $port,
|
private ?string $port,
|
||||||
private ?string $requestUri,
|
private ?string $requestUri,
|
||||||
private ?string $requestMethod,
|
private ?string $requestMethod
|
||||||
private ?string $region,
|
|
||||||
private ?string $geo
|
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -26,9 +24,7 @@ final class RequestContext
|
|||||||
isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off' ? 'https' : 'http',
|
isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off' ? 'https' : 'http',
|
||||||
$_SERVER['SERVER_PORT'] ?? null,
|
$_SERVER['SERVER_PORT'] ?? null,
|
||||||
$_SERVER['REQUEST_URI'] ?? null,
|
$_SERVER['REQUEST_URI'] ?? null,
|
||||||
$_SERVER['REQUEST_METHOD'] ?? null,
|
$_SERVER['REQUEST_METHOD'] ?? null
|
||||||
$_ENV['AWS_REGION'] ?? $_ENV['REGION'] ?? null,
|
|
||||||
$_ENV['GEO_REGION'] ?? null
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -61,14 +57,4 @@ final class RequestContext
|
|||||||
{
|
{
|
||||||
return $this->requestMethod;
|
return $this->requestMethod;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getRegion(): ?string
|
|
||||||
{
|
|
||||||
return $this->region;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getGeo(): ?string
|
|
||||||
{
|
|
||||||
return $this->geo;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace App\Framework\Analytics;
|
namespace App\Framework\Analytics;
|
||||||
|
|
||||||
|
use App\Framework\Config\Environment;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Analytics-Konfiguration
|
* Analytics-Konfiguration
|
||||||
*/
|
*/
|
||||||
@@ -28,20 +30,20 @@ final readonly class AnalyticsConfig
|
|||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function fromEnvironment(): self
|
public static function fromEnvironment(Environment $env): self
|
||||||
{
|
{
|
||||||
return new self(
|
return new self(
|
||||||
enabled: filter_var($_ENV['ANALYTICS_ENABLED'] ?? true, FILTER_VALIDATE_BOOLEAN),
|
enabled: $env->getBool('ANALYTICS_ENABLED', true),
|
||||||
samplingRate: (float)($_ENV['ANALYTICS_SAMPLING_RATE'] ?? 1.0),
|
samplingRate: $env->getFloat('ANALYTICS_SAMPLING_RATE', 1.0),
|
||||||
securityAnalyticsEnabled: filter_var($_ENV['SECURITY_ANALYTICS_ENABLED'] ?? true, FILTER_VALIDATE_BOOLEAN),
|
securityAnalyticsEnabled: $env->getBool('SECURITY_ANALYTICS_ENABLED', true),
|
||||||
dataPath: $_ENV['ANALYTICS_DATA_PATH'] ?? '/var/www/html/storage/analytics',
|
dataPath: $env->getString('ANALYTICS_DATA_PATH', '/var/www/html/storage/analytics'),
|
||||||
bufferSize: (int)($_ENV['ANALYTICS_BUFFER_SIZE'] ?? 1000),
|
bufferSize: $env->getInt('ANALYTICS_BUFFER_SIZE', 1000),
|
||||||
retentionDays: (int)($_ENV['ANALYTICS_RETENTION_DAYS'] ?? 365),
|
retentionDays: $env->getInt('ANALYTICS_RETENTION_DAYS', 365),
|
||||||
trackPageViews: filter_var($_ENV['ANALYTICS_TRACK_PAGE_VIEWS'] ?? true, FILTER_VALIDATE_BOOLEAN),
|
trackPageViews: $env->getBool('ANALYTICS_TRACK_PAGE_VIEWS', true),
|
||||||
trackApiCalls: filter_var($_ENV['ANALYTICS_TRACK_API_CALLS'] ?? true, FILTER_VALIDATE_BOOLEAN),
|
trackApiCalls: $env->getBool('ANALYTICS_TRACK_API_CALLS', true),
|
||||||
trackUserActions: filter_var($_ENV['ANALYTICS_TRACK_USER_ACTIONS'] ?? true, FILTER_VALIDATE_BOOLEAN),
|
trackUserActions: $env->getBool('ANALYTICS_TRACK_USER_ACTIONS', true),
|
||||||
trackErrors: filter_var($_ENV['ANALYTICS_TRACK_ERRORS'] ?? true, FILTER_VALIDATE_BOOLEAN),
|
trackErrors: $env->getBool('ANALYTICS_TRACK_ERRORS', true),
|
||||||
trackPerformance: filter_var($_ENV['ANALYTICS_TRACK_PERFORMANCE'] ?? true, FILTER_VALIDATE_BOOLEAN),
|
trackPerformance: $env->getBool('ANALYTICS_TRACK_PERFORMANCE', true),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,13 +4,14 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace App\Framework\Config;
|
namespace App\Framework\Config;
|
||||||
|
|
||||||
|
use App\Framework\Core\ValueObjects\Version;
|
||||||
use App\Framework\DateTime\Timezone;
|
use App\Framework\DateTime\Timezone;
|
||||||
|
|
||||||
final readonly class AppConfig
|
final readonly class AppConfig
|
||||||
{
|
{
|
||||||
public function __construct(
|
public function __construct(
|
||||||
public string $name = 'Framework App',
|
public string $name = 'Framework App',
|
||||||
public string $version = '1.0.0',
|
public Version $version = new Version(1, 0, 0),
|
||||||
public string $environment = 'production',
|
public string $environment = 'production',
|
||||||
public bool $debug = false,
|
public bool $debug = false,
|
||||||
public Timezone $timezone = Timezone::EuropeBerlin,
|
public Timezone $timezone = Timezone::EuropeBerlin,
|
||||||
@@ -43,4 +44,9 @@ final readonly class AppConfig
|
|||||||
{
|
{
|
||||||
return $this->debug || $this->type->isDebugEnabled();
|
return $this->debug || $this->type->isDebugEnabled();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function isDebug(): bool
|
||||||
|
{
|
||||||
|
return $this->isDebugEnabled();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,7 +41,8 @@ final readonly class EncryptedEnvLoader
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Load environment-specific files
|
// Load environment-specific files
|
||||||
$appEnv = $variables['APP_ENV'] ?? $_ENV['APP_ENV'] ?? 'development';
|
// Priority: 1. Already loaded from .env file, 2. Default to development
|
||||||
|
$appEnv = $variables['APP_ENV'] ?? 'development';
|
||||||
$envSpecificFile = $baseDir->join(".env.{$appEnv}");
|
$envSpecificFile = $baseDir->join(".env.{$appEnv}");
|
||||||
if ($envSpecificFile->exists()) {
|
if ($envSpecificFile->exists()) {
|
||||||
$envSpecificVariables = $this->parseEnvFile($envSpecificFile);
|
$envSpecificVariables = $this->parseEnvFile($envSpecificFile);
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ declare(strict_types=1);
|
|||||||
namespace App\Framework\Config;
|
namespace App\Framework\Config;
|
||||||
|
|
||||||
use App\Framework\Config\External\ExternalApiConfig;
|
use App\Framework\Config\External\ExternalApiConfig;
|
||||||
|
use App\Framework\Core\ValueObjects\Version;
|
||||||
use App\Framework\Database\Config\DatabaseConfig;
|
use App\Framework\Database\Config\DatabaseConfig;
|
||||||
use App\Framework\Database\Config\DatabaseConfigInitializer;
|
use App\Framework\Database\Config\DatabaseConfigInitializer;
|
||||||
use App\Framework\DateTime\Timezone;
|
use App\Framework\DateTime\Timezone;
|
||||||
@@ -51,10 +52,11 @@ final readonly class TypedConfigInitializer
|
|||||||
private function createAppConfig(): AppConfig
|
private function createAppConfig(): AppConfig
|
||||||
{
|
{
|
||||||
$environmentType = EnvironmentType::fromEnvironment($this->env);
|
$environmentType = EnvironmentType::fromEnvironment($this->env);
|
||||||
|
$versionString = $this->env->getString('APP_VERSION', '1.0.0');
|
||||||
|
|
||||||
return new AppConfig(
|
return new AppConfig(
|
||||||
name: $this->env->getString(EnvKey::APP_NAME, 'Framework App'),
|
name: $this->env->getString(EnvKey::APP_NAME, 'Framework App'),
|
||||||
version: $this->env->getString('APP_VERSION', '1.0.0'),
|
version: Version::fromString($versionString),
|
||||||
environment: $this->env->getString(
|
environment: $this->env->getString(
|
||||||
key: EnvKey::APP_ENV,
|
key: EnvKey::APP_ENV,
|
||||||
default: 'production'
|
default: 'production'
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace App\Framework\Console\Analytics\Middleware;
|
namespace App\Framework\Console\Analytics\Middleware;
|
||||||
|
|
||||||
|
use App\Framework\Config\AppConfig;
|
||||||
|
use App\Framework\Config\Environment;
|
||||||
use App\Framework\Console\Analytics\Repository\CommandUsageRepository;
|
use App\Framework\Console\Analytics\Repository\CommandUsageRepository;
|
||||||
use App\Framework\Console\Analytics\ValueObjects\CommandUsageMetric;
|
use App\Framework\Console\Analytics\ValueObjects\CommandUsageMetric;
|
||||||
use App\Framework\Console\ExitCode;
|
use App\Framework\Console\ExitCode;
|
||||||
@@ -17,6 +19,8 @@ final readonly class AnalyticsCollectionMiddleware implements ConsoleMiddleware
|
|||||||
{
|
{
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private CommandUsageRepository $repository,
|
private CommandUsageRepository $repository,
|
||||||
|
private AppConfig $appConfig,
|
||||||
|
private Environment $environment,
|
||||||
private bool $enabled = true
|
private bool $enabled = true
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
@@ -92,7 +96,7 @@ final readonly class AnalyticsCollectionMiddleware implements ConsoleMiddleware
|
|||||||
{
|
{
|
||||||
// In a real implementation, this would extract user ID from context
|
// In a real implementation, this would extract user ID from context
|
||||||
// Could be from environment variables, session data, etc.
|
// Could be from environment variables, session data, etc.
|
||||||
return $_ENV['CONSOLE_USER_ID'] ?? null;
|
return $this->environment->getString('CONSOLE_USER_ID');
|
||||||
}
|
}
|
||||||
|
|
||||||
private function collectMetadata(ConsoleInput $input, ExitCode $exitCode): array
|
private function collectMetadata(ConsoleInput $input, ExitCode $exitCode): array
|
||||||
@@ -103,13 +107,9 @@ final readonly class AnalyticsCollectionMiddleware implements ConsoleMiddleware
|
|||||||
'php_version' => PHP_VERSION,
|
'php_version' => PHP_VERSION,
|
||||||
'memory_peak' => memory_get_peak_usage(true),
|
'memory_peak' => memory_get_peak_usage(true),
|
||||||
'exit_code_name' => $exitCode->name,
|
'exit_code_name' => $exitCode->name,
|
||||||
|
'environment' => $this->appConfig->type->value,
|
||||||
];
|
];
|
||||||
|
|
||||||
// Add environment context
|
|
||||||
if (isset($_ENV['APP_ENV'])) {
|
|
||||||
$metadata['environment'] = $_ENV['APP_ENV'];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add process info
|
// Add process info
|
||||||
if (function_exists('posix_getpid')) {
|
if (function_exists('posix_getpid')) {
|
||||||
$metadata['process_id'] = posix_getpid();
|
$metadata['process_id'] = posix_getpid();
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace App\Framework\Console\Layout\Commands;
|
namespace App\Framework\Console\Layout\Commands;
|
||||||
|
|
||||||
|
use App\Framework\Config\AppConfig;
|
||||||
use App\Framework\Console\Attributes\ConsoleCommand;
|
use App\Framework\Console\Attributes\ConsoleCommand;
|
||||||
use App\Framework\Console\ExitCode;
|
use App\Framework\Console\ExitCode;
|
||||||
use App\Framework\Console\Input\ConsoleInput;
|
use App\Framework\Console\Input\ConsoleInput;
|
||||||
@@ -13,6 +14,10 @@ use App\Framework\Console\Output\ConsoleOutput;
|
|||||||
|
|
||||||
final readonly class LayoutTestCommand
|
final readonly class LayoutTestCommand
|
||||||
{
|
{
|
||||||
|
public function __construct(
|
||||||
|
private AppConfig $appConfig
|
||||||
|
) {
|
||||||
|
}
|
||||||
#[ConsoleCommand(
|
#[ConsoleCommand(
|
||||||
name: 'layout:test',
|
name: 'layout:test',
|
||||||
description: 'Test responsive terminal layout features'
|
description: 'Test responsive terminal layout features'
|
||||||
@@ -48,8 +53,8 @@ final readonly class LayoutTestCommand
|
|||||||
'Framework Version' => '1.0.0',
|
'Framework Version' => '1.0.0',
|
||||||
'PHP Version' => PHP_VERSION,
|
'PHP Version' => PHP_VERSION,
|
||||||
'Memory Usage' => memory_get_usage(true) . ' bytes',
|
'Memory Usage' => memory_get_usage(true) . ' bytes',
|
||||||
'Environment' => $_ENV['APP_ENV'] ?? 'development',
|
'Environment' => $this->appConfig->type->value,
|
||||||
'Debug Mode' => 'Enabled',
|
'Debug Mode' => $this->appConfig->isDebug() ? 'Enabled' : 'Disabled',
|
||||||
];
|
];
|
||||||
|
|
||||||
$responsiveOutput->writeKeyValue($data);
|
$responsiveOutput->writeKeyValue($data);
|
||||||
|
|||||||
@@ -4,8 +4,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace App\Framework\Context;
|
namespace App\Framework\Context;
|
||||||
|
|
||||||
use App\Framework\Config\Environment;
|
use App\Framework\Config\AppConfig;
|
||||||
use App\Framework\Config\EnvKey;
|
|
||||||
use App\Framework\Http\ServerEnvironment;
|
use App\Framework\Http\ServerEnvironment;
|
||||||
|
|
||||||
final readonly class ExecutionContext
|
final readonly class ExecutionContext
|
||||||
@@ -68,10 +67,8 @@ final readonly class ExecutionContext
|
|||||||
], $this->metadata);
|
], $this->metadata);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function detect(?Environment $environment = null): self
|
public static function detect(?AppConfig $appConfig = null): self
|
||||||
{
|
{
|
||||||
#$environment->get(EnvKey::APP_ENV);
|
|
||||||
|
|
||||||
// Debug logging
|
// Debug logging
|
||||||
#error_log("ExecutionContext::detect() - SAPI: " . php_sapi_name());
|
#error_log("ExecutionContext::detect() - SAPI: " . php_sapi_name());
|
||||||
#error_log("ExecutionContext::detect() - REQUEST_METHOD: " . ($_SERVER['REQUEST_METHOD'] ?? 'not set'));
|
#error_log("ExecutionContext::detect() - REQUEST_METHOD: " . ($_SERVER['REQUEST_METHOD'] ?? 'not set'));
|
||||||
@@ -80,7 +77,7 @@ final readonly class ExecutionContext
|
|||||||
|
|
||||||
// Test Environment
|
// Test Environment
|
||||||
if (defined('PHPUNIT_COMPOSER_INSTALL') ||
|
if (defined('PHPUNIT_COMPOSER_INSTALL') ||
|
||||||
isset($_ENV['APP_ENV']) && $_ENV['APP_ENV'] === 'testing') {
|
($appConfig !== null && $appConfig->type->isTesting())) {
|
||||||
return new self(ContextType::TEST, ['detected_by' => 'phpunit_or_env']);
|
return new self(ContextType::TEST, ['detected_by' => 'phpunit_or_env']);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -53,11 +53,12 @@ final readonly class AppBootstrapper
|
|||||||
// Make Environment available throughout the application
|
// Make Environment available throughout the application
|
||||||
$this->container->instance(Environment::class, $env);
|
$this->container->instance(Environment::class, $env);
|
||||||
|
|
||||||
$this->container->instance(TypedConfiguration::class, new TypedConfigInitializer($env)($this->container));
|
$typedConfig = new TypedConfigInitializer($env)($this->container);
|
||||||
|
$this->container->instance(TypedConfiguration::class, $typedConfig);
|
||||||
|
|
||||||
|
|
||||||
// ExecutionContext detection sollte das erste sein, das nach dem Instanziieren des containers passiert. noch bevor dem bootstrap des containers.
|
// ExecutionContext detection sollte das erste sein, das nach dem Instanziieren des containers passiert. noch bevor dem bootstrap des containers.
|
||||||
$executionContext = ExecutionContext::detect($env);
|
$executionContext = ExecutionContext::detect($typedConfig->app);
|
||||||
$this->container->instance(ExecutionContext::class, $executionContext);
|
$this->container->instance(ExecutionContext::class, $executionContext);
|
||||||
|
|
||||||
// Register MemoryMonitor as singleton
|
// Register MemoryMonitor as singleton
|
||||||
|
|||||||
@@ -213,6 +213,7 @@ final readonly class ContainerBootstrapper
|
|||||||
private function autowire(Container $container): void
|
private function autowire(Container $container): void
|
||||||
{
|
{
|
||||||
// Discovery service bootstrapping
|
// Discovery service bootstrapping
|
||||||
|
error_log("🔧 CONTAINER BOOTSTRAP: autowire() starting Discovery");
|
||||||
$clock = $container->get(Clock::class);
|
$clock = $container->get(Clock::class);
|
||||||
$bootstrapper = new DiscoveryServiceBootstrapper($container, $clock);
|
$bootstrapper = new DiscoveryServiceBootstrapper($container, $clock);
|
||||||
$results = $bootstrapper->bootstrap();
|
$results = $bootstrapper->bootstrap();
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace App\Framework\Database\Driver;
|
namespace App\Framework\Database\Driver;
|
||||||
|
|
||||||
|
use App\Framework\Config\AppConfig;
|
||||||
use PDO;
|
use PDO;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -47,8 +48,7 @@ final readonly class PostgresDriver implements Driver
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Application name for pg_stat_activity monitoring
|
// Application name for pg_stat_activity monitoring
|
||||||
$appName = $_ENV['APP_NAME'] ?? 'custom-php-framework';
|
$parts['application_name'] = 'php-app';
|
||||||
$parts['application_name'] = $appName;
|
|
||||||
|
|
||||||
// Connect timeout (5 seconds default)
|
// Connect timeout (5 seconds default)
|
||||||
$parts['connect_timeout'] = '5';
|
$parts['connect_timeout'] = '5';
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace App\Framework\Database\Migration\Commands;
|
namespace App\Framework\Database\Migration\Commands;
|
||||||
|
|
||||||
|
use App\Framework\Config\AppConfig;
|
||||||
use App\Framework\Console\ConsoleCommand;
|
use App\Framework\Console\ConsoleCommand;
|
||||||
use App\Framework\Console\ExitCode;
|
use App\Framework\Console\ExitCode;
|
||||||
use App\Framework\Core\PathProvider;
|
use App\Framework\Core\PathProvider;
|
||||||
@@ -20,7 +21,8 @@ final readonly class MakeMigrationFromDiffCommand
|
|||||||
public function __construct(
|
public function __construct(
|
||||||
private DatabaseManager $databaseManager,
|
private DatabaseManager $databaseManager,
|
||||||
private MigrationGenerator $migrationGenerator,
|
private MigrationGenerator $migrationGenerator,
|
||||||
private PathProvider $pathProvider
|
private PathProvider $pathProvider,
|
||||||
|
private AppConfig $appConfig
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -111,7 +113,7 @@ final readonly class MakeMigrationFromDiffCommand
|
|||||||
return ExitCode::SUCCESS;
|
return ExitCode::SUCCESS;
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
echo "Error creating migration: {$e->getMessage()}\n";
|
echo "Error creating migration: {$e->getMessage()}\n";
|
||||||
if (isset($_ENV['APP_DEBUG']) && $_ENV['APP_DEBUG']) {
|
if ($this->appConfig->isDebug()) {
|
||||||
echo $e->getTraceAsString() . "\n";
|
echo $e->getTraceAsString() . "\n";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace App\Framework\Database\Migration\Services;
|
namespace App\Framework\Database\Migration\Services;
|
||||||
|
|
||||||
|
use App\Framework\Config\AppConfig;
|
||||||
use App\Framework\Core\ValueObjects\Byte;
|
use App\Framework\Core\ValueObjects\Byte;
|
||||||
use App\Framework\Database\ConnectionInterface;
|
use App\Framework\Database\ConnectionInterface;
|
||||||
use App\Framework\Database\Migration\Migration;
|
use App\Framework\Database\Migration\Migration;
|
||||||
@@ -18,7 +19,8 @@ final readonly class MigrationValidator
|
|||||||
{
|
{
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private ConnectionInterface $connection,
|
private ConnectionInterface $connection,
|
||||||
private DatabasePlatform $platform
|
private DatabasePlatform $platform,
|
||||||
|
private AppConfig $appConfig
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -309,7 +311,7 @@ final readonly class MigrationValidator
|
|||||||
private function getEnvironmentDetails(): array
|
private function getEnvironmentDetails(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
'app_env' => $_ENV['APP_ENV'] ?? 'unknown',
|
'app_env' => $this->appConfig->type->value,
|
||||||
'php_version' => PHP_VERSION,
|
'php_version' => PHP_VERSION,
|
||||||
'server_name' => $_SERVER['SERVER_NAME'] ?? 'unknown',
|
'server_name' => $_SERVER['SERVER_NAME'] ?? 'unknown',
|
||||||
'database_driver' => $this->platform->getName(),
|
'database_driver' => $this->platform->getName(),
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace App\Framework\Database\Schema\Commands;
|
namespace App\Framework\Database\Schema\Commands;
|
||||||
|
|
||||||
|
use App\Framework\Config\AppConfig;
|
||||||
use App\Framework\Console\ConsoleCommand;
|
use App\Framework\Console\ConsoleCommand;
|
||||||
use App\Framework\Console\ExitCode;
|
use App\Framework\Console\ExitCode;
|
||||||
use App\Framework\Database\DatabaseManager;
|
use App\Framework\Database\DatabaseManager;
|
||||||
@@ -15,7 +16,8 @@ use App\Framework\Database\Schema\Comparison\SchemaComparator;
|
|||||||
final readonly class SchemaDiffCommand
|
final readonly class SchemaDiffCommand
|
||||||
{
|
{
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private DatabaseManager $databaseManager
|
private DatabaseManager $databaseManager,
|
||||||
|
private AppConfig $appConfig
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -98,7 +100,7 @@ final readonly class SchemaDiffCommand
|
|||||||
return ExitCode::SUCCESS;
|
return ExitCode::SUCCESS;
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
echo "Error comparing schemas: {$e->getMessage()}\n";
|
echo "Error comparing schemas: {$e->getMessage()}\n";
|
||||||
if (isset($_ENV['APP_DEBUG']) && $_ENV['APP_DEBUG']) {
|
if ($this->appConfig->isDebug()) {
|
||||||
echo $e->getTraceAsString() . "\n";
|
echo $e->getTraceAsString() . "\n";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -48,14 +48,23 @@ final readonly class DiscoveryServiceBootstrapper
|
|||||||
$currentContext = ExecutionContext::detect();
|
$currentContext = ExecutionContext::detect();
|
||||||
$contextString = $currentContext->getType()->value;
|
$contextString = $currentContext->getType()->value;
|
||||||
|
|
||||||
|
// TEMPORARY DEBUG LOGGING
|
||||||
|
error_log("🔍 DISCOVERY DEBUG: Context detected = {$contextString}");
|
||||||
|
error_log("🔍 DISCOVERY DEBUG: Source path = " . $pathProvider->getSourcePath());
|
||||||
|
|
||||||
// Direkter Cache-Check mit expliziter toArray/fromArray Serialisierung
|
// Direkter Cache-Check mit expliziter toArray/fromArray Serialisierung
|
||||||
$defaultPaths = [$pathProvider->getSourcePath()];
|
$defaultPaths = [$pathProvider->getSourcePath()];
|
||||||
$cacheKey = DiscoveryCacheIdentifiers::fullDiscoveryKey($defaultPaths, $contextString);
|
$cacheKey = DiscoveryCacheIdentifiers::fullDiscoveryKey($defaultPaths, $contextString);
|
||||||
|
|
||||||
|
error_log("🔍 DISCOVERY DEBUG: Cache key = {$cacheKey->toString()}");
|
||||||
|
|
||||||
|
|
||||||
$cachedItem = $cache->get($cacheKey);
|
$cachedItem = $cache->get($cacheKey);
|
||||||
|
|
||||||
|
error_log("🔍 DISCOVERY DEBUG: Cache hit = " . ($cachedItem->isHit ? 'YES' : 'NO'));
|
||||||
|
|
||||||
if ($cachedItem->isHit) {
|
if ($cachedItem->isHit) {
|
||||||
|
error_log("🔍 DISCOVERY DEBUG: Loading from cache...");
|
||||||
// Ensure DiscoveryRegistry class is loaded before attempting deserialization
|
// Ensure DiscoveryRegistry class is loaded before attempting deserialization
|
||||||
if (! class_exists(DiscoveryRegistry::class, true)) {
|
if (! class_exists(DiscoveryRegistry::class, true)) {
|
||||||
$cachedRegistry = null;
|
$cachedRegistry = null;
|
||||||
@@ -82,6 +91,9 @@ final readonly class DiscoveryServiceBootstrapper
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ($cachedRegistry !== null && ! $cachedRegistry->isEmpty()) {
|
if ($cachedRegistry !== null && ! $cachedRegistry->isEmpty()) {
|
||||||
|
$routeCount = count($cachedRegistry->attributes->get(\App\Framework\Attributes\Route::class));
|
||||||
|
error_log("🔍 DISCOVERY DEBUG: Cached registry loaded - Route count: {$routeCount}");
|
||||||
|
|
||||||
$this->container->singleton(DiscoveryRegistry::class, $cachedRegistry);
|
$this->container->singleton(DiscoveryRegistry::class, $cachedRegistry);
|
||||||
|
|
||||||
// Process DefaultImplementation attributes first (before Initializers)
|
// Process DefaultImplementation attributes first (before Initializers)
|
||||||
@@ -97,10 +109,18 @@ final readonly class DiscoveryServiceBootstrapper
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Fallback: Vollständige Discovery durchführen
|
// Fallback: Vollständige Discovery durchführen
|
||||||
|
error_log("🔍 DISCOVERY DEBUG: Performing fresh discovery...");
|
||||||
$results = $this->performBootstrap($pathProvider, $cache, $discoveryConfig);
|
$results = $this->performBootstrap($pathProvider, $cache, $discoveryConfig);
|
||||||
|
error_log("🔍 DISCOVERY DEBUG: Discovery completed - isEmpty: " . ($results->isEmpty() ? 'YES' : 'NO'));
|
||||||
|
|
||||||
// Nach der Discovery explizit in unserem eigenen Cache-Format speichern
|
// Nach der Discovery explizit in unserem eigenen Cache-Format speichern
|
||||||
$consoleCommandCount = count($results->attributes->get(\App\Framework\Console\ConsoleCommand::class));
|
$consoleCommandCount = count($results->attributes->get(\App\Framework\Console\ConsoleCommand::class));
|
||||||
|
$routeCount = count($results->attributes->get(\App\Framework\Attributes\Route::class));
|
||||||
|
$initializerCount = count($results->attributes->get(\App\Framework\DI\Initializer::class));
|
||||||
|
|
||||||
|
error_log("🔍 DISCOVERY DEBUG: Found {$routeCount} routes");
|
||||||
|
error_log("🔍 DISCOVERY DEBUG: Found {$consoleCommandCount} console commands");
|
||||||
|
error_log("🔍 DISCOVERY DEBUG: Found {$initializerCount} initializers");
|
||||||
|
|
||||||
// Only cache if we found meaningful results
|
// Only cache if we found meaningful results
|
||||||
// An empty discovery likely indicates initialization timing issues
|
// An empty discovery likely indicates initialization timing issues
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ final readonly class ErrorEvent
|
|||||||
/**
|
/**
|
||||||
* Creates ErrorEvent from ErrorHandlerContext
|
* Creates ErrorEvent from ErrorHandlerContext
|
||||||
*/
|
*/
|
||||||
public static function fromErrorHandlerContext(ErrorHandlerContext $context, \App\Framework\DateTime\Clock $clock): self
|
public static function fromErrorHandlerContext(ErrorHandlerContext $context, \App\Framework\DateTime\Clock $clock, bool $isDebug = false): self
|
||||||
{
|
{
|
||||||
return new self(
|
return new self(
|
||||||
id: new Ulid($clock),
|
id: new Ulid($clock),
|
||||||
@@ -54,7 +54,7 @@ final readonly class ErrorEvent
|
|||||||
userId: $context->request->userId ?? null,
|
userId: $context->request->userId ?? null,
|
||||||
clientIp: $context->request->clientIp,
|
clientIp: $context->request->clientIp,
|
||||||
isSecurityEvent: $context->exception->metadata['security_event'] ?? false,
|
isSecurityEvent: $context->exception->metadata['security_event'] ?? false,
|
||||||
stackTrace: self::extractStackTrace($context),
|
stackTrace: self::extractStackTrace($context, $isDebug),
|
||||||
userAgent: $context->request->userAgent,
|
userAgent: $context->request->userAgent,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -263,10 +263,10 @@ final readonly class ErrorEvent
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private static function extractStackTrace(ErrorHandlerContext $context): ?string
|
private static function extractStackTrace(ErrorHandlerContext $context, bool $isDebug = false): ?string
|
||||||
{
|
{
|
||||||
// Don't include stack traces for security events in production
|
// Don't include stack traces for security events in production
|
||||||
if (($context->exception->metadata['security_event'] ?? false) && ! ($_ENV['APP_DEBUG'] ?? false)) {
|
if (($context->exception->metadata['security_event'] ?? false) && ! $isDebug) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -51,8 +51,13 @@ final readonly class ErrorHandler
|
|||||||
?SecurityEventHandler $securityHandler = null
|
?SecurityEventHandler $securityHandler = null
|
||||||
) {
|
) {
|
||||||
$this->isDebugMode = $isDebugMode ?? $this->getDebugModeFromEnvironment();
|
$this->isDebugMode = $isDebugMode ?? $this->getDebugModeFromEnvironment();
|
||||||
$this->logger = new ErrorLogger($logger);
|
|
||||||
$this->securityHandler = $securityHandler ?? SecurityEventHandler::createDefault($logger);
|
// Get Logger and AppConfig from container if not provided
|
||||||
|
$logger = $logger ?? $container->get(Logger::class);
|
||||||
|
$appConfig = $container->get(\App\Framework\Config\AppConfig::class);
|
||||||
|
|
||||||
|
$this->logger = new ErrorLogger($logger, $appConfig);
|
||||||
|
$this->securityHandler = $securityHandler ?? SecurityEventHandler::createDefault($logger, $appConfig);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function register(): void
|
public function register(): void
|
||||||
@@ -456,9 +461,12 @@ final readonly class ErrorHandler
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get AppConfig from container
|
||||||
|
$appConfig = $this->container->get(\App\Framework\Config\AppConfig::class);
|
||||||
|
|
||||||
$htmlRenderer = $templateRenderer ?
|
$htmlRenderer = $templateRenderer ?
|
||||||
new ErrorTemplateRenderer($templateRenderer) :
|
new ErrorTemplateRenderer($templateRenderer, $appConfig) :
|
||||||
new ErrorTemplateRenderer(new DummyTemplateRenderer());
|
new ErrorTemplateRenderer(new DummyTemplateRenderer(), $appConfig);
|
||||||
|
|
||||||
$apiRenderer = new ApiErrorRenderer();
|
$apiRenderer = new ApiErrorRenderer();
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace App\Framework\ErrorHandling;
|
namespace App\Framework\ErrorHandling;
|
||||||
|
|
||||||
|
use App\Framework\Config\AppConfig;
|
||||||
use App\Framework\Exception\Core\ErrorSeverity;
|
use App\Framework\Exception\Core\ErrorSeverity;
|
||||||
use App\Framework\Exception\ErrorHandlerContext;
|
use App\Framework\Exception\ErrorHandlerContext;
|
||||||
use App\Framework\Logging\Logger;
|
use App\Framework\Logging\Logger;
|
||||||
@@ -16,7 +17,8 @@ use App\Framework\Logging\ValueObjects\LogContext;
|
|||||||
final readonly class ErrorLogger
|
final readonly class ErrorLogger
|
||||||
{
|
{
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private ?Logger $logger = null
|
private Logger $logger,
|
||||||
|
private AppConfig $appConfig
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -42,13 +44,8 @@ final readonly class ErrorLogger
|
|||||||
'additionalData' => $context->additionalData,
|
'additionalData' => $context->additionalData,
|
||||||
];
|
];
|
||||||
|
|
||||||
if ($this->logger !== null) {
|
|
||||||
$logLevel = $this->mapErrorSeverityToLogLevel($context->level);
|
$logLevel = $this->mapErrorSeverityToLogLevel($context->level);
|
||||||
$this->logger->log($logLevel, $message, $contextData);
|
$this->logger->log($logLevel, $message, $contextData);
|
||||||
} else {
|
|
||||||
// Fallback auf error_log
|
|
||||||
$this->logToErrorLog($context);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -73,7 +70,6 @@ final readonly class ErrorLogger
|
|||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
if ($this->logger !== null) {
|
|
||||||
$logLevel = $this->determineLogLevel($context);
|
$logLevel = $this->determineLogLevel($context);
|
||||||
|
|
||||||
// Erstelle LogContext mit strukturierten Daten
|
// Erstelle LogContext mit strukturierten Daten
|
||||||
@@ -86,10 +82,6 @@ final readonly class ErrorLogger
|
|||||||
}
|
}
|
||||||
|
|
||||||
$this->logger->log($logLevel, $message, $logContext);
|
$this->logger->log($logLevel, $message, $logContext);
|
||||||
} else {
|
|
||||||
// Fallback auf error_log
|
|
||||||
$this->logHandlerContextToErrorLog($context);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -97,18 +89,13 @@ final readonly class ErrorLogger
|
|||||||
*/
|
*/
|
||||||
private function logSecurityEvent(ErrorHandlerContext $context): void
|
private function logSecurityEvent(ErrorHandlerContext $context): void
|
||||||
{
|
{
|
||||||
$securityLog = $context->toSecurityEventFormat($_ENV['APP_NAME'] ?? 'app');
|
$securityLog = $context->toSecurityEventFormat($this->appConfig->name);
|
||||||
|
|
||||||
if ($this->logger !== null) {
|
|
||||||
// Verwende LogContext für strukturiertes Security Event Logging
|
// Verwende LogContext für strukturiertes Security Event Logging
|
||||||
$logContext = LogContext::withData($securityLog)
|
$logContext = LogContext::withData($securityLog)
|
||||||
->addTags('security_event', 'owasp');
|
->addTags('security_event', 'owasp');
|
||||||
|
|
||||||
$this->logger->log(LogLevel::WARNING, 'Security Event Detected', $logContext);
|
$this->logger->log(LogLevel::WARNING, 'Security Event Detected', $logContext);
|
||||||
} else {
|
|
||||||
// Fallback nur als letzte Option
|
|
||||||
error_log('SECURITY_EVENT: ' . json_encode($securityLog, JSON_UNESCAPED_SLASHES));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -164,51 +151,6 @@ final readonly class ErrorLogger
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Fallback-Methode für ErrorHandlerContext ohne Framework Logger
|
|
||||||
*/
|
|
||||||
private function logHandlerContextToErrorLog(ErrorHandlerContext $context): void
|
|
||||||
{
|
|
||||||
$message = sprintf(
|
|
||||||
'[%s] [%s] %s: %s',
|
|
||||||
date('Y-m-d H:i:s'),
|
|
||||||
$context->exception->metadata['error_level'] ?? 'ERROR',
|
|
||||||
$context->exception->component ?? 'Application',
|
|
||||||
$this->extractExceptionMessage($context)
|
|
||||||
);
|
|
||||||
|
|
||||||
error_log($message);
|
|
||||||
|
|
||||||
// Security Events auch separat loggen
|
|
||||||
if ($context->exception->metadata['security_event'] ?? false) {
|
|
||||||
$securityLog = $context->toSecurityEventJson($_ENV['APP_NAME'] ?? 'app');
|
|
||||||
error_log('SECURITY_EVENT: ' . $securityLog);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fallback-Methode für Logging ohne Framework Logger (Legacy)
|
|
||||||
*/
|
|
||||||
private function logToErrorLog(ErrorContext $context): void
|
|
||||||
{
|
|
||||||
$exception = $context->exception;
|
|
||||||
$message = sprintf(
|
|
||||||
'[%s] [%s] %s: %s in %s:%d',
|
|
||||||
date('Y-m-d H:i:s'),
|
|
||||||
$context->level->name,
|
|
||||||
get_class($exception),
|
|
||||||
$exception->getMessage(),
|
|
||||||
$exception->getFile(),
|
|
||||||
$exception->getLine()
|
|
||||||
);
|
|
||||||
|
|
||||||
error_log($message);
|
|
||||||
|
|
||||||
// Bei kritischen Fehlern auch den Stacktrace loggen
|
|
||||||
if ($context->level === ErrorSeverity::CRITICAL) {
|
|
||||||
error_log("Stack trace: \n" . $exception->getTraceAsString());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Mappt ErrorSeverity auf Framework LogLevel
|
* Mappt ErrorSeverity auf Framework LogLevel
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ use Throwable;
|
|||||||
final readonly class SecurityEventHandler
|
final readonly class SecurityEventHandler
|
||||||
{
|
{
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private SecurityEventLogger $securityLogger,
|
private ?SecurityEventLogger $securityLogger,
|
||||||
private ?SecurityAlertManager $alertManager = null
|
private ?SecurityAlertManager $alertManager = null
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
@@ -28,6 +28,11 @@ final readonly class SecurityEventHandler
|
|||||||
SecurityException $exception,
|
SecurityException $exception,
|
||||||
?MiddlewareContext $context = null
|
?MiddlewareContext $context = null
|
||||||
): void {
|
): void {
|
||||||
|
// Skip if no logger available
|
||||||
|
if ($this->securityLogger === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Erstelle ErrorHandlerContext für OWASP-Format
|
// Erstelle ErrorHandlerContext für OWASP-Format
|
||||||
$errorHandlerContext = $this->createErrorHandlerContext($exception, $context);
|
$errorHandlerContext = $this->createErrorHandlerContext($exception, $context);
|
||||||
@@ -71,7 +76,7 @@ final readonly class SecurityEventHandler
|
|||||||
): void {
|
): void {
|
||||||
if ($this->alertManager) {
|
if ($this->alertManager) {
|
||||||
$this->alertManager->sendAlert($exception, $context);
|
$this->alertManager->sendAlert($exception, $context);
|
||||||
} else {
|
} elseif ($this->securityLogger !== null) {
|
||||||
// Fallback: Logge als kritisches Event
|
// Fallback: Logge als kritisches Event
|
||||||
$this->securityLogger->logCriticalAlert($exception, $context);
|
$this->securityLogger->logCriticalAlert($exception, $context);
|
||||||
}
|
}
|
||||||
@@ -135,9 +140,16 @@ final readonly class SecurityEventHandler
|
|||||||
/**
|
/**
|
||||||
* Factory-Methode mit Standard-Konfiguration
|
* Factory-Methode mit Standard-Konfiguration
|
||||||
*/
|
*/
|
||||||
public static function createDefault(?Logger $logger = null): self
|
public static function createDefault(?Logger $logger = null, ?\App\Framework\Config\AppConfig $appConfig = null): self
|
||||||
{
|
{
|
||||||
$securityLogger = new SecurityEventLogger($logger);
|
// If AppConfig not provided, we can't create SecurityEventLogger properly
|
||||||
|
// This is a temporary solution - ideally AppConfig should always be provided
|
||||||
|
if ($logger === null || $appConfig === null) {
|
||||||
|
// Return a minimal handler without SecurityEventLogger
|
||||||
|
return new self(null, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
$securityLogger = new SecurityEventLogger($logger, $appConfig);
|
||||||
$alertManager = null; // Kann später konfiguriert werden
|
$alertManager = null; // Kann später konfiguriert werden
|
||||||
|
|
||||||
return new self($securityLogger, $alertManager);
|
return new self($securityLogger, $alertManager);
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace App\Framework\ErrorHandling;
|
namespace App\Framework\ErrorHandling;
|
||||||
|
|
||||||
|
use App\Framework\Config\AppConfig;
|
||||||
use App\Framework\Exception\ErrorHandlerContext;
|
use App\Framework\Exception\ErrorHandlerContext;
|
||||||
use App\Framework\Exception\SecurityException;
|
use App\Framework\Exception\SecurityException;
|
||||||
use App\Framework\Exception\SecurityLogLevel;
|
use App\Framework\Exception\SecurityLogLevel;
|
||||||
@@ -16,8 +17,8 @@ use App\Framework\Logging\LogLevel;
|
|||||||
final readonly class SecurityEventLogger
|
final readonly class SecurityEventLogger
|
||||||
{
|
{
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private ?Logger $logger = null,
|
private Logger $logger,
|
||||||
private string $applicationId = 'app'
|
private AppConfig $appConfig
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -33,7 +34,6 @@ final readonly class SecurityEventLogger
|
|||||||
// OWASP-konformes Log generieren
|
// OWASP-konformes Log generieren
|
||||||
$owaspLog = $this->createOWASPLog($exception, $context);
|
$owaspLog = $this->createOWASPLog($exception, $context);
|
||||||
|
|
||||||
if ($this->logger) {
|
|
||||||
// Strukturiertes Logging über Framework Logger
|
// Strukturiertes Logging über Framework Logger
|
||||||
$frameworkLogLevel = $this->mapSecurityLevelToFrameworkLevel($securityEvent->getLogLevel());
|
$frameworkLogLevel = $this->mapSecurityLevelToFrameworkLevel($securityEvent->getLogLevel());
|
||||||
|
|
||||||
@@ -49,10 +49,6 @@ final readonly class SecurityEventLogger
|
|||||||
'context_data' => $context->forLogging(),
|
'context_data' => $context->forLogging(),
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
} else {
|
|
||||||
// Fallback auf error_log
|
|
||||||
$this->logToErrorLog($owaspLog);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -64,15 +60,11 @@ final readonly class SecurityEventLogger
|
|||||||
): void {
|
): void {
|
||||||
$alertData = $this->createAlertData($exception, $context);
|
$alertData = $this->createAlertData($exception, $context);
|
||||||
|
|
||||||
if ($this->logger) {
|
|
||||||
$this->logger->log(
|
$this->logger->log(
|
||||||
LogLevel::CRITICAL,
|
LogLevel::CRITICAL,
|
||||||
'SECURITY_ALERT: ' . $exception->getSecurityEvent()->getEventIdentifier(),
|
'SECURITY_ALERT: ' . $exception->getSecurityEvent()->getEventIdentifier(),
|
||||||
$alertData
|
$alertData
|
||||||
);
|
);
|
||||||
} else {
|
|
||||||
error_log('SECURITY_ALERT: ' . json_encode($alertData));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -86,7 +78,7 @@ final readonly class SecurityEventLogger
|
|||||||
|
|
||||||
return [
|
return [
|
||||||
'datetime' => date('c'),
|
'datetime' => date('c'),
|
||||||
'appid' => $this->applicationId,
|
'appid' => $this->appConfig->name,
|
||||||
'event' => $securityEvent->getEventIdentifier(),
|
'event' => $securityEvent->getEventIdentifier(),
|
||||||
'level' => $securityEvent->getLogLevel()->value,
|
'level' => $securityEvent->getLogLevel()->value,
|
||||||
'description' => $securityEvent->getDescription(),
|
'description' => $securityEvent->getDescription(),
|
||||||
@@ -98,8 +90,6 @@ final readonly class SecurityEventLogger
|
|||||||
'port' => $context->request->port,
|
'port' => $context->request->port,
|
||||||
'request_uri' => $context->request->requestUri,
|
'request_uri' => $context->request->requestUri,
|
||||||
'request_method' => $context->request->requestMethod,
|
'request_method' => $context->request->requestMethod,
|
||||||
'region' => $_ENV['AWS_REGION'] ?? 'unknown',
|
|
||||||
'geo' => $_ENV['GEO_LOCATION'] ?? 'unknown',
|
|
||||||
'category' => $securityEvent->getCategory(),
|
'category' => $securityEvent->getCategory(),
|
||||||
'requires_alert' => $securityEvent->requiresAlert(),
|
'requires_alert' => $securityEvent->requiresAlert(),
|
||||||
];
|
];
|
||||||
@@ -127,15 +117,6 @@ final readonly class SecurityEventLogger
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Fallback-Logging über error_log
|
|
||||||
*/
|
|
||||||
private function logToErrorLog(array $owaspLog): void
|
|
||||||
{
|
|
||||||
$logMessage = 'SECURITY_EVENT: ' . json_encode($owaspLog, JSON_UNESCAPED_SLASHES);
|
|
||||||
error_log($logMessage);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Mappt SecurityLogLevel auf Framework LogLevel
|
* Mappt SecurityLogLevel auf Framework LogLevel
|
||||||
*/
|
*/
|
||||||
@@ -150,15 +131,4 @@ final readonly class SecurityEventLogger
|
|||||||
SecurityLogLevel::FATAL => LogLevel::CRITICAL,
|
SecurityLogLevel::FATAL => LogLevel::CRITICAL,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Factory-Methode für Standard-Konfiguration
|
|
||||||
*/
|
|
||||||
public static function create(?Logger $logger = null): self
|
|
||||||
{
|
|
||||||
return new self(
|
|
||||||
$logger,
|
|
||||||
$_ENV['APP_NAME'] ?? $_ENV['APP_ID'] ?? 'app'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace App\Framework\ErrorHandling\View;
|
namespace App\Framework\ErrorHandling\View;
|
||||||
|
|
||||||
|
use App\Framework\Config\AppConfig;
|
||||||
use App\Framework\Core\ValueObjects\Byte;
|
use App\Framework\Core\ValueObjects\Byte;
|
||||||
use App\Framework\Core\ValueObjects\Duration;
|
use App\Framework\Core\ValueObjects\Duration;
|
||||||
use App\Framework\ErrorHandling\StackTrace;
|
use App\Framework\ErrorHandling\StackTrace;
|
||||||
@@ -23,6 +24,7 @@ final readonly class ErrorTemplateRenderer implements ErrorViewRendererInterface
|
|||||||
{
|
{
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private TemplateRenderer $renderer,
|
private TemplateRenderer $renderer,
|
||||||
|
private AppConfig $appConfig,
|
||||||
private string $debugTemplate = 'enhanced-debug',
|
private string $debugTemplate = 'enhanced-debug',
|
||||||
private string $productionTemplate = 'production'
|
private string $productionTemplate = 'production'
|
||||||
) {
|
) {
|
||||||
@@ -65,7 +67,7 @@ final readonly class ErrorTemplateRenderer implements ErrorViewRendererInterface
|
|||||||
'userAgent' => (string) ($context->request->userAgent ?? 'Unknown'),
|
'userAgent' => (string) ($context->request->userAgent ?? 'Unknown'),
|
||||||
'traceCount' => 0, // Will be updated below if trace is available
|
'traceCount' => 0, // Will be updated below if trace is available
|
||||||
// Environment information
|
// Environment information
|
||||||
'environment' => $_ENV['APP_ENV'] ?? 'development',
|
'environment' => $this->appConfig->type->value,
|
||||||
'debugMode' => $isDebug ? 'Enabled' : 'Disabled',
|
'debugMode' => $isDebug ? 'Enabled' : 'Disabled',
|
||||||
'phpVersion' => PHP_VERSION,
|
'phpVersion' => PHP_VERSION,
|
||||||
'frameworkVersion' => '1.0.0-dev',
|
'frameworkVersion' => '1.0.0-dev',
|
||||||
|
|||||||
@@ -48,7 +48,8 @@ final readonly class ErrorReport
|
|||||||
public static function fromThrowable(
|
public static function fromThrowable(
|
||||||
Throwable $throwable,
|
Throwable $throwable,
|
||||||
string $level = 'error',
|
string $level = 'error',
|
||||||
array $context = []
|
array $context = [],
|
||||||
|
?string $environment = null
|
||||||
): self {
|
): self {
|
||||||
return new self(
|
return new self(
|
||||||
id: self::generateId(),
|
id: self::generateId(),
|
||||||
@@ -60,7 +61,7 @@ final readonly class ErrorReport
|
|||||||
line: $throwable->getLine(),
|
line: $throwable->getLine(),
|
||||||
trace: $throwable->getTraceAsString(),
|
trace: $throwable->getTraceAsString(),
|
||||||
context: $context,
|
context: $context,
|
||||||
environment: $_ENV['APP_ENV'] ?? 'production',
|
environment: $environment ?? 'production',
|
||||||
serverInfo: self::getServerInfo()
|
serverInfo: self::getServerInfo()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -72,7 +73,8 @@ final readonly class ErrorReport
|
|||||||
string $level,
|
string $level,
|
||||||
string $message,
|
string $message,
|
||||||
array $context = [],
|
array $context = [],
|
||||||
?Throwable $exception = null
|
?Throwable $exception = null,
|
||||||
|
?string $environment = null
|
||||||
): self {
|
): self {
|
||||||
return new self(
|
return new self(
|
||||||
id: self::generateId(),
|
id: self::generateId(),
|
||||||
@@ -84,7 +86,7 @@ final readonly class ErrorReport
|
|||||||
line: $exception?->getLine() ?? debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 1)[0]['line'] ?? 0,
|
line: $exception?->getLine() ?? debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 1)[0]['line'] ?? 0,
|
||||||
trace: $exception?->getTraceAsString() ?? self::getCurrentTrace(),
|
trace: $exception?->getTraceAsString() ?? self::getCurrentTrace(),
|
||||||
context: $context,
|
context: $context,
|
||||||
environment: $_ENV['APP_ENV'] ?? 'production',
|
environment: $environment ?? 'production',
|
||||||
serverInfo: self::getServerInfo()
|
serverInfo: self::getServerInfo()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace App\Framework\ErrorReporting;
|
namespace App\Framework\ErrorReporting;
|
||||||
|
|
||||||
|
use App\Framework\Config\Environment;
|
||||||
use App\Framework\Database\ConnectionInterface;
|
use App\Framework\Database\ConnectionInterface;
|
||||||
use App\Framework\DateTime\Clock;
|
use App\Framework\DateTime\Clock;
|
||||||
use App\Framework\DI\Container;
|
use App\Framework\DI\Container;
|
||||||
@@ -25,7 +26,8 @@ final readonly class ErrorReportingInitializer
|
|||||||
#[Initializer]
|
#[Initializer]
|
||||||
public function initialize(Container $container): void
|
public function initialize(Container $container): void
|
||||||
{
|
{
|
||||||
$enabled = (bool) ($_ENV['ERROR_REPORTING_ENABLED'] ?? true);
|
$env = $container->get(Environment::class);
|
||||||
|
$enabled = $env->getBool('ERROR_REPORTING_ENABLED', true);
|
||||||
|
|
||||||
// Storage
|
// Storage
|
||||||
$container->bind(ErrorReportStorageInterface::class, function (Container $container) use ($enabled) {
|
$container->bind(ErrorReportStorageInterface::class, function (Container $container) use ($enabled) {
|
||||||
@@ -55,6 +57,7 @@ final readonly class ErrorReportingInitializer
|
|||||||
return new NullErrorReporter();
|
return new NullErrorReporter();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$env = $container->get(Environment::class);
|
||||||
$processors = [];
|
$processors = [];
|
||||||
$filters = [];
|
$filters = [];
|
||||||
|
|
||||||
@@ -68,15 +71,17 @@ final readonly class ErrorReportingInitializer
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Add environment-based filters
|
// Add environment-based filters
|
||||||
if (($_ENV['ERROR_REPORTING_FILTER_LEVELS'] ?? null)) {
|
$filterLevels = $env->getString('ERROR_REPORTING_FILTER_LEVELS', '');
|
||||||
$allowedLevels = explode(',', $_ENV['ERROR_REPORTING_FILTER_LEVELS']);
|
if ($filterLevels !== '') {
|
||||||
|
$allowedLevels = explode(',', $filterLevels);
|
||||||
$filters[] = function (ErrorReport $report) use ($allowedLevels) {
|
$filters[] = function (ErrorReport $report) use ($allowedLevels) {
|
||||||
return in_array($report->level, $allowedLevels);
|
return in_array($report->level, $allowedLevels);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add environment filter for production
|
// Add environment filter for production
|
||||||
if (($_ENV['APP_ENV'] ?? 'production') === 'production') {
|
$appEnv = $env->getString('APP_ENV', 'production');
|
||||||
|
if ($appEnv === 'production') {
|
||||||
$filters[] = function (ErrorReport $report) {
|
$filters[] = function (ErrorReport $report) {
|
||||||
// Don't report debug/info in production
|
// Don't report debug/info in production
|
||||||
return ! in_array($report->level, ['debug', 'info']);
|
return ! in_array($report->level, ['debug', 'info']);
|
||||||
@@ -88,7 +93,7 @@ final readonly class ErrorReportingInitializer
|
|||||||
clock: $container->get(Clock::class),
|
clock: $container->get(Clock::class),
|
||||||
logger: $container->has(Logger::class) ? $container->get(Logger::class) : null,
|
logger: $container->has(Logger::class) ? $container->get(Logger::class) : null,
|
||||||
queue: $container->has(Queue::class) ? $container->get(Queue::class) : null,
|
queue: $container->has(Queue::class) ? $container->get(Queue::class) : null,
|
||||||
asyncProcessing: (bool) ($_ENV['ERROR_REPORTING_ASYNC'] ?? true),
|
asyncProcessing: $env->getBool('ERROR_REPORTING_ASYNC', true),
|
||||||
processors: $processors,
|
processors: $processors,
|
||||||
filters: $filters
|
filters: $filters
|
||||||
);
|
);
|
||||||
@@ -100,6 +105,7 @@ final readonly class ErrorReportingInitializer
|
|||||||
throw new \RuntimeException('ErrorReporter is disabled. Use ErrorReporterInterface instead.');
|
throw new \RuntimeException('ErrorReporter is disabled. Use ErrorReporterInterface instead.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$env = $container->get(Environment::class);
|
||||||
$processors = [];
|
$processors = [];
|
||||||
$filters = [];
|
$filters = [];
|
||||||
|
|
||||||
@@ -113,15 +119,17 @@ final readonly class ErrorReportingInitializer
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Add environment-based filters
|
// Add environment-based filters
|
||||||
if (($_ENV['ERROR_REPORTING_FILTER_LEVELS'] ?? null)) {
|
$filterLevels = $env->getString('ERROR_REPORTING_FILTER_LEVELS', '');
|
||||||
$allowedLevels = explode(',', $_ENV['ERROR_REPORTING_FILTER_LEVELS']);
|
if ($filterLevels !== '') {
|
||||||
|
$allowedLevels = explode(',', $filterLevels);
|
||||||
$filters[] = function (ErrorReport $report) use ($allowedLevels) {
|
$filters[] = function (ErrorReport $report) use ($allowedLevels) {
|
||||||
return in_array($report->level, $allowedLevels);
|
return in_array($report->level, $allowedLevels);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add environment filter for production
|
// Add environment filter for production
|
||||||
if (($_ENV['APP_ENV'] ?? 'production') === 'production') {
|
$appEnv = $env->getString('APP_ENV', 'production');
|
||||||
|
if ($appEnv === 'production') {
|
||||||
$filters[] = function (ErrorReport $report) {
|
$filters[] = function (ErrorReport $report) {
|
||||||
// Don't report debug/info in production
|
// Don't report debug/info in production
|
||||||
return ! in_array($report->level, ['debug', 'info']);
|
return ! in_array($report->level, ['debug', 'info']);
|
||||||
@@ -133,17 +141,17 @@ final readonly class ErrorReportingInitializer
|
|||||||
clock: $container->get(Clock::class),
|
clock: $container->get(Clock::class),
|
||||||
logger: $container->has(Logger::class) ? $container->get(Logger::class) : null,
|
logger: $container->has(Logger::class) ? $container->get(Logger::class) : null,
|
||||||
queue: $container->has(Queue::class) ? $container->get(Queue::class) : null,
|
queue: $container->has(Queue::class) ? $container->get(Queue::class) : null,
|
||||||
asyncProcessing: (bool) ($_ENV['ERROR_REPORTING_ASYNC'] ?? true),
|
asyncProcessing: $env->getBool('ERROR_REPORTING_ASYNC', true),
|
||||||
processors: $processors,
|
processors: $processors,
|
||||||
filters: $filters
|
filters: $filters
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Middleware
|
// Middleware
|
||||||
$container->bind(ErrorReportingMiddleware::class, function (Container $container) {
|
$container->bind(ErrorReportingMiddleware::class, function (Container $container) use ($enabled) {
|
||||||
return new ErrorReportingMiddleware(
|
return new ErrorReportingMiddleware(
|
||||||
reporter: $container->get(ErrorReporter::class),
|
reporter: $container->get(ErrorReporter::class),
|
||||||
enabled: (bool) ($_ENV['ERROR_REPORTING_ENABLED'] ?? true)
|
enabled: $enabled
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -145,8 +145,6 @@ final readonly class ErrorHandlerContext
|
|||||||
'port' => $this->request->port,
|
'port' => $this->request->port,
|
||||||
'request_uri' => $this->request->requestUri,
|
'request_uri' => $this->request->requestUri,
|
||||||
'request_method' => $this->request->requestMethod,
|
'request_method' => $this->request->requestMethod,
|
||||||
'region' => $_ENV['AWS_REGION'] ?? 'unknown',
|
|
||||||
'geo' => $_ENV['GEO_LOCATION'] ?? 'unknown',
|
|
||||||
];
|
];
|
||||||
|
|
||||||
// Security-Event-spezifische Daten falls verfügbar
|
// Security-Event-spezifische Daten falls verfügbar
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace App\Framework\Filesystem;
|
namespace App\Framework\Filesystem;
|
||||||
|
|
||||||
|
use App\Framework\Config\Environment;
|
||||||
use App\Framework\DI\Container;
|
use App\Framework\DI\Container;
|
||||||
use App\Framework\DI\Initializer;
|
use App\Framework\DI\Initializer;
|
||||||
use App\Framework\Filesystem\Serializers\CsvSerializer;
|
use App\Framework\Filesystem\Serializers\CsvSerializer;
|
||||||
@@ -23,26 +24,28 @@ final readonly class FilesystemInitializer
|
|||||||
public function initializeFilesystem(Container $container): void
|
public function initializeFilesystem(Container $container): void
|
||||||
{
|
{
|
||||||
// Filesystem Config
|
// Filesystem Config
|
||||||
$container->singleton(FilesystemConfig::class, function () {
|
$container->singleton(FilesystemConfig::class, function (Container $container) {
|
||||||
|
$env = $container->get(Environment::class);
|
||||||
|
|
||||||
return new FilesystemConfig(
|
return new FilesystemConfig(
|
||||||
defaultStorage: $_ENV['FILESYSTEM_DEFAULT_STORAGE'] ?? 'local',
|
defaultStorage: $env->getString('FILESYSTEM_DEFAULT_STORAGE', 'local'),
|
||||||
storages: [
|
storages: [
|
||||||
'local' => [
|
'local' => [
|
||||||
'type' => 'file',
|
'type' => 'file',
|
||||||
'path' => $_ENV['FILESYSTEM_LOCAL_PATH'] ?? '/var/www/html/storage',
|
'path' => $env->getString('FILESYSTEM_LOCAL_PATH', '/var/www/html/storage'),
|
||||||
],
|
],
|
||||||
'temp' => [
|
'temp' => [
|
||||||
'type' => 'file',
|
'type' => 'file',
|
||||||
'path' => $_ENV['FILESYSTEM_TEMP_PATH'] ?? '/tmp',
|
'path' => $env->getString('FILESYSTEM_TEMP_PATH', '/tmp'),
|
||||||
],
|
],
|
||||||
'analytics' => [
|
'analytics' => [
|
||||||
'type' => 'file',
|
'type' => 'file',
|
||||||
'path' => $_ENV['ANALYTICS_DATA_PATH'] ?? '/var/www/html/storage/analytics',
|
'path' => $env->getString('ANALYTICS_DATA_PATH', '/var/www/html/storage/analytics'),
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
enableCompression: filter_var($_ENV['FILESYSTEM_COMPRESSION'] ?? 'false', FILTER_VALIDATE_BOOLEAN),
|
enableCompression: $env->getBool('FILESYSTEM_COMPRESSION', false),
|
||||||
enableAtomicWrites: filter_var($_ENV['FILESYSTEM_ATOMIC_WRITES'] ?? 'true', FILTER_VALIDATE_BOOLEAN),
|
enableAtomicWrites: $env->getBool('FILESYSTEM_ATOMIC_WRITES', true),
|
||||||
enableFileLocking: filter_var($_ENV['FILESYSTEM_FILE_LOCKING'] ?? 'true', FILTER_VALIDATE_BOOLEAN)
|
enableFileLocking: $env->getBool('FILESYSTEM_FILE_LOCKING', true)
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -72,12 +75,13 @@ final readonly class FilesystemInitializer
|
|||||||
});
|
});
|
||||||
|
|
||||||
// FileValidator - Default Validator (always cached for performance)
|
// FileValidator - Default Validator (always cached for performance)
|
||||||
$container->singleton(FileValidator::class, function () {
|
$container->singleton(FileValidator::class, function (Container $container) {
|
||||||
|
$env = $container->get(Environment::class);
|
||||||
$validator = FileValidator::createDefault();
|
$validator = FileValidator::createDefault();
|
||||||
|
|
||||||
// Always use caching for optimal performance
|
// Always use caching for optimal performance
|
||||||
// Disable only for debugging: FILESYSTEM_DISABLE_CACHE=true
|
// Disable only for debugging: FILESYSTEM_DISABLE_CACHE=true
|
||||||
$disableCache = filter_var($_ENV['FILESYSTEM_DISABLE_CACHE'] ?? 'false', FILTER_VALIDATE_BOOLEAN);
|
$disableCache = $env->getBool('FILESYSTEM_DISABLE_CACHE', false);
|
||||||
|
|
||||||
if ($disableCache) {
|
if ($disableCache) {
|
||||||
return $validator;
|
return $validator;
|
||||||
@@ -91,10 +95,11 @@ final readonly class FilesystemInitializer
|
|||||||
});
|
});
|
||||||
|
|
||||||
// FileValidator - Upload Validator (always cached)
|
// FileValidator - Upload Validator (always cached)
|
||||||
$container->singleton('filesystem.validator.upload', function () {
|
$container->singleton('filesystem.validator.upload', function (Container $container) {
|
||||||
|
$env = $container->get(Environment::class);
|
||||||
$validator = FileValidator::forUploads();
|
$validator = FileValidator::forUploads();
|
||||||
|
|
||||||
$disableCache = filter_var($_ENV['FILESYSTEM_DISABLE_CACHE'] ?? 'false', FILTER_VALIDATE_BOOLEAN);
|
$disableCache = $env->getBool('FILESYSTEM_DISABLE_CACHE', false);
|
||||||
|
|
||||||
if ($disableCache) {
|
if ($disableCache) {
|
||||||
return $validator;
|
return $validator;
|
||||||
@@ -104,10 +109,11 @@ final readonly class FilesystemInitializer
|
|||||||
});
|
});
|
||||||
|
|
||||||
// FileValidator - Image Validator (always cached)
|
// FileValidator - Image Validator (always cached)
|
||||||
$container->singleton('filesystem.validator.image', function () {
|
$container->singleton('filesystem.validator.image', function (Container $container) {
|
||||||
|
$env = $container->get(Environment::class);
|
||||||
$validator = FileValidator::forImages();
|
$validator = FileValidator::forImages();
|
||||||
|
|
||||||
$disableCache = filter_var($_ENV['FILESYSTEM_DISABLE_CACHE'] ?? 'false', FILTER_VALIDATE_BOOLEAN);
|
$disableCache = $env->getBool('FILESYSTEM_DISABLE_CACHE', false);
|
||||||
|
|
||||||
if ($disableCache) {
|
if ($disableCache) {
|
||||||
return $validator;
|
return $validator;
|
||||||
@@ -144,7 +150,8 @@ final readonly class FilesystemInitializer
|
|||||||
{
|
{
|
||||||
// Always enable storage caching for optimal performance
|
// Always enable storage caching for optimal performance
|
||||||
// Disable only for debugging: FILESYSTEM_DISABLE_CACHE=true
|
// Disable only for debugging: FILESYSTEM_DISABLE_CACHE=true
|
||||||
$disableCache = filter_var($_ENV['FILESYSTEM_DISABLE_CACHE'] ?? 'false', FILTER_VALIDATE_BOOLEAN);
|
$env = $container->get(Environment::class);
|
||||||
|
$disableCache = $env->getBool('FILESYSTEM_DISABLE_CACHE', false);
|
||||||
$enableStorageCache = !$disableCache;
|
$enableStorageCache = !$disableCache;
|
||||||
|
|
||||||
// Default Storage - nutzt PathProvider aus Container
|
// Default Storage - nutzt PathProvider aus Container
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ namespace App\Framework\Http;
|
|||||||
|
|
||||||
use App\Framework\Cache\Cache;
|
use App\Framework\Cache\Cache;
|
||||||
use App\Framework\CircuitBreaker\CircuitBreaker;
|
use App\Framework\CircuitBreaker\CircuitBreaker;
|
||||||
|
use App\Framework\Config\Environment;
|
||||||
use App\Framework\DI\Container;
|
use App\Framework\DI\Container;
|
||||||
use App\Framework\Http\Exceptions\MiddlewareTimeoutException;
|
use App\Framework\Http\Exceptions\MiddlewareTimeoutException;
|
||||||
use App\Framework\Http\Metrics\MiddlewareMetricsCollector;
|
use App\Framework\Http\Metrics\MiddlewareMetricsCollector;
|
||||||
@@ -46,7 +47,18 @@ final readonly class MiddlewareInvoker
|
|||||||
?MiddlewareMetricsCollector $metricsCollector = null
|
?MiddlewareMetricsCollector $metricsCollector = null
|
||||||
) {
|
) {
|
||||||
$this->logger = $this->container->get(DefaultLogger::class);
|
$this->logger = $this->container->get(DefaultLogger::class);
|
||||||
$this->defaultTimeout = $defaultTimeout ?? (float)($_ENV['MIDDLEWARE_TIMEOUT'] ?? 5.0);
|
|
||||||
|
if ($defaultTimeout === null) {
|
||||||
|
try {
|
||||||
|
$env = $this->container->get(Environment::class);
|
||||||
|
$this->defaultTimeout = $env->getFloat('MIDDLEWARE_TIMEOUT', 5.0);
|
||||||
|
} catch (\Throwable) {
|
||||||
|
$this->defaultTimeout = 5.0;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$this->defaultTimeout = $defaultTimeout;
|
||||||
|
}
|
||||||
|
|
||||||
$this->middlewareTimeouts = $middlewareTimeouts;
|
$this->middlewareTimeouts = $middlewareTimeouts;
|
||||||
$this->circuitBreaker = $circuitBreaker ?? new MiddlewareCircuitBreaker(
|
$this->circuitBreaker = $circuitBreaker ?? new MiddlewareCircuitBreaker(
|
||||||
$this->container->get(CircuitBreaker::class)
|
$this->container->get(CircuitBreaker::class)
|
||||||
|
|||||||
@@ -204,7 +204,7 @@ final readonly class MiddlewareManager implements MiddlewareManagerInterface
|
|||||||
\App\Framework\Http\Session\SessionMiddleware::class,
|
\App\Framework\Http\Session\SessionMiddleware::class,
|
||||||
|
|
||||||
// 2. Security und Rate Limiting
|
// 2. Security und Rate Limiting
|
||||||
RateLimitMiddleware::class,
|
//RateLimitMiddleware::class,
|
||||||
#\App\Application\Security\Middleware\SecurityEventMiddleware::class,
|
#\App\Application\Security\Middleware\SecurityEventMiddleware::class,
|
||||||
|
|
||||||
// 3. Headers und CORS
|
// 3. Headers und CORS
|
||||||
|
|||||||
@@ -19,13 +19,11 @@ final readonly class RequestId
|
|||||||
/**
|
/**
|
||||||
* Erstellt eine neue Request-ID oder parsed eine bestehende
|
* Erstellt eine neue Request-ID oder parsed eine bestehende
|
||||||
*
|
*
|
||||||
|
* @param string $secret Das Secret für die HMAC-Signatur (REQUIRED)
|
||||||
* @param string|null $combined Wenn nicht null, wird diese ID validiert und verwendet
|
* @param string|null $combined Wenn nicht null, wird diese ID validiert und verwendet
|
||||||
* @param string $secret Das Secret für die HMAC-Signatur
|
|
||||||
*/
|
*/
|
||||||
public function __construct(?string $combined = null, string $secret = '')
|
public function __construct(string $secret, ?string $combined = null)
|
||||||
{
|
{
|
||||||
// Secret über eine Umgebungsvariable beziehen, falls nicht angegeben
|
|
||||||
$secret = $secret ?: ($_ENV['APP_SECRET'] ?? 'default-secret-change-me');
|
|
||||||
|
|
||||||
if ($combined !== null && self::isValidFormat($combined)) {
|
if ($combined !== null && self::isValidFormat($combined)) {
|
||||||
// Bestehende ID parsen
|
// Bestehende ID parsen
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ declare(strict_types=1);
|
|||||||
namespace App\Framework\Http;
|
namespace App\Framework\Http;
|
||||||
|
|
||||||
use App\Framework\Attributes\Singleton;
|
use App\Framework\Attributes\Singleton;
|
||||||
|
use App\Framework\Config\Environment;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Service zur Verwaltung der Request-ID für den aktuellen Request.
|
* Service zur Verwaltung der Request-ID für den aktuellen Request.
|
||||||
@@ -17,15 +18,22 @@ final class RequestIdGenerator
|
|||||||
|
|
||||||
private ?RequestId $requestId = null;
|
private ?RequestId $requestId = null;
|
||||||
|
|
||||||
private string $secret;
|
private readonly string $secret;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialisiert den RequestIdGenerator mit einem optionalen Secret
|
* Initialisiert den RequestIdGenerator mit Environment für Secret-Auflösung
|
||||||
*/
|
*/
|
||||||
public function __construct(string $secret = '')
|
public function __construct(?Environment $env = null, string $secret = '')
|
||||||
{
|
{
|
||||||
// Secret über eine Umgebungsvariable beziehen, falls nicht angegeben
|
// Fallback für BC: Wenn kein Environment übergeben wird und Secret angegeben ist
|
||||||
$this->secret = $secret ?: ($_ENV['APP_SECRET'] ?? 'default-secret-change-me');
|
if ($secret !== '') {
|
||||||
|
$this->secret = $secret;
|
||||||
|
} elseif ($env !== null) {
|
||||||
|
$this->secret = $env->getString('APP_SECRET', 'default-secret-change-me');
|
||||||
|
} else {
|
||||||
|
// Final fallback für alte Verwendung ohne Environment
|
||||||
|
$this->secret = 'default-secret-change-me';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -156,18 +156,13 @@ final readonly class SessionFingerprintConfig
|
|||||||
/**
|
/**
|
||||||
* Erstellt eine Konfiguration aus Umgebungsvariablen
|
* Erstellt eine Konfiguration aus Umgebungsvariablen
|
||||||
*/
|
*/
|
||||||
public static function fromEnvironment(): self
|
public static function fromEnvironment(\App\Framework\Config\Environment $env): self
|
||||||
{
|
{
|
||||||
$strictMode = filter_var(
|
$strictMode = $env->getBool('SESSION_FINGERPRINT_STRICT', false);
|
||||||
$_ENV['SESSION_FINGERPRINT_STRICT'] ?? false,
|
|
||||||
FILTER_VALIDATE_BOOLEAN
|
|
||||||
);
|
|
||||||
|
|
||||||
// Default threshold abhängig vom Modus
|
// Default threshold abhängig vom Modus
|
||||||
$defaultThreshold = $strictMode ? 1.0 : 0.7;
|
$defaultThreshold = $strictMode ? 1.0 : 0.7;
|
||||||
$threshold = isset($_ENV['SESSION_FINGERPRINT_THRESHOLD'])
|
$threshold = $env->getFloat('SESSION_FINGERPRINT_THRESHOLD', $defaultThreshold);
|
||||||
? (float) $_ENV['SESSION_FINGERPRINT_THRESHOLD']
|
|
||||||
: $defaultThreshold;
|
|
||||||
|
|
||||||
// Auto-Korrektur: Im strict mode MUSS threshold 1.0 sein
|
// Auto-Korrektur: Im strict mode MUSS threshold 1.0 sein
|
||||||
if ($strictMode && $threshold < 1.0) {
|
if ($strictMode && $threshold < 1.0) {
|
||||||
@@ -180,30 +175,12 @@ final readonly class SessionFingerprintConfig
|
|||||||
// Überschreibe mit spezifischen Env-Vars wenn vorhanden
|
// Überschreibe mit spezifischen Env-Vars wenn vorhanden
|
||||||
return new self(
|
return new self(
|
||||||
strictMode: $strictMode,
|
strictMode: $strictMode,
|
||||||
userAgent: filter_var(
|
userAgent: $env->getBool('SESSION_FINGERPRINT_USER_AGENT', $config->userAgent),
|
||||||
$_ENV['SESSION_FINGERPRINT_USER_AGENT'] ?? $config->userAgent,
|
acceptLanguage: $env->getBool('SESSION_FINGERPRINT_ACCEPT_LANGUAGE', $config->acceptLanguage),
|
||||||
FILTER_VALIDATE_BOOLEAN
|
acceptEncoding: $env->getBool('SESSION_FINGERPRINT_ACCEPT_ENCODING', $config->acceptEncoding),
|
||||||
),
|
ipPrefix: $env->getBool('SESSION_FINGERPRINT_IP_PREFIX', $config->ipPrefix),
|
||||||
acceptLanguage: filter_var(
|
secChUa: $env->getBool('SESSION_FINGERPRINT_SEC_CH_UA', $config->secChUa),
|
||||||
$_ENV['SESSION_FINGERPRINT_ACCEPT_LANGUAGE'] ?? $config->acceptLanguage,
|
dnt: $env->getBool('SESSION_FINGERPRINT_DNT', $config->dnt),
|
||||||
FILTER_VALIDATE_BOOLEAN
|
|
||||||
),
|
|
||||||
acceptEncoding: filter_var(
|
|
||||||
$_ENV['SESSION_FINGERPRINT_ACCEPT_ENCODING'] ?? $config->acceptEncoding,
|
|
||||||
FILTER_VALIDATE_BOOLEAN
|
|
||||||
),
|
|
||||||
ipPrefix: filter_var(
|
|
||||||
$_ENV['SESSION_FINGERPRINT_IP_PREFIX'] ?? $config->ipPrefix,
|
|
||||||
FILTER_VALIDATE_BOOLEAN
|
|
||||||
),
|
|
||||||
secChUa: filter_var(
|
|
||||||
$_ENV['SESSION_FINGERPRINT_SEC_CH_UA'] ?? $config->secChUa,
|
|
||||||
FILTER_VALIDATE_BOOLEAN
|
|
||||||
),
|
|
||||||
dnt: filter_var(
|
|
||||||
$_ENV['SESSION_FINGERPRINT_DNT'] ?? $config->dnt,
|
|
||||||
FILTER_VALIDATE_BOOLEAN
|
|
||||||
),
|
|
||||||
similarityThreshold: $threshold,
|
similarityThreshold: $threshold,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace App\Framework\Http\Session;
|
namespace App\Framework\Http\Session;
|
||||||
|
|
||||||
|
use App\Framework\Config\Environment;
|
||||||
use App\Framework\Context\ContextType;
|
use App\Framework\Context\ContextType;
|
||||||
use App\Framework\Core\Events\EventDispatcher;
|
use App\Framework\Core\Events\EventDispatcher;
|
||||||
use App\Framework\DateTime\Clock;
|
use App\Framework\DateTime\Clock;
|
||||||
@@ -55,7 +56,8 @@ final readonly class SessionInitializer
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Session Fingerprinting konfigurieren
|
// Session Fingerprinting konfigurieren
|
||||||
$fingerprintConfig = SessionFingerprintConfig::fromEnvironment();
|
$env = $this->container->get(Environment::class);
|
||||||
|
$fingerprintConfig = SessionFingerprintConfig::fromEnvironment($env);
|
||||||
$fingerprint = new SessionFingerprint($fingerprintConfig);
|
$fingerprint = new SessionFingerprint($fingerprintConfig);
|
||||||
|
|
||||||
// EventDispatcher optional laden
|
// EventDispatcher optional laden
|
||||||
@@ -65,7 +67,8 @@ final readonly class SessionInitializer
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Cookie-Konfiguration basierend auf Umgebung
|
// Cookie-Konfiguration basierend auf Umgebung
|
||||||
$isProduction = ($_ENV['APP_ENV'] ?? 'development') === 'production';
|
$appEnv = $env->getString('APP_ENV', 'development');
|
||||||
|
$isProduction = $appEnv === 'production';
|
||||||
$cookieConfig = $isProduction
|
$cookieConfig = $isProduction
|
||||||
? SessionCookieConfig::forProduction()
|
? SessionCookieConfig::forProduction()
|
||||||
: SessionCookieConfig::forDevelopment();
|
: SessionCookieConfig::forDevelopment();
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace App\Framework\MachineLearning\ModelManagement;
|
namespace App\Framework\MachineLearning\ModelManagement;
|
||||||
|
|
||||||
|
use App\Framework\Config\Environment;
|
||||||
use App\Framework\Core\ValueObjects\Duration;
|
use App\Framework\Core\ValueObjects\Duration;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -53,21 +54,19 @@ final readonly class MLConfig
|
|||||||
/**
|
/**
|
||||||
* Create configuration from environment
|
* Create configuration from environment
|
||||||
*/
|
*/
|
||||||
public static function fromEnvironment(array $env = []): self
|
public static function fromEnvironment(Environment $env): self
|
||||||
{
|
{
|
||||||
$getEnv = fn(string $key, mixed $default = null): mixed => $env[$key] ?? $_ENV[$key] ?? $default;
|
|
||||||
|
|
||||||
return new self(
|
return new self(
|
||||||
monitoringEnabled: filter_var($getEnv('ML_MONITORING_ENABLED', true), FILTER_VALIDATE_BOOLEAN),
|
monitoringEnabled: $env->getBool('ML_MONITORING_ENABLED', true),
|
||||||
driftThreshold: (float) $getEnv('ML_DRIFT_THRESHOLD', 0.15),
|
driftThreshold: $env->getFloat('ML_DRIFT_THRESHOLD', 0.15),
|
||||||
performanceWindow: Duration::fromHours((int) $getEnv('ML_PERFORMANCE_WINDOW_HOURS', 24)),
|
performanceWindow: Duration::fromHours($env->getInt('ML_PERFORMANCE_WINDOW_HOURS', 24)),
|
||||||
autoTuningEnabled: filter_var($getEnv('ML_AUTO_TUNING_ENABLED', false), FILTER_VALIDATE_BOOLEAN),
|
autoTuningEnabled: $env->getBool('ML_AUTO_TUNING_ENABLED', false),
|
||||||
predictionCacheTtl: Duration::fromSeconds((int) $getEnv('ML_PREDICTION_CACHE_TTL', 3600)),
|
predictionCacheTtl: Duration::fromSeconds($env->getInt('ML_PREDICTION_CACHE_TTL', 3600)),
|
||||||
modelCacheTtl: Duration::fromSeconds((int) $getEnv('ML_MODEL_CACHE_TTL', 7200)),
|
modelCacheTtl: Duration::fromSeconds($env->getInt('ML_MODEL_CACHE_TTL', 7200)),
|
||||||
baselineUpdateInterval: Duration::fromSeconds((int) $getEnv('ML_BASELINE_UPDATE_INTERVAL', 86400)),
|
baselineUpdateInterval: Duration::fromSeconds($env->getInt('ML_BASELINE_UPDATE_INTERVAL', 86400)),
|
||||||
minPredictionsForDrift: (int) $getEnv('ML_MIN_PREDICTIONS_FOR_DRIFT', 100),
|
minPredictionsForDrift: $env->getInt('ML_MIN_PREDICTIONS_FOR_DRIFT', 100),
|
||||||
confidenceAlertThreshold: (float) $getEnv('ML_CONFIDENCE_ALERT_THRESHOLD', 0.65),
|
confidenceAlertThreshold: $env->getFloat('ML_CONFIDENCE_ALERT_THRESHOLD', 0.65),
|
||||||
accuracyAlertThreshold: (float) $getEnv('ML_ACCURACY_ALERT_THRESHOLD', 0.75)
|
accuracyAlertThreshold: $env->getFloat('ML_ACCURACY_ALERT_THRESHOLD', 0.75)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ declare(strict_types=1);
|
|||||||
namespace App\Framework\Notification\Channels\Telegram\Webhook;
|
namespace App\Framework\Notification\Channels\Telegram\Webhook;
|
||||||
|
|
||||||
use App\Framework\Attributes\Route;
|
use App\Framework\Attributes\Route;
|
||||||
|
use App\Framework\Config\Environment;
|
||||||
use App\Framework\Http\HttpRequest;
|
use App\Framework\Http\HttpRequest;
|
||||||
use App\Framework\Http\Method;
|
use App\Framework\Http\Method;
|
||||||
use App\Framework\Router\Result\JsonResult;
|
use App\Framework\Router\Result\JsonResult;
|
||||||
@@ -20,7 +21,8 @@ use App\Framework\Webhook\Processing\WebhookRequestHandler;
|
|||||||
final readonly class TelegramWebhookController
|
final readonly class TelegramWebhookController
|
||||||
{
|
{
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private WebhookRequestHandler $webhookHandler
|
private WebhookRequestHandler $webhookHandler,
|
||||||
|
private Environment $environment
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -47,7 +49,7 @@ final readonly class TelegramWebhookController
|
|||||||
public function handleWebhook(HttpRequest $request): JsonResult
|
public function handleWebhook(HttpRequest $request): JsonResult
|
||||||
{
|
{
|
||||||
// Get secret token from environment
|
// Get secret token from environment
|
||||||
$secretToken = $_ENV['TELEGRAM_WEBHOOK_SECRET'] ?? '';
|
$secretToken = $this->environment->getString('TELEGRAM_WEBHOOK_SECRET', '');
|
||||||
|
|
||||||
if (empty($secretToken)) {
|
if (empty($secretToken)) {
|
||||||
return new JsonResult([
|
return new JsonResult([
|
||||||
|
|||||||
@@ -24,6 +24,8 @@ final readonly class HttpRouter implements Router
|
|||||||
$host = $request->server->getHttpHost();
|
$host = $request->server->getHttpHost();
|
||||||
$subdomain = $this->extractSubdomain($host);
|
$subdomain = $this->extractSubdomain($host);
|
||||||
|
|
||||||
|
error_log("🔍 ROUTER DEBUG: Host={$host}, Subdomain=" . ($subdomain ?: 'NONE') . ", Path={$path}, Method={$method->value}");
|
||||||
|
|
||||||
// 1. Try subdomain-specific routes first
|
// 1. Try subdomain-specific routes first
|
||||||
if ($subdomain) {
|
if ($subdomain) {
|
||||||
$subdomainKey = 'exact:' . $subdomain;
|
$subdomainKey = 'exact:' . $subdomain;
|
||||||
|
|||||||
@@ -35,12 +35,39 @@ final readonly class RouterSetup
|
|||||||
#$routeCache = new RouteCache($this->pathProvider->getCachePath('routes.cache.php'));
|
#$routeCache = new RouteCache($this->pathProvider->getCachePath('routes.cache.php'));
|
||||||
|
|
||||||
$routeCount = $this->results->attributes->getCount(Route::class);
|
$routeCount = $this->results->attributes->getCount(Route::class);
|
||||||
|
error_log("🚦 ROUTER SETUP: Route count from discovery = {$routeCount}");
|
||||||
|
|
||||||
if ($routeCount > 0) {
|
if ($routeCount > 0) {
|
||||||
$discoveredRoutes = $this->results->attributes->get(Route::class);
|
$discoveredRoutes = $this->results->attributes->get(Route::class);
|
||||||
|
|
||||||
// Direct DiscoveredAttribute API
|
// Direct DiscoveredAttribute API
|
||||||
|
error_log("🚦 ROUTER SETUP: About to compile routes");
|
||||||
|
try {
|
||||||
$optimizedRoutes = $this->routeCompiler->compileOptimized(...$discoveredRoutes);
|
$optimizedRoutes = $this->routeCompiler->compileOptimized(...$discoveredRoutes);
|
||||||
|
error_log("🚦 ROUTER SETUP: Routes compiled successfully");
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
error_log("🚦 ROUTER SETUP: ERROR compiling routes: " . $e->getMessage());
|
||||||
|
error_log("🚦 ROUTER SETUP: Exception trace: " . $e->getTraceAsString());
|
||||||
|
throw $e;
|
||||||
|
}
|
||||||
|
|
||||||
|
// DEBUG: Log available static routes structure
|
||||||
|
error_log("🚦 ROUTER SETUP: About to analyze route structure");
|
||||||
|
$staticRoutes = $optimizedRoutes->getStaticRoutes();
|
||||||
|
error_log("🚦 ROUTER SETUP: HTTP Methods count = " . count($staticRoutes));
|
||||||
|
|
||||||
|
// Check GET method subdomain keys
|
||||||
|
$getSubdomainKeys = $optimizedRoutes->getSubdomainKeys(\App\Framework\Http\Method::GET);
|
||||||
|
error_log("🚦 ROUTER SETUP: GET method has subdomain keys: " . json_encode($getSubdomainKeys));
|
||||||
|
|
||||||
|
// Log routes per subdomain for GET
|
||||||
|
foreach ($getSubdomainKeys as $subdomainKey) {
|
||||||
|
$routes = $optimizedRoutes->getStaticRoutesForSubdomain(\App\Framework\Http\Method::GET, $subdomainKey);
|
||||||
|
error_log("🚦 ROUTER SETUP: GET subdomain '{$subdomainKey}' has " . count($routes) . " routes");
|
||||||
|
foreach ($routes as $path => $route) {
|
||||||
|
error_log("🚦 ROUTER SETUP: - {$path}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Cache speichern
|
// Cache speichern
|
||||||
#$routeCache->save($optimizedRoutes);
|
#$routeCache->save($optimizedRoutes);
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace App\Framework\Security\RequestSigning;
|
namespace App\Framework\Security\RequestSigning;
|
||||||
|
|
||||||
|
use App\Framework\Config\Environment;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Configuration for request signing functionality
|
* Configuration for request signing functionality
|
||||||
*/
|
*/
|
||||||
@@ -24,26 +26,28 @@ final readonly class RequestSigningConfig
|
|||||||
/**
|
/**
|
||||||
* Create configuration from environment variables
|
* Create configuration from environment variables
|
||||||
*/
|
*/
|
||||||
public static function fromEnvironment(): self
|
public static function fromEnvironment(Environment $env): self
|
||||||
{
|
{
|
||||||
$enabled = filter_var($_ENV['REQUEST_SIGNING_ENABLED'] ?? 'false', FILTER_VALIDATE_BOOLEAN);
|
$enabled = $env->getBool('REQUEST_SIGNING_ENABLED', false);
|
||||||
$requireSignature = filter_var($_ENV['REQUEST_SIGNING_REQUIRED'] ?? 'false', FILTER_VALIDATE_BOOLEAN);
|
$requireSignature = $env->getBool('REQUEST_SIGNING_REQUIRED', false);
|
||||||
|
|
||||||
$exemptPaths = [];
|
$exemptPaths = [];
|
||||||
if (isset($_ENV['REQUEST_SIGNING_EXEMPT_PATHS'])) {
|
$exemptPathsString = $env->getString('REQUEST_SIGNING_EXEMPT_PATHS', '');
|
||||||
$exemptPaths = array_filter(array_map('trim', explode(',', $_ENV['REQUEST_SIGNING_EXEMPT_PATHS'])));
|
if ($exemptPathsString !== '') {
|
||||||
|
$exemptPaths = array_filter(array_map('trim', explode(',', $exemptPathsString)));
|
||||||
}
|
}
|
||||||
|
|
||||||
$defaultHeaders = ['(request-target)', 'host', 'date'];
|
$defaultHeaders = ['(request-target)', 'host', 'date'];
|
||||||
if (isset($_ENV['REQUEST_SIGNING_DEFAULT_HEADERS'])) {
|
$defaultHeadersString = $env->getString('REQUEST_SIGNING_DEFAULT_HEADERS', '');
|
||||||
$defaultHeaders = array_filter(array_map('trim', explode(',', $_ENV['REQUEST_SIGNING_DEFAULT_HEADERS'])));
|
if ($defaultHeadersString !== '') {
|
||||||
|
$defaultHeaders = array_filter(array_map('trim', explode(',', $defaultHeadersString)));
|
||||||
}
|
}
|
||||||
|
|
||||||
$maxClockSkew = (int) ($_ENV['REQUEST_SIGNING_MAX_CLOCK_SKEW'] ?? 300);
|
$maxClockSkew = $env->getInt('REQUEST_SIGNING_MAX_CLOCK_SKEW', 300);
|
||||||
$defaultExpiry = (int) ($_ENV['REQUEST_SIGNING_DEFAULT_EXPIRY'] ?? 3600);
|
$defaultExpiry = $env->getInt('REQUEST_SIGNING_DEFAULT_EXPIRY', 3600);
|
||||||
|
|
||||||
$algorithm = SigningAlgorithm::tryFrom($_ENV['REQUEST_SIGNING_ALGORITHM'] ?? 'hmac-sha256')
|
$algorithmString = $env->getString('REQUEST_SIGNING_ALGORITHM', 'hmac-sha256');
|
||||||
?? SigningAlgorithm::HMAC_SHA256;
|
$algorithm = SigningAlgorithm::tryFrom($algorithmString) ?? SigningAlgorithm::HMAC_SHA256;
|
||||||
|
|
||||||
return new self(
|
return new self(
|
||||||
enabled: $enabled,
|
enabled: $enabled,
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ declare(strict_types=1);
|
|||||||
namespace App\Framework\Security\RequestSigning;
|
namespace App\Framework\Security\RequestSigning;
|
||||||
|
|
||||||
use App\Framework\Cache\Cache;
|
use App\Framework\Cache\Cache;
|
||||||
|
use App\Framework\Config\Environment;
|
||||||
use App\Framework\Database\EntityManager;
|
use App\Framework\Database\EntityManager;
|
||||||
use App\Framework\DateTime\Clock;
|
use App\Framework\DateTime\Clock;
|
||||||
use App\Framework\DI\Container;
|
use App\Framework\DI\Container;
|
||||||
@@ -58,7 +59,9 @@ final readonly class RequestSigningInitializer
|
|||||||
*/
|
*/
|
||||||
private function getConfig(): RequestSigningConfig
|
private function getConfig(): RequestSigningConfig
|
||||||
{
|
{
|
||||||
$isProduction = ($_ENV['APP_ENV'] ?? 'development') === 'production';
|
$env = $this->container->get(Environment::class);
|
||||||
|
$appEnv = $env->getString('APP_ENV', 'development');
|
||||||
|
$isProduction = $appEnv === 'production';
|
||||||
|
|
||||||
return $isProduction
|
return $isProduction
|
||||||
? RequestSigningConfig::production()
|
? RequestSigningConfig::production()
|
||||||
@@ -70,7 +73,9 @@ final readonly class RequestSigningInitializer
|
|||||||
*/
|
*/
|
||||||
private function createKeyRepository(RequestSigningConfig $config): SigningKeyRepository
|
private function createKeyRepository(RequestSigningConfig $config): SigningKeyRepository
|
||||||
{
|
{
|
||||||
$isProduction = ($_ENV['APP_ENV'] ?? 'development') === 'production';
|
$env = $this->container->get(Environment::class);
|
||||||
|
$appEnv = $env->getString('APP_ENV', 'development');
|
||||||
|
$isProduction = $appEnv === 'production';
|
||||||
|
|
||||||
if ($isProduction && $this->container->has(EntityManager::class)) {
|
if ($isProduction && $this->container->has(EntityManager::class)) {
|
||||||
return new EntityManagerSigningKeyRepository(
|
return new EntityManagerSigningKeyRepository(
|
||||||
@@ -94,7 +99,9 @@ final readonly class RequestSigningInitializer
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Add a default development key
|
// Add a default development key
|
||||||
if (($_ENV['APP_ENV'] ?? 'development') === 'development') {
|
$env = $this->container->get(Environment::class);
|
||||||
|
$appEnv = $env->getString('APP_ENV', 'development');
|
||||||
|
if ($appEnv === 'development') {
|
||||||
if ($keyRepository instanceof InMemorySigningKeyRepository) {
|
if ($keyRepository instanceof InMemorySigningKeyRepository) {
|
||||||
$keyRepository->addDefaultTestKey();
|
$keyRepository->addDefaultTestKey();
|
||||||
}
|
}
|
||||||
@@ -109,9 +116,11 @@ final readonly class RequestSigningInitializer
|
|||||||
*/
|
*/
|
||||||
private function loadKeysFromEnvironment(SigningKeyRepository $keyRepository): void
|
private function loadKeysFromEnvironment(SigningKeyRepository $keyRepository): void
|
||||||
{
|
{
|
||||||
|
$env = $this->container->get(Environment::class);
|
||||||
|
|
||||||
// Load HMAC keys from environment
|
// Load HMAC keys from environment
|
||||||
$hmacKeys = $_ENV['REQUEST_SIGNING_HMAC_KEYS'] ?? '';
|
$hmacKeys = $env->getString('REQUEST_SIGNING_HMAC_KEYS', '');
|
||||||
if ($hmacKeys) {
|
if ($hmacKeys !== '') {
|
||||||
$keys = json_decode($hmacKeys, true);
|
$keys = json_decode($hmacKeys, true);
|
||||||
if (is_array($keys)) {
|
if (is_array($keys)) {
|
||||||
foreach ($keys as $keyData) {
|
foreach ($keys as $keyData) {
|
||||||
@@ -138,8 +147,8 @@ final readonly class RequestSigningInitializer
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Load RSA keys from environment
|
// Load RSA keys from environment
|
||||||
$rsaKeys = $_ENV['REQUEST_SIGNING_RSA_KEYS'] ?? '';
|
$rsaKeys = $env->getString('REQUEST_SIGNING_RSA_KEYS', '');
|
||||||
if ($rsaKeys) {
|
if ($rsaKeys !== '') {
|
||||||
$keys = json_decode($rsaKeys, true);
|
$keys = json_decode($rsaKeys, true);
|
||||||
if (is_array($keys)) {
|
if (is_array($keys)) {
|
||||||
foreach ($keys as $keyData) {
|
foreach ($keys as $keyData) {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace App\Framework\View\Dom\Transformer;
|
namespace App\Framework\View\Dom\Transformer;
|
||||||
|
|
||||||
|
use App\Framework\Config\AppConfig;
|
||||||
use App\Framework\LiveComponents\Contracts\ComponentRegistryInterface;
|
use App\Framework\LiveComponents\Contracts\ComponentRegistryInterface;
|
||||||
use App\Framework\LiveComponents\Performance\ComponentMetadataCacheInterface;
|
use App\Framework\LiveComponents\Performance\ComponentMetadataCacheInterface;
|
||||||
use App\Framework\LiveComponents\ValueObjects\ComponentId;
|
use App\Framework\LiveComponents\ValueObjects\ComponentId;
|
||||||
@@ -34,7 +35,8 @@ final class XComponentTransformer implements NodeVisitor, AstTransformer
|
|||||||
private readonly ComponentRegistryInterface $componentRegistry,
|
private readonly ComponentRegistryInterface $componentRegistry,
|
||||||
private readonly StaticComponentRenderer $staticComponentRenderer,
|
private readonly StaticComponentRenderer $staticComponentRenderer,
|
||||||
private readonly ComponentMetadataCacheInterface $metadataCache,
|
private readonly ComponentMetadataCacheInterface $metadataCache,
|
||||||
private readonly HtmlParser $parser
|
private readonly HtmlParser $parser,
|
||||||
|
private readonly AppConfig $appConfig
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -391,7 +393,7 @@ final class XComponentTransformer implements NodeVisitor, AstTransformer
|
|||||||
*/
|
*/
|
||||||
private function handleTransformError(ElementNode $element, \Throwable $e): void
|
private function handleTransformError(ElementNode $element, \Throwable $e): void
|
||||||
{
|
{
|
||||||
$isDebug = ($_ENV['APP_ENV'] ?? 'production') === 'development';
|
$isDebug = $this->appConfig->isDebug();
|
||||||
|
|
||||||
if ($isDebug) {
|
if ($isDebug) {
|
||||||
// Create error node in place of component
|
// Create error node in place of component
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ declare(strict_types=1);
|
|||||||
namespace App\Framework\View\Processors;
|
namespace App\Framework\View\Processors;
|
||||||
|
|
||||||
use App\Framework\Config\AppConfig;
|
use App\Framework\Config\AppConfig;
|
||||||
|
use App\Framework\Config\Environment;
|
||||||
use App\Framework\DI\Container;
|
use App\Framework\DI\Container;
|
||||||
use App\Framework\LiveComponents\ComponentRegistry;
|
use App\Framework\LiveComponents\ComponentRegistry;
|
||||||
use App\Framework\LiveComponents\Contracts\LiveComponentContract;
|
use App\Framework\LiveComponents\Contracts\LiveComponentContract;
|
||||||
@@ -449,8 +450,13 @@ final class PlaceholderReplacer implements StringProcessor
|
|||||||
|
|
||||||
return $appConfig->isDebug();
|
return $appConfig->isDebug();
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
// Fallback zu Environment-Variable falls AppConfig nicht verfügbar
|
// Fallback zu Environment falls AppConfig nicht verfügbar
|
||||||
return ($_ENV['APP_ENV'] ?? 'production') === 'development';
|
try {
|
||||||
|
$env = $this->container->get(Environment::class);
|
||||||
|
return $env->getString('APP_ENV', 'production') === 'development';
|
||||||
|
} catch (\Throwable) {
|
||||||
|
return false; // Safe fallback: production mode
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace App\Framework\View\Processors;
|
namespace App\Framework\View\Processors;
|
||||||
|
|
||||||
|
use App\Framework\Config\AppConfig;
|
||||||
use App\Framework\LiveComponents\Contracts\ComponentRegistryInterface;
|
use App\Framework\LiveComponents\Contracts\ComponentRegistryInterface;
|
||||||
use App\Framework\LiveComponents\Performance\ComponentMetadataCacheInterface;
|
use App\Framework\LiveComponents\Performance\ComponentMetadataCacheInterface;
|
||||||
use App\Framework\LiveComponents\ValueObjects\ComponentData;
|
use App\Framework\LiveComponents\ValueObjects\ComponentData;
|
||||||
@@ -42,7 +43,8 @@ final readonly class XComponentProcessor implements DomProcessor
|
|||||||
public function __construct(
|
public function __construct(
|
||||||
private ComponentRegistryInterface $componentRegistry,
|
private ComponentRegistryInterface $componentRegistry,
|
||||||
private ComponentMetadataCacheInterface $metadataCache,
|
private ComponentMetadataCacheInterface $metadataCache,
|
||||||
private DomComponentService $componentService
|
private DomComponentService $componentService,
|
||||||
|
private AppConfig $appConfig
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -349,8 +351,8 @@ final readonly class XComponentProcessor implements DomProcessor
|
|||||||
HTMLElement $element,
|
HTMLElement $element,
|
||||||
\Throwable $e
|
\Throwable $e
|
||||||
): void {
|
): void {
|
||||||
// Check if we're in debug mode (via environment or config)
|
// Check if we're in debug mode
|
||||||
$isDebug = ($_ENV['APP_ENV'] ?? 'production') === 'development';
|
$isDebug = $this->appConfig->isDebug();
|
||||||
|
|
||||||
if ($isDebug) {
|
if ($isDebug) {
|
||||||
// Show error message in place of component
|
// Show error message in place of component
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ declare(strict_types=1);
|
|||||||
namespace App\Framework\WebPush\Controllers;
|
namespace App\Framework\WebPush\Controllers;
|
||||||
|
|
||||||
use App\Framework\Attributes\Route;
|
use App\Framework\Attributes\Route;
|
||||||
|
use App\Framework\Config\Environment;
|
||||||
use App\Framework\Http\Method;
|
use App\Framework\Http\Method;
|
||||||
use App\Framework\Http\Request;
|
use App\Framework\Http\Request;
|
||||||
use App\Framework\Http\Session\Session;
|
use App\Framework\Http\Session\Session;
|
||||||
@@ -26,6 +27,7 @@ final readonly class WebPushController
|
|||||||
private SubscriptionRepository $subscriptionRepository,
|
private SubscriptionRepository $subscriptionRepository,
|
||||||
private WebPushService $webPushService,
|
private WebPushService $webPushService,
|
||||||
private Session $session,
|
private Session $session,
|
||||||
|
private Environment $environment
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -305,8 +307,7 @@ final readonly class WebPushController
|
|||||||
#[Route(path: '/api/push/vapid-key', method: Method::GET)]
|
#[Route(path: '/api/push/vapid-key', method: Method::GET)]
|
||||||
public function vapidKey(Request $request): JsonResult
|
public function vapidKey(Request $request): JsonResult
|
||||||
{
|
{
|
||||||
// This should be injected from config
|
$publicKey = $this->environment->getString('VAPID_PUBLIC_KEY');
|
||||||
$publicKey = $_ENV['VAPID_PUBLIC_KEY'] ?? null;
|
|
||||||
|
|
||||||
if ($publicKey === null) {
|
if ($publicKey === null) {
|
||||||
return new JsonResult(
|
return new JsonResult(
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ declare(strict_types=1);
|
|||||||
namespace App\Framework\WebPush;
|
namespace App\Framework\WebPush;
|
||||||
|
|
||||||
use App\Framework\Cache\Cache;
|
use App\Framework\Cache\Cache;
|
||||||
|
use App\Framework\Config\Environment;
|
||||||
use App\Framework\DI\Container;
|
use App\Framework\DI\Container;
|
||||||
use App\Framework\DI\Initializer;
|
use App\Framework\DI\Initializer;
|
||||||
use App\Framework\WebPush\Console\VapidKeyCommands;
|
use App\Framework\WebPush\Console\VapidKeyCommands;
|
||||||
@@ -42,9 +43,11 @@ final readonly class WebPushInitializer
|
|||||||
public function initWebPushService(): ?WebPushService
|
public function initWebPushService(): ?WebPushService
|
||||||
{
|
{
|
||||||
// Try to load VAPID keys from environment
|
// Try to load VAPID keys from environment
|
||||||
$publicKey = $_ENV['VAPID_PUBLIC_KEY'] ?? null;
|
$env = $this->container->get(Environment::class);
|
||||||
$privateKey = $_ENV['VAPID_PRIVATE_KEY'] ?? null;
|
|
||||||
$subject = $_ENV['VAPID_SUBJECT'] ?? 'mailto:admin@example.com';
|
$publicKey = $env->getString('VAPID_PUBLIC_KEY');
|
||||||
|
$privateKey = $env->getString('VAPID_PRIVATE_KEY');
|
||||||
|
$subject = $env->getString('VAPID_SUBJECT', 'mailto:admin@example.com');
|
||||||
|
|
||||||
if ($publicKey !== null && $privateKey !== null) {
|
if ($publicKey !== null && $privateKey !== null) {
|
||||||
$vapidKeys = new VapidKeyPair($publicKey, $privateKey);
|
$vapidKeys = new VapidKeyPair($publicKey, $privateKey);
|
||||||
|
|||||||
57
tests/debug/check-discovery.php
Normal file
57
tests/debug/check-discovery.php
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
require __DIR__ . '/../../vendor/autoload.php';
|
||||||
|
|
||||||
|
use App\Framework\Core\AppBootstrapper;
|
||||||
|
use App\Framework\Discovery\DiscoveryRegistry;
|
||||||
|
use App\Framework\Attributes\Route;
|
||||||
|
|
||||||
|
echo "=== Discovery Debug Script ===\n\n";
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Bootstrap application
|
||||||
|
$bootstrapper = new AppBootstrapper();
|
||||||
|
$container = $bootstrapper->bootstrap();
|
||||||
|
|
||||||
|
// Get DiscoveryRegistry
|
||||||
|
$registry = $container->get(DiscoveryRegistry::class);
|
||||||
|
|
||||||
|
// Check Route attribute count
|
||||||
|
$routeCount = $registry->attributes->getCount(Route::class);
|
||||||
|
|
||||||
|
echo "📊 Discovery Stats:\n";
|
||||||
|
echo " - Total Route Attributes Found: {$routeCount}\n\n";
|
||||||
|
|
||||||
|
if ($routeCount === 0) {
|
||||||
|
echo "❌ NO ROUTES DISCOVERED!\n";
|
||||||
|
echo " This explains why the router cannot find any routes.\n\n";
|
||||||
|
|
||||||
|
// Check if discovery ran at all
|
||||||
|
echo "🔍 Checking discovery results...\n";
|
||||||
|
$allAttributes = $registry->attributes->all();
|
||||||
|
echo " - Total attribute types discovered: " . count($allAttributes) . "\n";
|
||||||
|
foreach ($allAttributes as $attrType => $instances) {
|
||||||
|
echo " - {$attrType}: " . count($instances) . " instances\n";
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
echo "✅ Routes discovered successfully!\n";
|
||||||
|
echo " Listing first 10 routes:\n\n";
|
||||||
|
|
||||||
|
$routes = $registry->attributes->get(Route::class);
|
||||||
|
$count = 0;
|
||||||
|
foreach ($routes as $routeData) {
|
||||||
|
if ($count++ >= 10) break;
|
||||||
|
|
||||||
|
$route = $routeData->instance;
|
||||||
|
echo " - {$route->method->value} {$route->path}\n";
|
||||||
|
echo " Controller: {$routeData->className}\n";
|
||||||
|
echo " Method: {$routeData->method->name}\n\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
echo "❌ Error: " . $e->getMessage() . "\n";
|
||||||
|
echo " File: {$e->getFile()}:{$e->getLine()}\n";
|
||||||
|
echo " Stack trace:\n{$e->getTraceAsString()}\n";
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user