Compare commits
14 Commits
cba9aef518
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 52b3a23b46 | |||
| 05fe1fedf0 | |||
| 75612064d9 | |||
| 14c83635f9 | |||
| 5f216f1317 | |||
| 4953d192c0 | |||
| 77206224a2 | |||
| 7548e241be | |||
| 5835eb15ed | |||
| 81374cc659 | |||
| c0c346a9ed | |||
| 0fb028d19a | |||
| 58c7907915 | |||
| 320f2f30af |
17
.gitea/workflows/test.yml
Normal file
17
.gitea/workflows/test.yml
Normal file
@@ -0,0 +1,17 @@
|
||||
name: PHPUnit & CS-Fixer
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main, develop ]
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install dependencies
|
||||
run: composer install --no-interaction --prefer-dist
|
||||
- name: Run php-cs-fixer
|
||||
run: vendor/bin/php-cs-fixer fix --dry-run --diff
|
||||
- name: Run PHPUnit
|
||||
run: vendor/bin/phpunit
|
||||
10
.gitignore
vendored
10
.gitignore
vendored
@@ -84,3 +84,13 @@ docker-compose.override.yml
|
||||
/var/
|
||||
/vendor/
|
||||
###< symfony/framework-bundle ###
|
||||
|
||||
###> phpunit/phpunit ###
|
||||
/phpunit.xml
|
||||
/.phpunit.cache/
|
||||
###< phpunit/phpunit ###
|
||||
|
||||
###> friendsofphp/php-cs-fixer ###
|
||||
/.php-cs-fixer.php
|
||||
/.php-cs-fixer.cache
|
||||
###< friendsofphp/php-cs-fixer ###
|
||||
|
||||
30
.php-cs-fixer.dist.php
Normal file
30
.php-cs-fixer.dist.php
Normal file
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
$finder = (new PhpCsFixer\Finder())
|
||||
->in(__DIR__)
|
||||
->exclude('var')
|
||||
->exclude('vendor')
|
||||
->exclude('migrations')
|
||||
->notPath('bin/console')
|
||||
->notPath('public/index.php')
|
||||
;
|
||||
|
||||
return (new PhpCsFixer\Config())
|
||||
->setRules([
|
||||
'@Symfony' => true,
|
||||
'array_syntax' => ['syntax' => 'short'],
|
||||
'ordered_imports' => ['sort_algorithm' => 'alpha'],
|
||||
'no_unused_imports' => true,
|
||||
'not_operator_with_successor_space' => true,
|
||||
'trailing_comma_in_multiline' => true,
|
||||
'phpdoc_scalar' => true,
|
||||
'unary_operator_spaces' => true,
|
||||
'binary_operator_spaces' => true,
|
||||
'blank_line_before_statement' => [
|
||||
'statements' => ['break', 'continue', 'declare', 'return', 'throw', 'try'],
|
||||
],
|
||||
'phpdoc_single_line_var_spacing' => true,
|
||||
'phpdoc_var_without_name' => true,
|
||||
])
|
||||
->setFinder($finder)
|
||||
;
|
||||
57
Dockerfile
57
Dockerfile
@@ -1,46 +1,57 @@
|
||||
# Production-ready Symfony Dockerfile
|
||||
FROM php:8.4-apache
|
||||
|
||||
# Install system dependencies
|
||||
RUN apt-get update && apt-get install -y \
|
||||
git \
|
||||
curl \
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
libpng-dev \
|
||||
libonig-dev \
|
||||
libxml2-dev \
|
||||
libicu-dev \
|
||||
zip \
|
||||
unzip \
|
||||
libzip-dev \
|
||||
&& apt-get clean && rm -rf /var/lib/apt/lists/*
|
||||
git \
|
||||
curl \
|
||||
unzip \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Install PHP extensions for Symfony
|
||||
# Install PHP extensions needed by Symfony
|
||||
RUN docker-php-ext-configure intl \
|
||||
&& docker-php-ext-install pdo_mysql mysqli mbstring exif pcntl bcmath gd zip intl opcache
|
||||
&& docker-php-ext-install -j$(nproc) \
|
||||
pdo_mysql \
|
||||
mbstring \
|
||||
exif \
|
||||
pcntl \
|
||||
bcmath \
|
||||
gd \
|
||||
zip \
|
||||
intl \
|
||||
opcache
|
||||
|
||||
# Configure opcache for Symfony
|
||||
RUN echo "opcache.enable=1" >> /usr/local/etc/php/conf.d/docker-php-ext-opcache.ini \
|
||||
&& echo "opcache.memory_consumption=256" >> /usr/local/etc/php/conf.d/docker-php-ext-opcache.ini \
|
||||
&& echo "opcache.max_accelerated_files=20000" >> /usr/local/etc/php/conf.d/docker-php-ext-opcache.ini \
|
||||
&& echo "opcache.validate_timestamps=0" >> /usr/local/etc/php/conf.d/docker-php-ext-opcache.ini
|
||||
# Enable Apache modules
|
||||
RUN a2enmod rewrite headers
|
||||
|
||||
# Enable Apache mod_rewrite
|
||||
RUN a2enmod rewrite
|
||||
|
||||
# Enable Apache headers module
|
||||
RUN a2enmod headers
|
||||
|
||||
# Copy custom Apache configuration
|
||||
# Copy Apache configuration
|
||||
COPY ./docker/apache/000-default.conf /etc/apache2/sites-available/000-default.conf
|
||||
|
||||
# Install Composer
|
||||
COPY --from=composer:latest /usr/bin/composer /usr/bin/composer
|
||||
# Copy Composer from official image
|
||||
COPY --from=composer:2 /usr/bin/composer /usr/bin/composer
|
||||
|
||||
# Set working directory
|
||||
WORKDIR /var/www/html
|
||||
|
||||
# Change ownership
|
||||
# Copy Symfony app
|
||||
COPY . /var/www/html
|
||||
|
||||
# Optimize permissions
|
||||
RUN chown -R www-data:www-data /var/www/html
|
||||
|
||||
# Configure opcache for production
|
||||
RUN { \
|
||||
echo "opcache.enable=1"; \
|
||||
echo "opcache.memory_consumption=256"; \
|
||||
echo "opcache.max_accelerated_files=20000"; \
|
||||
echo "opcache.validate_timestamps=0"; \
|
||||
} > /usr/local/etc/php/conf.d/opcache.ini
|
||||
|
||||
# Expose port 80
|
||||
EXPOSE 80
|
||||
|
||||
|
||||
407
README.md
407
README.md
@@ -1,367 +1,154 @@
|
||||
# Immorechner
|
||||
|
||||
Eine moderne Webanwendung zur Immobilienberechnung, entwickelt mit Symfony 7.3, PHP 8.4 und MariaDB.
|
||||
Eine moderne Webanwendung zur Immobilienberechnung mit interaktivem Frontend und vollständiger REST-API.
|
||||
|
||||
## Schnellstart
|
||||
|
||||
```bash
|
||||
# Repository klonen
|
||||
git clone <repository-url>
|
||||
cd immorechner
|
||||
|
||||
# Docker-Container starten
|
||||
docker-compose up -d --build
|
||||
|
||||
# Dependencies installieren
|
||||
docker-compose exec web composer install
|
||||
|
||||
# Datenbank initialisieren
|
||||
docker-compose exec web php bin/console doctrine:migrations:migrate --no-interaction
|
||||
|
||||
# Anwendung öffnen
|
||||
# Frontend: http://localhost:8080
|
||||
# API-Docs: http://localhost:8080/api/docs.html
|
||||
# phpMyAdmin: http://localhost:8081
|
||||
```
|
||||
|
||||
## Features
|
||||
|
||||
- **REST-API**: Vollständige REST-API mit automatischer Dokumentation über API Platform
|
||||
- **Web-Interface**: Benutzerfreundliche Weboberfläche mit Twig-Templates
|
||||
- **User Management**: Benutzerverwaltung mit Rollenbasierter Zugriffskontrolle (USER, ADMIN, MODERATOR)
|
||||
- **Docker-Setup**: Vollständig containerisierte Entwicklungsumgebung
|
||||
- **Datenbank-Migrationen**: Versionskontrollierte Datenbankschema-Verwaltung mit Doctrine
|
||||
- **CORS-Unterstützung**: Konfigurierbare CORS-Einstellungen für API-Zugriffe
|
||||
- **phpMyAdmin**: Integriertes Datenbank-Verwaltungstool
|
||||
### Frontend (Web-Interface)
|
||||
- 🧮 **Interaktiver Immobilienrechner** mit Live-Berechnungen
|
||||
- 🔗 **URL-Sharing** für anonyme Nutzer (keine Registrierung nötig)
|
||||
- 👤 **Benutzer-Registrierung & Login**
|
||||
- 💾 **Immobilien speichern** (nur für registrierte Nutzer)
|
||||
- 📊 **Automatische Berechnungen**: Grunderwerbsteuer, Gesamtkosten, Abschreibung, etc.
|
||||
- 📱 **Responsive Design** (Mobile-optimiert)
|
||||
|
||||
## Technologie-Stack
|
||||
### Backend (REST-API)
|
||||
- 🔌 **Vollständige REST-API** mit API Platform 4.2
|
||||
- 🔐 **Dual-Authentifizierung**: Session-basiert (Frontend) & API-Key (API)
|
||||
- 🔑 **Rollenbasierte Zugriffskontrolle**: USER, ADMIN, MODERATOR, TECHNICAL
|
||||
- 📖 **Swagger/OpenAPI** Dokumentation
|
||||
- 🏢 **Mandantenfähigkeit**: Jeder User sieht nur seine eigenen Immobilien
|
||||
- 🌍 **Öffentliche Ressourcen**: Bundesländer und Heizungstypen ohne Authentifizierung
|
||||
|
||||
- **Backend**: PHP 8.4
|
||||
- **Framework**: Symfony 7.3
|
||||
- **Datenbank**: MariaDB (Latest)
|
||||
### Technologie
|
||||
- **Backend**: PHP 8.4, Symfony 7.3
|
||||
- **Frontend**: Twig, jQuery, separates CSS/JS
|
||||
- **Datenbank**: MariaDB
|
||||
- **Container**: Docker & Docker Compose
|
||||
- **ORM**: Doctrine 3.0
|
||||
- **API**: API Platform 4.2
|
||||
- **Template Engine**: Twig 3.22
|
||||
- **Webserver**: Apache 2.4 mit mod_rewrite
|
||||
- **Container**: Docker & Docker Compose
|
||||
- **Datenbank-Tool**: phpMyAdmin
|
||||
|
||||
## Voraussetzungen
|
||||
## Dokumentation
|
||||
|
||||
- Docker Desktop (Windows/Mac) oder Docker Engine + Docker Compose (Linux)
|
||||
- Git
|
||||
📚 **Detaillierte Dokumentation:**
|
||||
|
||||
## Installation
|
||||
|
||||
### 1. Repository klonen
|
||||
|
||||
```bash
|
||||
git clone <repository-url>
|
||||
cd immorechner
|
||||
```
|
||||
|
||||
### 2. Docker-Container starten
|
||||
|
||||
```bash
|
||||
docker-compose up -d --build
|
||||
```
|
||||
|
||||
Dieser Befehl:
|
||||
- Baut das PHP 8.4 Image mit allen benötigten Extensions
|
||||
- Startet MariaDB
|
||||
- Startet phpMyAdmin
|
||||
- Startet den Apache-Webserver
|
||||
|
||||
### 3. Dependencies installieren
|
||||
|
||||
```bash
|
||||
docker-compose exec web composer install
|
||||
```
|
||||
|
||||
### 4. Datenbank-Schema erstellen
|
||||
|
||||
```bash
|
||||
docker-compose exec web php bin/console doctrine:migrations:migrate --no-interaction
|
||||
```
|
||||
|
||||
### 5. Cache leeren
|
||||
|
||||
```bash
|
||||
docker-compose exec web php bin/console cache:clear
|
||||
```
|
||||
|
||||
## Verwendung
|
||||
|
||||
### Anwendung aufrufen
|
||||
|
||||
- **Web-Interface**: http://localhost:8080
|
||||
- **API-Dokumentation**: http://localhost:8080/api
|
||||
- **phpMyAdmin**: http://localhost:8081
|
||||
- Server: `db`
|
||||
- Benutzer: `root`
|
||||
- Passwort: `root`
|
||||
|
||||
### REST-API Endpoints
|
||||
|
||||
#### User-API
|
||||
|
||||
| Methode | Endpoint | Beschreibung |
|
||||
|---------|----------|--------------|
|
||||
| GET | `/api/users` | Alle User abrufen |
|
||||
| GET | `/api/users/{id}` | Einzelnen User abrufen |
|
||||
| POST | `/api/users` | Neuen User erstellen |
|
||||
| PUT | `/api/users/{id}` | User aktualisieren |
|
||||
| DELETE | `/api/users/{id}` | User löschen |
|
||||
|
||||
#### Beispiel: User erstellen
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:8080/api/users \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"name": "Max Mustermann",
|
||||
"email": "max@example.com",
|
||||
"role": "user"
|
||||
}'
|
||||
```
|
||||
|
||||
#### Beispiel: Alle User abrufen
|
||||
|
||||
```bash
|
||||
curl http://localhost:8080/api/users
|
||||
```
|
||||
|
||||
## Datenbank-Schema
|
||||
|
||||
### User-Tabelle
|
||||
|
||||
| Feld | Typ | Beschreibung |
|
||||
|------|-----|--------------|
|
||||
| id | INT | Primärschlüssel (Auto-Increment) |
|
||||
| name | VARCHAR(255) | Benutzername |
|
||||
| email | VARCHAR(255) | E-Mail-Adresse (Unique) |
|
||||
| role | ENUM | Benutzerrolle (user, admin, moderator) |
|
||||
| created_at | DATETIME | Erstellungsdatum |
|
||||
|
||||
## Entwicklung
|
||||
|
||||
### Neue Entity erstellen
|
||||
|
||||
```bash
|
||||
docker-compose exec web php bin/console make:entity
|
||||
```
|
||||
|
||||
### Migration erstellen
|
||||
|
||||
```bash
|
||||
docker-compose exec web php bin/console doctrine:migrations:diff
|
||||
```
|
||||
|
||||
### Migration ausführen
|
||||
|
||||
```bash
|
||||
docker-compose exec web php bin/console doctrine:migrations:migrate
|
||||
```
|
||||
|
||||
### Controller erstellen
|
||||
|
||||
```bash
|
||||
docker-compose exec web php bin/console make:controller
|
||||
```
|
||||
|
||||
### Cache leeren
|
||||
|
||||
```bash
|
||||
docker-compose exec web php bin/console cache:clear
|
||||
```
|
||||
|
||||
### Symfony Console aufrufen
|
||||
|
||||
```bash
|
||||
docker-compose exec web php bin/console
|
||||
```
|
||||
|
||||
## Docker-Befehle
|
||||
|
||||
### Container starten
|
||||
|
||||
```bash
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
### Container stoppen
|
||||
|
||||
```bash
|
||||
docker-compose down
|
||||
```
|
||||
|
||||
### Container neu bauen
|
||||
|
||||
```bash
|
||||
docker-compose up -d --build
|
||||
```
|
||||
|
||||
### Container-Logs anzeigen
|
||||
|
||||
```bash
|
||||
# Alle Container
|
||||
docker-compose logs -f
|
||||
|
||||
# Nur Web-Container
|
||||
docker-compose logs -f web
|
||||
|
||||
# Nur Datenbank-Container
|
||||
docker-compose logs -f db
|
||||
```
|
||||
|
||||
### In Container einloggen
|
||||
|
||||
```bash
|
||||
# Web-Container (PHP/Apache)
|
||||
docker-compose exec web bash
|
||||
|
||||
# Datenbank-Container
|
||||
docker-compose exec db bash
|
||||
```
|
||||
|
||||
### Composer-Befehle ausführen
|
||||
|
||||
```bash
|
||||
docker-compose exec web composer require <paket-name>
|
||||
docker-compose exec web composer update
|
||||
docker-compose exec web composer dump-autoload
|
||||
```
|
||||
- **[Installation & Setup](docs/installation.md)** - Schritt-für-Schritt Installationsanleitung
|
||||
- **[Features & Funktionalität](docs/features.md)** - Übersicht aller Funktionen
|
||||
- **[Technische Dokumentation](docs/technical.md)** - Architektur, Datenbank-Schema, Konfiguration
|
||||
- **[API-Dokumentation](docs/api.md)** - REST-API Endpunkte, Authentifizierung, Beispiele
|
||||
- **[Entwicklung](docs/development.md)** - Testing, Code Quality, Entwickler-Workflow
|
||||
- **[Docker](docs/docker.md)** - Docker-Befehle und Container-Management
|
||||
- **[Fehlerbehebung](docs/troubleshooting.md)** - Lösungen für häufige Probleme
|
||||
|
||||
## Projekt-Struktur
|
||||
|
||||
```
|
||||
immorechner/
|
||||
├── bin/ # Symfony Console und andere Binaries
|
||||
├── config/ # Symfony Konfigurationsdateien
|
||||
├── docker/ # Docker-Konfigurationsdateien
|
||||
│ └── apache/ # Apache VirtualHost Konfiguration
|
||||
├── migrations/ # Doctrine Datenbank-Migrationen
|
||||
├── public/ # Web-Root (index.php, Assets)
|
||||
├── config/ # Symfony Konfiguration
|
||||
├── docs/ # Dokumentation
|
||||
├── migrations/ # Datenbank-Migrationen
|
||||
├── public/ # Web-Root, CSS, JS, Assets
|
||||
├── src/ # PHP-Quellcode
|
||||
│ ├── Controller/ # Controller
|
||||
│ ├── Controller/ # Controller (Frontend & API)
|
||||
│ ├── Entity/ # Doctrine Entities
|
||||
│ ├── Enum/ # PHP Enums
|
||||
│ ├── Repository/ # Doctrine Repositories
|
||||
│ └── Kernel.php # Symfony Kernel
|
||||
│ └── Security/ # Authentifizierung
|
||||
├── templates/ # Twig-Templates
|
||||
│ ├── base.html.twig # Basis-Template
|
||||
│ └── home/ # Homepage-Templates
|
||||
├── var/ # Cache, Logs
|
||||
├── vendor/ # Composer Dependencies
|
||||
├── .env # Umgebungsvariablen
|
||||
├── .env.example # Beispiel-Umgebungsvariablen
|
||||
├── .gitignore # Git Ignore-Datei
|
||||
├── composer.json # Composer-Konfiguration
|
||||
├── docker-compose.yml # Docker Compose-Konfiguration
|
||||
├── Dockerfile # Docker-Image für PHP/Apache
|
||||
└── README.md # Diese Datei
|
||||
└── Dockerfile # Docker-Image für PHP/Apache
|
||||
```
|
||||
|
||||
## Umgebungsvariablen
|
||||
## Verwendung
|
||||
|
||||
Die Anwendung verwendet folgende Umgebungsvariablen (definiert in `.env`):
|
||||
### Frontend
|
||||
|
||||
```env
|
||||
# Symfony
|
||||
APP_ENV=dev
|
||||
APP_SECRET=<generierter-secret-key>
|
||||
**Startseite (Rechner):** http://localhost:8080
|
||||
- Immobiliendaten eingeben
|
||||
- Live-Berechnungen ansehen
|
||||
- Link zum Teilen generieren
|
||||
- Bei Anmeldung: Immobilie speichern
|
||||
|
||||
# Datenbank
|
||||
DATABASE_URL="mysql://immorechner_user:immorechner_pass@db:3306/immorechner?serverVersion=mariadb-11.7.1&charset=utf8mb4"
|
||||
**Registrierung:** http://localhost:8080/register
|
||||
|
||||
# CORS
|
||||
CORS_ALLOW_ORIGIN='^https?://(localhost|127\.0\.0\.1)(:[0-9]+)?$'
|
||||
**Login:** http://localhost:8080/login
|
||||
|
||||
# Routing
|
||||
DEFAULT_URI=http://localhost
|
||||
```
|
||||
**Meine Immobilien:** http://localhost:8080/meine-immobilien (nach Login)
|
||||
|
||||
## Konfiguration
|
||||
### API
|
||||
|
||||
### CORS anpassen
|
||||
**API-Dokumentation (Swagger UI):** http://localhost:8080/api/docs.html
|
||||
|
||||
CORS-Einstellungen können in `config/packages/nelmio_cors.yaml` angepasst werden.
|
||||
**API Entrypoint:** http://localhost:8080/api
|
||||
|
||||
### Datenbank-Verbindung ändern
|
||||
**Öffentliche Endpunkte (ohne Authentifizierung):**
|
||||
- `GET /api/bundeslands` - Alle Bundesländer
|
||||
- `GET /api/heizungstyps` - Alle Heizungstypen
|
||||
|
||||
Datenbank-Verbindung in `.env` anpassen:
|
||||
**Geschützte Endpunkte (API-Key erforderlich):**
|
||||
- `/api/immobilies` - Immobilien-Management
|
||||
- `/api/users` - Benutzer-Management
|
||||
|
||||
```env
|
||||
DATABASE_URL="mysql://user:password@host:port/database?serverVersion=mariadb-11.7.1"
|
||||
```
|
||||
|
||||
### Apache-Konfiguration
|
||||
|
||||
Apache-VirtualHost-Konfiguration befindet sich in `docker/apache/000-default.conf`.
|
||||
|
||||
## Fehlerbehebung
|
||||
|
||||
### Container starten nicht
|
||||
## Schnelle Befehle
|
||||
|
||||
```bash
|
||||
# Container-Logs prüfen
|
||||
docker-compose logs
|
||||
# Container starten
|
||||
docker-compose up -d
|
||||
|
||||
# Container-Status prüfen
|
||||
docker ps -a
|
||||
|
||||
# Container neu bauen
|
||||
# Container stoppen
|
||||
docker-compose down
|
||||
docker-compose up -d --build
|
||||
```
|
||||
|
||||
### "Class not found" Fehler
|
||||
# Logs anzeigen
|
||||
docker-compose logs -f
|
||||
|
||||
```bash
|
||||
# Autoloader neu generieren
|
||||
docker-compose exec web composer dump-autoload
|
||||
# In Container einloggen
|
||||
docker-compose exec web bash
|
||||
|
||||
# Cache leeren
|
||||
docker-compose exec web php bin/console cache:clear
|
||||
|
||||
# Container neu starten
|
||||
docker-compose restart web
|
||||
```
|
||||
|
||||
### Datenbank-Verbindungsfehler
|
||||
|
||||
```bash
|
||||
# Prüfen ob DB-Container läuft
|
||||
docker ps | grep db
|
||||
|
||||
# DB-Logs prüfen
|
||||
docker-compose logs db
|
||||
|
||||
# In DB-Container einloggen und testen
|
||||
docker-compose exec db mysql -u root -proot
|
||||
```
|
||||
|
||||
### Port bereits belegt
|
||||
|
||||
Wenn Port 8080, 8081 oder 3306 bereits belegt ist, können die Ports in `docker-compose.yml` angepasst werden:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
web:
|
||||
ports:
|
||||
- "8090:80" # Statt 8080:80
|
||||
```
|
||||
|
||||
## API-Dokumentation
|
||||
|
||||
Die vollständige API-Dokumentation ist verfügbar unter:
|
||||
- **Swagger UI**: http://localhost:8080/api
|
||||
|
||||
Die API unterstützt mehrere Formate:
|
||||
- JSON-LD (Standard)
|
||||
- JSON
|
||||
- HTML
|
||||
|
||||
Beispiel für JSON-Format:
|
||||
|
||||
```bash
|
||||
curl -H "Accept: application/json" http://localhost:8080/api/users
|
||||
# Tests ausführen
|
||||
docker-compose exec web php bin/phpunit
|
||||
```
|
||||
|
||||
## Sicherheitshinweise
|
||||
|
||||
**Wichtig für Produktion:**
|
||||
**⚠️ Wichtig für Produktion:**
|
||||
|
||||
1. Ändern Sie alle Standardpasswörter in `docker-compose.yml` und `.env`
|
||||
2. Generieren Sie einen neuen `APP_SECRET` für `.env`
|
||||
3. Setzen Sie `APP_ENV=prod` in der Produktion
|
||||
4. Konfigurieren Sie CORS entsprechend Ihrer Domain
|
||||
5. Aktivieren Sie HTTPS
|
||||
6. Verwenden Sie sichere Datenbank-Credentials
|
||||
7. Entfernen Sie phpMyAdmin aus der Produktion
|
||||
2. Generieren Sie einen neuen `APP_SECRET`
|
||||
3. Setzen Sie `APP_ENV=prod`
|
||||
4. Aktivieren Sie HTTPS
|
||||
5. Entfernen Sie phpMyAdmin
|
||||
6. Konfigurieren Sie CORS für Ihre Domain
|
||||
|
||||
Details siehe [Technische Dokumentation](docs/technical.md#sicherheit).
|
||||
|
||||
## Lizenz
|
||||
|
||||
[Lizenz hier einfügen]
|
||||
|
||||
## Kontakt
|
||||
## Support
|
||||
|
||||
[Kontaktinformationen hier einfügen]
|
||||
Bei Problemen siehe [Fehlerbehebung](docs/troubleshooting.md) oder öffnen Sie ein Issue.
|
||||
|
||||
4
bin/phpunit
Normal file
4
bin/phpunit
Normal file
@@ -0,0 +1,4 @@
|
||||
#!/usr/bin/env php
|
||||
<?php
|
||||
|
||||
require dirname(__DIR__).'/vendor/phpunit/phpunit/phpunit';
|
||||
@@ -45,5 +45,11 @@
|
||||
"cache:clear": "symfony-cmd",
|
||||
"assets:install %PUBLIC_DIR%": "symfony-cmd"
|
||||
}
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "^12.4",
|
||||
"symfony/browser-kit": "^7.3",
|
||||
"symfony/css-selector": "^7.3",
|
||||
"friendsofphp/php-cs-fixer": "^3.89"
|
||||
}
|
||||
}
|
||||
|
||||
3281
composer.lock
generated
3281
composer.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,40 @@
|
||||
api_platform:
|
||||
title: Hello API Platform
|
||||
title: 'Immorechner API'
|
||||
description: |
|
||||
# Immobilien-Verwaltungs-API
|
||||
|
||||
Diese REST-API bietet umfassende Funktionen zur Verwaltung von Immobilien und Benutzern.
|
||||
|
||||
## Hauptfunktionen
|
||||
- Verwaltung von Immobilien (CRUD-Operationen)
|
||||
- Benutzerverwaltung mit Rollensystem
|
||||
- Beziehungsverwaltung zwischen Usern und Immobilien
|
||||
- Automatische Berechnungen (Preis pro m², Gesamtfläche)
|
||||
|
||||
## Datenformate
|
||||
- JSON-LD (Hydra)
|
||||
- JSON
|
||||
- HAL+JSON
|
||||
|
||||
## Authentifizierung
|
||||
Derzeit keine Authentifizierung erforderlich (Entwicklungsversion)
|
||||
version: 1.0.0
|
||||
mapping:
|
||||
paths: ['%kernel.project_dir%/src/Entity']
|
||||
defaults:
|
||||
stateless: true
|
||||
cache_headers:
|
||||
vary: ['Content-Type', 'Authorization', 'Origin']
|
||||
formats:
|
||||
jsonld: ['application/ld+json']
|
||||
json: ['application/json']
|
||||
docs_formats:
|
||||
jsonld: ['application/ld+json']
|
||||
jsonopenapi: ['application/vnd.openapi+json']
|
||||
html: ['text/html']
|
||||
swagger:
|
||||
versions: [3]
|
||||
api_keys:
|
||||
apiKey:
|
||||
name: Authorization
|
||||
type: header
|
||||
|
||||
@@ -2,28 +2,60 @@ security:
|
||||
# https://symfony.com/doc/current/security.html#registering-the-user-hashing-passwords
|
||||
password_hashers:
|
||||
Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface: 'auto'
|
||||
|
||||
# https://symfony.com/doc/current/security.html#loading-the-user-the-user-provider
|
||||
providers:
|
||||
users_in_memory: { memory: null }
|
||||
app_user_provider:
|
||||
entity:
|
||||
class: App\Entity\User
|
||||
property: email
|
||||
|
||||
firewalls:
|
||||
dev:
|
||||
pattern: ^/(_(profiler|wdt)|css|images|js)/
|
||||
security: false
|
||||
|
||||
# API-Dokumentation (öffentlich zugänglich)
|
||||
api_docs:
|
||||
pattern: ^/api/docs
|
||||
stateless: true
|
||||
security: false
|
||||
|
||||
# Öffentliche Endpunkte (Bundesländer, Heizungstypen)
|
||||
public_api:
|
||||
pattern: ^/api/(bundeslands|heizungstyps)
|
||||
stateless: true
|
||||
security: false
|
||||
|
||||
# API mit API-Key-Authentifizierung
|
||||
api:
|
||||
pattern: ^/api
|
||||
stateless: true
|
||||
custom_authenticators:
|
||||
- App\Security\ApiKeyAuthenticator
|
||||
|
||||
main:
|
||||
lazy: true
|
||||
provider: users_in_memory
|
||||
|
||||
# activate different ways to authenticate
|
||||
# https://symfony.com/doc/current/security.html#the-firewall
|
||||
|
||||
# https://symfony.com/doc/current/security/impersonating_user.html
|
||||
# switch_user: true
|
||||
provider: app_user_provider
|
||||
form_login:
|
||||
login_path: app_login
|
||||
check_path: app_login
|
||||
default_target_path: app_home
|
||||
logout:
|
||||
path: app_logout
|
||||
target: app_home
|
||||
|
||||
# Easy way to control access for large sections of your site
|
||||
# Note: Only the *first* access control that matches will be used
|
||||
access_control:
|
||||
# - { path: ^/admin, roles: ROLE_ADMIN }
|
||||
# - { path: ^/profile, roles: ROLE_USER }
|
||||
# API-Dokumentation ist öffentlich
|
||||
- { path: ^/api/docs, roles: PUBLIC_ACCESS }
|
||||
# Öffentlicher Zugriff auf Bundesländer und Heizungstypen (GET)
|
||||
- { path: ^/api/(bundeslands|heizungstyps), methods: [GET], roles: PUBLIC_ACCESS }
|
||||
# Admin und Technical User Zugriff für Änderungen an Bundesländern und Heizungstypen
|
||||
- { path: ^/api/(bundeslands|heizungstyps), methods: [POST, PUT, DELETE, PATCH], roles: [ROLE_ADMIN, ROLE_TECHNICAL] }
|
||||
# Alle anderen API-Endpunkte erfordern Authentifizierung
|
||||
- { path: ^/api, roles: IS_AUTHENTICATED_FULLY }
|
||||
|
||||
when@test:
|
||||
security:
|
||||
|
||||
@@ -15,6 +15,20 @@ services:
|
||||
# this creates a service per class whose id is the fully-qualified class name
|
||||
App\:
|
||||
resource: '../src/'
|
||||
exclude:
|
||||
- '../src/Entity/'
|
||||
- '../src/Repository/'
|
||||
- '../src/Kernel.php'
|
||||
|
||||
# controllers are imported separately to make sure services can be injected
|
||||
# as action arguments even if you don't extend any base controller class
|
||||
App\Controller\:
|
||||
resource: '../src/Controller/'
|
||||
tags: ['controller.service_arguments']
|
||||
|
||||
# Explicitly register repositories
|
||||
App\Repository\:
|
||||
resource: '../src/Repository/'
|
||||
|
||||
# add more service definitions when explicit configuration is needed
|
||||
# please note that last definitions always *replace* previous ones
|
||||
|
||||
185
docs/api.md
Normal file
185
docs/api.md
Normal file
@@ -0,0 +1,185 @@
|
||||
# API-Dokumentation
|
||||
|
||||
[← Zurück zur Hauptseite](../README.md)
|
||||
|
||||
## Übersicht
|
||||
|
||||
Die Immorechner-API ist eine vollständige REST-API basierend auf API Platform 4.2 mit OpenAPI/Swagger-Dokumentation.
|
||||
|
||||
**Basis-URL:** `http://localhost:8080/api`
|
||||
|
||||
**Formate:**
|
||||
- `application/ld+json` (Standard, JSON-LD mit Hydra)
|
||||
- `application/json` (Einfaches JSON)
|
||||
|
||||
## Interaktive Dokumentation
|
||||
|
||||
**Swagger UI:** http://localhost:8080/api/docs.html
|
||||
|
||||
Hier können Sie alle Endpunkte direkt im Browser testen.
|
||||
|
||||
## Authentifizierung
|
||||
|
||||
### API-Key (für API-Zugriff)
|
||||
|
||||
Fügen Sie den API-Key im Header hinzu:
|
||||
|
||||
```bash
|
||||
curl -H "X-API-KEY: ihr-api-key-hier" http://localhost:8080/api/immobilies
|
||||
```
|
||||
|
||||
### API-Key erhalten
|
||||
|
||||
```bash
|
||||
# User erstellen
|
||||
curl -X POST http://localhost:8080/api/users \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"name": "Max Mustermann",
|
||||
"email": "max@example.com",
|
||||
"role": "user"
|
||||
}'
|
||||
|
||||
# Response enthält apiKey
|
||||
{
|
||||
"apiKey": "a1b2c3d4e5f6...", # Diesen Key verwenden
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
## Endpunkte
|
||||
|
||||
### Bundesländer (Öffentlich)
|
||||
|
||||
| Methode | Endpoint | Beschreibung | Auth |
|
||||
|---------|----------|--------------|------|
|
||||
| GET | `/api/bundeslands` | Alle Bundesländer | Nein |
|
||||
| GET | `/api/bundeslands/{id}` | Einzelnes Bundesland | Nein |
|
||||
| POST | `/api/bundeslands` | Neues Bundesland erstellen | ADMIN/TECHNICAL |
|
||||
| PUT | `/api/bundeslands/{id}` | Bundesland aktualisieren | ADMIN/TECHNICAL |
|
||||
| DELETE | `/api/bundeslands/{id}` | Bundesland löschen | ADMIN/TECHNICAL |
|
||||
|
||||
**Beispiel:**
|
||||
```bash
|
||||
curl http://localhost:8080/api/bundeslands
|
||||
```
|
||||
|
||||
### Heizungstypen (Öffentlich)
|
||||
|
||||
| Methode | Endpoint | Beschreibung | Auth |
|
||||
|---------|----------|--------------|------|
|
||||
| GET | `/api/heizungstyps` | Alle Heizungstypen | Nein |
|
||||
| GET | `/api/heizungstyps/{id}` | Einzelner Heizungstyp | Nein |
|
||||
| POST | `/api/heizungstyps` | Neuen Typ erstellen | ADMIN/TECHNICAL |
|
||||
| PUT | `/api/heizungstyps/{id}` | Typ aktualisieren | ADMIN/TECHNICAL |
|
||||
| DELETE | `/api/heizungstyps/{id}` | Typ löschen | ADMIN/TECHNICAL |
|
||||
|
||||
### Immobilien (Geschützt)
|
||||
|
||||
| Methode | Endpoint | Beschreibung | Auth |
|
||||
|---------|----------|--------------|------|
|
||||
| GET | `/api/immobilies` | Eigene Immobilien | API-Key |
|
||||
| GET | `/api/immobilies/{id}` | Einzelne Immobilie | API-Key |
|
||||
| POST | `/api/immobilies` | Neue Immobilie | API-Key |
|
||||
| PATCH | `/api/immobilies/{id}` | Immobilie aktualisieren | API-Key |
|
||||
| DELETE | `/api/immobilies/{id}` | Immobilie löschen | API-Key |
|
||||
|
||||
**Beispiel - Immobilie erstellen:**
|
||||
```bash
|
||||
curl -X POST http://localhost:8080/api/immobilies \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "X-API-KEY: your-api-key" \
|
||||
-d '{
|
||||
"verwalter": "/api/users/1",
|
||||
"adresse": "Hauptstraße 123, 12345 Musterstadt",
|
||||
"wohnflaeche": 85,
|
||||
"nutzflaeche": 15,
|
||||
"zimmer": 3,
|
||||
"typ": "wohnung",
|
||||
"kaufpreis": 300000,
|
||||
"bundesland": "/api/bundeslands/1",
|
||||
"heizungstyp": "/api/heizungstyps/1"
|
||||
}'
|
||||
```
|
||||
|
||||
### User (Geschützt)
|
||||
|
||||
| Methode | Endpoint | Beschreibung | Auth |
|
||||
|---------|----------|--------------|------|
|
||||
| GET | `/api/users` | Alle User | API-Key |
|
||||
| GET | `/api/users/{id}` | Einzelner User | API-Key |
|
||||
| POST | `/api/users` | Neuen User erstellen | Nein* |
|
||||
| PUT | `/api/users/{id}` | User aktualisieren | API-Key |
|
||||
| DELETE | `/api/users/{id}` | User löschen | API-Key |
|
||||
|
||||
*POST ohne Auth zum Registrieren neuer User
|
||||
|
||||
## Rollen & Berechtigungen
|
||||
|
||||
| Rolle | Rechte |
|
||||
|-------|--------|
|
||||
| `user` | Eigene Immobilien verwalten |
|
||||
| `admin` | Alle Ressourcen, alle Immobilien |
|
||||
| `technical` | Bundesländer & Heizungstypen verwalten |
|
||||
| `moderator` | Erweiterte Rechte |
|
||||
|
||||
## Enums
|
||||
|
||||
### ImmobilienTyp
|
||||
- `wohnung` - Wohnung
|
||||
- `haus` - Haus
|
||||
- `grundstueck` - Grundstück
|
||||
- `gewerbe` - Gewerbe
|
||||
- `buero` - Büro
|
||||
|
||||
### UserRole
|
||||
- `user` - Benutzer
|
||||
- `admin` - Administrator
|
||||
- `moderator` - Moderator
|
||||
- `technical` - Technischer User
|
||||
|
||||
## Pagination
|
||||
|
||||
Alle Collection-Endpunkte unterstützen Pagination:
|
||||
|
||||
```bash
|
||||
# Erste Seite (Standard: 30 Items)
|
||||
curl http://localhost:8080/api/immobilies
|
||||
|
||||
# Zweite Seite
|
||||
curl http://localhost:8080/api/immobilies?page=2
|
||||
|
||||
# Custom Page Size
|
||||
curl "http://localhost:8080/api/immobilies?itemsPerPage=10"
|
||||
```
|
||||
|
||||
## Filter
|
||||
|
||||
```bash
|
||||
# Nach Typ filtern
|
||||
curl "http://localhost:8080/api/immobilies?typ=wohnung"
|
||||
|
||||
# Nach Bundesland filtern
|
||||
curl "http://localhost:8080/api/immobilies?bundesland=/api/bundeslands/1"
|
||||
```
|
||||
|
||||
## Fehler-Codes
|
||||
|
||||
| Status Code | Bedeutung |
|
||||
|-------------|-----------|
|
||||
| 200 | OK - Erfolgreiche Anfrage |
|
||||
| 201 | Created - Ressource erstellt |
|
||||
| 204 | No Content - Erfolgreich gelöscht |
|
||||
| 400 | Bad Request - Ungültige Daten |
|
||||
| 401 | Unauthorized - Authentifizierung fehlgeschlagen |
|
||||
| 403 | Forbidden - Keine Berechtigung |
|
||||
| 404 | Not Found - Ressource nicht gefunden |
|
||||
| 500 | Internal Server Error - Server-Fehler |
|
||||
|
||||
---
|
||||
|
||||
**Weitere Informationen:**
|
||||
- [Features](features.md) - Funktionsübersicht
|
||||
- [Technical](technical.md) - Datenbank-Schema & Architektur
|
||||
|
||||
[← Zurück zur Hauptseite](../README.md)
|
||||
302
docs/development.md
Normal file
302
docs/development.md
Normal file
@@ -0,0 +1,302 @@
|
||||
# Entwicklung
|
||||
|
||||
[← Zurück zur Hauptseite](../README.md)
|
||||
|
||||
## Entwickler-Workflow
|
||||
|
||||
### Entwicklungsumgebung einrichten
|
||||
|
||||
Siehe [Installation](installation.md)
|
||||
|
||||
### Code-Änderungen testen
|
||||
|
||||
```bash
|
||||
# Container starten
|
||||
docker-compose up -d
|
||||
|
||||
# In Web-Container einloggen
|
||||
docker-compose exec web bash
|
||||
|
||||
# Cache leeren nach Änderungen
|
||||
php bin/console cache:clear
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
### PHPUnit Tests
|
||||
|
||||
Das Projekt verwendet PHPUnit 12.4 für Tests.
|
||||
|
||||
#### Alle Tests ausführen
|
||||
|
||||
```bash
|
||||
docker-compose exec web php bin/phpunit
|
||||
```
|
||||
|
||||
#### Spezifische Tests
|
||||
|
||||
```bash
|
||||
# Nur Entity-Tests
|
||||
docker-compose exec web php bin/phpunit tests/Entity
|
||||
|
||||
# Nur API-Tests
|
||||
docker-compose exec web php bin/phpunit tests/Api
|
||||
|
||||
# Einzelne Testklasse
|
||||
docker-compose exec web php bin/phpunit tests/Entity/UserTest.php
|
||||
```
|
||||
|
||||
#### Mit Details
|
||||
|
||||
```bash
|
||||
docker-compose exec web php bin/phpunit --verbose
|
||||
```
|
||||
|
||||
#### Code Coverage (optional)
|
||||
|
||||
```bash
|
||||
docker-compose exec web php bin/phpunit --coverage-text
|
||||
```
|
||||
|
||||
### Test-Struktur
|
||||
|
||||
```
|
||||
tests/
|
||||
├── Entity/
|
||||
│ ├── UserTest.php # User-Entity Tests
|
||||
│ ├── ImmobilieTest.php # Immobilie-Entity Tests
|
||||
│ ├── BundeslandTest.php # Bundesland-Entity Tests
|
||||
│ └── HeizungstypTest.php # Heizungstyp-Entity Tests
|
||||
└── Api/
|
||||
├── BundeslandApiTest.php # Bundesländer-API Tests
|
||||
├── HeizungstypApiTest.php # Heizungstypen-API Tests
|
||||
└── ApiDocumentationTest.php # API-Docs Tests
|
||||
```
|
||||
|
||||
## Code Quality
|
||||
|
||||
### PHP-CS-Fixer
|
||||
|
||||
Für konsistenten Code-Style nach Symfony Standards.
|
||||
|
||||
#### Code-Style prüfen (Dry-Run)
|
||||
|
||||
```bash
|
||||
docker-compose exec web vendor/bin/php-cs-fixer fix --dry-run --diff
|
||||
```
|
||||
|
||||
#### Code automatisch formatieren
|
||||
|
||||
```bash
|
||||
docker-compose exec web vendor/bin/php-cs-fixer fix
|
||||
```
|
||||
|
||||
#### Bestimmte Verzeichnisse
|
||||
|
||||
```bash
|
||||
# Nur src/
|
||||
docker-compose exec web vendor/bin/php-cs-fixer fix src
|
||||
|
||||
# Nur tests/
|
||||
docker-compose exec web vendor/bin/php-cs-fixer fix tests
|
||||
```
|
||||
|
||||
### Code-Style Regeln
|
||||
|
||||
Konfiguration in `.php-cs-fixer.dist.php`:
|
||||
- Symfony Coding Standards
|
||||
- PSR-12 kompatibel
|
||||
- Short Array Syntax
|
||||
- Sortierte Imports
|
||||
- Trailing Commas in Arrays
|
||||
|
||||
## Datenbank
|
||||
|
||||
### Neue Entity erstellen
|
||||
|
||||
```bash
|
||||
docker-compose exec web php bin/console make:entity
|
||||
```
|
||||
|
||||
### Migration erstellen
|
||||
|
||||
Nach Änderungen an Entities:
|
||||
|
||||
```bash
|
||||
docker-compose exec web php bin/console doctrine:migrations:diff
|
||||
```
|
||||
|
||||
### Migration ausführen
|
||||
|
||||
```bash
|
||||
docker-compose exec web php bin/console doctrine:migrations:migrate
|
||||
```
|
||||
|
||||
### Migration rückgängig machen
|
||||
|
||||
```bash
|
||||
# Letzte Migration
|
||||
docker-compose exec web php bin/console doctrine:migrations:migrate prev
|
||||
|
||||
# Zu spezifischer Version
|
||||
docker-compose exec web php bin/console doctrine:migrations:migrate DoctrineMigrations\\Version20251109100000
|
||||
```
|
||||
|
||||
### Migration-Status
|
||||
|
||||
```bash
|
||||
docker-compose exec web php bin/console doctrine:migrations:status
|
||||
```
|
||||
|
||||
### Datenbank-Schema validieren
|
||||
|
||||
```bash
|
||||
docker-compose exec web php bin/console doctrine:schema:validate
|
||||
```
|
||||
|
||||
## Controller & Routes
|
||||
|
||||
### Controller erstellen
|
||||
|
||||
```bash
|
||||
docker-compose exec web php bin/console make:controller
|
||||
```
|
||||
|
||||
### Routes anzeigen
|
||||
|
||||
```bash
|
||||
# Alle Routes
|
||||
docker-compose exec web php bin/console debug:router
|
||||
|
||||
# Spezifische Route
|
||||
docker-compose exec web php bin/console debug:router app_home
|
||||
```
|
||||
|
||||
## Symfony Console
|
||||
|
||||
### Cache
|
||||
|
||||
```bash
|
||||
# Cache leeren
|
||||
docker-compose exec web php bin/console cache:clear
|
||||
|
||||
# Cache warmup
|
||||
docker-compose exec web php bin/console cache:warmup
|
||||
```
|
||||
|
||||
### Services debuggen
|
||||
|
||||
```bash
|
||||
# Alle Services
|
||||
docker-compose exec web php bin/console debug:container
|
||||
|
||||
# Spezifischer Service
|
||||
docker-compose exec web php bin/console debug:container UserRepository
|
||||
```
|
||||
|
||||
### Ereignisse anzeigen
|
||||
|
||||
```bash
|
||||
docker-compose exec web php bin/console debug:event-dispatcher
|
||||
```
|
||||
|
||||
### Konfiguration anzeigen
|
||||
|
||||
```bash
|
||||
docker-compose exec web php bin/console debug:config framework
|
||||
```
|
||||
|
||||
## Git Workflow
|
||||
|
||||
### Feature-Branch erstellen
|
||||
|
||||
```bash
|
||||
git checkout -b feature/neue-funktion
|
||||
```
|
||||
|
||||
### Änderungen committen
|
||||
|
||||
```bash
|
||||
git add .
|
||||
git commit -m "Feature: Beschreibung der Änderung"
|
||||
```
|
||||
|
||||
### Pull Request erstellen
|
||||
|
||||
1. Push auf Remote Branch
|
||||
2. Pull Request in GitHub/GitLab erstellen
|
||||
3. Code Review abwarten
|
||||
4. Nach Approval mergen
|
||||
|
||||
## Best Practices
|
||||
|
||||
### Code-Organisation
|
||||
|
||||
1. **Controller:** Dünn halten, Logik in Services auslagern
|
||||
2. **Entities:** Nur Datenmodell, keine Business-Logik
|
||||
3. **Services:** Wiederverwendbare Business-Logik
|
||||
4. **Repositories:** Nur Datenbank-Queries
|
||||
|
||||
### Namenskonventionen
|
||||
|
||||
- **Controller:** `XyzController.php`
|
||||
- **Entity:** `Xyz.php`
|
||||
- **Repository:** `XyzRepository.php`
|
||||
- **Service:** `XyzService.php`
|
||||
- **Test:** `XyzTest.php`
|
||||
|
||||
### Security
|
||||
|
||||
- Nie Passwörter im Klartext speichern
|
||||
- Immer UserPasswordHasher verwenden
|
||||
- CSRF-Tokens bei allen Forms
|
||||
- Input-Validierung auf Server-Seite
|
||||
- Output-Escaping (Twig macht automatisch)
|
||||
|
||||
### Performance
|
||||
|
||||
- Doctrine Query Cache nutzen
|
||||
- Eager Loading für Relationen
|
||||
- Opcache in Produktion aktivieren
|
||||
- Assets kompilieren für Produktion
|
||||
|
||||
## Deployment
|
||||
|
||||
### Vorbereitung
|
||||
|
||||
```bash
|
||||
# .env.local für Produktion erstellen
|
||||
cp .env .env.local
|
||||
|
||||
# Produktions-Werte setzen
|
||||
APP_ENV=prod
|
||||
APP_SECRET=<neues-secret-generieren>
|
||||
```
|
||||
|
||||
### Build für Produktion
|
||||
|
||||
```bash
|
||||
# Composer Dependencies ohne Dev
|
||||
composer install --no-dev --optimize-autoloader
|
||||
|
||||
# Cache warmup
|
||||
php bin/console cache:warmup --env=prod
|
||||
|
||||
# Assets installieren
|
||||
php bin/console assets:install public --symlink --relative --env=prod
|
||||
```
|
||||
|
||||
### Database Migration
|
||||
|
||||
```bash
|
||||
php bin/console doctrine:migrations:migrate --no-interaction --env=prod
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**Siehe auch:**
|
||||
- [Technical](technical.md) - Architektur & Konfiguration
|
||||
- [Docker](docker.md) - Container-Management
|
||||
- [Troubleshooting](troubleshooting.md) - Fehler beheben
|
||||
|
||||
[← Zurück zur Hauptseite](../README.md)
|
||||
337
docs/docker.md
Normal file
337
docs/docker.md
Normal file
@@ -0,0 +1,337 @@
|
||||
# Docker
|
||||
|
||||
[← Zurück zur Hauptseite](../README.md)
|
||||
|
||||
## Container-Übersicht
|
||||
|
||||
Das Projekt verwendet 3 Docker-Container:
|
||||
|
||||
| Container | Image | Port | Beschreibung |
|
||||
|-----------|-------|------|--------------|
|
||||
| `immorechner_web` | Custom (PHP 8.4 + Apache) | 8080 | Web-Server & PHP |
|
||||
| `immorechner_db` | mariadb:latest | 3306 | Datenbank |
|
||||
| `immorechner_phpmyadmin` | phpmyadmin:latest | 8081 | DB-Verwaltung |
|
||||
|
||||
## Container-Management
|
||||
|
||||
### Starten
|
||||
|
||||
```bash
|
||||
# Alle Container starten
|
||||
docker-compose up -d
|
||||
|
||||
# Mit Build (nach Dockerfile-Änderungen)
|
||||
docker-compose up -d --build
|
||||
|
||||
# Im Vordergrund (mit Logs)
|
||||
docker-compose up
|
||||
```
|
||||
|
||||
### Stoppen
|
||||
|
||||
```bash
|
||||
# Container stoppen
|
||||
docker-compose stop
|
||||
|
||||
# Container stoppen und entfernen
|
||||
docker-compose down
|
||||
|
||||
# Container stoppen, entfernen + Volumes löschen
|
||||
docker-compose down -v
|
||||
```
|
||||
|
||||
### Neustarten
|
||||
|
||||
```bash
|
||||
# Alle Container
|
||||
docker-compose restart
|
||||
|
||||
# Einzelner Container
|
||||
docker-compose restart web
|
||||
docker-compose restart db
|
||||
```
|
||||
|
||||
### Status prüfen
|
||||
|
||||
```bash
|
||||
# Container-Status
|
||||
docker-compose ps
|
||||
|
||||
# Alle Container (auch gestoppte)
|
||||
docker ps -a
|
||||
|
||||
# Resource-Usage
|
||||
docker stats
|
||||
```
|
||||
|
||||
## Logs
|
||||
|
||||
### Container-Logs anzeigen
|
||||
|
||||
```bash
|
||||
# Alle Container (Live)
|
||||
docker-compose logs -f
|
||||
|
||||
# Nur Web-Container
|
||||
docker-compose logs -f web
|
||||
|
||||
# Nur Datenbank
|
||||
docker-compose logs -f db
|
||||
|
||||
# Nur phpMyAdmin
|
||||
docker-compose logs -f phpmyadmin
|
||||
|
||||
# Letzte 100 Zeilen
|
||||
docker-compose logs --tail=100 web
|
||||
```
|
||||
|
||||
### Apache-Logs
|
||||
|
||||
```bash
|
||||
# Access Log
|
||||
docker-compose exec web tail -f /var/log/apache2/access.log
|
||||
|
||||
# Error Log
|
||||
docker-compose exec web tail -f /var/log/apache2/error.log
|
||||
```
|
||||
|
||||
### Symfony-Logs
|
||||
|
||||
```bash
|
||||
# Dev Log
|
||||
docker-compose exec web tail -f /var/www/html/var/log/dev.log
|
||||
|
||||
# Prod Log
|
||||
docker-compose exec web tail -f /var/www/html/var/log/prod.log
|
||||
```
|
||||
|
||||
## Container-Zugriff
|
||||
|
||||
### In Container einloggen
|
||||
|
||||
```bash
|
||||
# Web-Container (Bash)
|
||||
docker-compose exec web bash
|
||||
|
||||
# Datenbank-Container
|
||||
docker-compose exec db bash
|
||||
|
||||
# phpMyAdmin-Container
|
||||
docker-compose exec phpmyadmin bash
|
||||
```
|
||||
|
||||
### Als Root einloggen
|
||||
|
||||
```bash
|
||||
docker-compose exec -u root web bash
|
||||
```
|
||||
|
||||
## Befehle im Container ausführen
|
||||
|
||||
### PHP/Symfony
|
||||
|
||||
```bash
|
||||
# Symfony Console
|
||||
docker-compose exec web php bin/console
|
||||
|
||||
# PHP-Version prüfen
|
||||
docker-compose exec web php -v
|
||||
|
||||
# PHP-Module anzeigen
|
||||
docker-compose exec web php -m
|
||||
|
||||
# PHP-Konfiguration
|
||||
docker-compose exec web php -i
|
||||
```
|
||||
|
||||
### Composer
|
||||
|
||||
```bash
|
||||
# Install
|
||||
docker-compose exec web composer install
|
||||
|
||||
# Update
|
||||
docker-compose exec web composer update
|
||||
|
||||
# Paket hinzufügen
|
||||
docker-compose exec web composer require vendor/package
|
||||
|
||||
# Paket entfernen
|
||||
docker-compose exec web composer remove vendor/package
|
||||
|
||||
# Autoloader neu generieren
|
||||
docker-compose exec web composer dump-autoload
|
||||
```
|
||||
|
||||
### Datenbank
|
||||
|
||||
```bash
|
||||
# MariaDB-Client öffnen
|
||||
docker-compose exec db mariadb -u root -proot immorechner
|
||||
|
||||
# SQL-Datei importieren
|
||||
docker-compose exec -T db mariadb -u root -proot immorechner < backup.sql
|
||||
|
||||
# Datenbank exportieren
|
||||
docker-compose exec db mariadb-dump -u root -proot immorechner > backup.sql
|
||||
|
||||
# Tabellen anzeigen
|
||||
docker-compose exec db mariadb -u root -proot -e "SHOW TABLES" immorechner
|
||||
```
|
||||
|
||||
## Volumes
|
||||
|
||||
### Volumes anzeigen
|
||||
|
||||
```bash
|
||||
docker volume ls
|
||||
```
|
||||
|
||||
### Volume-Details
|
||||
|
||||
```bash
|
||||
docker volume inspect immorechner_db_data
|
||||
```
|
||||
|
||||
### Volume löschen
|
||||
|
||||
```bash
|
||||
# ACHTUNG: Löscht alle Daten!
|
||||
docker volume rm immorechner_db_data
|
||||
```
|
||||
|
||||
### Backup erstellen
|
||||
|
||||
```bash
|
||||
# Datenbank-Backup
|
||||
docker-compose exec db mariadb-dump -u root -proot immorechner > backup_$(date +%Y%m%d).sql
|
||||
|
||||
# Volume-Backup
|
||||
docker run --rm -v immorechner_db_data:/data -v $(pwd):/backup alpine tar czf /backup/db_backup.tar.gz /data
|
||||
```
|
||||
|
||||
## Images
|
||||
|
||||
### Images anzeigen
|
||||
|
||||
```bash
|
||||
docker images
|
||||
```
|
||||
|
||||
### Image neu bauen
|
||||
|
||||
```bash
|
||||
docker-compose build web
|
||||
```
|
||||
|
||||
### Image mit Tag
|
||||
|
||||
```bash
|
||||
docker build -t immorechner:latest .
|
||||
```
|
||||
|
||||
### Image löschen
|
||||
|
||||
```bash
|
||||
docker rmi immorechner-web
|
||||
```
|
||||
|
||||
## Netzwerk
|
||||
|
||||
### Netzwerke anzeigen
|
||||
|
||||
```bash
|
||||
docker network ls
|
||||
```
|
||||
|
||||
### Netzwerk-Details
|
||||
|
||||
```bash
|
||||
docker network inspect immorechner_immorechner_network
|
||||
```
|
||||
|
||||
### Container im Netzwerk
|
||||
|
||||
```bash
|
||||
docker network inspect immorechner_immorechner_network | grep Name
|
||||
```
|
||||
|
||||
## Cleanup
|
||||
|
||||
### Gestoppte Container entfernen
|
||||
|
||||
```bash
|
||||
docker container prune
|
||||
```
|
||||
|
||||
### Ungenutzte Images entfernen
|
||||
|
||||
```bash
|
||||
docker image prune
|
||||
```
|
||||
|
||||
### Ungenutzte Volumes entfernen
|
||||
|
||||
```bash
|
||||
docker volume prune
|
||||
```
|
||||
|
||||
### Alles aufräumen
|
||||
|
||||
```bash
|
||||
# ACHTUNG: Löscht alles Ungenutzte!
|
||||
docker system prune -a --volumes
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Container startet nicht
|
||||
|
||||
```bash
|
||||
# Logs prüfen
|
||||
docker-compose logs web
|
||||
|
||||
# Container im Debug-Modus starten
|
||||
docker-compose up web
|
||||
```
|
||||
|
||||
### Port bereits belegt
|
||||
|
||||
```bash
|
||||
# Ports prüfen
|
||||
netstat -ano | findstr :8080 # Windows
|
||||
lsof -i :8080 # Mac/Linux
|
||||
|
||||
# In docker-compose.yml ändern:
|
||||
ports:
|
||||
- "8090:80" # Statt 8080:80
|
||||
```
|
||||
|
||||
### Berechtigungsprobleme
|
||||
|
||||
```bash
|
||||
# Berechtigungen setzen
|
||||
docker-compose exec -u root web chown -R www-data:www-data /var/www/html
|
||||
|
||||
# Schreibrechte für var/
|
||||
docker-compose exec -u root web chmod -R 777 /var/www/html/var
|
||||
```
|
||||
|
||||
### Container-Reset
|
||||
|
||||
```bash
|
||||
# Komplett neu starten
|
||||
docker-compose down -v
|
||||
docker-compose up -d --build
|
||||
docker-compose exec web composer install
|
||||
docker-compose exec web php bin/console doctrine:migrations:migrate --no-interaction
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**Siehe auch:**
|
||||
- [Installation](installation.md) - Setup-Anleitung
|
||||
- [Development](development.md) - Entwickler-Workflow
|
||||
- [Troubleshooting](troubleshooting.md) - Fehler beheben
|
||||
|
||||
[← Zurück zur Hauptseite](../README.md)
|
||||
275
docs/features.md
Normal file
275
docs/features.md
Normal file
@@ -0,0 +1,275 @@
|
||||
# Features & Funktionalität
|
||||
|
||||
[← Zurück zur Hauptseite](../README.md)
|
||||
|
||||
## Übersicht
|
||||
|
||||
Der Immorechner bietet zwei Hauptkomponenten:
|
||||
1. **Web-Frontend** - Interaktive Benutzeroberfläche für Endanwender
|
||||
2. **REST-API** - Programmgesteuerter Zugriff für Integrationen
|
||||
|
||||
---
|
||||
|
||||
## Frontend-Features
|
||||
|
||||
### 1. Immobilienrechner (Startseite)
|
||||
|
||||
**URL:** http://localhost:8080/
|
||||
|
||||
Der Immobilienrechner ermöglicht die schnelle Berechnung wichtiger Kennzahlen einer Immobilie.
|
||||
|
||||
#### Eingabefelder
|
||||
|
||||
| Feld | Typ | Beschreibung | Pflicht |
|
||||
|------|-----|--------------|---------|
|
||||
| Adresse | Text | Vollständige Adresse der Immobilie | Nein |
|
||||
| Kaufpreis | Zahl | Kaufpreis in Euro | Empfohlen |
|
||||
| Wohnfläche | Zahl | Wohnfläche in m² | Ja |
|
||||
| Nutzfläche | Zahl | Nutzfläche in m² | Nein |
|
||||
| Zimmer | Zahl | Anzahl der Zimmer | Empfohlen |
|
||||
| Baujahr | Zahl | Baujahr der Immobilie | Nein |
|
||||
| Etage | Zahl | Stockwerk | Nein |
|
||||
| Immobilientyp | Auswahl | Wohnung, Haus oder Gewerbe | Empfohlen |
|
||||
| Bundesland | Auswahl | Deutsches Bundesland | Empfohlen |
|
||||
| Heizungstyp | Auswahl | Ölheizung, Gasheizung, Wärmepumpe, Pelletheizung | Nein |
|
||||
| Abschreibungszeit | Zahl | Jahre für Abschreibung (Standard: 50) | Nein |
|
||||
| Garage | Checkbox | Garage vorhanden? | Nein |
|
||||
|
||||
#### Live-Berechnungen
|
||||
|
||||
Alle Werte werden **in Echtzeit** berechnet (ohne Seitenreload):
|
||||
|
||||
1. **Gesamtfläche**
|
||||
- Formel: Wohnfläche + Nutzfläche
|
||||
- Einheit: m²
|
||||
|
||||
2. **Preis pro m² Wohnfläche**
|
||||
- Formel: Kaufpreis / Wohnfläche
|
||||
- Einheit: €/m²
|
||||
|
||||
3. **Grunderwerbsteuer**
|
||||
- Formel: Kaufpreis × Grunderwerbsteuersatz des Bundeslandes
|
||||
- Sätze: 3,5% (Bayern) bis 6,5% (Brandenburg, NRW, Saarland, SH)
|
||||
- Einheit: €
|
||||
|
||||
4. **Gesamtkosten**
|
||||
- Formel: Kaufpreis + Grunderwerbsteuer
|
||||
- Einheit: €
|
||||
|
||||
5. **Jährliche Abschreibung**
|
||||
- Formel: Kaufpreis / Abschreibungszeit
|
||||
- Standard-Abschreibungszeit: 50 Jahre
|
||||
- Einheit: €/Jahr
|
||||
|
||||
6. **Alter der Immobilie**
|
||||
- Formel: Aktuelles Jahr - Baujahr
|
||||
- Einheit: Jahre
|
||||
|
||||
### 2. URL-Sharing für anonyme Nutzer
|
||||
|
||||
**Funktion:** "Link teilen" Button
|
||||
|
||||
- Kodiert alle Formulardaten in URL-Parameter
|
||||
- Ermöglicht Teilen der Berechnung ohne Registrierung
|
||||
- Link funktioniert auch nach Tagen/Wochen
|
||||
- Daten werden **nicht** in der Datenbank gespeichert
|
||||
- Ideal für schnelle Berechnungen und Vergleiche
|
||||
|
||||
**Beispiel-URL:**
|
||||
```
|
||||
http://localhost:8080/?adresse=Teststr+123&kaufpreis=300000&wohnflaeche=85&...
|
||||
```
|
||||
|
||||
### 3. Benutzer-Authentifizierung
|
||||
|
||||
#### Registrierung
|
||||
|
||||
**URL:** http://localhost:8080/register
|
||||
|
||||
- Name (Pflicht, mind. 2 Zeichen)
|
||||
- E-Mail (Pflicht, muss gültig und eindeutig sein)
|
||||
- Passwort (Pflicht, mind. 6 Zeichen)
|
||||
- Passwort bestätigen
|
||||
|
||||
**Sicherheit:**
|
||||
- Passwort wird mit bcrypt gehasht
|
||||
- E-Mail muss eindeutig sein
|
||||
- Validierung auf Client- und Server-Seite
|
||||
|
||||
#### Login
|
||||
|
||||
**URL:** http://localhost:8080/login
|
||||
|
||||
- E-Mail
|
||||
- Passwort
|
||||
|
||||
**Session-basiert:**
|
||||
- Bleibt aktiv bis zum Logout
|
||||
- Cookie-basierte Session
|
||||
- CSRF-Schutz integriert
|
||||
|
||||
#### Logout
|
||||
|
||||
- Link im Header "Abmelden"
|
||||
- Beendet Session
|
||||
- Redirect zur Startseite
|
||||
|
||||
### 4. Immobilien speichern (nur eingeloggte Nutzer)
|
||||
|
||||
**Funktion:** "Speichern" Button (erscheint nur nach Login)
|
||||
|
||||
- Speichert alle eingegebenen Daten in der Datenbank
|
||||
- Immobilie wird dem eingeloggten Nutzer zugeordnet
|
||||
- Nutzer kann nur eigene Immobilien sehen
|
||||
- AJAX-basiert (kein Seitenreload)
|
||||
- Erfolgs-/Fehlermeldung als Alert
|
||||
|
||||
### 5. Meine Immobilien
|
||||
|
||||
**URL:** http://localhost:8080/meine-immobilien (nur nach Login)
|
||||
|
||||
**Funktionen:**
|
||||
- Übersicht aller gespeicherten Immobilien
|
||||
- Anzeige aller Details pro Immobilie
|
||||
- Berechnete Werte werden angezeigt
|
||||
- Sortierung nach Erstellungsdatum (neueste zuerst)
|
||||
|
||||
**Angezeigt werden:**
|
||||
- Typ, Kaufpreis, Flächen
|
||||
- Zimmer, Baujahr, Etage
|
||||
- Bundesland, Heizungstyp
|
||||
- Garage (Ja/Nein)
|
||||
- Erstellungsdatum
|
||||
|
||||
---
|
||||
|
||||
## API-Features
|
||||
|
||||
Vollständige Details siehe [API-Dokumentation](api.md)
|
||||
|
||||
### 1. Dual-Authentifizierung
|
||||
|
||||
**Session-basiert (Frontend):**
|
||||
- Form-Login mit E-Mail/Passwort
|
||||
- Cookie-basierte Sessions
|
||||
- Für Web-Interface
|
||||
|
||||
**API-Key-basiert (API):**
|
||||
- Header: `X-API-KEY`
|
||||
- Automatisch generiert bei User-Erstellung
|
||||
- Für programmatischen Zugriff
|
||||
|
||||
### 2. Rollenbasierte Zugriffskontrolle
|
||||
|
||||
| Rolle | Rechte |
|
||||
|-------|--------|
|
||||
| **USER** | Eigene Immobilien verwalten |
|
||||
| **ADMIN** | Alle Immobilien verwalten, alle User sehen |
|
||||
| **MODERATOR** | Erweiterte Rechte |
|
||||
| **TECHNICAL** | Bundesländer & Heizungstypen verwalten |
|
||||
|
||||
### 3. Mandantenfähigkeit
|
||||
|
||||
- Jeder User sieht nur eigene Immobilien
|
||||
- Admins sehen alle Immobilien
|
||||
- Automatische Filterung auf Basis des eingeloggten Users
|
||||
- Keine manuelle Filterung nötig
|
||||
|
||||
### 4. Öffentliche Ressourcen
|
||||
|
||||
**Ohne Authentifizierung verfügbar:**
|
||||
- `GET /api/bundeslands` - Alle Bundesländer mit Grunderwerbsteuersätzen
|
||||
- `GET /api/heizungstyps` - Alle Heizungstypen
|
||||
|
||||
**Vorbefüllt:**
|
||||
- 16 deutsche Bundesländer mit aktuellen Steuersätzen
|
||||
- 4 Heizungstypen (Öl, Gas, Wärmepumpe, Pellet)
|
||||
|
||||
### 5. Swagger/OpenAPI-Dokumentation
|
||||
|
||||
**URL:** http://localhost:8080/api/docs.html
|
||||
|
||||
**Features:**
|
||||
- Interaktive API-Dokumentation
|
||||
- "Try it out" Funktion für alle Endpunkte
|
||||
- Request/Response Beispiele
|
||||
- Schema-Dokumentation
|
||||
- Export als JSON/YAML
|
||||
|
||||
---
|
||||
|
||||
## Technische Features
|
||||
|
||||
### 1. Responsive Design
|
||||
|
||||
- Mobile-optimiert
|
||||
- Grid-Layout passt sich an Bildschirmgröße an
|
||||
- Touch-freundliche Buttons und Formulare
|
||||
|
||||
### 2. Performance
|
||||
|
||||
- **Live-Berechnungen** ohne Server-Anfragen
|
||||
- **AJAX** für Speichern-Funktion (kein Reload)
|
||||
- **Opcache** aktiviert in Produktion
|
||||
- **Doctrine Query Cache**
|
||||
|
||||
### 3. Datenvalidierung
|
||||
|
||||
**Frontend:**
|
||||
- HTML5-Validierung
|
||||
- Required-Felder
|
||||
- Type-Checking (number, email, etc.)
|
||||
|
||||
**Backend:**
|
||||
- Symfony Validator
|
||||
- Doctrine Constraints
|
||||
- Custom Validation Rules
|
||||
|
||||
### 4. Sicherheit
|
||||
|
||||
- **CSRF-Protection** für alle Forms
|
||||
- **Passwort-Hashing** mit bcrypt
|
||||
- **SQL-Injection-Schutz** durch Doctrine ORM
|
||||
- **XSS-Schutz** durch Twig Auto-Escaping
|
||||
- **CORS-Konfiguration** für API
|
||||
|
||||
### 5. Separation of Concerns
|
||||
|
||||
- **CSS** in separaten Dateien (`public/css/`)
|
||||
- **JavaScript** in separaten Dateien (`public/js/`)
|
||||
- **Templates** mit Twig
|
||||
- **Controller** für Logik
|
||||
- **Entities** für Datenmodell
|
||||
|
||||
---
|
||||
|
||||
## Berechnungslogik
|
||||
|
||||
### Grunderwerbsteuer
|
||||
|
||||
Bundesland-spezifisch (Stand 2025):
|
||||
|
||||
| Bundesland | Steuersatz |
|
||||
|------------|-----------|
|
||||
| Bayern | 3,5% |
|
||||
| Baden-Württemberg, Bremen, Niedersachsen, Rheinland-Pfalz, Sachsen-Anhalt, Thüringen | 5,0% |
|
||||
| Hamburg, Sachsen | 5,5% |
|
||||
| Berlin, Hessen, Mecklenburg-Vorpommern | 6,0% |
|
||||
| Brandenburg, NRW, Saarland, Schleswig-Holstein | 6,5% |
|
||||
|
||||
### Kaufnebenkosten (nur API)
|
||||
|
||||
Bei API-Anfragen werden zusätzlich berechnet:
|
||||
- **Notarkosten:** ca. 1,5% des Kaufpreises
|
||||
- **Grundbuchkosten:** ca. 0,5% des Kaufpreises
|
||||
- **Grunderwerbsteuer:** siehe oben
|
||||
- **Gesamt:** Summe aller Nebenkosten
|
||||
|
||||
---
|
||||
|
||||
**Siehe auch:**
|
||||
- [Technische Dokumentation](technical.md) - Architektur & Datenbank
|
||||
- [API-Dokumentation](api.md) - REST-API Details
|
||||
- [Installation](installation.md) - Setup-Anleitung
|
||||
|
||||
[← Zurück zur Hauptseite](../README.md)
|
||||
291
docs/installation.md
Normal file
291
docs/installation.md
Normal file
@@ -0,0 +1,291 @@
|
||||
# Installation & Setup
|
||||
|
||||
[← Zurück zur Hauptseite](../README.md)
|
||||
|
||||
## Voraussetzungen
|
||||
|
||||
- **Docker Desktop** (Windows/Mac) oder **Docker Engine + Docker Compose** (Linux)
|
||||
- **Git**
|
||||
- Mindestens 2GB freier RAM für Docker
|
||||
- Ports 8080, 8081 und 3306 verfügbar
|
||||
|
||||
## Installations-Schritte
|
||||
|
||||
### 1. Repository klonen
|
||||
|
||||
```bash
|
||||
git clone <repository-url>
|
||||
cd immorechner
|
||||
```
|
||||
|
||||
### 2. Docker-Container starten
|
||||
|
||||
```bash
|
||||
docker-compose up -d --build
|
||||
```
|
||||
|
||||
Dieser Befehl:
|
||||
- Baut das PHP 8.4 Image mit allen benötigten Extensions
|
||||
- Startet MariaDB Container
|
||||
- Startet phpMyAdmin Container
|
||||
- Startet den Apache-Webserver
|
||||
|
||||
**Hinweis:** Der erste Build kann 5-10 Minuten dauern.
|
||||
|
||||
### 3. Dependencies installieren
|
||||
|
||||
```bash
|
||||
docker-compose exec web composer install
|
||||
```
|
||||
|
||||
### 4. Bundle-Assets installieren
|
||||
|
||||
```bash
|
||||
docker-compose exec web php bin/console assets:install public --symlink --relative
|
||||
```
|
||||
|
||||
Dieser Schritt ist wichtig für die Swagger UI (CSS, JS, Bilder).
|
||||
|
||||
### 5. Datenbank-Schema erstellen
|
||||
|
||||
```bash
|
||||
docker-compose exec web php bin/console doctrine:migrations:migrate --no-interaction
|
||||
```
|
||||
|
||||
Diese Migration erstellt:
|
||||
- Users-Tabelle mit Passwort- und API-Key-Feldern
|
||||
- Bundesländer-Tabelle (vorbefüllt mit allen 16 deutschen Bundesländern)
|
||||
- Heizungstypen-Tabelle (vorbefüllt mit Ölheizung, Gasheizung, Wärmepumpe, Pelletheizung)
|
||||
- Immobilien-Tabelle mit allen Relationen
|
||||
|
||||
### 6. Cache leeren
|
||||
|
||||
```bash
|
||||
docker-compose exec web php bin/console cache:clear
|
||||
```
|
||||
|
||||
## Überprüfung der Installation
|
||||
|
||||
### Anwendung testen
|
||||
|
||||
Öffnen Sie die folgenden URLs in Ihrem Browser:
|
||||
|
||||
1. **Frontend (Rechner):** http://localhost:8080
|
||||
- Sie sollten den Immobilienrechner sehen
|
||||
- Bundesländer-Dropdown sollte befüllt sein
|
||||
- Heizungstypen-Dropdown sollte befüllt sein
|
||||
|
||||
2. **API-Dokumentation:** http://localhost:8080/api/docs.html
|
||||
- Swagger UI sollte mit CSS/Styling laden
|
||||
- Alle API-Endpunkte sollten sichtbar sein
|
||||
|
||||
3. **phpMyAdmin:** http://localhost:8081
|
||||
- Server: `db`
|
||||
- Benutzer: `root`
|
||||
- Passwort: `root`
|
||||
- Datenbank `immorechner` sollte existieren
|
||||
|
||||
### Datenbank überprüfen
|
||||
|
||||
```bash
|
||||
# In Container einloggen
|
||||
docker-compose exec db bash
|
||||
|
||||
# MariaDB-Client starten
|
||||
mariadb -u root -proot immorechner
|
||||
|
||||
# Tabellen anzeigen
|
||||
SHOW TABLES;
|
||||
|
||||
# Bundesländer überprüfen (sollte 16 Einträge haben)
|
||||
SELECT COUNT(*) FROM bundeslaender;
|
||||
|
||||
# Heizungstypen überprüfen (sollte 4 Einträge haben)
|
||||
SELECT COUNT(*) FROM heizungstypen;
|
||||
|
||||
# Beenden
|
||||
EXIT;
|
||||
```
|
||||
|
||||
## Erste Schritte nach der Installation
|
||||
|
||||
### 1. Testbenutzer anlegen
|
||||
|
||||
#### Via Frontend (Empfohlen)
|
||||
|
||||
1. Öffnen Sie http://localhost:8080/register
|
||||
2. Registrieren Sie sich mit:
|
||||
- Name: "Test User"
|
||||
- E-Mail: "test@example.com"
|
||||
- Passwort: "test123" (min. 6 Zeichen)
|
||||
3. Nach der Registrierung werden Sie zum Login weitergeleitet
|
||||
4. Melden Sie sich an
|
||||
|
||||
#### Via API
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:8080/api/users \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"name": "Test User",
|
||||
"email": "test@example.com",
|
||||
"role": "user"
|
||||
}'
|
||||
```
|
||||
|
||||
**Hinweis:** Die API-Response enthält den `apiKey`, den Sie für API-Anfragen benötigen.
|
||||
|
||||
### 2. Erste Immobilie berechnen
|
||||
|
||||
#### Via Frontend
|
||||
|
||||
1. Gehen Sie zu http://localhost:8080/
|
||||
2. Füllen Sie das Formular aus (mindestens: Adresse, Kaufpreis, Wohnfläche)
|
||||
3. Sehen Sie die Live-Berechnungen rechts
|
||||
4. Optional: Klicken Sie auf "Link teilen" zum Teilen
|
||||
5. Wenn angemeldet: Klicken Sie auf "Speichern"
|
||||
|
||||
#### Via API
|
||||
|
||||
```bash
|
||||
# Ersetzen Sie YOUR_API_KEY mit dem API-Key aus Schritt 1
|
||||
curl -X POST http://localhost:8080/api/immobilies \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "X-API-KEY: YOUR_API_KEY" \
|
||||
-d '{
|
||||
"verwalter": "/api/users/1",
|
||||
"adresse": "Teststraße 123, 12345 Teststadt",
|
||||
"wohnflaeche": 85,
|
||||
"nutzflaeche": 15,
|
||||
"zimmer": 3,
|
||||
"typ": "wohnung",
|
||||
"kaufpreis": 300000,
|
||||
"bundesland": "/api/bundeslands/1"
|
||||
}'
|
||||
```
|
||||
|
||||
## Umgebungsvariablen anpassen
|
||||
|
||||
Die Datei `.env` enthält alle wichtigen Konfigurationen:
|
||||
|
||||
```env
|
||||
# Symfony
|
||||
APP_ENV=dev # Für Produktion: prod
|
||||
APP_SECRET=<generiert> # Für Produktion: neu generieren
|
||||
|
||||
# Datenbank
|
||||
DATABASE_URL="mysql://immorechner_user:immorechner_pass@db:3306/immorechner?serverVersion=mariadb-11.7.1&charset=utf8mb4"
|
||||
|
||||
# CORS (nur für API relevant)
|
||||
CORS_ALLOW_ORIGIN='^https?://(localhost|127\.0\.0\.1)(:[0-9]+)?$'
|
||||
```
|
||||
|
||||
### Für Produktion
|
||||
|
||||
Erstellen Sie eine `.env.local` Datei (wird von Git ignoriert):
|
||||
|
||||
```env
|
||||
APP_ENV=prod
|
||||
APP_SECRET=<neue-generierte-secret-hier>
|
||||
DATABASE_URL="mysql://prod_user:sichere_passwort@db-host:3306/prod_db?serverVersion=mariadb-11.7.1&charset=utf8mb4"
|
||||
CORS_ALLOW_ORIGIN='^https?://(ihre-domain\.com)(:[0-9]+)?$'
|
||||
```
|
||||
|
||||
## Ports anpassen
|
||||
|
||||
Falls die Standard-Ports bereits belegt sind, können Sie diese in `docker-compose.yml` ändern:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
web:
|
||||
ports:
|
||||
- "8090:80" # Statt 8080:80
|
||||
|
||||
db:
|
||||
ports:
|
||||
- "3307:3306" # Statt 3306:3306
|
||||
|
||||
phpmyadmin:
|
||||
ports:
|
||||
- "8091:80" # Statt 8081:80
|
||||
```
|
||||
|
||||
## Häufige Probleme bei der Installation
|
||||
|
||||
### Container starten nicht
|
||||
|
||||
```bash
|
||||
# Container-Logs prüfen
|
||||
docker-compose logs
|
||||
|
||||
# Spezifischen Container-Log prüfen
|
||||
docker-compose logs web
|
||||
docker-compose logs db
|
||||
|
||||
# Container-Status prüfen
|
||||
docker ps -a
|
||||
```
|
||||
|
||||
### "Permission Denied" Fehler
|
||||
|
||||
```bash
|
||||
# Container neu bauen und Berechtigungen setzen
|
||||
docker-compose down
|
||||
docker-compose up -d --build
|
||||
|
||||
# Berechtigungen im Container setzen
|
||||
docker-compose exec web chown -R www-data:www-data /var/www/html/var
|
||||
```
|
||||
|
||||
### Datenbank-Verbindungsfehler
|
||||
|
||||
```bash
|
||||
# Prüfen ob DB-Container läuft
|
||||
docker ps | grep db
|
||||
|
||||
# DB-Logs prüfen
|
||||
docker-compose logs db
|
||||
|
||||
# Warten bis DB bereit ist (kann 30-60 Sekunden dauern)
|
||||
docker-compose exec web php bin/console doctrine:query:sql "SELECT 1"
|
||||
```
|
||||
|
||||
### Composer-Fehler
|
||||
|
||||
```bash
|
||||
# Composer Cache leeren
|
||||
docker-compose exec web composer clear-cache
|
||||
|
||||
# Dependencies neu installieren
|
||||
docker-compose exec web rm -rf vendor
|
||||
docker-compose exec web composer install
|
||||
```
|
||||
|
||||
## Deinstallation
|
||||
|
||||
### Nur Container stoppen
|
||||
|
||||
```bash
|
||||
docker-compose down
|
||||
```
|
||||
|
||||
### Container und Volumes löschen (Datenbank wird gelöscht!)
|
||||
|
||||
```bash
|
||||
docker-compose down -v
|
||||
```
|
||||
|
||||
### Alles entfernen (inkl. Images)
|
||||
|
||||
```bash
|
||||
docker-compose down -v --rmi all
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**Nächste Schritte:**
|
||||
- [Features & Funktionalität](features.md) - Übersicht aller Funktionen
|
||||
- [API-Dokumentation](api.md) - REST-API verwenden
|
||||
- [Entwicklung](development.md) - Development-Workflow
|
||||
|
||||
[← Zurück zur Hauptseite](../README.md)
|
||||
424
docs/technical.md
Normal file
424
docs/technical.md
Normal file
@@ -0,0 +1,424 @@
|
||||
# Technische Dokumentation
|
||||
|
||||
[← Zurück zur Hauptseite](../README.md)
|
||||
|
||||
## Technologie-Stack
|
||||
|
||||
- **Backend**: PHP 8.4
|
||||
- **Framework**: Symfony 7.3
|
||||
- **Datenbank**: MariaDB 11.7
|
||||
- **ORM**: Doctrine 3.0
|
||||
- **API**: API Platform 4.2
|
||||
- **Template Engine**: Twig 3.22
|
||||
- **Webserver**: Apache 2.4 mit mod_rewrite
|
||||
- **Container**: Docker & Docker Compose
|
||||
- **Frontend**: jQuery 3.7.1, separates CSS/JS
|
||||
|
||||
## Architektur
|
||||
|
||||
### Schichtenmodell
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────┐
|
||||
│ Presentation Layer │
|
||||
│ (Twig Templates, CSS, JavaScript) │
|
||||
└─────────────────────────────────────┘
|
||||
↓
|
||||
┌─────────────────────────────────────┐
|
||||
│ Controller Layer │
|
||||
│ (HomeController, AuthController, │
|
||||
│ ImmobilienSaveController) │
|
||||
└─────────────────────────────────────┘
|
||||
↓
|
||||
┌─────────────────────────────────────┐
|
||||
│ Service Layer │
|
||||
│ (Security, Validation, Business │
|
||||
│ Logic) │
|
||||
└─────────────────────────────────────┘
|
||||
↓
|
||||
┌─────────────────────────────────────┐
|
||||
│ Persistence Layer │
|
||||
│ (Doctrine ORM, Repositories) │
|
||||
└─────────────────────────────────────┘
|
||||
↓
|
||||
┌─────────────────────────────────────┐
|
||||
│ Database Layer │
|
||||
│ (MariaDB) │
|
||||
└─────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### Komponenten
|
||||
|
||||
#### Frontend
|
||||
- **Templates:** `templates/` (Twig)
|
||||
- **CSS:** `public/css/` (calculator.css, auth.css)
|
||||
- **JavaScript:** `public/js/` (calculator.js mit jQuery)
|
||||
|
||||
#### Backend
|
||||
- **Controller:** `src/Controller/` (Request Handling)
|
||||
- **Entity:** `src/Entity/` (Datenmodelle)
|
||||
- **Repository:** `src/Repository/` (Datenbank-Queries)
|
||||
- **Security:** `src/Security/` (Authentifizierung)
|
||||
- **Enum:** `src/Enum/` (PHP Enums)
|
||||
|
||||
## Datenbank-Schema
|
||||
|
||||
### Entity-Relationship-Diagram
|
||||
|
||||
```
|
||||
┌──────────────┐ ┌────────────────┐
|
||||
│ User │ 1 * │ Immobilie │
|
||||
│──────────────│◄────────│────────────────│
|
||||
│ id │ │ id │
|
||||
│ name │ │ verwalter_id │
|
||||
│ email │ │ adresse │
|
||||
│ password │ │ wohnflaeche │
|
||||
│ role │ │ nutzflaeche │
|
||||
│ api_key │ │ zimmer │
|
||||
│ created_at │ │ kaufpreis │
|
||||
└──────────────┘ │ typ │
|
||||
│ ... │
|
||||
└────────────────┘
|
||||
│
|
||||
┌──────────────┴──────────────┐
|
||||
│ │
|
||||
▼ ▼
|
||||
┌───────────────┐ ┌─────────────────┐
|
||||
│ Bundesland │ │ Heizungstyp │
|
||||
│───────────────│ │─────────────────│
|
||||
│ id │ │ id │
|
||||
│ name │ │ name │
|
||||
│ grunderwerbst.│ └─────────────────┘
|
||||
└───────────────┘
|
||||
```
|
||||
|
||||
### Tabellen-Details
|
||||
|
||||
#### User-Tabelle
|
||||
|
||||
| Feld | Typ | Constraints | Beschreibung |
|
||||
|------|-----|-------------|--------------|
|
||||
| id | INT | PK, AUTO_INCREMENT | Eindeutige ID |
|
||||
| name | VARCHAR(255) | NOT NULL | Benutzername |
|
||||
| email | VARCHAR(255) | NOT NULL, UNIQUE | E-Mail-Adresse |
|
||||
| password | VARCHAR(255) | NULL | Bcrypt-Hash des Passworts |
|
||||
| role | VARCHAR(255) | NOT NULL | Benutzerrolle (Enum) |
|
||||
| api_key | VARCHAR(64) | NOT NULL, UNIQUE | SHA256 API-Key |
|
||||
| created_at | DATETIME | NOT NULL | Erstellungsdatum |
|
||||
|
||||
**Benutzerrollen (Enum):**
|
||||
- `user` - Normaler Benutzer
|
||||
- `admin` - Administrator
|
||||
- `moderator` - Moderator
|
||||
- `technical` - Technischer User
|
||||
|
||||
#### Bundesland-Tabelle
|
||||
|
||||
| Feld | Typ | Constraints | Beschreibung |
|
||||
|------|-----|-------------|--------------|
|
||||
| id | INT | PK, AUTO_INCREMENT | Eindeutige ID |
|
||||
| name | VARCHAR(100) | NOT NULL, UNIQUE | Name des Bundeslandes |
|
||||
| grunderwerbsteuer | DECIMAL(4,2) | NOT NULL | Steuersatz in % |
|
||||
|
||||
**Vorbefüllt mit 16 deutschen Bundesländern**
|
||||
|
||||
#### Heizungstyp-Tabelle
|
||||
|
||||
| Feld | Typ | Constraints | Beschreibung |
|
||||
|------|-----|-------------|--------------|
|
||||
| id | INT | PK, AUTO_INCREMENT | Eindeutige ID |
|
||||
| name | VARCHAR(100) | NOT NULL, UNIQUE | Name des Heizungstyps |
|
||||
|
||||
**Vorbefüllt mit:** Ölheizung, Gasheizung, Wärmepumpe, Pelletheizung
|
||||
|
||||
#### Immobilien-Tabelle
|
||||
|
||||
| Feld | Typ | Constraints | Beschreibung |
|
||||
|------|-----|-------------|--------------|
|
||||
| id | INT | PK, AUTO_INCREMENT | Eindeutige ID |
|
||||
| verwalter_id | INT | FK → users.id, NOT NULL | Besitzer der Immobilie |
|
||||
| bundesland_id | INT | FK → bundeslaender.id, NULL | Bundesland |
|
||||
| heizungstyp_id | INT | FK → heizungstypen.id, NULL | Heizungstyp |
|
||||
| adresse | VARCHAR(255) | NOT NULL | Vollständige Adresse |
|
||||
| wohnflaeche | INT | NOT NULL | Wohnfläche in m² |
|
||||
| nutzflaeche | INT | NOT NULL | Nutzfläche in m² |
|
||||
| garage | BOOLEAN | NOT NULL, DEFAULT false | Garage vorhanden? |
|
||||
| zimmer | INT | NOT NULL | Anzahl Zimmer |
|
||||
| baujahr | INT | NULL | Baujahr (1800-2100) |
|
||||
| typ | VARCHAR(255) | NOT NULL | Immobilientyp (Enum) |
|
||||
| beschreibung | TEXT | NULL | Freitextbeschreibung |
|
||||
| etage | INT | NULL | Stockwerk (0-10) |
|
||||
| kaufpreis | INT | NULL | Kaufpreis in Euro |
|
||||
| abschreibungszeit | INT | NULL | Abschreibungszeit in Jahren |
|
||||
| created_at | DATETIME | NOT NULL | Erstellungsdatum |
|
||||
| updated_at | DATETIME | NOT NULL | Letzte Änderung |
|
||||
|
||||
**Immobilientyp (Enum):**
|
||||
- `wohnung` - Wohnung
|
||||
- `haus` - Haus
|
||||
- `grundstueck` - Grundstück
|
||||
- `gewerbe` - Gewerbe
|
||||
- `buero` - Büro
|
||||
|
||||
### Indizes
|
||||
|
||||
```sql
|
||||
-- User
|
||||
CREATE UNIQUE INDEX UNIQ_1483A5E9E7927C74 ON users (email);
|
||||
CREATE UNIQUE INDEX UNIQ_1483A5E9C912ED9D ON users (api_key);
|
||||
|
||||
-- Bundesland
|
||||
CREATE UNIQUE INDEX UNIQ_DF7DFAB25E237E06 ON bundeslaender (name);
|
||||
|
||||
-- Heizungstyp
|
||||
CREATE UNIQUE INDEX UNIQ_6161C2A65E237E06 ON heizungstypen (name);
|
||||
|
||||
-- Immobilie
|
||||
CREATE INDEX IDX_2C789D3E5F66D3 ON immobilien (verwalter_id);
|
||||
CREATE INDEX IDX_2C789DC1B4DB52 ON immobilien (heizungstyp_id);
|
||||
CREATE INDEX IDX_2C789DB74FDBEB ON immobilien (bundesland_id);
|
||||
```
|
||||
|
||||
## Konfiguration
|
||||
|
||||
### Umgebungsvariablen (.env)
|
||||
|
||||
```env
|
||||
# Symfony
|
||||
APP_ENV=dev # Umgebung: dev | prod | test
|
||||
APP_SECRET=<32-zeichen-hex> # Symfony Secret für Verschlüsselung
|
||||
|
||||
# Datenbank
|
||||
DATABASE_URL="mysql://immorechner_user:immorechner_pass@db:3306/immorechner?serverVersion=mariadb-11.7.1&charset=utf8mb4"
|
||||
|
||||
# Routing
|
||||
DEFAULT_URI=http://localhost
|
||||
|
||||
# CORS (für API)
|
||||
CORS_ALLOW_ORIGIN='^https?://(localhost|127\.0\.0\.1)(:[0-9]+)?$'
|
||||
```
|
||||
|
||||
### Services (config/services.yaml)
|
||||
|
||||
```yaml
|
||||
services:
|
||||
_defaults:
|
||||
autowire: true
|
||||
autoconfigure: true
|
||||
|
||||
App\:
|
||||
resource: '../src/'
|
||||
exclude:
|
||||
- '../src/Entity/'
|
||||
- '../src/Repository/'
|
||||
- '../src/Kernel.php'
|
||||
|
||||
App\Controller\:
|
||||
resource: '../src/Controller/'
|
||||
tags: ['controller.service_arguments']
|
||||
|
||||
App\Repository\:
|
||||
resource: '../src/Repository/'
|
||||
```
|
||||
|
||||
### Security (config/packages/security.yaml)
|
||||
|
||||
```yaml
|
||||
security:
|
||||
password_hashers:
|
||||
Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface: 'auto'
|
||||
|
||||
providers:
|
||||
app_user_provider:
|
||||
entity:
|
||||
class: App\Entity\User
|
||||
property: email
|
||||
|
||||
firewalls:
|
||||
# API mit API-Key
|
||||
api:
|
||||
pattern: ^/api
|
||||
stateless: true
|
||||
custom_authenticators:
|
||||
- App\Security\ApiKeyAuthenticator
|
||||
|
||||
# Frontend mit Form-Login
|
||||
main:
|
||||
lazy: true
|
||||
provider: app_user_provider
|
||||
form_login:
|
||||
login_path: app_login
|
||||
check_path: app_login
|
||||
default_target_path: app_home
|
||||
logout:
|
||||
path: app_logout
|
||||
target: app_home
|
||||
```
|
||||
|
||||
### API Platform (config/packages/api_platform.yaml)
|
||||
|
||||
```yaml
|
||||
api_platform:
|
||||
title: 'Immorechner API'
|
||||
version: 1.0.0
|
||||
mapping:
|
||||
paths: ['%kernel.project_dir%/src/Entity']
|
||||
defaults:
|
||||
stateless: true
|
||||
formats:
|
||||
jsonld: ['application/ld+json']
|
||||
json: ['application/json']
|
||||
```
|
||||
|
||||
## Docker-Konfiguration
|
||||
|
||||
### docker-compose.yml
|
||||
|
||||
```yaml
|
||||
services:
|
||||
web:
|
||||
build: .
|
||||
container_name: immorechner_web
|
||||
ports:
|
||||
- "8080:80"
|
||||
volumes:
|
||||
- .:/var/www/html
|
||||
depends_on:
|
||||
- db
|
||||
|
||||
db:
|
||||
image: mariadb:latest
|
||||
container_name: immorechner_db
|
||||
environment:
|
||||
MYSQL_ROOT_PASSWORD: root
|
||||
MYSQL_DATABASE: immorechner
|
||||
MYSQL_USER: immorechner_user
|
||||
MYSQL_PASSWORD: immorechner_pass
|
||||
ports:
|
||||
- "3306:3306"
|
||||
volumes:
|
||||
- db_data:/var/lib/mysql
|
||||
|
||||
phpmyadmin:
|
||||
image: phpmyadmin:latest
|
||||
container_name: immorechner_phpmyadmin
|
||||
ports:
|
||||
- "8081:80"
|
||||
environment:
|
||||
PMA_HOST: db
|
||||
PMA_USER: root
|
||||
PMA_PASSWORD: root
|
||||
```
|
||||
|
||||
### Dockerfile
|
||||
|
||||
```dockerfile
|
||||
FROM php:8.4-apache
|
||||
|
||||
# Install dependencies
|
||||
RUN apt-get update && apt-get install -y \
|
||||
libpng-dev libonig-dev libxml2-dev libicu-dev \
|
||||
libzip-dev git curl unzip
|
||||
|
||||
# Install PHP extensions
|
||||
RUN docker-php-ext-install pdo_mysql mbstring exif \
|
||||
pcntl bcmath gd zip intl opcache
|
||||
|
||||
# Enable Apache modules
|
||||
RUN a2enmod rewrite headers
|
||||
|
||||
# Copy Composer
|
||||
COPY --from=composer:2 /usr/bin/composer /usr/bin/composer
|
||||
|
||||
# Set working directory
|
||||
WORKDIR /var/www/html
|
||||
|
||||
# Copy application
|
||||
COPY . /var/www/html
|
||||
|
||||
# Set permissions
|
||||
RUN chown -R www-data:www-data /var/www/html
|
||||
```
|
||||
|
||||
## Performance-Optimierungen
|
||||
|
||||
### Opcache (Produktion)
|
||||
|
||||
In Produktion ist Opcache aktiviert:
|
||||
|
||||
```ini
|
||||
opcache.enable=1
|
||||
opcache.memory_consumption=256
|
||||
opcache.max_accelerated_files=20000
|
||||
opcache.validate_timestamps=0
|
||||
```
|
||||
|
||||
### Doctrine Query Cache
|
||||
|
||||
Doctrine nutzt automatisch Query-Cache in Produktion.
|
||||
|
||||
### Asset Compilation
|
||||
|
||||
Assets werden bei Deployment kompiliert:
|
||||
|
||||
```bash
|
||||
php bin/console assets:install public --symlink --relative
|
||||
```
|
||||
|
||||
## Sicherheit
|
||||
|
||||
### CSRF-Schutz
|
||||
|
||||
Alle Forms verwenden CSRF-Tokens:
|
||||
|
||||
```twig
|
||||
<input type="hidden" name="_csrf_token" value="{{ csrf_token('authenticate') }}">
|
||||
```
|
||||
|
||||
### Passwort-Hashing
|
||||
|
||||
Passwörter werden mit bcrypt gehasht:
|
||||
|
||||
```php
|
||||
$hashedPassword = $passwordHasher->hashPassword($user, $password);
|
||||
```
|
||||
|
||||
### API-Key-Generierung
|
||||
|
||||
API-Keys sind SHA256-Hashes:
|
||||
|
||||
```php
|
||||
return hash('sha256', random_bytes(32).microtime(true));
|
||||
```
|
||||
|
||||
### SQL-Injection-Schutz
|
||||
|
||||
Doctrine ORM verhindert SQL-Injection durch Prepared Statements.
|
||||
|
||||
### XSS-Schutz
|
||||
|
||||
Twig escaped automatisch alle Ausgaben.
|
||||
|
||||
## Migrations
|
||||
|
||||
Migrationen werden mit Doctrine Migrations verwaltet:
|
||||
|
||||
```bash
|
||||
# Neue Migration erstellen
|
||||
php bin/console doctrine:migrations:diff
|
||||
|
||||
# Migrationen ausführen
|
||||
php bin/console doctrine:migrations:migrate
|
||||
|
||||
# Migration-Status anzeigen
|
||||
php bin/console doctrine:migrations:status
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**Siehe auch:**
|
||||
- [Installation](installation.md) - Setup-Anleitung
|
||||
- [Development](development.md) - Entwickler-Workflow
|
||||
- [Docker](docker.md) - Container-Management
|
||||
|
||||
[← Zurück zur Hauptseite](../README.md)
|
||||
432
docs/troubleshooting.md
Normal file
432
docs/troubleshooting.md
Normal file
@@ -0,0 +1,432 @@
|
||||
# Fehlerbehebung
|
||||
|
||||
[← Zurück zur Hauptseite](../README.md)
|
||||
|
||||
## Häufige Probleme
|
||||
|
||||
### Frontend-Probleme
|
||||
|
||||
#### Seite lädt ohne CSS/JavaScript
|
||||
|
||||
**Problem:** Seite wird angezeigt, aber ohne Styling oder JavaScript-Funktionalität.
|
||||
|
||||
**Lösung:**
|
||||
```bash
|
||||
# Assets installieren
|
||||
docker-compose exec web php bin/console assets:install public --symlink --relative
|
||||
|
||||
# Cache leeren
|
||||
docker-compose exec web php bin/console cache:clear
|
||||
|
||||
# Browser-Cache leeren (Strg+F5)
|
||||
```
|
||||
|
||||
#### Live-Berechnungen funktionieren nicht
|
||||
|
||||
**Problem:** Formular-Eingaben triggern keine Berechnungen.
|
||||
|
||||
**Lösung:**
|
||||
```bash
|
||||
# Browser-Konsole öffnen (F12)
|
||||
# Nach JavaScript-Fehlern suchen
|
||||
|
||||
# Prüfen ob jQuery geladen wurde
|
||||
# Sollte in Netzwerk-Tab sichtbar sein: jquery-3.7.1.min.js
|
||||
|
||||
# calculator.js prüfen
|
||||
curl http://localhost:8080/js/calculator.js
|
||||
```
|
||||
|
||||
#### Login funktioniert nicht
|
||||
|
||||
**Problem:** Login-Formular gibt Fehler oder leitet nicht weiter.
|
||||
|
||||
**Lösung:**
|
||||
```bash
|
||||
# Cache leeren
|
||||
docker-compose exec web php bin/console cache:clear
|
||||
|
||||
# Prüfen ob User existiert
|
||||
docker-compose exec db mariadb -u root -proot -e "SELECT email FROM users" immorechner
|
||||
|
||||
# Session-Verzeichnis prüfen
|
||||
docker-compose exec web ls -la /var/www/html/var/cache/dev/sessions
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### API-Probleme
|
||||
|
||||
#### Swagger UI lädt ohne CSS
|
||||
|
||||
**Problem:** http://localhost:8080/api/docs.html lädt, aber ohne Formatierung.
|
||||
|
||||
**Lösung:**
|
||||
```bash
|
||||
# Bundle-Assets installieren
|
||||
docker-compose exec web php bin/console assets:install public --symlink --relative
|
||||
|
||||
# Cache leeren
|
||||
docker-compose exec web php bin/console cache:clear
|
||||
|
||||
# Prüfen ob Assets existieren
|
||||
docker-compose exec web ls -la /var/www/html/public/bundles/apiplatform
|
||||
```
|
||||
|
||||
#### API gibt 401 Unauthorized
|
||||
|
||||
**Problem:** API-Anfrage mit API-Key schlägt fehl.
|
||||
|
||||
**Lösung:**
|
||||
```bash
|
||||
# API-Key prüfen
|
||||
docker-compose exec db mariadb -u root -proot -e "SELECT id, email, api_key FROM users" immorechner
|
||||
|
||||
# Korrekten Header verwenden
|
||||
curl -H "X-API-KEY: ihr-api-key" http://localhost:8080/api/immobilies
|
||||
|
||||
# Nicht "Authorization" Header!
|
||||
```
|
||||
|
||||
#### API gibt 500 Internal Server Error
|
||||
|
||||
**Problem:** API-Anfrage schlägt mit Server-Fehler fehl.
|
||||
|
||||
**Lösung:**
|
||||
```bash
|
||||
# Logs prüfen
|
||||
docker-compose logs web
|
||||
|
||||
# Symfony Logs prüfen
|
||||
docker-compose exec web tail -f /var/www/html/var/log/dev.log
|
||||
|
||||
# Datenbank-Verbindung testen
|
||||
docker-compose exec web php bin/console doctrine:query:sql "SELECT 1"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Datenbank-Probleme
|
||||
|
||||
#### Connection refused
|
||||
|
||||
**Problem:** Kann nicht zur Datenbank verbinden.
|
||||
|
||||
**Lösung:**
|
||||
```bash
|
||||
# DB-Container läuft?
|
||||
docker ps | grep db
|
||||
|
||||
# DB-Container-Logs
|
||||
docker-compose logs db
|
||||
|
||||
# Warten (DB braucht ~30-60 Sekunden)
|
||||
sleep 60
|
||||
|
||||
# DB-Verbindung testen
|
||||
docker-compose exec db mariadb -u root -proot -e "SELECT 1"
|
||||
|
||||
# Neustart
|
||||
docker-compose restart db
|
||||
```
|
||||
|
||||
#### Migrations schlagen fehl
|
||||
|
||||
**Problem:** `doctrine:migrations:migrate` gibt Fehler.
|
||||
|
||||
**Lösung:**
|
||||
```bash
|
||||
# Migration-Status prüfen
|
||||
docker-compose exec web php bin/console doctrine:migrations:status
|
||||
|
||||
# Datenbank droppen und neu erstellen
|
||||
docker-compose exec web php bin/console doctrine:database:drop --force
|
||||
docker-compose exec web php bin/console doctrine:database:create
|
||||
docker-compose exec web php bin/console doctrine:migrations:migrate --no-interaction
|
||||
|
||||
# Oder: Container komplett neu aufsetzen
|
||||
docker-compose down -v
|
||||
docker-compose up -d --build
|
||||
docker-compose exec web composer install
|
||||
docker-compose exec web php bin/console doctrine:migrations:migrate --no-interaction
|
||||
```
|
||||
|
||||
#### Tabellen fehlen
|
||||
|
||||
**Problem:** "Table 'immorechner.users' doesn't exist"
|
||||
|
||||
**Lösung:**
|
||||
```bash
|
||||
# Migrations ausführen
|
||||
docker-compose exec web php bin/console doctrine:migrations:migrate --no-interaction
|
||||
|
||||
# Tabellen prüfen
|
||||
docker-compose exec db mariadb -u root -proot -e "SHOW TABLES" immorechner
|
||||
|
||||
# Falls leer: Migration erneut ausführen
|
||||
docker-compose exec web php bin/console doctrine:migrations:migrate --no-interaction
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Docker-Probleme
|
||||
|
||||
#### Container starten nicht
|
||||
|
||||
**Problem:** `docker-compose up` schlägt fehl.
|
||||
|
||||
**Lösung:**
|
||||
```bash
|
||||
# Logs prüfen
|
||||
docker-compose logs
|
||||
|
||||
# Alte Container stoppen
|
||||
docker-compose down
|
||||
|
||||
# Neu starten mit Build
|
||||
docker-compose up -d --build
|
||||
|
||||
# Docker neu starten
|
||||
# Windows: Docker Desktop neu starten
|
||||
# Linux: sudo systemctl restart docker
|
||||
```
|
||||
|
||||
#### Port bereits belegt
|
||||
|
||||
**Problem:** "Port 8080 is already allocated"
|
||||
|
||||
**Lösung 1 - Port freigeben:**
|
||||
```bash
|
||||
# Windows
|
||||
netstat -ano | findstr :8080
|
||||
taskkill /PID <prozess-id> /F
|
||||
|
||||
# Linux/Mac
|
||||
lsof -i :8080
|
||||
kill -9 <prozess-id>
|
||||
```
|
||||
|
||||
**Lösung 2 - Port ändern:**
|
||||
```yaml
|
||||
# In docker-compose.yml
|
||||
services:
|
||||
web:
|
||||
ports:
|
||||
- "8090:80" # Statt 8080:80
|
||||
```
|
||||
|
||||
#### Volumes löschen nicht möglich
|
||||
|
||||
**Problem:** "Volume is in use"
|
||||
|
||||
**Lösung:**
|
||||
```bash
|
||||
# Alle Container stoppen
|
||||
docker-compose down
|
||||
|
||||
# Volume prüfen
|
||||
docker volume ls
|
||||
|
||||
# Container mit Volume finden
|
||||
docker ps -a --filter volume=immorechner_db_data
|
||||
|
||||
# Container löschen
|
||||
docker rm <container-id>
|
||||
|
||||
# Volume löschen
|
||||
docker volume rm immorechner_db_data
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Composer-Probleme
|
||||
|
||||
#### "Class not found" Fehler
|
||||
|
||||
**Problem:** PHP kann Klasse nicht finden.
|
||||
|
||||
**Lösung:**
|
||||
```bash
|
||||
# Autoloader neu generieren
|
||||
docker-compose exec web composer dump-autoload
|
||||
|
||||
# Cache leeren
|
||||
docker-compose exec web php bin/console cache:clear
|
||||
|
||||
# Dependencies neu installieren
|
||||
docker-compose exec web rm -rf vendor
|
||||
docker-compose exec web composer install
|
||||
```
|
||||
|
||||
#### Composer sehr langsam
|
||||
|
||||
**Problem:** `composer install` braucht ewig.
|
||||
|
||||
**Lösung:**
|
||||
```bash
|
||||
# Im Container ausführen (schneller als via Volume)
|
||||
docker-compose exec web composer install
|
||||
|
||||
# Cache leeren
|
||||
docker-compose exec web composer clear-cache
|
||||
|
||||
# Parallel Downloads aktivieren
|
||||
docker-compose exec web composer config --global process-timeout 2000
|
||||
docker-compose exec web composer config --global repos.packagist composer https://packagist.org
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Berechtigungs-Probleme
|
||||
|
||||
#### "Permission denied" in var/
|
||||
|
||||
**Problem:** Kann nicht in `var/cache` oder `var/log` schreiben.
|
||||
|
||||
**Lösung:**
|
||||
```bash
|
||||
# Berechtigungen setzen
|
||||
docker-compose exec -u root web chown -R www-data:www-data /var/www/html/var
|
||||
|
||||
# Oder: 777 Rechte (nur Development!)
|
||||
docker-compose exec -u root web chmod -R 777 /var/www/html/var
|
||||
|
||||
# Cache neu erstellen
|
||||
docker-compose exec web php bin/console cache:clear
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Performance-Probleme
|
||||
|
||||
#### Seite lädt sehr langsam
|
||||
|
||||
**Problem:** Frontend/API antwortet langsam.
|
||||
|
||||
**Lösung:**
|
||||
```bash
|
||||
# Opcache Status prüfen
|
||||
docker-compose exec web php -i | grep opcache
|
||||
|
||||
# Cache warmup
|
||||
docker-compose exec web php bin/console cache:warmup
|
||||
|
||||
# Für Produktion: APP_ENV=prod setzen in .env
|
||||
# Opcache aktiviert sich automatisch
|
||||
|
||||
# Docker-Resources erhöhen
|
||||
# Docker Desktop -> Settings -> Resources -> Memory: 4GB+
|
||||
```
|
||||
|
||||
#### Datenbank-Queries langsam
|
||||
|
||||
**Problem:** API-Anfragen langsam bei vielen Datensätzen.
|
||||
|
||||
**Lösung:**
|
||||
```bash
|
||||
# Indizes prüfen
|
||||
docker-compose exec web php bin/console doctrine:schema:validate
|
||||
|
||||
# Query-Log aktivieren (temporär)
|
||||
# In config/packages/doctrine.yaml:
|
||||
logging: true
|
||||
profiling: true
|
||||
|
||||
# Slow Query Log in MariaDB aktivieren
|
||||
docker-compose exec db mariadb -u root -proot -e "SET GLOBAL slow_query_log = 'ON'"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Browser-Probleme
|
||||
|
||||
#### Alte Daten werden angezeigt
|
||||
|
||||
**Problem:** Änderungen werden nicht sichtbar.
|
||||
|
||||
**Lösung:**
|
||||
```bash
|
||||
# Browser-Cache leeren
|
||||
# Chrome/Firefox: Strg+Shift+Delete
|
||||
|
||||
# Hard Reload
|
||||
# Chrome/Firefox: Strg+F5
|
||||
# Mac: Cmd+Shift+R
|
||||
|
||||
# Server-Cache leeren
|
||||
docker-compose exec web php bin/console cache:clear
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Debug-Modus aktivieren
|
||||
|
||||
```bash
|
||||
# .env
|
||||
APP_ENV=dev
|
||||
APP_DEBUG=true
|
||||
|
||||
# Cache leeren
|
||||
docker-compose exec web php bin/console cache:clear
|
||||
|
||||
# Symfony Profiler in Browser verwenden
|
||||
# Unten auf der Seite erscheint Toolbar
|
||||
```
|
||||
|
||||
## Komplett-Reset (Letzter Ausweg)
|
||||
|
||||
**WARNUNG:** Löscht alle Daten!
|
||||
|
||||
```bash
|
||||
# Alles stoppen und löschen
|
||||
docker-compose down -v
|
||||
|
||||
# Docker-Images löschen
|
||||
docker rmi immorechner-web
|
||||
|
||||
# Neu aufbauen
|
||||
docker-compose up -d --build
|
||||
|
||||
# Dependencies installieren
|
||||
docker-compose exec web composer install
|
||||
|
||||
# Datenbank initialisieren
|
||||
docker-compose exec web php bin/console doctrine:migrations:migrate --no-interaction
|
||||
|
||||
# Cache leeren
|
||||
docker-compose exec web php bin/console cache:clear
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Support
|
||||
|
||||
Wenn das Problem weiterhin besteht:
|
||||
|
||||
1. **Logs sammeln:**
|
||||
```bash
|
||||
docker-compose logs > debug_logs.txt
|
||||
```
|
||||
|
||||
2. **Systeminfo sammeln:**
|
||||
```bash
|
||||
docker version > system_info.txt
|
||||
docker-compose version >> system_info.txt
|
||||
php -v >> system_info.txt
|
||||
```
|
||||
|
||||
3. **Issue erstellen** mit:
|
||||
- Problembeschreibung
|
||||
- Fehlermeldung
|
||||
- Logs (debug_logs.txt)
|
||||
- Systeminfo (system_info.txt)
|
||||
- Schritte zum Reproduzieren
|
||||
|
||||
---
|
||||
|
||||
**Siehe auch:**
|
||||
- [Docker](docker.md) - Container-Management
|
||||
- [Development](development.md) - Entwickler-Workflow
|
||||
- [Installation](installation.md) - Setup-Anleitung
|
||||
|
||||
[← Zurück zur Hauptseite](../README.md)
|
||||
@@ -1,31 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace DoctrineMigrations;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
/**
|
||||
* Auto-generated Migration: Please modify to your needs!
|
||||
*/
|
||||
final class Version20251108171050 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return '';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
// this up() migration is auto-generated, please modify it to your needs
|
||||
$this->addSql('CREATE TABLE users (id INT AUTO_INCREMENT NOT NULL, name VARCHAR(255) NOT NULL, email VARCHAR(255) NOT NULL, role VARCHAR(255) NOT NULL, created_at DATETIME NOT NULL, UNIQUE INDEX UNIQ_1483A5E9E7927C74 (email), PRIMARY KEY (id)) DEFAULT CHARACTER SET utf8mb4');
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
// this down() migration is auto-generated, please modify it to your needs
|
||||
$this->addSql('DROP TABLE users');
|
||||
}
|
||||
}
|
||||
120
migrations/Version20251109100000.php
Normal file
120
migrations/Version20251109100000.php
Normal file
@@ -0,0 +1,120 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace DoctrineMigrations;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
/**
|
||||
* Initial migration: Creates all tables and populates reference data
|
||||
*/
|
||||
final class Version20251109100000 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'Initial database schema with all tables and reference data';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
// Create users table
|
||||
$this->addSql('CREATE TABLE users (
|
||||
id INT AUTO_INCREMENT NOT NULL,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
email VARCHAR(255) NOT NULL,
|
||||
role VARCHAR(255) NOT NULL,
|
||||
created_at DATETIME NOT NULL,
|
||||
api_key VARCHAR(64) NOT NULL,
|
||||
UNIQUE INDEX UNIQ_1483A5E9E7927C74 (email),
|
||||
UNIQUE INDEX UNIQ_1483A5E9C912ED9D (api_key),
|
||||
PRIMARY KEY (id)
|
||||
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci ENGINE = InnoDB');
|
||||
|
||||
// Create bundeslaender table
|
||||
$this->addSql('CREATE TABLE bundeslaender (
|
||||
id INT AUTO_INCREMENT NOT NULL,
|
||||
name VARCHAR(100) NOT NULL,
|
||||
grunderwerbsteuer NUMERIC(4, 2) NOT NULL,
|
||||
UNIQUE INDEX UNIQ_DF7DFAB25E237E06 (name),
|
||||
PRIMARY KEY (id)
|
||||
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci ENGINE = InnoDB');
|
||||
|
||||
// Create heizungstypen table
|
||||
$this->addSql('CREATE TABLE heizungstypen (
|
||||
id INT AUTO_INCREMENT NOT NULL,
|
||||
name VARCHAR(100) NOT NULL,
|
||||
UNIQUE INDEX UNIQ_6161C2A65E237E06 (name),
|
||||
PRIMARY KEY (id)
|
||||
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci ENGINE = InnoDB');
|
||||
|
||||
// Create immobilien table
|
||||
$this->addSql('CREATE TABLE immobilien (
|
||||
id INT AUTO_INCREMENT NOT NULL,
|
||||
verwalter_id INT NOT NULL,
|
||||
heizungstyp_id INT DEFAULT NULL,
|
||||
bundesland_id INT DEFAULT NULL,
|
||||
adresse VARCHAR(255) NOT NULL,
|
||||
wohnflaeche INT NOT NULL,
|
||||
nutzflaeche INT NOT NULL,
|
||||
garage TINYINT(1) NOT NULL,
|
||||
zimmer INT NOT NULL,
|
||||
baujahr INT DEFAULT NULL,
|
||||
typ VARCHAR(255) NOT NULL,
|
||||
beschreibung LONGTEXT DEFAULT NULL,
|
||||
etage INT DEFAULT NULL,
|
||||
kaufpreis INT DEFAULT NULL,
|
||||
abschreibungszeit INT DEFAULT NULL,
|
||||
created_at DATETIME NOT NULL,
|
||||
updated_at DATETIME NOT NULL,
|
||||
INDEX IDX_2C789D3E5F66D3 (verwalter_id),
|
||||
INDEX IDX_2C789DC1B4DB52 (heizungstyp_id),
|
||||
INDEX IDX_2C789DB74FDBEB (bundesland_id),
|
||||
PRIMARY KEY (id)
|
||||
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci ENGINE = InnoDB');
|
||||
|
||||
// Add foreign keys
|
||||
$this->addSql('ALTER TABLE immobilien ADD CONSTRAINT FK_2C789D3E5F66D3 FOREIGN KEY (verwalter_id) REFERENCES users (id)');
|
||||
$this->addSql('ALTER TABLE immobilien ADD CONSTRAINT FK_2C789DC1B4DB52 FOREIGN KEY (heizungstyp_id) REFERENCES heizungstypen (id)');
|
||||
$this->addSql('ALTER TABLE immobilien ADD CONSTRAINT FK_2C789DB74FDBEB FOREIGN KEY (bundesland_id) REFERENCES bundeslaender (id)');
|
||||
|
||||
// Populate Bundesländer with Grunderwerbsteuer rates
|
||||
$this->addSql("INSERT INTO bundeslaender (name, grunderwerbsteuer) VALUES
|
||||
('Baden-Württemberg', 5.00),
|
||||
('Bayern', 3.50),
|
||||
('Berlin', 6.00),
|
||||
('Brandenburg', 6.50),
|
||||
('Bremen', 5.00),
|
||||
('Hamburg', 5.50),
|
||||
('Hessen', 6.00),
|
||||
('Mecklenburg-Vorpommern', 6.00),
|
||||
('Niedersachsen', 5.00),
|
||||
('Nordrhein-Westfalen', 6.50),
|
||||
('Rheinland-Pfalz', 5.00),
|
||||
('Saarland', 6.50),
|
||||
('Sachsen', 5.50),
|
||||
('Sachsen-Anhalt', 5.00),
|
||||
('Schleswig-Holstein', 6.50),
|
||||
('Thüringen', 5.00)");
|
||||
|
||||
// Populate Heizungstypen
|
||||
$this->addSql("INSERT INTO heizungstypen (name) VALUES
|
||||
('Ölheizung'),
|
||||
('Gasheizung'),
|
||||
('Wärmepumpe'),
|
||||
('Pelletheizung')");
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
// Drop all tables
|
||||
$this->addSql('ALTER TABLE immobilien DROP FOREIGN KEY FK_2C789D3E5F66D3');
|
||||
$this->addSql('ALTER TABLE immobilien DROP FOREIGN KEY FK_2C789DC1B4DB52');
|
||||
$this->addSql('ALTER TABLE immobilien DROP FOREIGN KEY FK_2C789DB74FDBEB');
|
||||
$this->addSql('DROP TABLE immobilien');
|
||||
$this->addSql('DROP TABLE bundeslaender');
|
||||
$this->addSql('DROP TABLE heizungstypen');
|
||||
$this->addSql('DROP TABLE users');
|
||||
}
|
||||
}
|
||||
22
migrations/bundeslaender_data.sql
Normal file
22
migrations/bundeslaender_data.sql
Normal file
@@ -0,0 +1,22 @@
|
||||
-- Bundesländer mit Grunderwerbsteuer-Sätzen (Stand 2025)
|
||||
-- Dieses Script wird nach der Migration automatisch ausgeführt
|
||||
|
||||
INSERT INTO bundeslaender (name, grunderwerbsteuer) VALUES
|
||||
('Baden-Württemberg', 5.00),
|
||||
('Bayern', 3.50),
|
||||
('Berlin', 6.00),
|
||||
('Brandenburg', 6.50),
|
||||
('Bremen', 5.00),
|
||||
('Hamburg', 5.50),
|
||||
('Hessen', 6.00),
|
||||
('Mecklenburg-Vorpommern', 6.00),
|
||||
('Niedersachsen', 5.00),
|
||||
('Nordrhein-Westfalen', 6.50),
|
||||
('Rheinland-Pfalz', 5.00),
|
||||
('Saarland', 6.50),
|
||||
('Sachsen', 5.50),
|
||||
('Sachsen-Anhalt', 5.00),
|
||||
('Schleswig-Holstein', 6.50),
|
||||
('Thüringen', 5.00)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
grunderwerbsteuer = VALUES(grunderwerbsteuer);
|
||||
1
openapi.json
Normal file
1
openapi.json
Normal file
File diff suppressed because one or more lines are too long
44
phpunit.dist.xml
Normal file
44
phpunit.dist.xml
Normal file
@@ -0,0 +1,44 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<!-- https://phpunit.readthedocs.io/en/latest/configuration.html -->
|
||||
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd"
|
||||
colors="true"
|
||||
failOnDeprecation="true"
|
||||
failOnNotice="true"
|
||||
failOnWarning="true"
|
||||
bootstrap="tests/bootstrap.php"
|
||||
cacheDirectory=".phpunit.cache"
|
||||
>
|
||||
<php>
|
||||
<ini name="display_errors" value="1" />
|
||||
<ini name="error_reporting" value="-1" />
|
||||
<server name="APP_ENV" value="test" force="true" />
|
||||
<server name="SHELL_VERBOSITY" value="-1" />
|
||||
</php>
|
||||
|
||||
<testsuites>
|
||||
<testsuite name="Project Test Suite">
|
||||
<directory>tests</directory>
|
||||
</testsuite>
|
||||
</testsuites>
|
||||
|
||||
<source ignoreSuppressionOfDeprecations="true"
|
||||
ignoreIndirectDeprecations="true"
|
||||
restrictNotices="true"
|
||||
restrictWarnings="true"
|
||||
>
|
||||
<include>
|
||||
<directory>src</directory>
|
||||
</include>
|
||||
|
||||
<deprecationTrigger>
|
||||
<method>Doctrine\Deprecations\Deprecation::trigger</method>
|
||||
<method>Doctrine\Deprecations\Deprecation::delegateTriggerToBackend</method>
|
||||
<function>trigger_deprecation</function>
|
||||
</deprecationTrigger>
|
||||
</source>
|
||||
|
||||
<extensions>
|
||||
</extensions>
|
||||
</phpunit>
|
||||
96
public/css/auth.css
Normal file
96
public/css/auth.css
Normal file
@@ -0,0 +1,96 @@
|
||||
.auth-container {
|
||||
max-width: 450px;
|
||||
margin: 50px auto;
|
||||
}
|
||||
|
||||
.auth-box {
|
||||
background: white;
|
||||
padding: 40px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.auth-box h2 {
|
||||
text-align: center;
|
||||
color: #333;
|
||||
margin-bottom: 30px;
|
||||
padding-bottom: 15px;
|
||||
border-bottom: 2px solid #4CAF50;
|
||||
}
|
||||
|
||||
.auth-form .form-group {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.auth-form label {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
font-weight: 500;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
.auth-form input[type="text"],
|
||||
.auth-form input[type="email"],
|
||||
.auth-form input[type="password"] {
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.auth-form input:focus {
|
||||
outline: none;
|
||||
border-color: #4CAF50;
|
||||
}
|
||||
|
||||
.auth-form .btn-submit {
|
||||
width: 100%;
|
||||
padding: 14px;
|
||||
background-color: #4CAF50;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.3s;
|
||||
}
|
||||
|
||||
.auth-form .btn-submit:hover {
|
||||
background-color: #45a049;
|
||||
}
|
||||
|
||||
.error-message {
|
||||
background-color: #ffebee;
|
||||
color: #c62828;
|
||||
padding: 12px;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 20px;
|
||||
border-left: 4px solid #c62828;
|
||||
}
|
||||
|
||||
.success-message {
|
||||
background-color: #e8f5e9;
|
||||
color: #2e7d32;
|
||||
padding: 12px;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 20px;
|
||||
border-left: 4px solid #2e7d32;
|
||||
}
|
||||
|
||||
.auth-links {
|
||||
text-align: center;
|
||||
margin-top: 20px;
|
||||
padding-top: 20px;
|
||||
border-top: 1px solid #eee;
|
||||
}
|
||||
|
||||
.auth-links a {
|
||||
color: #4CAF50;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.auth-links a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
146
public/css/calculator.css
Normal file
146
public/css/calculator.css
Normal file
@@ -0,0 +1,146 @@
|
||||
.calculator-container {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 20px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.form-section, .results-section {
|
||||
background: white;
|
||||
padding: 30px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.form-section h2, .results-section h2 {
|
||||
color: #333;
|
||||
border-bottom: 2px solid #4CAF50;
|
||||
padding-bottom: 10px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
display: block;
|
||||
margin-bottom: 5px;
|
||||
font-weight: 500;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
.form-group input,
|
||||
.form-group select {
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.form-group input:focus,
|
||||
.form-group select:focus {
|
||||
outline: none;
|
||||
border-color: #4CAF50;
|
||||
}
|
||||
|
||||
.checkbox-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.checkbox-group input[type="checkbox"] {
|
||||
width: auto;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.result-item {
|
||||
padding: 15px;
|
||||
margin-bottom: 15px;
|
||||
background-color: #f8f9fa;
|
||||
border-left: 4px solid #4CAF50;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.result-item h3 {
|
||||
margin: 0 0 5px 0;
|
||||
color: #333;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.result-item .value {
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
color: #4CAF50;
|
||||
}
|
||||
|
||||
.result-item .description {
|
||||
margin-top: 5px;
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.button-group {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 12px 24px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
transition: background-color 0.3s;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background-color: #4CAF50;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background-color: #45a049;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background-color: #6c757d;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background-color: #5a6268;
|
||||
}
|
||||
|
||||
.share-link {
|
||||
margin-top: 15px;
|
||||
padding: 10px;
|
||||
background-color: #e8f5e9;
|
||||
border-radius: 4px;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.share-link input {
|
||||
width: 100%;
|
||||
padding: 8px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.info-banner {
|
||||
background-color: #e3f2fd;
|
||||
padding: 15px;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 20px;
|
||||
border-left: 4px solid #2196F3;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.calculator-container {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
112
public/js/calculator.js
Normal file
112
public/js/calculator.js
Normal file
@@ -0,0 +1,112 @@
|
||||
$(document).ready(function() {
|
||||
// Live calculation function
|
||||
function calculate() {
|
||||
const kaufpreis = parseFloat($('#kaufpreis').val()) || 0;
|
||||
const wohnflaeche = parseFloat($('#wohnflaeche').val()) || 0;
|
||||
const nutzflaeche = parseFloat($('#nutzflaeche').val()) || 0;
|
||||
const baujahr = parseInt($('#baujahr').val()) || 0;
|
||||
const abschreibungszeit = parseFloat($('#abschreibungszeit').val()) || 50;
|
||||
const bundeslandSteuer = parseFloat($('#bundesland_id option:selected').data('steuer')) || 0;
|
||||
|
||||
// Gesamtfläche
|
||||
const gesamtflaeche = wohnflaeche + nutzflaeche;
|
||||
$('#result-gesamtflaeche').text(gesamtflaeche.toLocaleString('de-DE') + ' m²');
|
||||
|
||||
// Preis pro m²
|
||||
const preisProQm = wohnflaeche > 0 ? kaufpreis / wohnflaeche : 0;
|
||||
$('#result-preis-pro-qm').text(preisProQm.toLocaleString('de-DE', {minimumFractionDigits: 2, maximumFractionDigits: 2}) + ' €');
|
||||
|
||||
// Grunderwerbsteuer
|
||||
const grunderwerbsteuer = kaufpreis * (bundeslandSteuer / 100);
|
||||
$('#result-grunderwerbsteuer').text(grunderwerbsteuer.toLocaleString('de-DE', {minimumFractionDigits: 2, maximumFractionDigits: 2}) + ' €');
|
||||
|
||||
// Gesamtkosten
|
||||
const gesamtkosten = kaufpreis + grunderwerbsteuer;
|
||||
$('#result-gesamtkosten').text(gesamtkosten.toLocaleString('de-DE', {minimumFractionDigits: 2, maximumFractionDigits: 2}) + ' €');
|
||||
|
||||
// Jährliche Abschreibung
|
||||
const abschreibung = abschreibungszeit > 0 ? kaufpreis / abschreibungszeit : 0;
|
||||
$('#result-abschreibung').text(abschreibung.toLocaleString('de-DE', {minimumFractionDigits: 2, maximumFractionDigits: 2}) + ' €');
|
||||
|
||||
// Alter der Immobilie
|
||||
const currentYear = new Date().getFullYear();
|
||||
const alter = baujahr > 0 ? currentYear - baujahr : 0;
|
||||
$('#result-alter').text(alter + ' Jahre');
|
||||
}
|
||||
|
||||
// Trigger calculation on any input change
|
||||
$('#immo-calculator-form input, #immo-calculator-form select').on('input change', function() {
|
||||
calculate();
|
||||
});
|
||||
|
||||
// Generate shareable link
|
||||
$('#share-link-btn').click(function() {
|
||||
const formData = $('#immo-calculator-form').serializeArray();
|
||||
const params = new URLSearchParams();
|
||||
|
||||
formData.forEach(item => {
|
||||
if (item.value) {
|
||||
params.append(item.name, item.value);
|
||||
}
|
||||
});
|
||||
|
||||
const shareUrl = window.location.origin + window.location.pathname + '?' + params.toString();
|
||||
$('#share-url').val(shareUrl);
|
||||
$('#share-link-container').slideDown();
|
||||
});
|
||||
|
||||
// Copy link to clipboard
|
||||
$('#copy-link-btn').click(function() {
|
||||
const shareUrl = $('#share-url');
|
||||
shareUrl.select();
|
||||
document.execCommand('copy');
|
||||
alert('Link wurde in die Zwischenablage kopiert!');
|
||||
});
|
||||
|
||||
// Reset form
|
||||
$('#reset-btn').click(function() {
|
||||
$('#immo-calculator-form')[0].reset();
|
||||
$('#share-link-container').slideUp();
|
||||
calculate();
|
||||
});
|
||||
|
||||
// Save immobilie (for logged in users)
|
||||
$('#save-immobilie-btn').click(function() {
|
||||
const formData = $('#immo-calculator-form').serializeArray();
|
||||
const data = {};
|
||||
|
||||
formData.forEach(item => {
|
||||
if (item.name === 'garage') {
|
||||
data[item.name] = $('#garage').is(':checked');
|
||||
} else {
|
||||
data[item.name] = item.value;
|
||||
}
|
||||
});
|
||||
|
||||
$.ajax({
|
||||
url: '/immobilie/save',
|
||||
method: 'POST',
|
||||
contentType: 'application/json',
|
||||
data: JSON.stringify(data),
|
||||
success: function(response) {
|
||||
if (response.success) {
|
||||
alert(response.message + '\n\nSie können Ihre gespeicherten Immobilien unter "Meine Immobilien" einsehen.');
|
||||
} else {
|
||||
alert('Fehler: ' + response.message);
|
||||
}
|
||||
},
|
||||
error: function(xhr) {
|
||||
if (xhr.status === 401) {
|
||||
alert('Sie müssen angemeldet sein, um Immobilien zu speichern.');
|
||||
window.location.href = '/login';
|
||||
} else {
|
||||
const response = xhr.responseJSON;
|
||||
alert('Fehler: ' + (response ? response.message : 'Unbekannter Fehler'));
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Initial calculation on page load
|
||||
calculate();
|
||||
});
|
||||
96
src/Controller/AuthController.php
Normal file
96
src/Controller/AuthController.php
Normal file
@@ -0,0 +1,96 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controller;
|
||||
|
||||
use App\Entity\User;
|
||||
use App\Repository\UserRepository;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
use Symfony\Component\Security\Http\Authentication\AuthenticationUtils;
|
||||
|
||||
class AuthController extends AbstractController
|
||||
{
|
||||
#[Route('/login', name: 'app_login')]
|
||||
public function login(AuthenticationUtils $authenticationUtils): Response
|
||||
{
|
||||
// Redirect if already logged in
|
||||
if ($this->getUser()) {
|
||||
return $this->redirectToRoute('app_home');
|
||||
}
|
||||
|
||||
// Get the login error if there is one
|
||||
$error = $authenticationUtils->getLastAuthenticationError();
|
||||
|
||||
// Last username entered by the user
|
||||
$lastUsername = $authenticationUtils->getLastUsername();
|
||||
|
||||
return $this->render('auth/login.html.twig', [
|
||||
'last_username' => $lastUsername,
|
||||
'error' => $error,
|
||||
]);
|
||||
}
|
||||
|
||||
#[Route('/register', name: 'app_register')]
|
||||
public function register(
|
||||
Request $request,
|
||||
UserPasswordHasherInterface $passwordHasher,
|
||||
EntityManagerInterface $entityManager,
|
||||
UserRepository $userRepository
|
||||
): Response {
|
||||
// Redirect if already logged in
|
||||
if ($this->getUser()) {
|
||||
return $this->redirectToRoute('app_home');
|
||||
}
|
||||
|
||||
$error = null;
|
||||
|
||||
if ($request->isMethod('POST')) {
|
||||
$name = $request->request->get('name');
|
||||
$email = $request->request->get('email');
|
||||
$password = $request->request->get('password');
|
||||
$passwordConfirm = $request->request->get('password_confirm');
|
||||
|
||||
// Validation
|
||||
if (empty($name) || empty($email) || empty($password)) {
|
||||
$error = 'Bitte füllen Sie alle Felder aus.';
|
||||
} elseif ($password !== $passwordConfirm) {
|
||||
$error = 'Die Passwörter stimmen nicht überein.';
|
||||
} elseif (strlen($password) < 6) {
|
||||
$error = 'Das Passwort muss mindestens 6 Zeichen lang sein.';
|
||||
} elseif ($userRepository->findOneBy(['email' => $email])) {
|
||||
$error = 'Diese E-Mail-Adresse ist bereits registriert.';
|
||||
} else {
|
||||
// Create new user
|
||||
$user = new User();
|
||||
$user->setName($name);
|
||||
$user->setEmail($email);
|
||||
|
||||
// Hash the password
|
||||
$hashedPassword = $passwordHasher->hashPassword($user, $password);
|
||||
$user->setPassword($hashedPassword);
|
||||
|
||||
$entityManager->persist($user);
|
||||
$entityManager->flush();
|
||||
|
||||
$this->addFlash('success', 'Registrierung erfolgreich! Sie können sich jetzt anmelden.');
|
||||
|
||||
return $this->redirectToRoute('app_login');
|
||||
}
|
||||
}
|
||||
|
||||
return $this->render('auth/register.html.twig', [
|
||||
'error' => $error,
|
||||
]);
|
||||
}
|
||||
|
||||
#[Route('/logout', name: 'app_logout')]
|
||||
public function logout(): void
|
||||
{
|
||||
// This method can be blank - it will be intercepted by the logout key on your firewall
|
||||
throw new \Exception('This should never be reached!');
|
||||
}
|
||||
}
|
||||
@@ -2,17 +2,45 @@
|
||||
|
||||
namespace App\Controller;
|
||||
|
||||
use App\Repository\BundeslandRepository;
|
||||
use App\Repository\HeizungstypRepository;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
|
||||
class HomeController extends AbstractController
|
||||
{
|
||||
#[Route('/', name: 'app_home')]
|
||||
public function index(): Response
|
||||
public function index(
|
||||
Request $request,
|
||||
BundeslandRepository $bundeslandRepository,
|
||||
HeizungstypRepository $heizungstypRepository
|
||||
): Response
|
||||
{
|
||||
$bundeslaender = $bundeslandRepository->findAll();
|
||||
$heizungstypen = $heizungstypRepository->findAll();
|
||||
|
||||
// Load data from URL parameters if present
|
||||
$immobilienData = [
|
||||
'adresse' => $request->query->get('adresse', ''),
|
||||
'kaufpreis' => $request->query->get('kaufpreis', ''),
|
||||
'wohnflaeche' => $request->query->get('wohnflaeche', ''),
|
||||
'nutzflaeche' => $request->query->get('nutzflaeche', ''),
|
||||
'zimmer' => $request->query->get('zimmer', ''),
|
||||
'baujahr' => $request->query->get('baujahr', ''),
|
||||
'garage' => $request->query->get('garage', false),
|
||||
'etage' => $request->query->get('etage', ''),
|
||||
'typ' => $request->query->get('typ', ''),
|
||||
'bundesland_id' => $request->query->get('bundesland_id', ''),
|
||||
'heizungstyp_id' => $request->query->get('heizungstyp_id', ''),
|
||||
'abschreibungszeit' => $request->query->get('abschreibungszeit', '50'),
|
||||
];
|
||||
|
||||
return $this->render('home/index.html.twig', [
|
||||
'controller_name' => 'HomeController',
|
||||
'bundeslaender' => $bundeslaender,
|
||||
'heizungstypen' => $heizungstypen,
|
||||
'immobilienData' => $immobilienData,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
37
src/Controller/ImmobilieController.php
Normal file
37
src/Controller/ImmobilieController.php
Normal file
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controller;
|
||||
|
||||
use App\Entity\Immobilie;
|
||||
use App\Repository\ImmobilieRepository;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
|
||||
#[Route('/immobilien')]
|
||||
class ImmobilieController extends AbstractController
|
||||
{
|
||||
#[Route('/', name: 'app_immobilie_index')]
|
||||
public function index(ImmobilieRepository $repository): Response
|
||||
{
|
||||
$immobilien = $repository->findVerfuegbare();
|
||||
|
||||
return $this->render('immobilie/index.html.twig', [
|
||||
'immobilien' => $immobilien,
|
||||
]);
|
||||
}
|
||||
|
||||
#[Route('/{id}', name: 'app_immobilie_show', requirements: ['id' => '\d+'])]
|
||||
public function show(Immobilie $immobilie): Response
|
||||
{
|
||||
return $this->render('immobilie/show.html.twig', [
|
||||
'immobilie' => $immobilie,
|
||||
]);
|
||||
}
|
||||
|
||||
#[Route('/suche', name: 'app_immobilie_suche')]
|
||||
public function suche(ImmobilieRepository $repository): Response
|
||||
{
|
||||
return $this->render('immobilie/suche.html.twig');
|
||||
}
|
||||
}
|
||||
119
src/Controller/ImmobilienSaveController.php
Normal file
119
src/Controller/ImmobilienSaveController.php
Normal file
@@ -0,0 +1,119 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controller;
|
||||
|
||||
use App\Entity\Immobilie;
|
||||
use App\Enum\ImmobilienTyp;
|
||||
use App\Repository\BundeslandRepository;
|
||||
use App\Repository\HeizungstypRepository;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
|
||||
class ImmobilienSaveController extends AbstractController
|
||||
{
|
||||
#[Route('/immobilie/save', name: 'app_immobilie_save', methods: ['POST'])]
|
||||
public function save(
|
||||
Request $request,
|
||||
EntityManagerInterface $entityManager,
|
||||
BundeslandRepository $bundeslandRepository,
|
||||
HeizungstypRepository $heizungstypRepository
|
||||
): JsonResponse {
|
||||
// Check if user is logged in
|
||||
if (!$this->getUser()) {
|
||||
return new JsonResponse([
|
||||
'success' => false,
|
||||
'message' => 'Sie müssen angemeldet sein, um Immobilien zu speichern.',
|
||||
], 401);
|
||||
}
|
||||
|
||||
$data = json_decode($request->getContent(), true);
|
||||
|
||||
// Validation
|
||||
if (empty($data['adresse']) || empty($data['kaufpreis']) || empty($data['wohnflaeche'])) {
|
||||
return new JsonResponse([
|
||||
'success' => false,
|
||||
'message' => 'Bitte füllen Sie mindestens Adresse, Kaufpreis und Wohnfläche aus.',
|
||||
], 400);
|
||||
}
|
||||
|
||||
// Create new Immobilie
|
||||
$immobilie = new Immobilie();
|
||||
$immobilie->setVerwalter($this->getUser());
|
||||
$immobilie->setAdresse($data['adresse']);
|
||||
$immobilie->setKaufpreis((int) $data['kaufpreis']);
|
||||
$immobilie->setWohnflaeche((int) $data['wohnflaeche']);
|
||||
$immobilie->setNutzflaeche((int) ($data['nutzflaeche'] ?? 0));
|
||||
$immobilie->setZimmer((int) ($data['zimmer'] ?? 0));
|
||||
|
||||
// Set Typ from string to enum
|
||||
$typ = ImmobilienTyp::WOHNUNG; // default
|
||||
if (!empty($data['typ'])) {
|
||||
$typ = match(strtolower($data['typ'])) {
|
||||
'haus' => ImmobilienTyp::HAUS,
|
||||
'gewerbe' => ImmobilienTyp::GEWERBE,
|
||||
'grundstück', 'grundstueck' => ImmobilienTyp::GRUNDSTUECK,
|
||||
'büro', 'buero' => ImmobilienTyp::BUERO,
|
||||
default => ImmobilienTyp::WOHNUNG,
|
||||
};
|
||||
}
|
||||
$immobilie->setTyp($typ);
|
||||
$immobilie->setGarage((bool) ($data['garage'] ?? false));
|
||||
|
||||
if (!empty($data['baujahr'])) {
|
||||
$immobilie->setBaujahr((int) $data['baujahr']);
|
||||
}
|
||||
|
||||
if (!empty($data['etage'])) {
|
||||
$immobilie->setEtage((int) $data['etage']);
|
||||
}
|
||||
|
||||
if (!empty($data['abschreibungszeit'])) {
|
||||
$immobilie->setAbschreibungszeit((int) $data['abschreibungszeit']);
|
||||
}
|
||||
|
||||
if (!empty($data['beschreibung'])) {
|
||||
$immobilie->setBeschreibung($data['beschreibung']);
|
||||
}
|
||||
|
||||
// Set Bundesland
|
||||
if (!empty($data['bundesland_id'])) {
|
||||
$bundesland = $bundeslandRepository->find($data['bundesland_id']);
|
||||
if ($bundesland) {
|
||||
$immobilie->setBundesland($bundesland);
|
||||
}
|
||||
}
|
||||
|
||||
// Set Heizungstyp
|
||||
if (!empty($data['heizungstyp_id'])) {
|
||||
$heizungstyp = $heizungstypRepository->find($data['heizungstyp_id']);
|
||||
if ($heizungstyp) {
|
||||
$immobilie->setHeizungstyp($heizungstyp);
|
||||
}
|
||||
}
|
||||
|
||||
$entityManager->persist($immobilie);
|
||||
$entityManager->flush();
|
||||
|
||||
return new JsonResponse([
|
||||
'success' => true,
|
||||
'message' => 'Immobilie erfolgreich gespeichert!',
|
||||
'id' => $immobilie->getId(),
|
||||
]);
|
||||
}
|
||||
|
||||
#[Route('/meine-immobilien', name: 'app_my_immobilien')]
|
||||
public function myImmobilien(): Response
|
||||
{
|
||||
$this->denyAccessUnlessGranted('IS_AUTHENTICATED_FULLY');
|
||||
|
||||
$immobilien = $this->getUser()->getImmobilien();
|
||||
|
||||
return $this->render('immobilie/my_immobilien.html.twig', [
|
||||
'immobilien' => $immobilien,
|
||||
]);
|
||||
}
|
||||
}
|
||||
58
src/Doctrine/CurrentUserExtension.php
Normal file
58
src/Doctrine/CurrentUserExtension.php
Normal file
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
namespace App\Doctrine;
|
||||
|
||||
use ApiPlatform\Doctrine\Orm\Extension\QueryCollectionExtensionInterface;
|
||||
use ApiPlatform\Doctrine\Orm\Extension\QueryItemExtensionInterface;
|
||||
use ApiPlatform\Doctrine\Orm\Util\QueryNameGeneratorInterface;
|
||||
use ApiPlatform\Metadata\Operation;
|
||||
use App\Entity\Immobilie;
|
||||
use App\Entity\User;
|
||||
use App\Enum\UserRole;
|
||||
use Doctrine\ORM\QueryBuilder;
|
||||
use Symfony\Bundle\SecurityBundle\Security;
|
||||
|
||||
class CurrentUserExtension implements QueryCollectionExtensionInterface, QueryItemExtensionInterface
|
||||
{
|
||||
public function __construct(
|
||||
private Security $security
|
||||
) {
|
||||
}
|
||||
|
||||
public function applyToCollection(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, ?Operation $operation = null, array $context = []): void
|
||||
{
|
||||
$this->addWhere($queryBuilder, $resourceClass);
|
||||
}
|
||||
|
||||
public function applyToItem(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, array $identifiers, ?Operation $operation = null, array $context = []): void
|
||||
{
|
||||
$this->addWhere($queryBuilder, $resourceClass);
|
||||
}
|
||||
|
||||
private function addWhere(QueryBuilder $queryBuilder, string $resourceClass): void
|
||||
{
|
||||
// Nur für Immobilie-Entity
|
||||
if (Immobilie::class !== $resourceClass) {
|
||||
return;
|
||||
}
|
||||
|
||||
$user = $this->security->getUser();
|
||||
|
||||
// Wenn nicht eingeloggt, keine Ergebnisse
|
||||
if (! $user instanceof User) {
|
||||
$queryBuilder->andWhere('1 = 0');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Admin sieht alles
|
||||
if (UserRole::ADMIN === $user->getRole()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Normale User sehen nur eigene Immobilien
|
||||
$rootAlias = $queryBuilder->getRootAliases()[0];
|
||||
$queryBuilder->andWhere(sprintf('%s.verwalter = :current_user', $rootAlias))
|
||||
->setParameter('current_user', $user);
|
||||
}
|
||||
}
|
||||
71
src/Entity/Bundesland.php
Normal file
71
src/Entity/Bundesland.php
Normal file
@@ -0,0 +1,71 @@
|
||||
<?php
|
||||
|
||||
namespace App\Entity;
|
||||
|
||||
use ApiPlatform\Metadata\ApiResource;
|
||||
use ApiPlatform\Metadata\Delete;
|
||||
use ApiPlatform\Metadata\Get;
|
||||
use ApiPlatform\Metadata\GetCollection;
|
||||
use ApiPlatform\Metadata\Post;
|
||||
use ApiPlatform\Metadata\Put;
|
||||
use App\Repository\BundeslandRepository;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
|
||||
#[ORM\Entity(repositoryClass: BundeslandRepository::class)]
|
||||
#[ORM\Table(name: 'bundeslaender')]
|
||||
#[ApiResource(
|
||||
operations: [
|
||||
new Get(security: 'is_granted("PUBLIC_ACCESS")'),
|
||||
new GetCollection(security: 'is_granted("PUBLIC_ACCESS")'),
|
||||
new Post(security: 'is_granted("ROLE_ADMIN") or is_granted("ROLE_TECHNICAL")'),
|
||||
new Put(security: 'is_granted("ROLE_ADMIN") or is_granted("ROLE_TECHNICAL")'),
|
||||
new Delete(security: 'is_granted("ROLE_ADMIN") or is_granted("ROLE_TECHNICAL")'),
|
||||
]
|
||||
)]
|
||||
class Bundesland
|
||||
{
|
||||
#[ORM\Id]
|
||||
#[ORM\GeneratedValue]
|
||||
#[ORM\Column(type: 'integer')]
|
||||
private ?int $id = null;
|
||||
|
||||
#[ORM\Column(type: 'string', length: 100, unique: true)]
|
||||
#[Assert\NotBlank]
|
||||
#[Assert\Length(min: 2, max: 100)]
|
||||
private string $name;
|
||||
|
||||
#[ORM\Column(type: 'decimal', precision: 4, scale: 2)]
|
||||
#[Assert\NotBlank]
|
||||
#[Assert\Range(min: 0, max: 100)]
|
||||
private float $grunderwerbsteuer;
|
||||
|
||||
public function getId(): ?int
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function getName(): string
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
public function setName(string $name): self
|
||||
{
|
||||
$this->name = $name;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getGrunderwerbsteuer(): float
|
||||
{
|
||||
return $this->grunderwerbsteuer;
|
||||
}
|
||||
|
||||
public function setGrunderwerbsteuer(float $grunderwerbsteuer): self
|
||||
{
|
||||
$this->grunderwerbsteuer = $grunderwerbsteuer;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
54
src/Entity/Heizungstyp.php
Normal file
54
src/Entity/Heizungstyp.php
Normal file
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
namespace App\Entity;
|
||||
|
||||
use ApiPlatform\Metadata\ApiResource;
|
||||
use ApiPlatform\Metadata\Delete;
|
||||
use ApiPlatform\Metadata\Get;
|
||||
use ApiPlatform\Metadata\GetCollection;
|
||||
use ApiPlatform\Metadata\Post;
|
||||
use ApiPlatform\Metadata\Put;
|
||||
use App\Repository\HeizungstypRepository;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
|
||||
#[ORM\Entity(repositoryClass: HeizungstypRepository::class)]
|
||||
#[ORM\Table(name: 'heizungstypen')]
|
||||
#[ApiResource(
|
||||
operations: [
|
||||
new Get(security: 'is_granted("PUBLIC_ACCESS")'),
|
||||
new GetCollection(security: 'is_granted("PUBLIC_ACCESS")'),
|
||||
new Post(security: 'is_granted("ROLE_ADMIN") or is_granted("ROLE_TECHNICAL")'),
|
||||
new Put(security: 'is_granted("ROLE_ADMIN") or is_granted("ROLE_TECHNICAL")'),
|
||||
new Delete(security: 'is_granted("ROLE_ADMIN") or is_granted("ROLE_TECHNICAL")'),
|
||||
]
|
||||
)]
|
||||
class Heizungstyp
|
||||
{
|
||||
#[ORM\Id]
|
||||
#[ORM\GeneratedValue]
|
||||
#[ORM\Column(type: 'integer')]
|
||||
private ?int $id = null;
|
||||
|
||||
#[ORM\Column(type: 'string', length: 100, unique: true)]
|
||||
#[Assert\NotBlank]
|
||||
#[Assert\Length(min: 2, max: 100)]
|
||||
private string $name;
|
||||
|
||||
public function getId(): ?int
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function getName(): string
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
public function setName(string $name): self
|
||||
{
|
||||
$this->name = $name;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
359
src/Entity/Immobilie.php
Normal file
359
src/Entity/Immobilie.php
Normal file
@@ -0,0 +1,359 @@
|
||||
<?php
|
||||
|
||||
namespace App\Entity;
|
||||
|
||||
use ApiPlatform\Metadata\ApiResource;
|
||||
use ApiPlatform\Metadata\Delete;
|
||||
use ApiPlatform\Metadata\Get;
|
||||
use ApiPlatform\Metadata\GetCollection;
|
||||
use ApiPlatform\Metadata\Patch;
|
||||
use ApiPlatform\Metadata\Post;
|
||||
use App\Enum\ImmobilienTyp;
|
||||
use App\Repository\ImmobilieRepository;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
|
||||
#[ORM\Entity(repositoryClass: ImmobilieRepository::class)]
|
||||
#[ORM\Table(name: 'immobilien')]
|
||||
#[ORM\HasLifecycleCallbacks]
|
||||
#[ApiResource(
|
||||
operations: [
|
||||
new Get(security: 'is_granted("view", object)'),
|
||||
new GetCollection(),
|
||||
new Post(),
|
||||
new Patch(security: 'is_granted("edit", object)'),
|
||||
new Delete(security: 'is_granted("delete", object)'),
|
||||
],
|
||||
security: 'is_granted("ROLE_USER")'
|
||||
)]
|
||||
class Immobilie
|
||||
{
|
||||
#[ORM\Id]
|
||||
#[ORM\GeneratedValue]
|
||||
#[ORM\Column(type: 'integer')]
|
||||
private ?int $id = null;
|
||||
|
||||
#[ORM\ManyToOne(targetEntity: User::class, inversedBy: 'immobilien')]
|
||||
#[ORM\JoinColumn(nullable: false)]
|
||||
#[Assert\NotNull]
|
||||
private User $verwalter;
|
||||
|
||||
#[ORM\Column(type: 'string', length: 255)]
|
||||
#[Assert\NotBlank]
|
||||
#[Assert\Length(min: 5, max: 255)]
|
||||
private string $adresse;
|
||||
|
||||
#[ORM\Column(type: 'integer')]
|
||||
#[Assert\NotBlank]
|
||||
#[Assert\Positive]
|
||||
private int $wohnflaeche;
|
||||
|
||||
#[ORM\Column(type: 'integer')]
|
||||
#[Assert\NotBlank]
|
||||
#[Assert\PositiveOrZero]
|
||||
private int $nutzflaeche;
|
||||
|
||||
#[ORM\Column(type: 'boolean')]
|
||||
private bool $garage = false;
|
||||
|
||||
#[ORM\Column(type: 'integer')]
|
||||
#[Assert\NotBlank]
|
||||
#[Assert\Positive]
|
||||
private int $zimmer;
|
||||
|
||||
#[ORM\Column(type: 'integer', nullable: true)]
|
||||
#[Assert\Range(min: 1800, max: 2100)]
|
||||
private ?int $baujahr = null;
|
||||
|
||||
#[ORM\Column(type: 'string', enumType: ImmobilienTyp::class)]
|
||||
private ImmobilienTyp $typ;
|
||||
|
||||
#[ORM\Column(type: 'text', nullable: true)]
|
||||
private ?string $beschreibung = null;
|
||||
|
||||
#[ORM\Column(type: 'integer', nullable: true)]
|
||||
#[Assert\Min(0)]
|
||||
#[Assert\Max(10)]
|
||||
private ?int $etage = null;
|
||||
|
||||
#[ORM\ManyToOne(targetEntity: Heizungstyp::class, inversedBy: 'immobilien')]
|
||||
#[ORM\JoinColumn(nullable: true)]
|
||||
private ?Heizungstyp $heizungstyp = null;
|
||||
|
||||
#[ORM\Column(type: 'integer', nullable: true)]
|
||||
#[Assert\Range(min: 0, max: 100)]
|
||||
private ?int $abschreibungszeit = null;
|
||||
|
||||
#[ORM\ManyToOne(targetEntity: Bundesland::class, inversedBy: 'immobilien')]
|
||||
#[ORM\JoinColumn(nullable: true)]
|
||||
private ?Bundesland $bundesland = null;
|
||||
|
||||
#[ORM\Column(type: 'integer', nullable: true)]
|
||||
#[Assert\PositiveOrZero]
|
||||
private ?int $kaufpreis = null;
|
||||
|
||||
#[ORM\Column(type: 'datetime')]
|
||||
private \DateTimeInterface $createdAt;
|
||||
|
||||
#[ORM\Column(type: 'datetime')]
|
||||
private \DateTimeInterface $updatedAt;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->createdAt = new \DateTime();
|
||||
$this->updatedAt = new \DateTime();
|
||||
$this->typ = ImmobilienTyp::WOHNUNG;
|
||||
$this->nutzflaeche = 0;
|
||||
}
|
||||
|
||||
#[ORM\PreUpdate]
|
||||
public function setUpdatedAtValue(): void
|
||||
{
|
||||
$this->updatedAt = new \DateTime();
|
||||
}
|
||||
|
||||
public function getId(): ?int
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function getAdresse(): string
|
||||
{
|
||||
return $this->adresse;
|
||||
}
|
||||
|
||||
public function setAdresse(string $adresse): self
|
||||
{
|
||||
$this->adresse = $adresse;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getWohnflaeche(): int
|
||||
{
|
||||
return $this->wohnflaeche;
|
||||
}
|
||||
|
||||
public function setWohnflaeche(int $wohnflaeche): self
|
||||
{
|
||||
$this->wohnflaeche = $wohnflaeche;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getNutzflaeche(): int
|
||||
{
|
||||
return $this->nutzflaeche;
|
||||
}
|
||||
|
||||
public function setNutzflaeche(int $nutzflaeche): self
|
||||
{
|
||||
$this->nutzflaeche = $nutzflaeche;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getGarage(): bool
|
||||
{
|
||||
return $this->garage;
|
||||
}
|
||||
|
||||
public function isGarage(): bool
|
||||
{
|
||||
return $this->garage;
|
||||
}
|
||||
|
||||
public function setGarage(bool $garage): self
|
||||
{
|
||||
$this->garage = $garage;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getZimmer(): int
|
||||
{
|
||||
return $this->zimmer;
|
||||
}
|
||||
|
||||
public function setZimmer(int $zimmer): self
|
||||
{
|
||||
$this->zimmer = $zimmer;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getBaujahr(): ?int
|
||||
{
|
||||
return $this->baujahr;
|
||||
}
|
||||
|
||||
public function setBaujahr(?int $baujahr): self
|
||||
{
|
||||
$this->baujahr = $baujahr;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getTyp(): ImmobilienTyp
|
||||
{
|
||||
return $this->typ;
|
||||
}
|
||||
|
||||
public function setTyp(ImmobilienTyp $typ): self
|
||||
{
|
||||
$this->typ = $typ;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getBeschreibung(): ?string
|
||||
{
|
||||
return $this->beschreibung;
|
||||
}
|
||||
|
||||
public function setBeschreibung(?string $beschreibung): self
|
||||
{
|
||||
$this->beschreibung = $beschreibung;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getEtage(): ?int
|
||||
{
|
||||
return $this->etage;
|
||||
}
|
||||
|
||||
public function setEtage(?int $etage): self
|
||||
{
|
||||
$this->etage = $etage;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getHeizungstyp(): ?Heizungstyp
|
||||
{
|
||||
return $this->heizungstyp;
|
||||
}
|
||||
|
||||
public function setHeizungstyp(?Heizungstyp $heizungstyp): self
|
||||
{
|
||||
$this->heizungstyp = $heizungstyp;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getCreatedAt(): \DateTimeInterface
|
||||
{
|
||||
return $this->createdAt;
|
||||
}
|
||||
|
||||
public function setCreatedAt(\DateTimeInterface $createdAt): self
|
||||
{
|
||||
$this->createdAt = $createdAt;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getUpdatedAt(): \DateTimeInterface
|
||||
{
|
||||
return $this->updatedAt;
|
||||
}
|
||||
|
||||
public function setUpdatedAt(\DateTimeInterface $updatedAt): self
|
||||
{
|
||||
$this->updatedAt = $updatedAt;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getAbschreibungszeit(): ?int
|
||||
{
|
||||
return $this->abschreibungszeit;
|
||||
}
|
||||
|
||||
public function setAbschreibungszeit(?int $abschreibungszeit): self
|
||||
{
|
||||
$this->abschreibungszeit = $abschreibungszeit;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getBundesland(): ?Bundesland
|
||||
{
|
||||
return $this->bundesland;
|
||||
}
|
||||
|
||||
public function setBundesland(?Bundesland $bundesland): self
|
||||
{
|
||||
$this->bundesland = $bundesland;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getKaufpreis(): ?int
|
||||
{
|
||||
return $this->kaufpreis;
|
||||
}
|
||||
|
||||
public function setKaufpreis(?int $kaufpreis): self
|
||||
{
|
||||
$this->kaufpreis = $kaufpreis;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getVerwalter(): User
|
||||
{
|
||||
return $this->verwalter;
|
||||
}
|
||||
|
||||
public function setVerwalter(User $verwalter): self
|
||||
{
|
||||
$this->verwalter = $verwalter;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Berechnet die Gesamtfläche (Wohnfläche + Nutzfläche).
|
||||
*/
|
||||
public function getGesamtflaeche(): int
|
||||
{
|
||||
return $this->wohnflaeche + $this->nutzflaeche;
|
||||
}
|
||||
|
||||
/**
|
||||
* Berechnet die Kaufnebenkosten basierend auf dem Bundesland
|
||||
* Rückgabe: Array mit Notar, Grundbuch, Grunderwerbsteuer und Gesamt.
|
||||
*/
|
||||
public function getKaufnebenkosten(): array
|
||||
{
|
||||
if (! $this->getKaufpreis() || ! $this->bundesland) {
|
||||
return [
|
||||
'notar' => 0,
|
||||
'grundbuch' => 0,
|
||||
'grunderwerbsteuer' => 0,
|
||||
'gesamt' => 0,
|
||||
];
|
||||
}
|
||||
|
||||
// Notarkosten: ca. 1,5% des Kaufpreises
|
||||
$notar = $this->getKaufpreis() * 0.015;
|
||||
|
||||
// Grundbuchkosten: ca. 0,5% des Kaufpreises
|
||||
$grundbuch = $this->getKaufpreis() * 0.005;
|
||||
|
||||
// Grunderwerbsteuer: abhängig vom Bundesland
|
||||
$grunderwerbsteuerSatz = $this->bundesland->getGrunderwerbsteuer() / 100;
|
||||
$grunderwerbsteuer = $this->getKaufpreis() * $grunderwerbsteuerSatz;
|
||||
|
||||
$gesamt = $notar + $grundbuch + $grunderwerbsteuer;
|
||||
|
||||
return [
|
||||
'notar' => $notar,
|
||||
'grundbuch' => $grundbuch,
|
||||
'grunderwerbsteuer' => $grunderwerbsteuer,
|
||||
'gesamt' => $gesamt,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -3,14 +3,17 @@
|
||||
namespace App\Entity;
|
||||
|
||||
use ApiPlatform\Metadata\ApiResource;
|
||||
use ApiPlatform\Metadata\Delete;
|
||||
use ApiPlatform\Metadata\Get;
|
||||
use ApiPlatform\Metadata\GetCollection;
|
||||
use ApiPlatform\Metadata\Post;
|
||||
use ApiPlatform\Metadata\Put;
|
||||
use ApiPlatform\Metadata\Delete;
|
||||
use App\Enum\UserRole;
|
||||
use App\Repository\UserRepository;
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Symfony\Component\Security\Core\User\UserInterface;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
|
||||
#[ORM\Entity(repositoryClass: UserRepository::class)]
|
||||
@@ -21,10 +24,10 @@ use Symfony\Component\Validator\Constraints as Assert;
|
||||
new GetCollection(),
|
||||
new Post(),
|
||||
new Put(),
|
||||
new Delete()
|
||||
new Delete(),
|
||||
]
|
||||
)]
|
||||
class User
|
||||
class User implements UserInterface
|
||||
{
|
||||
#[ORM\Id]
|
||||
#[ORM\GeneratedValue]
|
||||
@@ -44,13 +47,32 @@ class User
|
||||
#[ORM\Column(type: 'string', enumType: UserRole::class)]
|
||||
private UserRole $role;
|
||||
|
||||
#[ORM\Column(type: 'string', length: 64, unique: true)]
|
||||
private string $apiKey;
|
||||
|
||||
#[ORM\Column(type: 'string', length: 255, nullable: true)]
|
||||
private ?string $password = null;
|
||||
|
||||
#[ORM\Column(type: 'datetime')]
|
||||
private \DateTimeInterface $createdAt;
|
||||
|
||||
#[ORM\OneToMany(targetEntity: Immobilie::class, mappedBy: 'verwalter', orphanRemoval: true)]
|
||||
private Collection $immobilien;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->createdAt = new \DateTime();
|
||||
$this->role = UserRole::USER;
|
||||
$this->immobilien = new ArrayCollection();
|
||||
$this->apiKey = $this->generateApiKey();
|
||||
}
|
||||
|
||||
/**
|
||||
* Generiert einen eindeutigen API-Key.
|
||||
*/
|
||||
private function generateApiKey(): string
|
||||
{
|
||||
return hash('sha256', random_bytes(32).microtime(true));
|
||||
}
|
||||
|
||||
public function getId(): ?int
|
||||
@@ -66,6 +88,7 @@ class User
|
||||
public function setName(string $name): self
|
||||
{
|
||||
$this->name = $name;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
@@ -77,6 +100,7 @@ class User
|
||||
public function setEmail(string $email): self
|
||||
{
|
||||
$this->email = $email;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
@@ -88,6 +112,7 @@ class User
|
||||
public function setRole(UserRole $role): self
|
||||
{
|
||||
$this->role = $role;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
@@ -99,6 +124,87 @@ class User
|
||||
public function setCreatedAt(\DateTimeInterface $createdAt): self
|
||||
{
|
||||
$this->createdAt = $createdAt;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Collection<int, Immobilie>
|
||||
*/
|
||||
public function getImmobilien(): Collection
|
||||
{
|
||||
return $this->immobilien;
|
||||
}
|
||||
|
||||
public function addImmobilie(Immobilie $immobilie): self
|
||||
{
|
||||
if (! $this->immobilien->contains($immobilie)) {
|
||||
$this->immobilien->add($immobilie);
|
||||
$immobilie->setVerwalter($this);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function removeImmobilie(Immobilie $immobilie): self
|
||||
{
|
||||
$this->immobilien->removeElement($immobilie);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getApiKey(): string
|
||||
{
|
||||
return $this->apiKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generiert einen neuen API-Key.
|
||||
*/
|
||||
public function regenerateApiKey(): self
|
||||
{
|
||||
$this->apiKey = $this->generateApiKey();
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getPassword(): ?string
|
||||
{
|
||||
return $this->password;
|
||||
}
|
||||
|
||||
public function setPassword(?string $password): self
|
||||
{
|
||||
$this->password = $password;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* UserInterface Methods.
|
||||
*/
|
||||
public function getUserIdentifier(): string
|
||||
{
|
||||
return $this->email;
|
||||
}
|
||||
|
||||
public function getRoles(): array
|
||||
{
|
||||
$roles = ['ROLE_USER'];
|
||||
|
||||
if (UserRole::ADMIN === $this->role) {
|
||||
$roles[] = 'ROLE_ADMIN';
|
||||
} elseif (UserRole::MODERATOR === $this->role) {
|
||||
$roles[] = 'ROLE_MODERATOR';
|
||||
} elseif (UserRole::TECHNICAL === $this->role) {
|
||||
$roles[] = 'ROLE_TECHNICAL';
|
||||
}
|
||||
|
||||
return array_unique($roles);
|
||||
}
|
||||
|
||||
public function eraseCredentials(): void
|
||||
{
|
||||
// Nothing to erase
|
||||
}
|
||||
}
|
||||
|
||||
71
src/Enum/Bundesland.php
Normal file
71
src/Enum/Bundesland.php
Normal file
@@ -0,0 +1,71 @@
|
||||
<?php
|
||||
|
||||
namespace App\Enum;
|
||||
|
||||
enum Bundesland: string
|
||||
{
|
||||
case BADEN_WUERTTEMBERG = 'baden_wuerttemberg';
|
||||
case BAYERN = 'bayern';
|
||||
case BERLIN = 'berlin';
|
||||
case BRANDENBURG = 'brandenburg';
|
||||
case BREMEN = 'bremen';
|
||||
case HAMBURG = 'hamburg';
|
||||
case HESSEN = 'hessen';
|
||||
case MECKLENBURG_VORPOMMERN = 'mecklenburg_vorpommern';
|
||||
case NIEDERSACHSEN = 'niedersachsen';
|
||||
case NORDRHEIN_WESTFALEN = 'nordrhein_westfalen';
|
||||
case RHEINLAND_PFALZ = 'rheinland_pfalz';
|
||||
case SAARLAND = 'saarland';
|
||||
case SACHSEN = 'sachsen';
|
||||
case SACHSEN_ANHALT = 'sachsen_anhalt';
|
||||
case SCHLESWIG_HOLSTEIN = 'schleswig_holstein';
|
||||
case THUERINGEN = 'thueringen';
|
||||
|
||||
public function getLabel(): string
|
||||
{
|
||||
return match ($this) {
|
||||
self::BADEN_WUERTTEMBERG => 'Baden-Württemberg',
|
||||
self::BAYERN => 'Bayern',
|
||||
self::BERLIN => 'Berlin',
|
||||
self::BRANDENBURG => 'Brandenburg',
|
||||
self::BREMEN => 'Bremen',
|
||||
self::HAMBURG => 'Hamburg',
|
||||
self::HESSEN => 'Hessen',
|
||||
self::MECKLENBURG_VORPOMMERN => 'Mecklenburg-Vorpommern',
|
||||
self::NIEDERSACHSEN => 'Niedersachsen',
|
||||
self::NORDRHEIN_WESTFALEN => 'Nordrhein-Westfalen',
|
||||
self::RHEINLAND_PFALZ => 'Rheinland-Pfalz',
|
||||
self::SAARLAND => 'Saarland',
|
||||
self::SACHSEN => 'Sachsen',
|
||||
self::SACHSEN_ANHALT => 'Sachsen-Anhalt',
|
||||
self::SCHLESWIG_HOLSTEIN => 'Schleswig-Holstein',
|
||||
self::THUERINGEN => 'Thüringen',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Gibt die Grunderwerbsteuer in Prozent für das Bundesland zurück
|
||||
* Stand: 2025.
|
||||
*/
|
||||
public function getGrunderwerbsteuer(): float
|
||||
{
|
||||
return match ($this) {
|
||||
self::BADEN_WUERTTEMBERG => 5.0,
|
||||
self::BAYERN => 3.5,
|
||||
self::BERLIN => 6.0,
|
||||
self::BRANDENBURG => 6.5,
|
||||
self::BREMEN => 5.0,
|
||||
self::HAMBURG => 5.5,
|
||||
self::HESSEN => 6.0,
|
||||
self::MECKLENBURG_VORPOMMERN => 6.0,
|
||||
self::NIEDERSACHSEN => 5.0,
|
||||
self::NORDRHEIN_WESTFALEN => 6.5,
|
||||
self::RHEINLAND_PFALZ => 5.0,
|
||||
self::SAARLAND => 6.5,
|
||||
self::SACHSEN => 5.5,
|
||||
self::SACHSEN_ANHALT => 5.0,
|
||||
self::SCHLESWIG_HOLSTEIN => 6.5,
|
||||
self::THUERINGEN => 5.0,
|
||||
};
|
||||
}
|
||||
}
|
||||
21
src/Enum/Heizungstyp.php
Normal file
21
src/Enum/Heizungstyp.php
Normal file
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
namespace App\Enum;
|
||||
|
||||
enum Heizungstyp: string
|
||||
{
|
||||
case GASHEIZUNG = 'gasheizung';
|
||||
case WAERMEPUMPE = 'waermepumpe';
|
||||
case OELHEIZUNG = 'oelheizung';
|
||||
case PELLETHEIZUNG = 'pelletheizung';
|
||||
|
||||
public function getLabel(): string
|
||||
{
|
||||
return match ($this) {
|
||||
self::GASHEIZUNG => 'Gasheizung',
|
||||
self::WAERMEPUMPE => 'Wärmepumpe',
|
||||
self::OELHEIZUNG => 'Ölheizung',
|
||||
self::PELLETHEIZUNG => 'Pelletheizung',
|
||||
};
|
||||
}
|
||||
}
|
||||
23
src/Enum/ImmobilienTyp.php
Normal file
23
src/Enum/ImmobilienTyp.php
Normal file
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace App\Enum;
|
||||
|
||||
enum ImmobilienTyp: string
|
||||
{
|
||||
case WOHNUNG = 'wohnung';
|
||||
case HAUS = 'haus';
|
||||
case GRUNDSTUECK = 'grundstueck';
|
||||
case GEWERBE = 'gewerbe';
|
||||
case BUERO = 'buero';
|
||||
|
||||
public function getLabel(): string
|
||||
{
|
||||
return match ($this) {
|
||||
self::WOHNUNG => 'Wohnung',
|
||||
self::HAUS => 'Haus',
|
||||
self::GRUNDSTUECK => 'Grundstück',
|
||||
self::GEWERBE => 'Gewerbe',
|
||||
self::BUERO => 'Büro',
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -7,13 +7,15 @@ enum UserRole: string
|
||||
case USER = 'user';
|
||||
case ADMIN = 'admin';
|
||||
case MODERATOR = 'moderator';
|
||||
case TECHNICAL = 'technical';
|
||||
|
||||
public function getLabel(): string
|
||||
{
|
||||
return match($this) {
|
||||
return match ($this) {
|
||||
self::USER => 'Benutzer',
|
||||
self::ADMIN => 'Administrator',
|
||||
self::MODERATOR => 'Moderator',
|
||||
self::TECHNICAL => 'Technischer User',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
15
src/Repository/BundeslandRepository.php
Normal file
15
src/Repository/BundeslandRepository.php
Normal file
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
namespace App\Repository;
|
||||
|
||||
use App\Entity\Bundesland;
|
||||
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
|
||||
class BundeslandRepository extends ServiceEntityRepository
|
||||
{
|
||||
public function __construct(ManagerRegistry $registry)
|
||||
{
|
||||
parent::__construct($registry, Bundesland::class);
|
||||
}
|
||||
}
|
||||
15
src/Repository/HeizungstypRepository.php
Normal file
15
src/Repository/HeizungstypRepository.php
Normal file
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
namespace App\Repository;
|
||||
|
||||
use App\Entity\Heizungstyp;
|
||||
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
|
||||
class HeizungstypRepository extends ServiceEntityRepository
|
||||
{
|
||||
public function __construct(ManagerRegistry $registry)
|
||||
{
|
||||
parent::__construct($registry, Heizungstyp::class);
|
||||
}
|
||||
}
|
||||
137
src/Repository/ImmobilieRepository.php
Normal file
137
src/Repository/ImmobilieRepository.php
Normal file
@@ -0,0 +1,137 @@
|
||||
<?php
|
||||
|
||||
namespace App\Repository;
|
||||
|
||||
use App\Entity\Immobilie;
|
||||
use App\Enum\ImmobilienTyp;
|
||||
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
|
||||
/**
|
||||
* @extends ServiceEntityRepository<Immobilie>
|
||||
*/
|
||||
class ImmobilieRepository extends ServiceEntityRepository
|
||||
{
|
||||
public function __construct(ManagerRegistry $registry)
|
||||
{
|
||||
parent::__construct($registry, Immobilie::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find available properties.
|
||||
*/
|
||||
public function findVerfuegbare(): array
|
||||
{
|
||||
return $this->createQueryBuilder('i')
|
||||
->andWhere('i.verfuegbar = :verfuegbar')
|
||||
->setParameter('verfuegbar', true)
|
||||
->orderBy('i.createdAt', 'DESC')
|
||||
->getQuery()
|
||||
->getResult();
|
||||
}
|
||||
|
||||
/**
|
||||
* Find properties by type.
|
||||
*/
|
||||
public function findByTyp(ImmobilienTyp $typ): array
|
||||
{
|
||||
return $this->createQueryBuilder('i')
|
||||
->andWhere('i.typ = :typ')
|
||||
->setParameter('typ', $typ)
|
||||
->orderBy('i.preis', 'ASC')
|
||||
->getQuery()
|
||||
->getResult();
|
||||
}
|
||||
|
||||
/**
|
||||
* Find properties within price range.
|
||||
*/
|
||||
public function findByPreisRange(float $minPreis, float $maxPreis): array
|
||||
{
|
||||
return $this->createQueryBuilder('i')
|
||||
->andWhere('i.preis BETWEEN :minPreis AND :maxPreis')
|
||||
->andWhere('i.verfuegbar = :verfuegbar')
|
||||
->setParameter('minPreis', $minPreis)
|
||||
->setParameter('maxPreis', $maxPreis)
|
||||
->setParameter('verfuegbar', true)
|
||||
->orderBy('i.preis', 'ASC')
|
||||
->getQuery()
|
||||
->getResult();
|
||||
}
|
||||
|
||||
/**
|
||||
* Find properties within area range.
|
||||
*/
|
||||
public function findByFlaecheRange(float $minFlaeche, float $maxFlaeche): array
|
||||
{
|
||||
return $this->createQueryBuilder('i')
|
||||
->andWhere('i.flaeche BETWEEN :minFlaeche AND :maxFlaeche')
|
||||
->andWhere('i.verfuegbar = :verfuegbar')
|
||||
->setParameter('minFlaeche', $minFlaeche)
|
||||
->setParameter('maxFlaeche', $maxFlaeche)
|
||||
->setParameter('verfuegbar', true)
|
||||
->orderBy('i.flaeche', 'ASC')
|
||||
->getQuery()
|
||||
->getResult();
|
||||
}
|
||||
|
||||
/**
|
||||
* Find properties with garage.
|
||||
*/
|
||||
public function findMitGarage(): array
|
||||
{
|
||||
return $this->createQueryBuilder('i')
|
||||
->andWhere('i.garage = :garage')
|
||||
->andWhere('i.verfuegbar = :verfuegbar')
|
||||
->setParameter('garage', true)
|
||||
->setParameter('verfuegbar', true)
|
||||
->orderBy('i.preis', 'ASC')
|
||||
->getQuery()
|
||||
->getResult();
|
||||
}
|
||||
|
||||
/**
|
||||
* Find properties by minimum number of rooms.
|
||||
*/
|
||||
public function findByMinZimmer(int $minZimmer): array
|
||||
{
|
||||
return $this->createQueryBuilder('i')
|
||||
->andWhere('i.zimmer >= :minZimmer')
|
||||
->andWhere('i.verfuegbar = :verfuegbar')
|
||||
->setParameter('minZimmer', $minZimmer)
|
||||
->setParameter('verfuegbar', true)
|
||||
->orderBy('i.zimmer', 'ASC')
|
||||
->getQuery()
|
||||
->getResult();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get average price per sqm by type.
|
||||
*/
|
||||
public function getAveragePreisProQmByTyp(ImmobilienTyp $typ): float
|
||||
{
|
||||
$result = $this->createQueryBuilder('i')
|
||||
->select('AVG(i.preis / i.flaeche) as avgPreisProQm')
|
||||
->andWhere('i.typ = :typ')
|
||||
->andWhere('i.verfuegbar = :verfuegbar')
|
||||
->setParameter('typ', $typ)
|
||||
->setParameter('verfuegbar', true)
|
||||
->getQuery()
|
||||
->getSingleScalarResult();
|
||||
|
||||
return round((float) $result, 2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Search properties by address.
|
||||
*/
|
||||
public function searchByAdresse(string $search): array
|
||||
{
|
||||
return $this->createQueryBuilder('i')
|
||||
->andWhere('i.adresse LIKE :search')
|
||||
->setParameter('search', '%'.$search.'%')
|
||||
->orderBy('i.createdAt', 'DESC')
|
||||
->getQuery()
|
||||
->getResult();
|
||||
}
|
||||
}
|
||||
@@ -17,7 +17,7 @@ class UserRepository extends ServiceEntityRepository
|
||||
}
|
||||
|
||||
/**
|
||||
* Find users by role
|
||||
* Find users by role.
|
||||
*/
|
||||
public function findByRole(string $role): array
|
||||
{
|
||||
@@ -30,7 +30,7 @@ class UserRepository extends ServiceEntityRepository
|
||||
}
|
||||
|
||||
/**
|
||||
* Find user by email
|
||||
* Find user by email.
|
||||
*/
|
||||
public function findOneByEmail(string $email): ?User
|
||||
{
|
||||
|
||||
62
src/Security/ApiKeyAuthenticator.php
Normal file
62
src/Security/ApiKeyAuthenticator.php
Normal file
@@ -0,0 +1,62 @@
|
||||
<?php
|
||||
|
||||
namespace App\Security;
|
||||
|
||||
use App\Repository\UserRepository;
|
||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
||||
use Symfony\Component\Security\Core\Exception\AuthenticationException;
|
||||
use Symfony\Component\Security\Core\Exception\CustomUserMessageAuthenticationException;
|
||||
use Symfony\Component\Security\Http\Authenticator\AbstractAuthenticator;
|
||||
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge;
|
||||
use Symfony\Component\Security\Http\Authenticator\Passport\Passport;
|
||||
use Symfony\Component\Security\Http\Authenticator\Passport\SelfValidatingPassport;
|
||||
|
||||
class ApiKeyAuthenticator extends AbstractAuthenticator
|
||||
{
|
||||
public function __construct(
|
||||
private UserRepository $userRepository
|
||||
) {
|
||||
}
|
||||
|
||||
public function supports(Request $request): ?bool
|
||||
{
|
||||
return $request->headers->has('X-API-KEY');
|
||||
}
|
||||
|
||||
public function authenticate(Request $request): Passport
|
||||
{
|
||||
$apiKey = $request->headers->get('X-API-KEY');
|
||||
|
||||
if (null === $apiKey) {
|
||||
throw new CustomUserMessageAuthenticationException('No API key provided');
|
||||
}
|
||||
|
||||
return new SelfValidatingPassport(
|
||||
new UserBadge($apiKey, function ($apiKey) {
|
||||
$user = $this->userRepository->findOneBy(['apiKey' => $apiKey]);
|
||||
|
||||
if (! $user) {
|
||||
throw new CustomUserMessageAuthenticationException('Invalid API key');
|
||||
}
|
||||
|
||||
return $user;
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response
|
||||
{
|
||||
// On success, let the request continue
|
||||
return null;
|
||||
}
|
||||
|
||||
public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response
|
||||
{
|
||||
return new JsonResponse([
|
||||
'message' => strtr($exception->getMessageKey(), $exception->getMessageData()),
|
||||
], Response::HTTP_UNAUTHORIZED);
|
||||
}
|
||||
}
|
||||
67
src/Security/ImmobilieVoter.php
Normal file
67
src/Security/ImmobilieVoter.php
Normal file
@@ -0,0 +1,67 @@
|
||||
<?php
|
||||
|
||||
namespace App\Security;
|
||||
|
||||
use App\Entity\Immobilie;
|
||||
use App\Entity\User;
|
||||
use App\Enum\UserRole;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
||||
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
|
||||
|
||||
class ImmobilieVoter extends Voter
|
||||
{
|
||||
public const VIEW = 'view';
|
||||
public const EDIT = 'edit';
|
||||
public const DELETE = 'delete';
|
||||
|
||||
protected function supports(string $attribute, mixed $subject): bool
|
||||
{
|
||||
// Voter unterstützt nur diese Attribute und nur Immobilie-Objekte
|
||||
return in_array($attribute, [self::VIEW, self::EDIT, self::DELETE])
|
||||
&& $subject instanceof Immobilie;
|
||||
}
|
||||
|
||||
protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token): bool
|
||||
{
|
||||
$user = $token->getUser();
|
||||
|
||||
// User muss eingeloggt sein
|
||||
if (! $user instanceof User) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/** @var Immobilie $immobilie */
|
||||
$immobilie = $subject;
|
||||
|
||||
// Admin hat uneingeschränkten Zugriff
|
||||
if (UserRole::ADMIN === $user->getRole()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Prüfe je nach Attribut
|
||||
return match ($attribute) {
|
||||
self::VIEW => $this->canView($immobilie, $user),
|
||||
self::EDIT => $this->canEdit($immobilie, $user),
|
||||
self::DELETE => $this->canDelete($immobilie, $user),
|
||||
default => false
|
||||
};
|
||||
}
|
||||
|
||||
private function canView(Immobilie $immobilie, User $user): bool
|
||||
{
|
||||
// User kann nur eigene Immobilien sehen
|
||||
return $immobilie->getVerwalter() === $user;
|
||||
}
|
||||
|
||||
private function canEdit(Immobilie $immobilie, User $user): bool
|
||||
{
|
||||
// User kann nur eigene Immobilien bearbeiten
|
||||
return $immobilie->getVerwalter() === $user;
|
||||
}
|
||||
|
||||
private function canDelete(Immobilie $immobilie, User $user): bool
|
||||
{
|
||||
// User kann nur eigene Immobilien löschen
|
||||
return $immobilie->getVerwalter() === $user;
|
||||
}
|
||||
}
|
||||
27
symfony.lock
27
symfony.lock
@@ -49,6 +49,18 @@
|
||||
"./migrations/.gitignore"
|
||||
]
|
||||
},
|
||||
"friendsofphp/php-cs-fixer": {
|
||||
"version": "3.89",
|
||||
"recipe": {
|
||||
"repo": "github.com/symfony/recipes",
|
||||
"branch": "main",
|
||||
"version": "3.0",
|
||||
"ref": "be2103eb4a20942e28a6dd87736669b757132435"
|
||||
},
|
||||
"files": [
|
||||
".php-cs-fixer.dist.php"
|
||||
]
|
||||
},
|
||||
"nelmio/cors-bundle": {
|
||||
"version": "2.6",
|
||||
"recipe": {
|
||||
@@ -61,6 +73,21 @@
|
||||
"./config/packages/nelmio_cors.yaml"
|
||||
]
|
||||
},
|
||||
"phpunit/phpunit": {
|
||||
"version": "12.4",
|
||||
"recipe": {
|
||||
"repo": "github.com/symfony/recipes",
|
||||
"branch": "main",
|
||||
"version": "11.1",
|
||||
"ref": "1117deb12541f35793eec9fff7494d7aa12283fc"
|
||||
},
|
||||
"files": [
|
||||
".env.test",
|
||||
"phpunit.dist.xml",
|
||||
"tests/bootstrap.php",
|
||||
"bin/phpunit"
|
||||
]
|
||||
},
|
||||
"symfony/console": {
|
||||
"version": "7.3",
|
||||
"recipe": {
|
||||
|
||||
46
templates/auth/login.html.twig
Normal file
46
templates/auth/login.html.twig
Normal file
@@ -0,0 +1,46 @@
|
||||
{% extends 'base.html.twig' %}
|
||||
|
||||
{% block title %}Login - {{ parent() }}{% endblock %}
|
||||
|
||||
{% block stylesheets %}
|
||||
{{ parent() }}
|
||||
<link rel="stylesheet" href="{{ asset('css/auth.css') }}">
|
||||
{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<div class="auth-container">
|
||||
<div class="auth-box">
|
||||
<h2>Anmelden</h2>
|
||||
|
||||
{% for message in app.flashes('success') %}
|
||||
<div class="success-message">{{ message }}</div>
|
||||
{% endfor %}
|
||||
|
||||
{% if error %}
|
||||
<div class="error-message">
|
||||
{{ error.messageKey|trans(error.messageData, 'security') }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<form class="auth-form" method="post" action="{{ path('app_login') }}">
|
||||
<div class="form-group">
|
||||
<label for="username">E-Mail</label>
|
||||
<input type="email" id="username" name="_username" value="{{ last_username }}" required autofocus>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="password">Passwort</label>
|
||||
<input type="password" id="password" name="_password" required>
|
||||
</div>
|
||||
|
||||
<input type="hidden" name="_csrf_token" value="{{ csrf_token('authenticate') }}">
|
||||
|
||||
<button class="btn-submit" type="submit">Anmelden</button>
|
||||
</form>
|
||||
|
||||
<div class="auth-links">
|
||||
<p>Noch kein Konto? <a href="{{ path('app_register') }}">Jetzt registrieren</a></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
48
templates/auth/register.html.twig
Normal file
48
templates/auth/register.html.twig
Normal file
@@ -0,0 +1,48 @@
|
||||
{% extends 'base.html.twig' %}
|
||||
|
||||
{% block title %}Registrierung - {{ parent() }}{% endblock %}
|
||||
|
||||
{% block stylesheets %}
|
||||
{{ parent() }}
|
||||
<link rel="stylesheet" href="{{ asset('css/auth.css') }}">
|
||||
{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<div class="auth-container">
|
||||
<div class="auth-box">
|
||||
<h2>Registrieren</h2>
|
||||
|
||||
{% if error %}
|
||||
<div class="error-message">{{ error }}</div>
|
||||
{% endif %}
|
||||
|
||||
<form class="auth-form" method="post" action="{{ path('app_register') }}">
|
||||
<div class="form-group">
|
||||
<label for="name">Name</label>
|
||||
<input type="text" id="name" name="name" required autofocus>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="email">E-Mail</label>
|
||||
<input type="email" id="email" name="email" required>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="password">Passwort (min. 6 Zeichen)</label>
|
||||
<input type="password" id="password" name="password" required>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="password_confirm">Passwort bestätigen</label>
|
||||
<input type="password" id="password_confirm" name="password_confirm" required>
|
||||
</div>
|
||||
|
||||
<button class="btn-submit" type="submit">Registrieren</button>
|
||||
</form>
|
||||
|
||||
<div class="auth-links">
|
||||
<p>Bereits registriert? <a href="{{ path('app_login') }}">Jetzt anmelden</a></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -48,7 +48,19 @@
|
||||
<body>
|
||||
<header>
|
||||
<div class="container">
|
||||
<h1>Immorechner</h1>
|
||||
<div style="display: flex; justify-content: space-between; align-items: center;">
|
||||
<h1><a href="{{ path('app_home') }}" style="color: white; text-decoration: none;">Immorechner</a></h1>
|
||||
<nav>
|
||||
{% if app.user %}
|
||||
<span style="color: white; margin-right: 15px;">Hallo, {{ app.user.name }}!</span>
|
||||
<a href="{{ path('app_my_immobilien') }}" style="color: white; text-decoration: none; margin-right: 10px;">Meine Immobilien</a>
|
||||
<a href="{{ path('app_logout') }}" style="color: white; text-decoration: none; padding: 8px 16px; background-color: rgba(255,255,255,0.2); border-radius: 4px;">Abmelden</a>
|
||||
{% else %}
|
||||
<a href="{{ path('app_login') }}" style="color: white; text-decoration: none; margin-right: 10px;">Anmelden</a>
|
||||
<a href="{{ path('app_register') }}" style="color: white; text-decoration: none; padding: 8px 16px; background-color: rgba(255,255,255,0.2); border-radius: 4px;">Registrieren</a>
|
||||
{% endif %}
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
|
||||
@@ -1,67 +1,167 @@
|
||||
{% extends 'base.html.twig' %}
|
||||
|
||||
{% block title %}Willkommen - {{ parent() }}{% endblock %}
|
||||
{% block title %}Immobilienwert berechnen - {{ parent() }}{% endblock %}
|
||||
|
||||
{% block stylesheets %}
|
||||
{{ parent() }}
|
||||
<style>
|
||||
.welcome-box {
|
||||
background: white;
|
||||
padding: 30px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
}
|
||||
h2 {
|
||||
color: #333;
|
||||
border-bottom: 2px solid #4CAF50;
|
||||
padding-bottom: 10px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.info {
|
||||
margin: 20px 0;
|
||||
padding: 15px;
|
||||
background-color: #e8f5e9;
|
||||
border-left: 4px solid #4CAF50;
|
||||
}
|
||||
.api-link {
|
||||
display: inline-block;
|
||||
margin-top: 20px;
|
||||
padding: 10px 20px;
|
||||
background-color: #4CAF50;
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.api-link:hover {
|
||||
background-color: #45a049;
|
||||
}
|
||||
ul {
|
||||
margin-left: 20px;
|
||||
}
|
||||
ul li {
|
||||
margin: 8px 0;
|
||||
}
|
||||
</style>
|
||||
<link rel="stylesheet" href="{{ asset('css/calculator.css') }}">
|
||||
{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<div class="welcome-box">
|
||||
<h2>Willkommen bei Immorechner</h2>
|
||||
<div class="info-banner">
|
||||
<h2 style="margin-bottom: 10px; border: none;">Immobilienwert-Rechner</h2>
|
||||
<p>Berechnen Sie die Werthaltigkeit Ihrer Immobilie. Alle Berechnungen erfolgen in Echtzeit.</p>
|
||||
<p><strong>Ohne Registrierung:</strong> Teilen Sie Ihre Berechnung über einen Link.</p>
|
||||
<p><strong>Mit Registrierung:</strong> Speichern Sie Ihre Immobilien dauerhaft.</p>
|
||||
</div>
|
||||
|
||||
<div class="info">
|
||||
<p><strong>Symfony-Anwendung erfolgreich installiert!</strong></p>
|
||||
<p>Diese Anwendung verfügt über:</p>
|
||||
<ul>
|
||||
<li>Symfony 7.3 Framework</li>
|
||||
<li>MariaDB Datenbank (mit Docker)</li>
|
||||
<li>Doctrine ORM</li>
|
||||
<li>API Platform für REST-API</li>
|
||||
<li>Twig Template Engine für UI</li>
|
||||
</ul>
|
||||
<div class="calculator-container">
|
||||
<div class="form-section">
|
||||
<h2>Immobiliendaten</h2>
|
||||
|
||||
<form id="immo-calculator-form">
|
||||
<div class="form-group">
|
||||
<label for="adresse">Adresse</label>
|
||||
<input type="text" id="adresse" name="adresse" value="{{ immobilienData.adresse }}" placeholder="z.B. Musterstraße 123, 12345 Berlin">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="kaufpreis">Kaufpreis (€)</label>
|
||||
<input type="number" id="kaufpreis" name="kaufpreis" value="{{ immobilienData.kaufpreis }}" placeholder="z.B. 250000">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="wohnflaeche">Wohnfläche (m²)</label>
|
||||
<input type="number" id="wohnflaeche" name="wohnflaeche" value="{{ immobilienData.wohnflaeche }}" placeholder="z.B. 80">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="nutzflaeche">Nutzfläche (m²)</label>
|
||||
<input type="number" id="nutzflaeche" name="nutzflaeche" value="{{ immobilienData.nutzflaeche }}" placeholder="z.B. 100">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="zimmer">Anzahl Zimmer</label>
|
||||
<input type="number" id="zimmer" name="zimmer" value="{{ immobilienData.zimmer }}" placeholder="z.B. 3">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="baujahr">Baujahr</label>
|
||||
<input type="number" id="baujahr" name="baujahr" value="{{ immobilienData.baujahr }}" placeholder="z.B. 2015">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="etage">Etage</label>
|
||||
<input type="number" id="etage" name="etage" value="{{ immobilienData.etage }}" placeholder="z.B. 2">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="typ">Immobilientyp</label>
|
||||
<select id="typ" name="typ">
|
||||
<option value="">-- Bitte wählen --</option>
|
||||
<option value="Wohnung" {% if immobilienData.typ == 'Wohnung' %}selected{% endif %}>Wohnung</option>
|
||||
<option value="Haus" {% if immobilienData.typ == 'Haus' %}selected{% endif %}>Haus</option>
|
||||
<option value="Gewerbe" {% if immobilienData.typ == 'Gewerbe' %}selected{% endif %}>Gewerbe</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="bundesland_id">Bundesland</label>
|
||||
<select id="bundesland_id" name="bundesland_id">
|
||||
<option value="">-- Bitte wählen --</option>
|
||||
{% for bundesland in bundeslaender %}
|
||||
<option value="{{ bundesland.id }}"
|
||||
data-steuer="{{ bundesland.grunderwerbsteuer }}"
|
||||
{% if immobilienData.bundesland_id == bundesland.id %}selected{% endif %}>
|
||||
{{ bundesland.name }} ({{ bundesland.grunderwerbsteuer }}%)
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="heizungstyp_id">Heizungstyp</label>
|
||||
<select id="heizungstyp_id" name="heizungstyp_id">
|
||||
<option value="">-- Bitte wählen --</option>
|
||||
{% for heizungstyp in heizungstypen %}
|
||||
<option value="{{ heizungstyp.id }}"
|
||||
{% if immobilienData.heizungstyp_id == heizungstyp.id %}selected{% endif %}>
|
||||
{{ heizungstyp.name }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="abschreibungszeit">Abschreibungszeit (Jahre)</label>
|
||||
<input type="number" id="abschreibungszeit" name="abschreibungszeit" value="{{ immobilienData.abschreibungszeit }}" placeholder="z.B. 50">
|
||||
</div>
|
||||
|
||||
<div class="form-group checkbox-group">
|
||||
<input type="checkbox" id="garage" name="garage" {% if immobilienData.garage %}checked{% endif %}>
|
||||
<label for="garage">Garage vorhanden</label>
|
||||
</div>
|
||||
|
||||
<div class="button-group">
|
||||
{% if app.user %}
|
||||
<button type="button" class="btn btn-primary" id="save-immobilie-btn">Speichern</button>
|
||||
{% endif %}
|
||||
<button type="button" class="btn btn-primary" id="share-link-btn">Link teilen</button>
|
||||
<button type="button" class="btn btn-secondary" id="reset-btn">Zurücksetzen</button>
|
||||
</div>
|
||||
|
||||
<div class="share-link" id="share-link-container">
|
||||
<strong>Teilen Sie diese Berechnung:</strong>
|
||||
<input type="text" id="share-url" readonly>
|
||||
<button type="button" class="btn btn-secondary" style="margin-top: 10px;" id="copy-link-btn">Link kopieren</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<p>Die REST-API ist über API Platform verfügbar und bietet automatische Dokumentation.</p>
|
||||
<div class="results-section">
|
||||
<h2>Berechnungsergebnisse</h2>
|
||||
|
||||
<a href="/api" class="api-link">Zur API-Dokumentation</a>
|
||||
<div class="result-item">
|
||||
<h3>Gesamtfläche</h3>
|
||||
<div class="value" id="result-gesamtflaeche">0 m²</div>
|
||||
<div class="description">Wohnfläche + Nutzfläche</div>
|
||||
</div>
|
||||
|
||||
<div class="result-item">
|
||||
<h3>Preis pro m² Wohnfläche</h3>
|
||||
<div class="value" id="result-preis-pro-qm">0,00 €</div>
|
||||
<div class="description">Kaufpreis / Wohnfläche</div>
|
||||
</div>
|
||||
|
||||
<div class="result-item">
|
||||
<h3>Grunderwerbsteuer</h3>
|
||||
<div class="value" id="result-grunderwerbsteuer">0,00 €</div>
|
||||
<div class="description">Abhängig vom Bundesland</div>
|
||||
</div>
|
||||
|
||||
<div class="result-item">
|
||||
<h3>Gesamtkosten</h3>
|
||||
<div class="value" id="result-gesamtkosten">0,00 €</div>
|
||||
<div class="description">Kaufpreis + Grunderwerbsteuer</div>
|
||||
</div>
|
||||
|
||||
<div class="result-item">
|
||||
<h3>Jährliche Abschreibung</h3>
|
||||
<div class="value" id="result-abschreibung">0,00 €</div>
|
||||
<div class="description">Kaufpreis / Abschreibungszeit</div>
|
||||
</div>
|
||||
|
||||
<div class="result-item">
|
||||
<h3>Alter der Immobilie</h3>
|
||||
<div class="value" id="result-alter">0 Jahre</div>
|
||||
<div class="description">Aktuelles Jahr - Baujahr</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block javascripts %}
|
||||
{{ parent() }}
|
||||
<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
|
||||
<script src="{{ asset('js/calculator.js') }}"></script>
|
||||
{% endblock %}
|
||||
|
||||
153
templates/immobilie/index.html.twig
Normal file
153
templates/immobilie/index.html.twig
Normal file
@@ -0,0 +1,153 @@
|
||||
{% extends 'base.html.twig' %}
|
||||
|
||||
{% block title %}Immobilien - {{ parent() }}{% endblock %}
|
||||
|
||||
{% block stylesheets %}
|
||||
{{ parent() }}
|
||||
<style>
|
||||
.immobilien-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
||||
gap: 20px;
|
||||
margin-top: 30px;
|
||||
}
|
||||
.immobilie-card {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||||
overflow: hidden;
|
||||
transition: transform 0.2s, box-shadow 0.2s;
|
||||
}
|
||||
.immobilie-card:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
||||
}
|
||||
.immobilie-header {
|
||||
background: #4CAF50;
|
||||
color: white;
|
||||
padding: 15px;
|
||||
}
|
||||
.immobilie-typ {
|
||||
font-size: 12px;
|
||||
text-transform: uppercase;
|
||||
opacity: 0.9;
|
||||
}
|
||||
.immobilie-body {
|
||||
padding: 20px;
|
||||
}
|
||||
.immobilie-preis {
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
color: #4CAF50;
|
||||
margin: 10px 0;
|
||||
}
|
||||
.immobilie-details {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 15px 0;
|
||||
}
|
||||
.immobilie-details li {
|
||||
padding: 5px 0;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
.immobilie-details li:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
.badge {
|
||||
display: inline-block;
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
}
|
||||
.badge-success {
|
||||
background: #e8f5e9;
|
||||
color: #4CAF50;
|
||||
}
|
||||
.btn-details {
|
||||
display: block;
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
background: #4CAF50;
|
||||
color: white;
|
||||
text-align: center;
|
||||
text-decoration: none;
|
||||
border-radius: 4px;
|
||||
margin-top: 15px;
|
||||
}
|
||||
.btn-details:hover {
|
||||
background: #45a049;
|
||||
color: white;
|
||||
}
|
||||
.page-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.btn-primary {
|
||||
display: inline-block;
|
||||
padding: 10px 20px;
|
||||
background: #4CAF50;
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.btn-primary:hover {
|
||||
background: #45a049;
|
||||
color: white;
|
||||
}
|
||||
.no-results {
|
||||
text-align: center;
|
||||
padding: 40px;
|
||||
color: #666;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<div class="page-header">
|
||||
<h2>Verfügbare Immobilien</h2>
|
||||
<div>
|
||||
<a href="{{ path('app_immobilie_suche') }}" class="btn-primary">Erweiterte Suche</a>
|
||||
<a href="/api" class="btn-primary">API</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if immobilien|length > 0 %}
|
||||
<div class="immobilien-grid">
|
||||
{% for immobilie in immobilien %}
|
||||
<div class="immobilie-card">
|
||||
<div class="immobilie-header">
|
||||
<div class="immobilie-typ">{{ immobilie.typ.label }}</div>
|
||||
<strong>{{ immobilie.adresse }}</strong>
|
||||
</div>
|
||||
<div class="immobilie-body">
|
||||
<div class="immobilie-preis">{{ immobilie.preis|number_format(2, ',', '.') }} €</div>
|
||||
|
||||
<ul class="immobilie-details">
|
||||
<li>📐 Fläche: {{ immobilie.flaeche }} m²</li>
|
||||
<li>🛏️ Zimmer: {{ immobilie.zimmer }}</li>
|
||||
<li>💰 Preis/m²: {{ immobilie.preisProQm|number_format(2, ',', '.') }} €</li>
|
||||
{% if immobilie.baujahr %}
|
||||
<li>📅 Baujahr: {{ immobilie.baujahr }}</li>
|
||||
{% endif %}
|
||||
{% if immobilie.garage %}
|
||||
<li>🚗 <span class="badge badge-success">Mit Garage</span></li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
|
||||
<a href="{{ path('app_immobilie_show', {id: immobilie.id}) }}" class="btn-details">
|
||||
Details ansehen
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="no-results">
|
||||
<h3>Keine Immobilien verfügbar</h3>
|
||||
<p>Aktuell sind keine Immobilien in unserem System verfügbar.</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
152
templates/immobilie/my_immobilien.html.twig
Normal file
152
templates/immobilie/my_immobilien.html.twig
Normal file
@@ -0,0 +1,152 @@
|
||||
{% extends 'base.html.twig' %}
|
||||
|
||||
{% block title %}Meine Immobilien - {{ parent() }}{% endblock %}
|
||||
|
||||
{% block stylesheets %}
|
||||
{{ parent() }}
|
||||
<style>
|
||||
.immobilien-list {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.immobilie-card {
|
||||
background: white;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.immobilie-card h3 {
|
||||
color: #333;
|
||||
margin-bottom: 15px;
|
||||
padding-bottom: 10px;
|
||||
border-bottom: 2px solid #4CAF50;
|
||||
}
|
||||
|
||||
.immobilie-details {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.detail-item {
|
||||
padding: 10px;
|
||||
background-color: #f8f9fa;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.detail-item label {
|
||||
display: block;
|
||||
font-weight: 500;
|
||||
color: #666;
|
||||
font-size: 12px;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.detail-item value {
|
||||
display: block;
|
||||
color: #333;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 60px 20px;
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.empty-state h2 {
|
||||
color: #666;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.empty-state a {
|
||||
display: inline-block;
|
||||
padding: 12px 24px;
|
||||
background-color: #4CAF50;
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
border-radius: 4px;
|
||||
transition: background-color 0.3s;
|
||||
}
|
||||
|
||||
.empty-state a:hover {
|
||||
background-color: #45a049;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<h1>Meine Immobilien</h1>
|
||||
|
||||
{% if immobilien|length > 0 %}
|
||||
<div class="immobilien-list">
|
||||
{% for immobilie in immobilien %}
|
||||
<div class="immobilie-card">
|
||||
<h3>{{ immobilie.adresse }}</h3>
|
||||
<div class="immobilie-details">
|
||||
<div class="detail-item">
|
||||
<label>Typ</label>
|
||||
<value>{{ immobilie.typ.label }}</value>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<label>Kaufpreis</label>
|
||||
<value>{{ immobilie.kaufpreis|number_format(0, ',', '.') }} €</value>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<label>Wohnfläche</label>
|
||||
<value>{{ immobilie.wohnflaeche }} m²</value>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<label>Nutzfläche</label>
|
||||
<value>{{ immobilie.nutzflaeche }} m²</value>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<label>Gesamtfläche</label>
|
||||
<value>{{ immobilie.gesamtflaeche }} m²</value>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<label>Zimmer</label>
|
||||
<value>{{ immobilie.zimmer }}</value>
|
||||
</div>
|
||||
{% if immobilie.baujahr %}
|
||||
<div class="detail-item">
|
||||
<label>Baujahr</label>
|
||||
<value>{{ immobilie.baujahr }}</value>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if immobilie.bundesland %}
|
||||
<div class="detail-item">
|
||||
<label>Bundesland</label>
|
||||
<value>{{ immobilie.bundesland.name }}</value>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if immobilie.heizungstyp %}
|
||||
<div class="detail-item">
|
||||
<label>Heizungstyp</label>
|
||||
<value>{{ immobilie.heizungstyp.name }}</value>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="detail-item">
|
||||
<label>Garage</label>
|
||||
<value>{{ immobilie.garage ? 'Ja' : 'Nein' }}</value>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<label>Erstellt am</label>
|
||||
<value>{{ immobilie.createdAt|date('d.m.Y H:i') }}</value>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="empty-state">
|
||||
<h2>Sie haben noch keine Immobilien gespeichert</h2>
|
||||
<p>Nutzen Sie den Immobilienrechner, um Ihre erste Immobilie zu berechnen und zu speichern.</p>
|
||||
<a href="{{ path('app_home') }}">Zum Rechner</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
211
templates/immobilie/show.html.twig
Normal file
211
templates/immobilie/show.html.twig
Normal file
@@ -0,0 +1,211 @@
|
||||
{% extends 'base.html.twig' %}
|
||||
|
||||
{% block title %}{{ immobilie.adresse }} - {{ parent() }}{% endblock %}
|
||||
|
||||
{% block stylesheets %}
|
||||
{{ parent() }}
|
||||
<style>
|
||||
.immobilie-detail {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||||
padding: 30px;
|
||||
margin: 20px 0;
|
||||
}
|
||||
.immobilie-header {
|
||||
border-bottom: 2px solid #4CAF50;
|
||||
padding-bottom: 20px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
.immobilie-typ-badge {
|
||||
display: inline-block;
|
||||
background: #4CAF50;
|
||||
color: white;
|
||||
padding: 6px 12px;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
text-transform: uppercase;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.immobilie-adresse {
|
||||
font-size: 28px;
|
||||
margin: 10px 0;
|
||||
}
|
||||
.immobilie-preis-box {
|
||||
background: #e8f5e9;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
margin: 20px 0;
|
||||
}
|
||||
.preis-haupt {
|
||||
font-size: 36px;
|
||||
font-weight: bold;
|
||||
color: #4CAF50;
|
||||
}
|
||||
.preis-pro-qm {
|
||||
font-size: 16px;
|
||||
color: #666;
|
||||
margin-top: 5px;
|
||||
}
|
||||
.details-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
gap: 20px;
|
||||
margin: 30px 0;
|
||||
}
|
||||
.detail-box {
|
||||
background: #f9f9f9;
|
||||
padding: 15px;
|
||||
border-radius: 6px;
|
||||
border-left: 4px solid #4CAF50;
|
||||
}
|
||||
.detail-label {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
text-transform: uppercase;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
.detail-value {
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
.beschreibung-box {
|
||||
background: #f9f9f9;
|
||||
padding: 20px;
|
||||
border-radius: 6px;
|
||||
margin: 20px 0;
|
||||
}
|
||||
.status-badge {
|
||||
display: inline-block;
|
||||
padding: 8px 16px;
|
||||
border-radius: 20px;
|
||||
font-weight: bold;
|
||||
margin: 10px 0;
|
||||
}
|
||||
.status-verfuegbar {
|
||||
background: #4CAF50;
|
||||
color: white;
|
||||
}
|
||||
.status-nicht-verfuegbar {
|
||||
background: #f44336;
|
||||
color: white;
|
||||
}
|
||||
.back-link {
|
||||
display: inline-block;
|
||||
margin-bottom: 20px;
|
||||
color: #4CAF50;
|
||||
text-decoration: none;
|
||||
}
|
||||
.back-link:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
.features-list {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 20px 0;
|
||||
}
|
||||
.features-list li {
|
||||
padding: 10px;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
.features-list li:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
.feature-icon {
|
||||
font-size: 20px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<a href="{{ path('app_immobilie_index') }}" class="back-link">← Zurück zur Übersicht</a>
|
||||
|
||||
<div class="immobilie-detail">
|
||||
<div class="immobilie-header">
|
||||
<span class="immobilie-typ-badge">{{ immobilie.typ.label }}</span>
|
||||
<h2 class="immobilie-adresse">{{ immobilie.adresse }}</h2>
|
||||
<span class="status-badge {{ immobilie.verfuegbar ? 'status-verfuegbar' : 'status-nicht-verfuegbar' }}">
|
||||
{{ immobilie.verfuegbar ? 'Verfügbar' : 'Nicht verfügbar' }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="immobilie-preis-box">
|
||||
<div class="preis-haupt">{{ immobilie.preis|number_format(2, ',', '.') }} €</div>
|
||||
<div class="preis-pro-qm">{{ immobilie.preisProQm|number_format(2, ',', '.') }} € pro m²</div>
|
||||
</div>
|
||||
|
||||
<h3>Hauptmerkmale</h3>
|
||||
<div class="details-grid">
|
||||
<div class="detail-box">
|
||||
<div class="detail-label">Wohnfläche</div>
|
||||
<div class="detail-value">{{ immobilie.flaeche }} m²</div>
|
||||
</div>
|
||||
<div class="detail-box">
|
||||
<div class="detail-label">Zimmer</div>
|
||||
<div class="detail-value">{{ immobilie.zimmer }}</div>
|
||||
</div>
|
||||
{% if immobilie.baujahr %}
|
||||
<div class="detail-box">
|
||||
<div class="detail-label">Baujahr</div>
|
||||
<div class="detail-value">{{ immobilie.baujahr }}</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if immobilie.etage is not null %}
|
||||
<div class="detail-box">
|
||||
<div class="detail-label">Etage</div>
|
||||
<div class="detail-value">{{ immobilie.etage }}</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<h3>Ausstattung & Extras</h3>
|
||||
<ul class="features-list">
|
||||
<li>
|
||||
<span class="feature-icon">🚗</span>
|
||||
<strong>Garage:</strong> {{ immobilie.garage ? 'Ja' : 'Nein' }}
|
||||
</li>
|
||||
{% if immobilie.balkonFlaeche %}
|
||||
<li>
|
||||
<span class="feature-icon">🌿</span>
|
||||
<strong>Balkon:</strong> {{ immobilie.balkonFlaeche }} m²
|
||||
</li>
|
||||
{% endif %}
|
||||
{% if immobilie.kellerFlaeche %}
|
||||
<li>
|
||||
<span class="feature-icon">📦</span>
|
||||
<strong>Keller:</strong> {{ immobilie.kellerFlaeche }} m²
|
||||
</li>
|
||||
{% endif %}
|
||||
{% if immobilie.heizungstyp %}
|
||||
<li>
|
||||
<span class="feature-icon">🔥</span>
|
||||
<strong>Heizung:</strong> {{ immobilie.heizungstyp }}
|
||||
</li>
|
||||
{% endif %}
|
||||
{% if immobilie.nebenkosten %}
|
||||
<li>
|
||||
<span class="feature-icon">💶</span>
|
||||
<strong>Nebenkosten:</strong> {{ immobilie.nebenkosten|number_format(2, ',', '.') }} € / Monat
|
||||
</li>
|
||||
{% endif %}
|
||||
<li>
|
||||
<span class="feature-icon">📊</span>
|
||||
<strong>Gesamtfläche:</strong> {{ immobilie.gesamtflaeche }} m²
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
{% if immobilie.beschreibung %}
|
||||
<h3>Beschreibung</h3>
|
||||
<div class="beschreibung-box">
|
||||
{{ immobilie.beschreibung|nl2br }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div style="margin-top: 30px; padding-top: 20px; border-top: 1px solid #eee; color: #666; font-size: 14px;">
|
||||
Erstellt am: {{ immobilie.createdAt|date('d.m.Y H:i') }} Uhr<br>
|
||||
Zuletzt aktualisiert: {{ immobilie.updatedAt|date('d.m.Y H:i') }} Uhr
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
103
templates/immobilie/suche.html.twig
Normal file
103
templates/immobilie/suche.html.twig
Normal file
@@ -0,0 +1,103 @@
|
||||
{% extends 'base.html.twig' %}
|
||||
|
||||
{% block title %}Immobiliensuche - {{ parent() }}{% endblock %}
|
||||
|
||||
{% block stylesheets %}
|
||||
{{ parent() }}
|
||||
<style>
|
||||
.search-box {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||||
padding: 30px;
|
||||
margin: 20px 0;
|
||||
}
|
||||
.api-links {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
gap: 15px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
.api-link-card {
|
||||
background: #f9f9f9;
|
||||
padding: 20px;
|
||||
border-radius: 6px;
|
||||
border-left: 4px solid #4CAF50;
|
||||
}
|
||||
.api-link-card h4 {
|
||||
margin: 0 0 10px 0;
|
||||
color: #4CAF50;
|
||||
}
|
||||
.api-link-card a {
|
||||
color: #4CAF50;
|
||||
text-decoration: none;
|
||||
word-break: break-all;
|
||||
}
|
||||
.api-link-card a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
.api-link-card p {
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
margin: 10px 0;
|
||||
}
|
||||
.back-link {
|
||||
display: inline-block;
|
||||
margin-bottom: 20px;
|
||||
color: #4CAF50;
|
||||
text-decoration: none;
|
||||
}
|
||||
.back-link:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<a href="{{ path('app_immobilie_index') }}" class="back-link">← Zurück zur Übersicht</a>
|
||||
|
||||
<div class="search-box">
|
||||
<h2>Immobiliensuche über API</h2>
|
||||
<p>Nutzen Sie die REST-API für erweiterte Suchfunktionen. Hier sind einige nützliche Endpoints:</p>
|
||||
|
||||
<div class="api-links">
|
||||
<div class="api-link-card">
|
||||
<h4>Alle Immobilien</h4>
|
||||
<a href="/api/immobilies">
|
||||
/api/immobilies
|
||||
</a>
|
||||
<p>Liste aller Immobilien (JSON-LD Format)</p>
|
||||
</div>
|
||||
|
||||
<div class="api-link-card">
|
||||
<h4>API-Dokumentation</h4>
|
||||
<a href="/api">/api</a>
|
||||
<p>Vollständige API-Dokumentation mit allen verfügbaren Operationen</p>
|
||||
</div>
|
||||
|
||||
<div class="api-link-card">
|
||||
<h4>Einzelne Immobilie</h4>
|
||||
<a href="/api/immobilies/1">
|
||||
/api/immobilies/{id}
|
||||
</a>
|
||||
<p>Details zu einer bestimmten Immobilie</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3 style="margin-top: 40px;">Beispiel-Abfragen</h3>
|
||||
|
||||
<div class="api-link-card" style="margin-top: 20px;">
|
||||
<h4>Repository-Methoden (Backend)</h4>
|
||||
<p>Das ImmobilieRepository bietet folgende Suchmethoden:</p>
|
||||
<ul>
|
||||
<li><code>findVerfuegbare()</code> - Nur verfügbare Immobilien</li>
|
||||
<li><code>findByTyp(ImmobilienTyp)</code> - Nach Typ filtern</li>
|
||||
<li><code>findByPreisRange($min, $max)</code> - Preisspanne</li>
|
||||
<li><code>findByFlaecheRange($min, $max)</code> - Flächenbereich</li>
|
||||
<li><code>findMitGarage()</code> - Immobilien mit Garage</li>
|
||||
<li><code>findByMinZimmer($anzahl)</code> - Mindestanzahl Zimmer</li>
|
||||
<li><code>searchByAdresse($search)</code> - Adresssuche</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
28
tests/Api/ApiDocumentationTest.php
Normal file
28
tests/Api/ApiDocumentationTest.php
Normal file
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace App\Tests\Api;
|
||||
|
||||
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
|
||||
|
||||
class ApiDocumentationTest extends WebTestCase
|
||||
{
|
||||
public function testSwaggerUIAccessible(): void
|
||||
{
|
||||
$client = static::createClient();
|
||||
|
||||
// Test: Swagger UI ist öffentlich zugänglich
|
||||
$client->request('GET', '/api/docs.html');
|
||||
|
||||
$this->assertResponseIsSuccessful();
|
||||
}
|
||||
|
||||
public function testOpenAPIJsonLdAccessible(): void
|
||||
{
|
||||
$client = static::createClient();
|
||||
|
||||
// Test: OpenAPI JSON-LD ist öffentlich zugänglich
|
||||
$client->request('GET', '/api/docs.jsonld');
|
||||
|
||||
$this->assertResponseIsSuccessful();
|
||||
}
|
||||
}
|
||||
50
tests/Api/BundeslandApiTest.php
Normal file
50
tests/Api/BundeslandApiTest.php
Normal file
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
namespace App\Tests\Api;
|
||||
|
||||
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
|
||||
|
||||
class BundeslandApiTest extends WebTestCase
|
||||
{
|
||||
public function testGetBundeslaenderPublicAccess(): void
|
||||
{
|
||||
$client = static::createClient();
|
||||
|
||||
// Test: Bundesländer können ohne API-Key abgerufen werden
|
||||
$client->request('GET', '/api/bundeslands');
|
||||
|
||||
$this->assertResponseIsSuccessful();
|
||||
$this->assertResponseHeaderSame('content-type', 'application/ld+json; charset=utf-8');
|
||||
}
|
||||
|
||||
public function testGetSingleBundeslandPublicAccess(): void
|
||||
{
|
||||
$client = static::createClient();
|
||||
|
||||
// Test: Einzelnes Bundesland kann ohne API-Key abgerufen werden
|
||||
$client->request('GET', '/api/bundeslands/1');
|
||||
|
||||
// Response kann 200 (OK) oder 404 (Not Found) sein, beides ist akzeptabel
|
||||
$this->assertResponseStatusCodeSame(200);
|
||||
}
|
||||
|
||||
public function testCreateBundeslandRequiresAuthentication(): void
|
||||
{
|
||||
$client = static::createClient();
|
||||
|
||||
// Test: Bundesland erstellen ohne API-Key sollte fehlschlagen
|
||||
$client->request(
|
||||
'POST',
|
||||
'/api/bundeslands',
|
||||
[],
|
||||
[],
|
||||
['CONTENT_TYPE' => 'application/json'],
|
||||
json_encode([
|
||||
'name' => 'Test Bundesland',
|
||||
'grunderwerbsteuer' => 5.0,
|
||||
])
|
||||
);
|
||||
|
||||
$this->assertResponseStatusCodeSame(403); // Access Denied (no authentication on this firewall)
|
||||
}
|
||||
}
|
||||
49
tests/Api/HeizungstypApiTest.php
Normal file
49
tests/Api/HeizungstypApiTest.php
Normal file
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
namespace App\Tests\Api;
|
||||
|
||||
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
|
||||
|
||||
class HeizungstypApiTest extends WebTestCase
|
||||
{
|
||||
public function testGetHeizungstypenPublicAccess(): void
|
||||
{
|
||||
$client = static::createClient();
|
||||
|
||||
// Test: Heizungstypen können ohne API-Key abgerufen werden
|
||||
$client->request('GET', '/api/heizungstyps');
|
||||
|
||||
$this->assertResponseIsSuccessful();
|
||||
$this->assertResponseHeaderSame('content-type', 'application/ld+json; charset=utf-8');
|
||||
}
|
||||
|
||||
public function testGetSingleHeizungstypPublicAccess(): void
|
||||
{
|
||||
$client = static::createClient();
|
||||
|
||||
// Test: Einzelner Heizungstyp kann ohne API-Key abgerufen werden
|
||||
$client->request('GET', '/api/heizungstyps/1');
|
||||
|
||||
// Response kann 200 (OK) oder 404 (Not Found) sein
|
||||
$this->assertResponseStatusCodeSame(200);
|
||||
}
|
||||
|
||||
public function testCreateHeizungstypRequiresAuthentication(): void
|
||||
{
|
||||
$client = static::createClient();
|
||||
|
||||
// Test: Heizungstyp erstellen ohne API-Key sollte fehlschlagen
|
||||
$client->request(
|
||||
'POST',
|
||||
'/api/heizungstyps',
|
||||
[],
|
||||
[],
|
||||
['CONTENT_TYPE' => 'application/json'],
|
||||
json_encode([
|
||||
'name' => 'Test Heizung',
|
||||
])
|
||||
);
|
||||
|
||||
$this->assertResponseStatusCodeSame(401); // Unauthorized
|
||||
}
|
||||
}
|
||||
65
tests/Entity/BundeslandTest.php
Normal file
65
tests/Entity/BundeslandTest.php
Normal file
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
|
||||
namespace App\Tests\Entity;
|
||||
|
||||
use App\Entity\Bundesland;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class BundeslandTest extends TestCase
|
||||
{
|
||||
public function testBundeslandCreation(): void
|
||||
{
|
||||
$bundesland = new Bundesland();
|
||||
$bundesland->setName('Bayern');
|
||||
$bundesland->setGrunderwerbsteuer(3.5);
|
||||
|
||||
$this->assertEquals('Bayern', $bundesland->getName());
|
||||
$this->assertEquals(3.5, $bundesland->getGrunderwerbsteuer());
|
||||
}
|
||||
|
||||
public function testGrunderwerbsteuerValues(): void
|
||||
{
|
||||
$testCases = [
|
||||
['Baden-Württemberg', 5.0],
|
||||
['Bayern', 3.5],
|
||||
['Berlin', 6.0],
|
||||
['Brandenburg', 6.5],
|
||||
['Bremen', 5.0],
|
||||
['Hamburg', 5.5],
|
||||
['Hessen', 6.0],
|
||||
['Mecklenburg-Vorpommern', 6.0],
|
||||
['Niedersachsen', 5.0],
|
||||
['Nordrhein-Westfalen', 6.5],
|
||||
['Rheinland-Pfalz', 5.0],
|
||||
['Saarland', 6.5],
|
||||
['Sachsen', 5.5],
|
||||
['Sachsen-Anhalt', 5.0],
|
||||
['Schleswig-Holstein', 6.5],
|
||||
['Thüringen', 5.0],
|
||||
];
|
||||
|
||||
foreach ($testCases as [$name, $steuer]) {
|
||||
$bundesland = new Bundesland();
|
||||
$bundesland->setName($name);
|
||||
$bundesland->setGrunderwerbsteuer($steuer);
|
||||
|
||||
$this->assertEquals($name, $bundesland->getName());
|
||||
$this->assertEquals($steuer, $bundesland->getGrunderwerbsteuer());
|
||||
}
|
||||
}
|
||||
|
||||
public function testMinMaxGrunderwerbsteuer(): void
|
||||
{
|
||||
// Niedrigster Satz: Bayern mit 3.5%
|
||||
$bayern = new Bundesland();
|
||||
$bayern->setName('Bayern');
|
||||
$bayern->setGrunderwerbsteuer(3.5);
|
||||
$this->assertEquals(3.5, $bayern->getGrunderwerbsteuer());
|
||||
|
||||
// Höchster Satz: Brandenburg, NRW, Saarland, SH mit 6.5%
|
||||
$nrw = new Bundesland();
|
||||
$nrw->setName('Nordrhein-Westfalen');
|
||||
$nrw->setGrunderwerbsteuer(6.5);
|
||||
$this->assertEquals(6.5, $nrw->getGrunderwerbsteuer());
|
||||
}
|
||||
}
|
||||
29
tests/Entity/HeizungstypTest.php
Normal file
29
tests/Entity/HeizungstypTest.php
Normal file
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace App\Tests\Entity;
|
||||
|
||||
use App\Entity\Heizungstyp;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class HeizungstypTest extends TestCase
|
||||
{
|
||||
public function testHeizungstypCreation(): void
|
||||
{
|
||||
$heizungstyp = new Heizungstyp();
|
||||
$heizungstyp->setName('Wärmepumpe');
|
||||
|
||||
$this->assertEquals('Wärmepumpe', $heizungstyp->getName());
|
||||
}
|
||||
|
||||
public function testCommonHeizungstypen(): void
|
||||
{
|
||||
$typen = ['Gasheizung', 'Wärmepumpe', 'Ölheizung', 'Fernwärme', 'Pelletheizung'];
|
||||
|
||||
foreach ($typen as $typName) {
|
||||
$heizungstyp = new Heizungstyp();
|
||||
$heizungstyp->setName($typName);
|
||||
|
||||
$this->assertEquals($typName, $heizungstyp->getName());
|
||||
}
|
||||
}
|
||||
}
|
||||
160
tests/Entity/ImmobilieTest.php
Normal file
160
tests/Entity/ImmobilieTest.php
Normal file
@@ -0,0 +1,160 @@
|
||||
<?php
|
||||
|
||||
namespace App\Tests\Entity;
|
||||
|
||||
use App\Entity\Bundesland;
|
||||
use App\Entity\Immobilie;
|
||||
use App\Entity\User;
|
||||
use App\Enum\ImmobilienTyp;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class ImmobilieTest extends TestCase
|
||||
{
|
||||
private User $verwalter;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->verwalter = new User();
|
||||
$this->verwalter->setName('Test Verwalter');
|
||||
$this->verwalter->setEmail('verwalter@example.com');
|
||||
}
|
||||
|
||||
public function testImmobilieCreation(): void
|
||||
{
|
||||
$immobilie = new Immobilie();
|
||||
$immobilie->setVerwalter($this->verwalter);
|
||||
$immobilie->setAdresse('Teststraße 123, 12345 Teststadt');
|
||||
$immobilie->setWohnflaeche(100);
|
||||
$immobilie->setNutzflaeche(20);
|
||||
$immobilie->setZimmer(4);
|
||||
$immobilie->setTyp(ImmobilienTyp::WOHNUNG);
|
||||
|
||||
$this->assertEquals('Teststraße 123, 12345 Teststadt', $immobilie->getAdresse());
|
||||
$this->assertEquals(100, $immobilie->getWohnflaeche());
|
||||
$this->assertEquals(20, $immobilie->getNutzflaeche());
|
||||
$this->assertEquals(4, $immobilie->getZimmer());
|
||||
$this->assertEquals(ImmobilienTyp::WOHNUNG, $immobilie->getTyp());
|
||||
$this->assertEquals($this->verwalter, $immobilie->getVerwalter());
|
||||
}
|
||||
|
||||
public function testGesamtflaecheCalculation(): void
|
||||
{
|
||||
$immobilie = new Immobilie();
|
||||
$immobilie->setVerwalter($this->verwalter);
|
||||
$immobilie->setAdresse('Test');
|
||||
$immobilie->setWohnflaeche(85);
|
||||
$immobilie->setNutzflaeche(15);
|
||||
$immobilie->setZimmer(3);
|
||||
|
||||
$this->assertEquals(100, $immobilie->getGesamtflaeche());
|
||||
}
|
||||
|
||||
public function testKaufnebenkostenWithoutBundesland(): void
|
||||
{
|
||||
$immobilie = new Immobilie();
|
||||
$immobilie->setVerwalter($this->verwalter);
|
||||
$immobilie->setAdresse('Test');
|
||||
$immobilie->setWohnflaeche(100);
|
||||
$immobilie->setNutzflaeche(0);
|
||||
$immobilie->setZimmer(3);
|
||||
$immobilie->setKaufpreis(300000);
|
||||
|
||||
$kosten = $immobilie->getKaufnebenkosten();
|
||||
|
||||
$this->assertEquals(0, $kosten['notar']);
|
||||
$this->assertEquals(0, $kosten['grundbuch']);
|
||||
$this->assertEquals(0, $kosten['grunderwerbsteuer']);
|
||||
$this->assertEquals(0, $kosten['gesamt']);
|
||||
}
|
||||
|
||||
public function testKaufnebenkostenWithBundesland(): void
|
||||
{
|
||||
$bundesland = new Bundesland();
|
||||
$bundesland->setName('Bayern');
|
||||
$bundesland->setGrunderwerbsteuer(3.5);
|
||||
|
||||
$immobilie = new Immobilie();
|
||||
$immobilie->setVerwalter($this->verwalter);
|
||||
$immobilie->setAdresse('Test');
|
||||
$immobilie->setWohnflaeche(100);
|
||||
$immobilie->setNutzflaeche(0);
|
||||
$immobilie->setZimmer(3);
|
||||
$immobilie->setKaufpreis(300000);
|
||||
$immobilie->setBundesland($bundesland);
|
||||
|
||||
$kosten = $immobilie->getKaufnebenkosten();
|
||||
|
||||
// Notar: 1.5% von 300000 = 4500
|
||||
$this->assertEquals(4500, $kosten['notar']);
|
||||
|
||||
// Grundbuch: 0.5% von 300000 = 1500
|
||||
$this->assertEquals(1500, $kosten['grundbuch']);
|
||||
|
||||
// Grunderwerbsteuer: 3.5% von 300000 = 10500
|
||||
$this->assertEqualsWithDelta(10500, $kosten['grunderwerbsteuer'], 0.01);
|
||||
|
||||
// Gesamt: 4500 + 1500 + 10500 = 16500
|
||||
$this->assertEquals(16500, $kosten['gesamt']);
|
||||
}
|
||||
|
||||
public function testKaufnebenkostenWithDifferentBundesland(): void
|
||||
{
|
||||
$bundesland = new Bundesland();
|
||||
$bundesland->setName('Nordrhein-Westfalen');
|
||||
$bundesland->setGrunderwerbsteuer(6.5);
|
||||
|
||||
$immobilie = new Immobilie();
|
||||
$immobilie->setVerwalter($this->verwalter);
|
||||
$immobilie->setAdresse('Test');
|
||||
$immobilie->setWohnflaeche(100);
|
||||
$immobilie->setNutzflaeche(0);
|
||||
$immobilie->setZimmer(3);
|
||||
$immobilie->setKaufpreis(400000);
|
||||
$immobilie->setBundesland($bundesland);
|
||||
|
||||
$kosten = $immobilie->getKaufnebenkosten();
|
||||
|
||||
// Notar: 1.5% von 400000 = 6000
|
||||
$this->assertEquals(6000, $kosten['notar']);
|
||||
|
||||
// Grundbuch: 0.5% von 400000 = 2000
|
||||
$this->assertEquals(2000, $kosten['grundbuch']);
|
||||
|
||||
// Grunderwerbsteuer: 6.5% von 400000 = 26000
|
||||
$this->assertEquals(26000, $kosten['grunderwerbsteuer']);
|
||||
|
||||
// Gesamt: 6000 + 2000 + 26000 = 34000
|
||||
$this->assertEquals(34000, $kosten['gesamt']);
|
||||
}
|
||||
|
||||
public function testDefaultValues(): void
|
||||
{
|
||||
$immobilie = new Immobilie();
|
||||
|
||||
$this->assertEquals(ImmobilienTyp::WOHNUNG, $immobilie->getTyp());
|
||||
$this->assertEquals(0, $immobilie->getNutzflaeche());
|
||||
$this->assertFalse($immobilie->getGarage());
|
||||
$this->assertNotNull($immobilie->getCreatedAt());
|
||||
$this->assertNotNull($immobilie->getUpdatedAt());
|
||||
}
|
||||
|
||||
public function testOptionalFields(): void
|
||||
{
|
||||
$immobilie = new Immobilie();
|
||||
$immobilie->setVerwalter($this->verwalter);
|
||||
$immobilie->setAdresse('Test');
|
||||
$immobilie->setWohnflaeche(100);
|
||||
$immobilie->setNutzflaeche(0);
|
||||
$immobilie->setZimmer(3);
|
||||
|
||||
$immobilie->setBaujahr(2020);
|
||||
$immobilie->setEtage(3);
|
||||
$immobilie->setAbschreibungszeit(50);
|
||||
$immobilie->setBeschreibung('Schöne Wohnung');
|
||||
|
||||
$this->assertEquals(2020, $immobilie->getBaujahr());
|
||||
$this->assertEquals(3, $immobilie->getEtage());
|
||||
$this->assertEquals(50, $immobilie->getAbschreibungszeit());
|
||||
$this->assertEquals('Schöne Wohnung', $immobilie->getBeschreibung());
|
||||
}
|
||||
}
|
||||
92
tests/Entity/UserTest.php
Normal file
92
tests/Entity/UserTest.php
Normal file
@@ -0,0 +1,92 @@
|
||||
<?php
|
||||
|
||||
namespace App\Tests\Entity;
|
||||
|
||||
use App\Entity\User;
|
||||
use App\Enum\UserRole;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class UserTest extends TestCase
|
||||
{
|
||||
public function testUserCreation(): void
|
||||
{
|
||||
$user = new User();
|
||||
$user->setName('Max Mustermann');
|
||||
$user->setEmail('max@example.com');
|
||||
$user->setRole(UserRole::USER);
|
||||
|
||||
$this->assertEquals('Max Mustermann', $user->getName());
|
||||
$this->assertEquals('max@example.com', $user->getEmail());
|
||||
$this->assertEquals(UserRole::USER, $user->getRole());
|
||||
$this->assertNotNull($user->getApiKey());
|
||||
$this->assertNotNull($user->getCreatedAt());
|
||||
}
|
||||
|
||||
public function testApiKeyGeneration(): void
|
||||
{
|
||||
$user = new User();
|
||||
$user->setName('Test User');
|
||||
$user->setEmail('test@example.com');
|
||||
|
||||
$apiKey = $user->getApiKey();
|
||||
|
||||
$this->assertNotEmpty($apiKey);
|
||||
$this->assertEquals(64, strlen($apiKey)); // SHA256 hash length
|
||||
}
|
||||
|
||||
public function testRegenerateApiKey(): void
|
||||
{
|
||||
$user = new User();
|
||||
$user->setName('Test User');
|
||||
$user->setEmail('test@example.com');
|
||||
|
||||
$originalKey = $user->getApiKey();
|
||||
$user->regenerateApiKey();
|
||||
$newKey = $user->getApiKey();
|
||||
|
||||
$this->assertNotEquals($originalKey, $newKey);
|
||||
$this->assertEquals(64, strlen($newKey));
|
||||
}
|
||||
|
||||
public function testUserRoles(): void
|
||||
{
|
||||
// Test USER role
|
||||
$user = new User();
|
||||
$user->setRole(UserRole::USER);
|
||||
$this->assertContains('ROLE_USER', $user->getRoles());
|
||||
|
||||
// Test ADMIN role
|
||||
$admin = new User();
|
||||
$admin->setRole(UserRole::ADMIN);
|
||||
$this->assertContains('ROLE_ADMIN', $admin->getRoles());
|
||||
$this->assertContains('ROLE_USER', $admin->getRoles());
|
||||
|
||||
// Test TECHNICAL role
|
||||
$technical = new User();
|
||||
$technical->setRole(UserRole::TECHNICAL);
|
||||
$this->assertContains('ROLE_TECHNICAL', $technical->getRoles());
|
||||
$this->assertContains('ROLE_USER', $technical->getRoles());
|
||||
|
||||
// Test MODERATOR role
|
||||
$moderator = new User();
|
||||
$moderator->setRole(UserRole::MODERATOR);
|
||||
$this->assertContains('ROLE_MODERATOR', $moderator->getRoles());
|
||||
$this->assertContains('ROLE_USER', $moderator->getRoles());
|
||||
}
|
||||
|
||||
public function testUserIdentifier(): void
|
||||
{
|
||||
$user = new User();
|
||||
$user->setEmail('identifier@example.com');
|
||||
|
||||
$this->assertEquals('identifier@example.com', $user->getUserIdentifier());
|
||||
}
|
||||
|
||||
public function testDefaultRole(): void
|
||||
{
|
||||
$user = new User();
|
||||
|
||||
// Default role should be USER
|
||||
$this->assertEquals(UserRole::USER, $user->getRole());
|
||||
}
|
||||
}
|
||||
13
tests/bootstrap.php
Normal file
13
tests/bootstrap.php
Normal file
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
use Symfony\Component\Dotenv\Dotenv;
|
||||
|
||||
require dirname(__DIR__).'/vendor/autoload.php';
|
||||
|
||||
if (method_exists(Dotenv::class, 'bootEnv')) {
|
||||
(new Dotenv())->bootEnv(dirname(__DIR__).'/.env');
|
||||
}
|
||||
|
||||
if ($_SERVER['APP_DEBUG']) {
|
||||
umask(0000);
|
||||
}
|
||||
Reference in New Issue
Block a user