Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 98e4abcfb0 | |||
| 3226205889 | |||
| 0f9913faa2 | |||
| ba87c9773c | |||
| 2094b71335 | |||
| 5063ba80af | |||
| cd0d554fef |
@@ -0,0 +1,29 @@
|
|||||||
|
# ==========================================
|
||||||
|
# GardenPlan – Entwicklungsumgebung (Active)
|
||||||
|
# ==========================================
|
||||||
|
|
||||||
|
# --- Application ---
|
||||||
|
APP_ENV=dev
|
||||||
|
APP_SECRET=gardenplan_dev_secret_change_in_production_2024
|
||||||
|
TRUSTED_PROXIES=127.0.0.0/8,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16
|
||||||
|
TRUSTED_HOSTS=^.*$
|
||||||
|
|
||||||
|
# --- Database (PostgreSQL) ---
|
||||||
|
POSTGRES_DB=gardenplan
|
||||||
|
POSTGRES_USER=symfony
|
||||||
|
POSTGRES_PASSWORD=changeme
|
||||||
|
DATABASE_URL="postgresql://symfony:changeme@database:5432/gardenplan?serverVersion=16&charset=utf8"
|
||||||
|
|
||||||
|
# --- Keycloak (OIDC / JWT) ---
|
||||||
|
KEYCLOAK_SERVER_URL=http://keycloak:8080
|
||||||
|
KEYCLOAK_REALM=gardenplan
|
||||||
|
KEYCLOAK_CLIENT_ID=symfony-app
|
||||||
|
KEYCLOAK_CLIENT_SECRET=changeme_client_secret
|
||||||
|
KEYCLOAK_ADMIN=admin
|
||||||
|
KEYCLOAK_ADMIN_PASSWORD=adminsecret
|
||||||
|
|
||||||
|
# --- Mailer (lokales Dev-Tool) ---
|
||||||
|
MAILER_DSN=null://null
|
||||||
|
|
||||||
|
# --- Messenger (Queue) ---
|
||||||
|
MESSENGER_TRANSPORT_DSN=doctrine://default?queue_name=messenger_messages
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
# ==========================================
|
||||||
|
# GardenPlan – Entwicklungsumgebung
|
||||||
|
# ==========================================
|
||||||
|
|
||||||
|
# --- Application ---
|
||||||
|
APP_ENV=dev
|
||||||
|
APP_SECRET=change_me_to_a_random_string_in_production
|
||||||
|
TRUSTED_PROXIES=127.0.0.0/8,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16
|
||||||
|
TRUSTED_HOSTS=^.*$
|
||||||
|
|
||||||
|
# --- Database (PostgreSQL) ---
|
||||||
|
POSTGRES_DB=gardenplan
|
||||||
|
POSTGRES_USER=symfony
|
||||||
|
POSTGRES_PASSWORD=changeme
|
||||||
|
DATABASE_URL="postgresql://symfony:changeme@database:5432/gardenplan?serverVersion=16&charset=utf8"
|
||||||
|
|
||||||
|
# --- Keycloak (OIDC / JWT) ---
|
||||||
|
KEYCLOAK_SERVER_URL=http://keycloak:8080
|
||||||
|
KEYCLOAK_REALM=gardenplan
|
||||||
|
KEYCLOAK_CLIENT_ID=symfony-app
|
||||||
|
KEYCLOAK_CLIENT_SECRET=changeme_client_secret
|
||||||
|
KEYCLOAK_ADMIN=admin
|
||||||
|
KEYCLOAK_ADMIN_PASSWORD=adminsecret
|
||||||
|
|
||||||
|
# --- Mailer (lokales Dev-Tool) ---
|
||||||
|
MAILER_DSN=null://null
|
||||||
|
|
||||||
|
# --- Messenger (Queue) ---
|
||||||
|
MESSENGER_TRANSPORT_DSN=doctrine://default?queue_name=messenger_messages
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
# editorconfig.org
|
||||||
|
|
||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
charset = utf-8
|
||||||
|
end_of_line = lf
|
||||||
|
indent_size = 4
|
||||||
|
indent_style = space
|
||||||
|
insert_final_newline = true
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
|
||||||
|
[{compose.yaml,compose.*.yaml}]
|
||||||
|
indent_size = 2
|
||||||
|
|
||||||
|
[*.md]
|
||||||
|
trim_trailing_whitespace = false
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
# In all environments, the following files are loaded if they exist,
|
||||||
|
# the latter taking precedence over the former:
|
||||||
|
#
|
||||||
|
# * .env contains default values for the environment variables needed by the app
|
||||||
|
# * .env.local uncommitted file with local overrides
|
||||||
|
# * .env.$APP_ENV committed environment-specific defaults
|
||||||
|
# * .env.$APP_ENV.local uncommitted environment-specific overrides
|
||||||
|
#
|
||||||
|
# Real environment variables win over .env files.
|
||||||
|
#
|
||||||
|
# DO NOT DEFINE PRODUCTION SECRETS IN THIS FILE NOR IN ANY OTHER COMMITTED FILES.
|
||||||
|
# https://symfony.com/doc/current/configuration/secrets.html
|
||||||
|
#
|
||||||
|
# Run "composer dump-env prod" to compile .env files for production use (requires symfony/flex >=1.2).
|
||||||
|
# https://symfony.com/doc/current/best_practices.html#use-environment-variables-for-infrastructure-configuration
|
||||||
|
|
||||||
|
###> symfony/framework-bundle ###
|
||||||
|
APP_ENV=dev
|
||||||
|
APP_SECRET=
|
||||||
|
APP_SHARE_DIR=var/share
|
||||||
|
###< symfony/framework-bundle ###
|
||||||
|
|
||||||
|
###> symfony/routing ###
|
||||||
|
# Configure how to generate URLs in non-HTTP contexts, such as CLI commands.
|
||||||
|
# See https://symfony.com/doc/current/routing.html#generating-urls-in-commands
|
||||||
|
DEFAULT_URI=http://localhost
|
||||||
|
###< symfony/routing ###
|
||||||
|
|
||||||
|
###> doctrine/doctrine-bundle ###
|
||||||
|
# Format described at https://www.doctrine-project.org/projects/doctrine-dbal/en/latest/reference/configuration.html#connecting-using-a-url
|
||||||
|
# IMPORTANT: You MUST configure your server version, either here or in config/packages/doctrine.yaml
|
||||||
|
#
|
||||||
|
# DATABASE_URL="sqlite:///%kernel.project_dir%/var/data_%kernel.environment%.db"
|
||||||
|
# DATABASE_URL="mysql://app:!ChangeMe!@127.0.0.1:3306/app?serverVersion=8.0.32&charset=utf8mb4"
|
||||||
|
# DATABASE_URL="mysql://app:!ChangeMe!@127.0.0.1:3306/app?serverVersion=10.11.2-MariaDB&charset=utf8mb4"
|
||||||
|
DATABASE_URL="postgresql://symfony:changeme@database:5432/gardenplan?serverVersion=16&charset=utf8"
|
||||||
|
###< doctrine/doctrine-bundle ###
|
||||||
|
|
||||||
|
###> symfony/messenger ###
|
||||||
|
# Choose one of the transports below
|
||||||
|
# MESSENGER_TRANSPORT_DSN=amqp://guest:guest@localhost:5672/%2f/messages
|
||||||
|
# MESSENGER_TRANSPORT_DSN=redis://localhost:6379/messages
|
||||||
|
MESSENGER_TRANSPORT_DSN=doctrine://default?auto_setup=0
|
||||||
|
###< symfony/messenger ###
|
||||||
|
|
||||||
|
###> lexik/jwt-authentication-bundle ###
|
||||||
|
JWT_SECRET_KEY=%kernel.project_dir%/config/jwt/private.pem
|
||||||
|
JWT_PUBLIC_KEY=%kernel.project_dir%/config/jwt/public.pem
|
||||||
|
JWT_PASSPHRASE=8f813531f1ad5443c687f70247b99c825771c9791460384f22cdb0353248f8f9
|
||||||
|
###< lexik/jwt-authentication-bundle ###
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
# ==========================================
|
||||||
|
# GardenPlan – Entwicklung (lokal ohne Docker)
|
||||||
|
# ==========================================
|
||||||
|
|
||||||
|
# --- Application ---
|
||||||
|
APP_ENV=dev
|
||||||
|
APP_SECRET=gardenplan_dev_secret_change_in_production_2024
|
||||||
|
TRUSTED_PROXIES=127.0.0.0/8,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16
|
||||||
|
TRUSTED_HOSTS=^.*$
|
||||||
|
|
||||||
|
# --- Database (PostgreSQL lokal) ---
|
||||||
|
DATABASE_URL="postgresql://symfony:changeme@database:5432/gardenplan?serverVersion=16&charset=utf8"
|
||||||
|
|
||||||
|
# --- Keycloak (OIDC / JWT) ---
|
||||||
|
KEYCLOAK_SERVER_URL=http://keycloak:8080
|
||||||
|
KEYCLOAK_REALM=gardenplan
|
||||||
|
KEYCLOAK_CLIENT_ID=symfony-app
|
||||||
|
KEYCLOAK_CLIENT_SECRET=changeme_client_secret
|
||||||
|
KEYCLOAK_ADMIN=admin
|
||||||
|
KEYCLOAK_ADMIN_PASSWORD=adminsecret
|
||||||
|
|
||||||
|
# --- Mailer (lokales Dev-Tool) ---
|
||||||
|
MAILER_DSN=null://null
|
||||||
|
|
||||||
|
# --- Symfony Cache/Log ---
|
||||||
|
MESSENGER_TRANSPORT_DSN=doctrine://default?queue_name=messenger_messages
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
|
||||||
|
###> symfony/framework-bundle ###
|
||||||
|
/.env.local
|
||||||
|
/.env.local.php
|
||||||
|
/.env.*.local
|
||||||
|
/config/secrets/prod/prod.decrypt.private.php
|
||||||
|
/public/bundles/
|
||||||
|
/var/
|
||||||
|
/vendor/
|
||||||
|
###< symfony/framework-bundle ###
|
||||||
|
|
||||||
|
###> lexik/jwt-authentication-bundle ###
|
||||||
|
/config/jwt/*.pem
|
||||||
|
###< lexik/jwt-authentication-bundle ###
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
Copyright (c) Fabien Potencier
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is furnished
|
||||||
|
to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
THE SOFTWARE.
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
<?php declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Migrations;
|
||||||
|
|
||||||
|
use Doctrine\DBAL\Schema\Schema;
|
||||||
|
use Doctrine\Migrations\AbstractMigration;
|
||||||
|
|
||||||
|
final class Version20250617184300_create_saatgut_tables extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function getDescription(): string
|
||||||
|
{
|
||||||
|
return 'Erstellt die relationalen Tabellen für den Saatgut-Bestand: kategorie, pflanze, saatgut_bestand.';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function up(Schema $schema): void
|
||||||
|
{
|
||||||
|
// 1. Kategorie-Tabelle
|
||||||
|
$this->addSql(<<<SQL
|
||||||
|
CREATE TABLE kategorie (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
name VARCHAR(100) NOT NULL UNIQUE,
|
||||||
|
farbe VARCHAR(7) DEFAULT NULL
|
||||||
|
);
|
||||||
|
SQL);
|
||||||
|
|
||||||
|
// 2. Pflanze-Tabelle
|
||||||
|
$this->addSql(<<<SQL
|
||||||
|
CREATE TABLE pflanze (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
art_name VARCHAR(200) NOT NULL,
|
||||||
|
sorten_name VARCHAR(200) DEFAULT NULL,
|
||||||
|
kategorie_id INTEGER DEFAULT NULL REFERENCES kategorie(id) ON DELETE SET NULL,
|
||||||
|
beschreibung TEXT DEFAULT NULL
|
||||||
|
);
|
||||||
|
SQL);
|
||||||
|
|
||||||
|
// 3. SaatgutBestand-Tabelle mit Unique Constraint (nutzer_id + pflanze_id)
|
||||||
|
$this->addSql(<<<SQL
|
||||||
|
CREATE TABLE saatgut_bestand (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
nutzer_id VARCHAR(255) NOT NULL,
|
||||||
|
pflanze_id INTEGER NOT NULL REFERENCES pflanze(id) ON DELETE CASCADE,
|
||||||
|
menge DOUBLE PRECISION NOT NULL,
|
||||||
|
kaufdatum DATE NOT NULL,
|
||||||
|
ablaufdatum DATE DEFAULT NULL,
|
||||||
|
notizen TEXT DEFAULT NULL,
|
||||||
|
CONSTRAINT unik_nutzer_pflanze UNIQUE (nutzer_id, pflanze_id)
|
||||||
|
);
|
||||||
|
SQL);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(Schema $schema): void
|
||||||
|
{
|
||||||
|
$this->addSql('DROP TABLE IF EXISTS saatgut_bestand');
|
||||||
|
$this->addSql('DROP TABLE IF EXISTS pflanze');
|
||||||
|
$this->addSql('DROP TABLE IF EXISTS kategorie');
|
||||||
|
}
|
||||||
|
}
|
||||||
Executable
+21
@@ -0,0 +1,21 @@
|
|||||||
|
#!/usr/bin/env php
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Kernel;
|
||||||
|
use Symfony\Bundle\FrameworkBundle\Console\Application;
|
||||||
|
|
||||||
|
if (!is_dir(dirname(__DIR__).'/vendor')) {
|
||||||
|
throw new LogicException('Dependencies are missing. Try running "composer install".');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!is_file(dirname(__DIR__).'/vendor/autoload_runtime.php')) {
|
||||||
|
throw new LogicException('Symfony Runtime is missing. Try running "composer require symfony/runtime".');
|
||||||
|
}
|
||||||
|
|
||||||
|
require_once dirname(__DIR__).'/vendor/autoload_runtime.php';
|
||||||
|
|
||||||
|
return function (array $context) {
|
||||||
|
$kernel = new Kernel($context['APP_ENV'], (bool) $context['APP_DEBUG']);
|
||||||
|
|
||||||
|
return new Application($kernel);
|
||||||
|
};
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
|
||||||
|
services:
|
||||||
|
###> doctrine/doctrine-bundle ###
|
||||||
|
database:
|
||||||
|
ports:
|
||||||
|
- "5432"
|
||||||
|
###< doctrine/doctrine-bundle ###
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
|
||||||
|
services:
|
||||||
|
###> doctrine/doctrine-bundle ###
|
||||||
|
database:
|
||||||
|
image: postgres:${POSTGRES_VERSION:-16}-alpine
|
||||||
|
environment:
|
||||||
|
POSTGRES_DB: ${POSTGRES_DB:-app}
|
||||||
|
# You should definitely change the password in production
|
||||||
|
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-!ChangeMe!}
|
||||||
|
POSTGRES_USER: ${POSTGRES_USER:-app}
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "pg_isready", "-d", "${POSTGRES_DB:-app}", "-U", "${POSTGRES_USER:-app}"]
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
start_period: 60s
|
||||||
|
volumes:
|
||||||
|
- database_data:/var/lib/postgresql/data:rw
|
||||||
|
# You may use a bind-mounted host directory instead, so that it is harder to accidentally remove the volume and lose all your data!
|
||||||
|
# - ./docker/db/data:/var/lib/postgresql/data:rw
|
||||||
|
###< doctrine/doctrine-bundle ###
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
###> doctrine/doctrine-bundle ###
|
||||||
|
database_data:
|
||||||
|
###< doctrine/doctrine-bundle ###
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
{
|
||||||
|
"name": "symfony/skeleton",
|
||||||
|
"type": "project",
|
||||||
|
"license": "MIT",
|
||||||
|
"description": "A minimal Symfony project recommended to create bare bones applications",
|
||||||
|
"minimum-stability": "stable",
|
||||||
|
"prefer-stable": true,
|
||||||
|
"require": {
|
||||||
|
"php": ">=8.4",
|
||||||
|
"ext-ctype": "*",
|
||||||
|
"ext-iconv": "*",
|
||||||
|
"doctrine/doctrine-bundle": "*",
|
||||||
|
"doctrine/doctrine-fixtures-bundle": "^4.3",
|
||||||
|
"doctrine/orm": "^3.6",
|
||||||
|
"lexik/jwt-authentication-bundle": "^3.2",
|
||||||
|
"symfony/console": "8.1.*",
|
||||||
|
"symfony/dotenv": "8.1.*",
|
||||||
|
"symfony/flex": "^2",
|
||||||
|
"symfony/framework-bundle": "8.1.*",
|
||||||
|
"symfony/messenger": "*",
|
||||||
|
"symfony/runtime": "8.1.*",
|
||||||
|
"symfony/security-bundle": "8.1.*",
|
||||||
|
"symfony/validator": "*",
|
||||||
|
"symfony/yaml": "8.1.*"
|
||||||
|
},
|
||||||
|
"config": {
|
||||||
|
"allow-plugins": {
|
||||||
|
"php-http/discovery": true,
|
||||||
|
"symfony/flex": true,
|
||||||
|
"symfony/runtime": true
|
||||||
|
},
|
||||||
|
"bump-after-update": true,
|
||||||
|
"sort-packages": true
|
||||||
|
},
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"App\\": "src/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"autoload-dev": {
|
||||||
|
"psr-4": {
|
||||||
|
"App\\Tests\\": "tests/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"replace": {
|
||||||
|
"symfony/polyfill-ctype": "*",
|
||||||
|
"symfony/polyfill-iconv": "*",
|
||||||
|
"symfony/polyfill-php72": "*",
|
||||||
|
"symfony/polyfill-php73": "*",
|
||||||
|
"symfony/polyfill-php74": "*",
|
||||||
|
"symfony/polyfill-php80": "*",
|
||||||
|
"symfony/polyfill-php81": "*",
|
||||||
|
"symfony/polyfill-php82": "*",
|
||||||
|
"symfony/polyfill-php83": "*",
|
||||||
|
"symfony/polyfill-php84": "*"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"auto-scripts": {
|
||||||
|
"cache:clear": "symfony-cmd",
|
||||||
|
"assets:install %PUBLIC_DIR%": "symfony-cmd"
|
||||||
|
},
|
||||||
|
"post-install-cmd": [
|
||||||
|
"@auto-scripts"
|
||||||
|
],
|
||||||
|
"post-update-cmd": [
|
||||||
|
"@auto-scripts"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"conflict": {
|
||||||
|
"symfony/symfony": "*"
|
||||||
|
},
|
||||||
|
"extra": {
|
||||||
|
"symfony": {
|
||||||
|
"allow-contrib": false,
|
||||||
|
"require": "8.1.*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"phpstan/phpstan": "^2.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
Generated
+5117
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,9 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
return [
|
||||||
|
Symfony\Bundle\FrameworkBundle\FrameworkBundle::class => ['all' => true],
|
||||||
|
Doctrine\Bundle\DoctrineBundle\DoctrineBundle::class => ['all' => true],
|
||||||
|
Symfony\Bundle\SecurityBundle\SecurityBundle::class => ['all' => true],
|
||||||
|
Lexik\Bundle\JWTAuthenticationBundle\LexikJWTAuthenticationBundle::class => ['all' => true],
|
||||||
|
Doctrine\Bundle\FixturesBundle\DoctrineFixturesBundle::class => ['dev' => true, 'test' => true],
|
||||||
|
];
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
framework:
|
||||||
|
cache:
|
||||||
|
# Unique name of your app: used to compute stable namespaces for cache keys.
|
||||||
|
#prefix_seed: your_vendor_name/app_name
|
||||||
|
|
||||||
|
# The "app" cache stores to the filesystem by default.
|
||||||
|
# The data in this cache should persist between deploys.
|
||||||
|
# Other options include:
|
||||||
|
|
||||||
|
# Redis
|
||||||
|
#app: cache.adapter.redis
|
||||||
|
#default_redis_provider: redis://localhost
|
||||||
|
|
||||||
|
# APCu (not recommended with heavy random-write workloads as memory fragmentation can cause perf issues)
|
||||||
|
#app: cache.adapter.apcu
|
||||||
|
|
||||||
|
# Namespaced pools use the above "app" backend by default
|
||||||
|
#pools:
|
||||||
|
#my.dedicated.cache: null
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
doctrine:
|
||||||
|
dbal:
|
||||||
|
url: '%env(resolve:DATABASE_URL)%'
|
||||||
|
|
||||||
|
# IMPORTANT: You MUST configure your server version,
|
||||||
|
# either here or in the DATABASE_URL env var (see .env file)
|
||||||
|
#server_version: '16'
|
||||||
|
|
||||||
|
profiling_collect_backtrace: '%kernel.debug%'
|
||||||
|
orm:
|
||||||
|
validate_xml_mapping: true
|
||||||
|
naming_strategy: doctrine.orm.naming_strategy.underscore
|
||||||
|
identity_generation_preferences:
|
||||||
|
Doctrine\DBAL\Platforms\PostgreSQLPlatform: identity
|
||||||
|
auto_mapping: true
|
||||||
|
mappings:
|
||||||
|
App:
|
||||||
|
type: attribute
|
||||||
|
is_bundle: false
|
||||||
|
dir: '%kernel.project_dir%/src/Data/Doctrine/Entity/Saatgut'
|
||||||
|
prefix: 'App\Data\Doctrine\Entity\Saatgut'
|
||||||
|
alias: Saatgut
|
||||||
|
|
||||||
|
when@test:
|
||||||
|
doctrine:
|
||||||
|
dbal:
|
||||||
|
# "TEST_TOKEN" is typically set by ParaTest
|
||||||
|
dbname_suffix: '_test%env(default::TEST_TOKEN)%'
|
||||||
|
|
||||||
|
when@prod:
|
||||||
|
doctrine:
|
||||||
|
orm:
|
||||||
|
query_cache_driver:
|
||||||
|
type: pool
|
||||||
|
pool: doctrine.system_cache_pool
|
||||||
|
result_cache_driver:
|
||||||
|
type: pool
|
||||||
|
pool: doctrine.result_cache_pool
|
||||||
|
|
||||||
|
framework:
|
||||||
|
cache:
|
||||||
|
pools:
|
||||||
|
doctrine.result_cache_pool:
|
||||||
|
adapter: cache.app
|
||||||
|
doctrine.system_cache_pool:
|
||||||
|
adapter: cache.system
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
# see https://symfony.com/doc/current/reference/configuration/framework.html
|
||||||
|
framework:
|
||||||
|
secret: '%env(APP_SECRET)%'
|
||||||
|
|
||||||
|
# Note that the session will be started ONLY if you read or write from it.
|
||||||
|
session: true
|
||||||
|
|
||||||
|
#esi: true
|
||||||
|
#fragments: true
|
||||||
|
|
||||||
|
when@test:
|
||||||
|
framework:
|
||||||
|
test: true
|
||||||
|
session:
|
||||||
|
storage_factory_id: session.storage.factory.mock_file
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
lexik_jwt_authentication:
|
||||||
|
secret_key: '%env(resolve:JWT_SECRET_KEY)%'
|
||||||
|
public_key: '%env(resolve:JWT_PUBLIC_KEY)%'
|
||||||
|
pass_phrase: '%env(JWT_PASSPHRASE)%'
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
framework:
|
||||||
|
messenger:
|
||||||
|
# Uncomment this (and the failed transport below) to send failed messages to this transport for later handling.
|
||||||
|
# failure_transport: failed
|
||||||
|
|
||||||
|
transports:
|
||||||
|
# https://symfony.com/doc/current/messenger.html#transport-configuration
|
||||||
|
# async: '%env(MESSENGER_TRANSPORT_DSN)%'
|
||||||
|
# failed: 'doctrine://default?queue_name=failed'
|
||||||
|
sync: 'sync://'
|
||||||
|
|
||||||
|
routing:
|
||||||
|
# Route your messages to the transports
|
||||||
|
# 'App\Message\YourMessage': async
|
||||||
|
|
||||||
|
# when@test:
|
||||||
|
# framework:
|
||||||
|
# messenger:
|
||||||
|
# transports:
|
||||||
|
# # replace with your transport name here (e.g., my_transport: 'in-memory://')
|
||||||
|
# # For more Messenger testing tools, see https://github.com/zenstruck/messenger-test
|
||||||
|
# async: 'in-memory://'
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
framework:
|
||||||
|
property_info:
|
||||||
|
with_constructor_extractor: true
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
framework:
|
||||||
|
router:
|
||||||
|
# Configure how to generate URLs in non-HTTP contexts, such as CLI commands.
|
||||||
|
# See https://symfony.com/doc/current/routing.html#generating-urls-in-commands
|
||||||
|
default_uri: '%env(DEFAULT_URI)%'
|
||||||
|
|
||||||
|
when@prod:
|
||||||
|
framework:
|
||||||
|
router:
|
||||||
|
strict_requirements: null
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
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 }
|
||||||
|
|
||||||
|
firewalls:
|
||||||
|
dev:
|
||||||
|
# Ensure dev tools and static assets are always allowed
|
||||||
|
pattern: ^/(_profiler|_wdt|assets|build)/
|
||||||
|
security: false
|
||||||
|
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
|
||||||
|
|
||||||
|
# Note: Only the *first* matching rule is applied
|
||||||
|
access_control:
|
||||||
|
# - { path: ^/admin, roles: ROLE_ADMIN }
|
||||||
|
# - { path: ^/profile, roles: ROLE_USER }
|
||||||
|
|
||||||
|
when@test:
|
||||||
|
security:
|
||||||
|
password_hashers:
|
||||||
|
# Password hashers are resource-intensive by design to ensure security.
|
||||||
|
# In tests, it's safe to reduce their cost to improve performance.
|
||||||
|
Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface:
|
||||||
|
algorithm: auto
|
||||||
|
cost: 4 # Lowest possible value for bcrypt
|
||||||
|
time_cost: 3 # Lowest possible value for argon
|
||||||
|
memory_cost: 10 # Lowest possible value for argon
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
framework:
|
||||||
|
validation:
|
||||||
|
# Enables validator auto-mapping support.
|
||||||
|
# For instance, basic validation constraints will be inferred from Doctrine's metadata.
|
||||||
|
#auto_mapping:
|
||||||
|
# App\Entity\: []
|
||||||
|
|
||||||
|
when@test:
|
||||||
|
framework:
|
||||||
|
validation:
|
||||||
|
not_compromised_password: false
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
if (file_exists(dirname(__DIR__).'/var/cache/prod/App_KernelProdContainer.preload.php')) {
|
||||||
|
require dirname(__DIR__).'/var/cache/prod/App_KernelProdContainer.preload.php';
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,11 @@
|
|||||||
|
# yaml-language-server: $schema=../vendor/symfony/routing/Loader/schema/routing.schema.json
|
||||||
|
|
||||||
|
# This file is the entry point to configure the routes of your app.
|
||||||
|
# Methods with the #[Route] attribute are automatically imported.
|
||||||
|
# See also https://symfony.com/doc/current/routing.html
|
||||||
|
|
||||||
|
# To list all registered routes, run the following command:
|
||||||
|
# bin/console debug:router
|
||||||
|
|
||||||
|
controllers:
|
||||||
|
resource: routing.controllers
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
when@dev:
|
||||||
|
_errors:
|
||||||
|
resource: '@FrameworkBundle/Resources/config/routing/errors.php'
|
||||||
|
prefix: /_error
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
_security_logout:
|
||||||
|
resource: security.route_loader.logout
|
||||||
|
type: service
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
# yaml-language-server: $schema=../vendor/symfony/dependency-injection/Loader/schema/services.schema.json
|
||||||
|
|
||||||
|
# This file is the entry point to configure your own services.
|
||||||
|
# Files in the packages/ subdirectory configure your dependencies.
|
||||||
|
# See also https://symfony.com/doc/current/service_container/import.html
|
||||||
|
|
||||||
|
# Put parameters here that don't need to change on each machine where the app is deployed
|
||||||
|
# https://symfony.com/doc/current/best_practices.html#use-parameters-for-application-configuration
|
||||||
|
parameters:
|
||||||
|
|
||||||
|
services:
|
||||||
|
# default configuration for services in *this* file
|
||||||
|
_defaults:
|
||||||
|
autowire: true # Automatically injects dependencies in your services.
|
||||||
|
autoconfigure: true # Automatically registers your services as commands, event subscribers, etc.
|
||||||
|
|
||||||
|
# makes classes in src/ available to be used as services
|
||||||
|
# this creates a service per class whose id is the fully-qualified class name
|
||||||
|
App\:
|
||||||
|
resource: '../src/'
|
||||||
|
|
||||||
|
# add more service definitions when explicit configuration is needed
|
||||||
|
# please note that last definitions always *replace* previous ones
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
parameters:
|
||||||
|
level: 8
|
||||||
|
paths:
|
||||||
|
- src
|
||||||
|
ignoreErrors:
|
||||||
|
# Symfony Kernel has some generic unused methods that are technically framework internals.
|
||||||
|
- '#Method App\\Kernel::getAllowedEnvs\(\) is unused#'
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Kernel;
|
||||||
|
|
||||||
|
require_once dirname(__DIR__).'/vendor/autoload_runtime.php';
|
||||||
|
|
||||||
|
return static function (array $context) {
|
||||||
|
return new Kernel($context['APP_ENV'], (bool) $context['APP_DEBUG']);
|
||||||
|
};
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
<?php declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Data\Doctrine\Entity\Saatgut;
|
||||||
|
|
||||||
|
use Doctrine\ORM\Mapping as ORM;
|
||||||
|
|
||||||
|
#[ORM\Entity]
|
||||||
|
class KategorieEntity
|
||||||
|
{
|
||||||
|
#[ORM\Id]
|
||||||
|
#[ORM\GeneratedValue(strategy: 'IDENTITY')]
|
||||||
|
#[ORM\Column(type: 'integer')]
|
||||||
|
public ?int $id = null;
|
||||||
|
|
||||||
|
#[ORM\Column(type: 'string', length: 100, unique: true)]
|
||||||
|
private string $name;
|
||||||
|
|
||||||
|
#[ORM\Column(type: 'string', length: 7, nullable: true)]
|
||||||
|
private ?string $farbe = null;
|
||||||
|
|
||||||
|
public function __construct(string $name, ?string $farbe = null)
|
||||||
|
{
|
||||||
|
$this->name = $name;
|
||||||
|
$this->farbe = $farbe;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getId(): ?int
|
||||||
|
{
|
||||||
|
return $this->id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getName(): string
|
||||||
|
{
|
||||||
|
return $this->name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getFarbe(): ?string
|
||||||
|
{
|
||||||
|
return $this->farbe;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setFarbe(?string $farbe): self
|
||||||
|
{
|
||||||
|
$this->farbe = $farbe;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
<?php declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Data\Doctrine\Entity\Saatgut;
|
||||||
|
|
||||||
|
use Doctrine\ORM\Mapping as ORM;
|
||||||
|
|
||||||
|
#[ORM\Entity]
|
||||||
|
class PflanzeEntity
|
||||||
|
{
|
||||||
|
#[ORM\Id]
|
||||||
|
#[ORM\GeneratedValue(strategy: 'IDENTITY')]
|
||||||
|
#[ORM\Column(type: 'integer')]
|
||||||
|
public ?int $id = null;
|
||||||
|
|
||||||
|
#[ORM\Column(type: 'string', length: 200)]
|
||||||
|
private string $artName;
|
||||||
|
|
||||||
|
#[ORM\Column(type: 'string', length: 200, nullable: true)]
|
||||||
|
private ?string $sortenName = null;
|
||||||
|
|
||||||
|
#[ORM\Column(type: 'integer', nullable: true)]
|
||||||
|
private ?int $kategorieId = null;
|
||||||
|
|
||||||
|
#[ORM\Column(type: 'text', nullable: true)]
|
||||||
|
private ?string $beschreibung = null;
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
string $artName,
|
||||||
|
?string $sortenName = null,
|
||||||
|
?int $kategorieId = null,
|
||||||
|
?string $beschreibung = null,
|
||||||
|
) {
|
||||||
|
$this->artName = $artName;
|
||||||
|
$this->sortenName = $sortenName;
|
||||||
|
$this->kategorieId = $kategorieId;
|
||||||
|
$this->beschreibung = $beschreibung;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getId(): ?int
|
||||||
|
{
|
||||||
|
return $this->id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getArtName(): string
|
||||||
|
{
|
||||||
|
return $this->artName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getSortenName(): ?string
|
||||||
|
{
|
||||||
|
return $this->sortenName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setSortenName(?string $sortenName): self
|
||||||
|
{
|
||||||
|
$this->sortenName = $sortenName;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getKategorieId(): ?int
|
||||||
|
{
|
||||||
|
return $this->kategorieId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setKategorieId(?int $kategorieId): self
|
||||||
|
{
|
||||||
|
$this->kategorieId = $kategorieId;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getBeschreibung(): ?string
|
||||||
|
{
|
||||||
|
return $this->beschreibung;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setBeschreibung(?string $beschreibung): self
|
||||||
|
{
|
||||||
|
$this->beschreibung = $beschreibung;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,118 @@
|
|||||||
|
<?php declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Data\Doctrine\Entity\Saatgut;
|
||||||
|
|
||||||
|
use Doctrine\ORM\Mapping as ORM;
|
||||||
|
|
||||||
|
#[ORM\Entity]
|
||||||
|
#[ORM\Table(name: 'saatgut_bestand')]
|
||||||
|
#[ORM\UniqueConstraint(name: 'unik_nutzer_pflanze', columns: ['nutzer_id', 'pflanze_id'])]
|
||||||
|
class SaatgutBestandEntity
|
||||||
|
{
|
||||||
|
#[ORM\Id]
|
||||||
|
#[ORM\GeneratedValue(strategy: 'IDENTITY')]
|
||||||
|
#[ORM\Column(type: 'integer')]
|
||||||
|
public ?int $id = null;
|
||||||
|
|
||||||
|
#[ORM\Column(type: 'string', length: 255)]
|
||||||
|
private string $nutzerId;
|
||||||
|
|
||||||
|
#[ORM\Column(type: 'integer')]
|
||||||
|
private int $pflanzeId;
|
||||||
|
|
||||||
|
#[ORM\Column(type: 'float')]
|
||||||
|
private float $menge;
|
||||||
|
|
||||||
|
#[ORM\Column(type: 'date')]
|
||||||
|
private \DateTimeInterface $kaufdatum;
|
||||||
|
|
||||||
|
#[ORM\Column(type: 'date', nullable: true)]
|
||||||
|
private ?\DateTimeInterface $ablaufdatum = null;
|
||||||
|
|
||||||
|
#[ORM\Column(type: 'text', nullable: true)]
|
||||||
|
private ?string $notizen = null;
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
string $nutzerId,
|
||||||
|
int $pflanzeId,
|
||||||
|
float $menge,
|
||||||
|
\DateTimeInterface $kaufdatum,
|
||||||
|
?\DateTimeInterface $ablaufdatum = null,
|
||||||
|
?string $notizen = null,
|
||||||
|
) {
|
||||||
|
$this->nutzerId = $nutzerId;
|
||||||
|
$this->pflanzeId = $pflanzeId;
|
||||||
|
$this->menge = $menge;
|
||||||
|
$this->kaufdatum = $kaufdatum;
|
||||||
|
$this->ablaufdatum = $ablaufdatum;
|
||||||
|
$this->notizen = $notizen;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getId(): ?int
|
||||||
|
{
|
||||||
|
return $this->id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getNutzerId(): string
|
||||||
|
{
|
||||||
|
return $this->nutzerId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getPflanzeId(): int
|
||||||
|
{
|
||||||
|
return $this->pflanzeId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setPflanzeId(int $pflanzeId): self
|
||||||
|
{
|
||||||
|
$this->pflanzeId = $pflanzeId;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function menge(): float
|
||||||
|
{
|
||||||
|
return $this->menge;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setMenge(float $menge): self
|
||||||
|
{
|
||||||
|
$this->menge = $menge;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function kaufdatum(): \DateTimeInterface
|
||||||
|
{
|
||||||
|
return $this->kaufdatum;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setKaufdatum(\DateTimeInterface $kaufdatum): self
|
||||||
|
{
|
||||||
|
$this->kaufdatum = $kaufdatum;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function ablaufdatum(): ?\DateTimeInterface
|
||||||
|
{
|
||||||
|
return $this->ablaufdatum;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setAblaufdatum(?\DateTimeInterface $ablaufdatum): self
|
||||||
|
{
|
||||||
|
$this->ablaufdatum = $ablaufdatum;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function notizen(): ?string
|
||||||
|
{
|
||||||
|
return $this->notizen;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setNotizen(?string $notizen): void
|
||||||
|
{
|
||||||
|
$this->notizen = $notizen;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
<?php declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Data\Doctrine\Repository\Saatgut;
|
||||||
|
|
||||||
|
use App\Data\Doctrine\Entity\Saatgut\KategorieEntity;
|
||||||
|
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||||
|
use Doctrine\Persistence\ManagerRegistry;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @extends ServiceEntityRepository<KategorieEntity>
|
||||||
|
*/
|
||||||
|
class KategorieRepository extends ServiceEntityRepository
|
||||||
|
{
|
||||||
|
public function __construct(ManagerRegistry $registry)
|
||||||
|
{
|
||||||
|
parent::__construct($registry, KategorieEntity::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return string[]
|
||||||
|
*/
|
||||||
|
public function getAllNames(): array
|
||||||
|
{
|
||||||
|
return array_map(
|
||||||
|
fn(KategorieEntity $k) => $k->getName(),
|
||||||
|
$this->findAll()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
<?php declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Data\Doctrine\Repository\Saatgut;
|
||||||
|
|
||||||
|
use App\Data\Doctrine\Entity\Saatgut\PflanzeEntity;
|
||||||
|
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||||
|
use Doctrine\Persistence\ManagerRegistry;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @extends ServiceEntityRepository<PflanzeEntity>
|
||||||
|
*/
|
||||||
|
class PflanzeRepository extends ServiceEntityRepository
|
||||||
|
{
|
||||||
|
public function __construct(ManagerRegistry $registry)
|
||||||
|
{
|
||||||
|
parent::__construct($registry, PflanzeEntity::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find plants by category ID (flat, no joins)
|
||||||
|
* @return PflanzeEntity[]
|
||||||
|
*/
|
||||||
|
public function findByKategorieId(int $kategorieId): array
|
||||||
|
{
|
||||||
|
return $this->createQueryBuilder('p')
|
||||||
|
->andWhere('p.kategorieId = :kategorieId')
|
||||||
|
->setParameter('kategorieId', $kategorieId)
|
||||||
|
->orderBy('p.artName', 'ASC')
|
||||||
|
->getQuery()
|
||||||
|
->getResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find plant by exact art name and sort name (flat query)
|
||||||
|
*/
|
||||||
|
public function findByArtUndSorte(string $artName, string $sortenName): ?PflanzeEntity
|
||||||
|
{
|
||||||
|
return $this->createQueryBuilder('p')
|
||||||
|
->andWhere('p.artName = :artName')
|
||||||
|
->andWhere('p.sortenName = :sortenName')
|
||||||
|
->setParameter('artName', $artName)
|
||||||
|
->setParameter('sortenName', $sortenName)
|
||||||
|
->getQuery()
|
||||||
|
->getOneOrNullResult();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
<?php declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Data\Doctrine\Repository\Saatgut;
|
||||||
|
|
||||||
|
use App\Data\Doctrine\Entity\Saatgut\SaatgutBestandEntity;
|
||||||
|
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||||
|
use Doctrine\Persistence\ManagerRegistry;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @extends ServiceEntityRepository<SaatgutBestandEntity>
|
||||||
|
*/
|
||||||
|
class SaatgutBestandRepository extends ServiceEntityRepository
|
||||||
|
{
|
||||||
|
public function __construct(ManagerRegistry $registry)
|
||||||
|
{
|
||||||
|
parent::__construct($registry, SaatgutBestandEntity::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get inventory items for a specific user (flat query via ID reference)
|
||||||
|
* @return SaatgutBestandEntity[]
|
||||||
|
*/
|
||||||
|
public function findByNutzerId(string $nutzerId): array
|
||||||
|
{
|
||||||
|
return $this->createQueryBuilder('s')
|
||||||
|
->andWhere('s.nutzerId = :nutzerId')
|
||||||
|
->setParameter('nutzerId', $nutzerId)
|
||||||
|
->orderBy('s.kaufdatum', 'DESC')
|
||||||
|
->getQuery()
|
||||||
|
->getResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a specific user already has this plant in inventory (to enforce unique constraint semantically)
|
||||||
|
*/
|
||||||
|
public function existsByNutzerUndPflanze(string $nutzerId, int $pflanzeId): bool
|
||||||
|
{
|
||||||
|
return (bool) $this->createQueryBuilder('s')
|
||||||
|
->select('COUNT(s.id)')
|
||||||
|
->andWhere('s.nutzerId = :nutzerId')
|
||||||
|
->andWhere('s.pflanzeId = :pflanzeId')
|
||||||
|
->setParameter('nutzerId', $nutzerId)
|
||||||
|
->setParameter('pflanzeId', $pflanzeId)
|
||||||
|
->getQuery()
|
||||||
|
->getSingleScalarResult();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\DataFixtures;
|
||||||
|
|
||||||
|
use Doctrine\Bundle\FixturesBundle\Fixture;
|
||||||
|
use Doctrine\Persistence\ObjectManager;
|
||||||
|
|
||||||
|
class AppFixtures extends Fixture
|
||||||
|
{
|
||||||
|
public function load(ObjectManager $manager): void
|
||||||
|
{
|
||||||
|
// $product = new Product();
|
||||||
|
// $manager->persist($product);
|
||||||
|
|
||||||
|
$manager->flush();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
<?php declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\DataFixtures;
|
||||||
|
|
||||||
|
use App\Data\Doctrine\Entity\Saatgut\KategorieEntity;
|
||||||
|
use Doctrine\Bundle\FixturesBundle\Fixture;
|
||||||
|
use Doctrine\Persistence\ObjectManager;
|
||||||
|
|
||||||
|
class KategorieFixtures extends Fixture
|
||||||
|
{
|
||||||
|
private const array KATEGORIEN = [
|
||||||
|
['name' => 'Gemüse', 'farbe' => '#4CAF50'],
|
||||||
|
['name' => 'Kräuter', 'farbe' => '#8BC34A'],
|
||||||
|
['name' => 'Blumen', 'farbe' => '#E91E63'],
|
||||||
|
['name' => 'Obst', 'farbe' => '#FF9800'],
|
||||||
|
];
|
||||||
|
|
||||||
|
public function load(ObjectManager $manager): void
|
||||||
|
{
|
||||||
|
foreach (self::KATEGORIEN as $data) {
|
||||||
|
$kategorie = new KategorieEntity(
|
||||||
|
name: $data['name'],
|
||||||
|
farbe: $data['farbe'],
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->addReference("kategorie-{$data['name']}", $kategorie);
|
||||||
|
$manager->persist($kategorie);
|
||||||
|
}
|
||||||
|
|
||||||
|
$manager->flush();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
<?php declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\DataFixtures;
|
||||||
|
|
||||||
|
use App\Data\Doctrine\Entity\Saatgut\KategorieEntity;
|
||||||
|
use App\Data\Doctrine\Entity\Saatgut\PflanzeEntity;
|
||||||
|
use Doctrine\Bundle\FixturesBundle\Fixture;
|
||||||
|
use Doctrine\Common\DataFixtures\DependentFixtureInterface;
|
||||||
|
use Doctrine\Persistence\ObjectManager;
|
||||||
|
|
||||||
|
class PflanzeFixtures extends Fixture implements DependentFixtureInterface
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @return class-string<Fixture>[]
|
||||||
|
*/
|
||||||
|
public function getDependencies(): array
|
||||||
|
{
|
||||||
|
return [KategorieFixtures::class];
|
||||||
|
}
|
||||||
|
|
||||||
|
private const array PFLANZEN = [
|
||||||
|
['artName' => 'Tomate', 'sortenName' => 'Stelzer', 'kategorie' => 'Gemüse'],
|
||||||
|
['artName' => 'Paprika', 'sortenName' => 'Habanero', 'kategorie' => 'Gemüse'],
|
||||||
|
['artName' => 'Basilikum', 'sortenName' => 'Genovese', 'kategorie' => 'Kräuter'],
|
||||||
|
['artName' => 'Salbei', 'sortenName' => null, 'kategorie' => 'Kräuter'],
|
||||||
|
['artName' => 'Sonnenblume', 'sortenName' => 'Riesenblume', 'kategorie' => 'Blumen'],
|
||||||
|
['artName' => 'Erdbeere', 'sortenName' => 'Senga Sengana', 'kategorie' => 'Obst'],
|
||||||
|
];
|
||||||
|
|
||||||
|
public function load(ObjectManager $manager): void
|
||||||
|
{
|
||||||
|
foreach (self::PFLANZEN as $data) {
|
||||||
|
$kategorie = $this->getReference("kategorie-{$data['kategorie']}", KategorieEntity::class);
|
||||||
|
|
||||||
|
$pflanze = new PflanzeEntity(
|
||||||
|
artName: $data['artName'],
|
||||||
|
sortenName: $data['sortenName'] ?? null,
|
||||||
|
kategorieId: $kategorie->getId(),
|
||||||
|
beschreibung: "Katalogeintrag für {$data['artName']}",
|
||||||
|
);
|
||||||
|
|
||||||
|
$manager->persist($pflanze);
|
||||||
|
}
|
||||||
|
|
||||||
|
$manager->flush();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App;
|
||||||
|
|
||||||
|
use Symfony\Bundle\FrameworkBundle\Kernel\MicroKernelTrait;
|
||||||
|
use Symfony\Component\HttpKernel\Kernel as BaseKernel;
|
||||||
|
|
||||||
|
class Kernel extends BaseKernel
|
||||||
|
{
|
||||||
|
use MicroKernelTrait;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return list<string> An array of allowed values for APP_ENV
|
||||||
|
*/
|
||||||
|
private function getAllowedEnvs(): array
|
||||||
|
{
|
||||||
|
return ['prod', 'dev', 'test'];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,159 @@
|
|||||||
|
{
|
||||||
|
"doctrine/deprecations": {
|
||||||
|
"version": "1.1",
|
||||||
|
"recipe": {
|
||||||
|
"repo": "github.com/symfony/recipes",
|
||||||
|
"branch": "main",
|
||||||
|
"version": "1.0",
|
||||||
|
"ref": "fdd756167454623e21f1d769c5b814b243782a67"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"doctrine/doctrine-bundle": {
|
||||||
|
"version": "3.2",
|
||||||
|
"recipe": {
|
||||||
|
"repo": "github.com/symfony/recipes",
|
||||||
|
"branch": "main",
|
||||||
|
"version": "3.0",
|
||||||
|
"ref": "d39a3bd844edfe90c20ae520b804a3bf4f82b4ad"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"config/packages/doctrine.yaml",
|
||||||
|
"src/Entity/.gitignore",
|
||||||
|
"src/Repository/.gitignore"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"doctrine/doctrine-fixtures-bundle": {
|
||||||
|
"version": "4.3",
|
||||||
|
"recipe": {
|
||||||
|
"repo": "github.com/symfony/recipes",
|
||||||
|
"branch": "main",
|
||||||
|
"version": "3.0",
|
||||||
|
"ref": "1f5514cfa15b947298df4d771e694e578d4c204d"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"src/DataFixtures/AppFixtures.php"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"lexik/jwt-authentication-bundle": {
|
||||||
|
"version": "3.2",
|
||||||
|
"recipe": {
|
||||||
|
"repo": "github.com/symfony/recipes",
|
||||||
|
"branch": "main",
|
||||||
|
"version": "2.5",
|
||||||
|
"ref": "e9481b233a11ef7e15fe055a2b21fd3ac1aa2bb7"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"config/packages/lexik_jwt_authentication.yaml"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"phpstan/phpstan": {
|
||||||
|
"version": "2.2.2"
|
||||||
|
},
|
||||||
|
"symfony/console": {
|
||||||
|
"version": "8.1",
|
||||||
|
"recipe": {
|
||||||
|
"repo": "github.com/symfony/recipes",
|
||||||
|
"branch": "main",
|
||||||
|
"version": "5.3",
|
||||||
|
"ref": "1781ff40d8a17d87cf53f8d4cf0c8346ed2bb461"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"bin/console"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"symfony/flex": {
|
||||||
|
"version": "2.11",
|
||||||
|
"recipe": {
|
||||||
|
"repo": "github.com/symfony/recipes",
|
||||||
|
"branch": "main",
|
||||||
|
"version": "2.4",
|
||||||
|
"ref": "52e9754527a15e2b79d9a610f98185a1fe46622a"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
".env",
|
||||||
|
".env.dev"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"symfony/framework-bundle": {
|
||||||
|
"version": "8.1",
|
||||||
|
"recipe": {
|
||||||
|
"repo": "github.com/symfony/recipes",
|
||||||
|
"branch": "main",
|
||||||
|
"version": "8.1",
|
||||||
|
"ref": "312027aea160796a50bf2d185503afdb5d71f570"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"config/packages/cache.yaml",
|
||||||
|
"config/packages/framework.yaml",
|
||||||
|
"config/preload.php",
|
||||||
|
"config/routes/framework.yaml",
|
||||||
|
"config/services.yaml",
|
||||||
|
"public/index.php",
|
||||||
|
"src/Controller/.gitignore",
|
||||||
|
"src/Kernel.php",
|
||||||
|
".editorconfig"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"symfony/messenger": {
|
||||||
|
"version": "8.1",
|
||||||
|
"recipe": {
|
||||||
|
"repo": "github.com/symfony/recipes",
|
||||||
|
"branch": "main",
|
||||||
|
"version": "6.0",
|
||||||
|
"ref": "d8936e2e2230637ef97e5eecc0eea074eecae58b"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"config/packages/messenger.yaml"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"symfony/property-info": {
|
||||||
|
"version": "8.1",
|
||||||
|
"recipe": {
|
||||||
|
"repo": "github.com/symfony/recipes",
|
||||||
|
"branch": "main",
|
||||||
|
"version": "7.3",
|
||||||
|
"ref": "dae70df71978ae9226ae915ffd5fad817f5ca1f7"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"config/packages/property_info.yaml"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"symfony/routing": {
|
||||||
|
"version": "8.1",
|
||||||
|
"recipe": {
|
||||||
|
"repo": "github.com/symfony/recipes",
|
||||||
|
"branch": "main",
|
||||||
|
"version": "7.4",
|
||||||
|
"ref": "bc94c4fd86f393f3ab3947c18b830ea343e51ded"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"config/packages/routing.yaml",
|
||||||
|
"config/routes.yaml"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"symfony/security-bundle": {
|
||||||
|
"version": "8.1",
|
||||||
|
"recipe": {
|
||||||
|
"repo": "github.com/symfony/recipes",
|
||||||
|
"branch": "main",
|
||||||
|
"version": "7.4",
|
||||||
|
"ref": "c42fee7802181cdd50f61b8622715829f5d2335c"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"config/packages/security.yaml",
|
||||||
|
"config/routes/security.yaml"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"symfony/validator": {
|
||||||
|
"version": "8.1",
|
||||||
|
"recipe": {
|
||||||
|
"repo": "github.com/symfony/recipes",
|
||||||
|
"branch": "main",
|
||||||
|
"version": "7.0",
|
||||||
|
"ref": "8c1c4e28d26a124b0bb273f537ca8ce443472bfd"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"config/packages/validator.yaml"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
Dokument 2: Datenmodell & Persistenz (Backend)
|
||||||
|
|
||||||
|
Ziel: Implementierung der relationalen Struktur zur Verwaltung des Saatgut-Bestands.
|
||||||
|
|
||||||
|
Entity-Definitionen (Doctrine ORM):
|
||||||
|
Kategorie: id, name (unique), farbe.
|
||||||
|
Pflanze: id, art_name, sorten_name, kategorie_id (FK → Kategorie), beschreibung.
|
||||||
|
SaatgutBestand: id, nutzer_id (String aus Keycloak), pflanze_id (FK → Pflanze), menge, kaufdatum, ablaufdatum, notizen.
|
||||||
|
Daten-Integrität:
|
||||||
|
Kaskadierendes Löschen: Wenn eine Kategorie gelöscht wird, muss entschieden werden, wie mit den Pflanzen verfahren wird (z.B. Setzen auf null oder Mitlöschen).
|
||||||
|
Unique Constraint für nutzer_id + pflanze_id, um doppelte Einträge derselben Sorte pro Nutzer zu vermeiden (optional, je nach Wunsch).
|
||||||
|
Akzeptanzkriterien:
|
||||||
|
|
||||||
|
Datenbank-Migrationen sind erstellt und erfolgreich ausgeführt.
|
||||||
|
|
||||||
|
Testdaten für den globalen Katalog (Kategorie & Pflanze) wurden importiert.
|
||||||
|
|
||||||
|
Die Beziehungen zwischen den Tabellen sind via Doctrine korrekt abgebildet.
|
||||||
+31
-21
@@ -5,9 +5,9 @@ Das Ziel dieser Architektur ist eine strikte Trennung zwischen technischer Infra
|
|||||||
|
|
||||||
### Grundregeln:
|
### Grundregeln:
|
||||||
* **Unidirektionaler Fluss:** `UI` $\rightarrow$ `Logic` $\rightarrow$ `Data`. Ein Layer darf niemals Informationen aus einem übergeordneten Layer importieren.
|
* **Unidirektionaler Fluss:** `UI` $\rightarrow$ `Logic` $\rightarrow$ `Data`. Ein Layer darf niemals Informationen aus einem übergeordneten Layer importieren.
|
||||||
* **Dependency Inversion:** Die `Logic`-Schicht definiert die Anforderungen (Interfaces). Die `Data`-Schicht implementiert diese. Die Logik ist somit unabhängig von der Datenbank-Technologie.
|
* **Dependency Inversion:** Die `Logic`-Schicht definiert die Anforderungen (Interfaces). Die `Data`-Schicht implementiert diese. Die Logik ist somit vollständig unabhängig von der Datenbank-Technologie oder externen APIs.
|
||||||
* **Model-Zentrierung:** Das Model ist das Herzstück der Applikation. Es ist ein anämisches POPO (Plain Old PHP Object) und wird primär im gesamten System verwendet.
|
* **Model-Zentrierung:** Das Model ist das Herzstück der Applikation. Es ist ein anämisches POPO (Plain Old PHP Object) und dient als primärer Datenträger zwischen den Schichten.
|
||||||
* **Atomarität:** Business-Operationen sind atomar. Entweder alles wird gespeichert oder nichts (Transaction Management).
|
* **Atomarität:** Business-Operationen sind atomar. Entweder alles wird gespeichert oder nichts (Transaction Management via `TransactionManagerInterface`).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -19,40 +19,40 @@ Die UI-Schicht ist die dünne Eintrittspforte. Sie besitzt keine Geschäftslogik
|
|||||||
* **Verantwortlichkeiten:**
|
* **Verantwortlichkeiten:**
|
||||||
* Mapping von Input auf **DTOs**.
|
* Mapping von Input auf **DTOs**.
|
||||||
* Syntaktische Validierung via Symfony Constraints am DTO.
|
* Syntaktische Validierung via Symfony Constraints am DTO.
|
||||||
* Grobe Autorisierungsprüfung (Rollen-basiert, z.B. `ROLE_USER`).
|
* Grobe Autorisierungsprüfung (Rollen-basiert).
|
||||||
* Aufruf des entsprechenden **UseCase**.
|
* Aufruf des entsprechenden **UseCase**.
|
||||||
|
|
||||||
### 🔵 Logic Layer (Business Core)
|
### 🔵 Logic Layer (Business Core)
|
||||||
Hier wird definiert, *was* das System tut. Die Logik ist in Orchestrierung und zustandslose Fachlogik unterteilt.
|
Hier wird definiert, *was* das System tut. Die Logik ist in Orchestrierung und zustandslose Fachlogik unterteilt.
|
||||||
|
|
||||||
#### A. Orchestrierung & State
|
#### A. Orchestrierung & State
|
||||||
* **UseCase:** Der Dirigent eines Business-Prozesses. Er koordiniert den Ablauf: `Authorization` $\rightarrow$ `Validation` $\rightarrow$ `Calculation` $\rightarrow$ `Save`. Er steuert die Transaktionsgrenzen via `TransactionManagerInterface`.
|
* **UseCase:** Der Dirigent eines Business-Prozesses. Er koordiniert: `Authorization` $\rightarrow$ `Validation` $\rightarrow$ `Calculation` $\rightarrow$ `Save`. Er steuert die Transaktionsgrenzen.
|
||||||
* **Manager:** Zuständig für das **State Management**. Er implementiert das *Cache-Aside Pattern* (prüft Cache vor dem Provider) und steuert die Cache-Invalidierung nach Schreibvorgängen.
|
* **Manager:** Zuständig für das **State Management**. Implementiert das *Cache-Aside Pattern* (Prüfung Cache vor Provider) und steuert die Invalidierung via Cache-Tags nach Schreibvorgängen.
|
||||||
|
|
||||||
#### B. Spezialisierte Logik (Stateless Services)
|
#### B. Spezialisierte Logik (Stateless Services)
|
||||||
Um "Fat Managers" zu vermeiden, wird Fachlogik in spezialisierte Klassen ausgelagert:
|
Zur Vermeidung von "Fat Managers" wird Fachlogik ausgelagert:
|
||||||
* **Calculators:** Pure Functions für Berechnungen $\rightarrow$ gibt Werte zurück.
|
* **Calculators:** Pure Functions für Berechnungen $\rightarrow$ gibt Werte zurück.
|
||||||
* **Policies:** Business-Regelprüfungen $\rightarrow$ gibt `boolean` zurück.
|
* **Policies:** Business-Regelprüfungen $\rightarrow$ gibt `boolean` zurück.
|
||||||
* **Validators:** Komplexe Zustandsprüfungen $\rightarrow$ wirft `ValidationException`.
|
* **Validators:** Komplexe semantische Zustandsprüfungen $\rightarrow$ wirft `DomainException`.
|
||||||
* **Strategies:** Kapselung austauschbarer Algorithmen.
|
* **Strategies:** Kapselung austauschbarer Algorithmen.
|
||||||
|
|
||||||
#### C. Domain Objekte
|
#### C. Domain Objekte
|
||||||
* **Models:** Die primären Datencontainer (z.B. `GardenPlan`). Anämisch und unabhängig von der DB.
|
* **Models:** Primäre Datencontainer (z.B. `GardenPlan`). Anämisch und unabhängig von der DB.
|
||||||
* **DTOs:** Transportobjekte zwischen UI und Logic.
|
* **DTOs:** Transportobjekte zwischen UI und Logic.
|
||||||
* **Domain Events:** Dünne Ereignisse (ID & Typ), die via `DomainEventCollector` gesammelt und nach dem Commit gefeuert werden.
|
* **Domain Events:** Dünne Ereignisse, die manuell im UseCase nach erfolgreichem Commit getriggert werden.
|
||||||
|
|
||||||
### 🔴 Data Layer (Infrastructure)
|
### 🔴 Data Layer (Infrastructure)
|
||||||
Die technische Realisierung. Alles hier ist austauschbar, solange die Interfaces der Logic-Schicht erfüllt werden.
|
Die technische Realisierung. Alles hier ist austauschbar, solange die Interfaces der Logic-Schicht erfüllt werden.
|
||||||
* **Provider / Processor:** Implementierungen der in der Logic definierten Interfaces. Provider = Lesen | Processor = Schreiben.
|
* **Provider / Processor:** Implementierungen der Interfaces (`Provider` = Lesen | `Processor` = Schreiben).
|
||||||
* **Entities:** Die technischen Repräsentationen für Doctrine (z.B. `GardenPlanEntity`).
|
* **Entities:** Technische Repräsentationen für Doctrine (z.B. `GardenPlanEntity`).
|
||||||
* **Mappers:** Explizite Klassen, die zwischen `Entity` $\leftrightarrow$ `Model` transformieren.
|
* **Mappers:** Explizite Klassen, die zwischen `Entity` $\leftrightarrow$ `Model` transformieren (KI-generiert/manuell).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 3. Technischer Workflow & Pipeline
|
## 3. Technischer Workflow & Pipeline
|
||||||
|
|
||||||
### Der Schreib-Pfad (Write Flow)
|
### Der Schreib-Pfad (Write Flow)
|
||||||
`UI (Controller/Command)` $\rightarrow$ `DTO` $\rightarrow$ `UseCase (Start Transaction)` $\rightarrow$ `Auth Check (Voter)` $\rightarrow$ `Validator` $\rightarrow$ `Policy` $\rightarrow$ `Calculator` $\rightarrow$ `Manager` $\rightarrow$ `Processor` $\rightarrow$ `Mapper` $\rightarrow$ `Entity` $\rightarrow$ `DB`.
|
`UI` $\rightarrow$ `DTO` $\rightarrow$ `UseCase (Start Transaction)` $\rightarrow$ `Auth Check` $\rightarrow$ `Validator` $\rightarrow$ `Policy` $\rightarrow$ `Calculator` $\rightarrow$ `Manager` $\rightarrow$ `Processor` $\rightarrow$ `Mapper` $\rightarrow$ `Entity` $\rightarrow$ `DB` $\rightarrow$ `Commit` $\rightarrow$ `Trigger Events`.
|
||||||
|
|
||||||
### Der Lese-Pfad (Read Flow)
|
### Der Lese-Pfad (Read Flow)
|
||||||
`UI` $\rightarrow$ `UseCase` $\rightarrow$ `Manager (Cache check)` $\rightarrow$ `Provider Interface` $\rightarrow$ `Implementation` $\rightarrow$ `Mapper` $\rightarrow$ `Model` $\rightarrow$ `UI`.
|
`UI` $\rightarrow$ `UseCase` $\rightarrow$ `Manager (Cache check)` $\rightarrow$ `Provider Interface` $\rightarrow$ `Implementation` $\rightarrow$ `Mapper` $\rightarrow$ `Model` $\rightarrow$ `UI`.
|
||||||
@@ -61,13 +61,23 @@ Die technische Realisierung. Alles hier ist austauschbar, solange die Interfaces
|
|||||||
|
|
||||||
## 4. Querschnittsfunktionen
|
## 4. Querschnittsfunktionen
|
||||||
|
|
||||||
### Transaktionssteuerung & Fehler
|
### Dependency Injection & Composition Root
|
||||||
* **Transaction Management:** Der UseCase nutzt ein `TransactionManagerInterface`, um sicherzustellen, dass alle Änderungen innerhalb eines Prozesses atomar erfolgen.
|
Um eine strikte Entkopplung zu gewährleisten, wird die `Logic`-Schicht niemals direkt auf konkrete Implementierungen referenzieren (kein hartcodiertes `#[Autowire(service: ...)]`).
|
||||||
* **Error Handling:** Eigene Domain-Exceptions in der Logic. Ein zentraler **Symfony ExceptionListener** mappt diese auf HTTP-Statuscodes (400, 403, 404, etc.).
|
* **Implicit Autowiring:** Bei einer einzigen Implementierung pro Interface nutzt Symfony das Standard-Autowiring.
|
||||||
|
* **Composition Root:** Die `services.yaml` fungiert als zentrale Konfigurationsinstanz, in der Interfaces auf konkrete Implementierungen gemappt werden (Alias/Bind), um die Infrastruktur von außen steuerbar zu machen.
|
||||||
|
|
||||||
### Asynchronität (Messenger)
|
### Fehlermanagement & Exception-Handling
|
||||||
* **Inbound:** `Message` $\rightarrow$ `Handler` $\rightarrow$ `DTO` $\rightarrow$ `UseCase`.
|
Ein zentrales System mappt fachliche Fehler auf HTTP-Standards:
|
||||||
* **Outbound:** Nachrichten werden erst **nach** dem erfolgreichen Commit der Transaktion verschickt (Post-Commit Trigger), um "Ghost Notifications" zu vermeiden.
|
* **Hierarchie:** Alle Fachfehler erben von einer Basis-`DomainException`.
|
||||||
|
* **Mapping:** Ein `Symfony ExceptionListener` fängt diese ab und mappt sie auf Statuscodes:
|
||||||
|
* `ResourceNotFoundException` $\rightarrow$ 404
|
||||||
|
* `AccessDeniedException` $\rightarrow$ 403
|
||||||
|
* `BusinessRuleViolationException` $\rightarrow$ 422/400
|
||||||
|
* **Format:** Die Antwort erfolgt im **RFC 7807 (Problem Details for HTTP APIs)** Format, um dem Frontend präzise, maschinenlesbare Fehler zu liefern.
|
||||||
|
|
||||||
|
### Transaktionssteuerung & Asynchronität
|
||||||
|
* **Transaction Management:** Abstrahiert über ein `TransactionManagerInterface`.
|
||||||
|
* **Messenger:** Outbound-Nachrichten werden erst nach erfolgreichem DB-Commit versendet, um inkonsistente Benachrichtigungen ("Ghost Notifications") zu vermeiden.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -114,4 +124,4 @@ src/
|
|||||||
│ └── Implementation/
|
│ └── Implementation/
|
||||||
│ ├── Provider/[Feature]/ # Realisierung der Interfaces
|
│ ├── Provider/[Feature]/ # Realisierung der Interfaces
|
||||||
│ └── Processor/[Feature]/ # Realisierung der Interfaces
|
│ └── Processor/[Feature]/ # Realisierung der Interfaces
|
||||||
```
|
```
|
||||||
+68
@@ -0,0 +1,68 @@
|
|||||||
|
# PHP & Symfony Engineering Guidelines
|
||||||
|
|
||||||
|
## 1. Core PHP Standard
|
||||||
|
Wir schreiben modernen, typensicheren und expliziten Code. Ziel ist es, Fehler zur Compile-Zeit bzw. via statischer Analyse zu finden.
|
||||||
|
|
||||||
|
* **Strict Typing:** Jede Datei beginnt zwingend mit `declare(strict_types=1);`.
|
||||||
|
* **Typisierung:**
|
||||||
|
* Konsequentes Type Hinting für alle Parameter und Rückgabewerte (inkl. `void`).
|
||||||
|
* Nutzung von Union- und Intersection Types.
|
||||||
|
* `mixed` ist verboten, außer in absolut unvermeidbaren Generics.
|
||||||
|
* **Modern Features (PHP 8.2+):**
|
||||||
|
* **Readonly:** Standard für DTOs, Value Objects und Services (`readonly class`).
|
||||||
|
* **Constructor Promotion:** Nutzung zur Reduktion von Boilerplate.
|
||||||
|
* **Enums:** Einsatz anstelle von Konstanten oder Strings für feste Zustände.
|
||||||
|
* **Match Expression:** Bevorzugt gegenüber `switch`.
|
||||||
|
* **Anti-Magic:** Magic Methods (`__call`, `__get` etc.) und dynamische Properties sind **strikt verboten**.
|
||||||
|
|
||||||
|
## 2. Control Flow & Readability
|
||||||
|
Lesbarkeit schlägt akademische Perfektion. Code muss für andere Senioren ohne "Rätselraten" sofort verstehbar sein.
|
||||||
|
|
||||||
|
* **Control Flow:** Nutzung von **Early Returns** und **Guard Clauses**. Verschachtelte `if-else`-Strukturen sind zu vermeiden, um die kognitive Last gering zu halten.
|
||||||
|
* **Self-Explaining Code:** Namen müssen den Intent beschreiben (`calculateTotalTax()` statt `calc()`). Booleans beginnen mit `is`, `has` oder `can`.
|
||||||
|
* **Kommentare:** Kommentare werden nur eingesetzt, wenn:
|
||||||
|
1. Der Code aufgrund einer Sonderlösung **nicht selbsterklärend** ist (Dokumentation des "Warum").
|
||||||
|
2. **Arrays/Collections typisiert** werden müssen (`/** @var UserDTO[] $users */`), sofern PHPStan dies nicht anders erfassen kann.
|
||||||
|
|
||||||
|
## 3. Data Structures & Collections
|
||||||
|
Wir vermeiden "Mystery Meat"-Datenstrukturen zugunsten von Typsicherheit.
|
||||||
|
|
||||||
|
* **No Associative Arrays:** Die Nutzung von assoziativen Arrays als Pseudo-Objekte/Datencontainer ist verboten. Es werden immer **dedizierte Objekte (DTOs)** verwendet.
|
||||||
|
* **Collections:**
|
||||||
|
* **Transport:** Für einfache Listen, die nur durchiteriert werden, reichen typisierte Arrays (`array<T>`).
|
||||||
|
* **Domain-Logik:** Sobald eine Liste fachliche Logik besitzt (z.B. Filterung, Aggregation), wird eine dedizierte **Collection-Klasse** erstellt (implementiert `IteratorAggregate` und `Countable`).
|
||||||
|
|
||||||
|
## 4. Infrastructure & Symfony Implementation
|
||||||
|
Symfony ist der Adapter zur Außenwelt, nicht das Zentrum der Applikation.
|
||||||
|
|
||||||
|
* **Dependency Injection:** Ausschließlich **Constructor Injection**. Kein Service-Locator (`$this->container->get()`). DI erfolgt primär gegen Interfaces (Ports).
|
||||||
|
* **Controller:** "Thin Controller". Aufgaben sind: Request-Daten validieren/extrahieren $\rightarrow$ Application-Service aufrufen $\rightarrow$ Response transformieren. Keine Business-Logik im Controller.
|
||||||
|
* **Third-Party SDKs:** Alle externen Services/SDKs werden konsequent hinter einem **eigenen Interface (Port)** in der Infrastructure-Schicht gekapselt. Vendor-Klassen dürfen niemals den Application- oder Domain-Layer erreichen.
|
||||||
|
|
||||||
|
## 5. Persistence & Entity Design
|
||||||
|
Wir minimieren die Koppelung an das ORM, um Performance und Testbarkeit zu sichern.
|
||||||
|
|
||||||
|
* **Simple Entities:** Entitäten werden flach gehalten.
|
||||||
|
* **Avoid Joins:** Gejointe Entitäten (komplexe `@ManyToOne` / `@OneToMany`) sind grundsätzlich zu vermeiden. Referenzen erfolgen über **IDs**. Die Zusammenführung erfolgt explizit in den Repositories/Services.
|
||||||
|
* **Mapping:** Transformationen zwischen Infrastructure-Entities und Domain-Modellen erfolgen ausschließlich über dedizierte **Mapper-Klassen in der Dataschicht**.
|
||||||
|
|
||||||
|
## 6. Validation Pipeline (Shift-Left)
|
||||||
|
Fehler werden so früh wie möglich abgefangen:
|
||||||
|
`Request` $\rightarrow$ `DTO Asserts/Validation` $\rightarrow$ `Application Logic` $\rightarrow$ `Domain Complex Tests`.
|
||||||
|
|
||||||
|
## 7. Quality Gates & Workflow (Definition of Done)
|
||||||
|
Bevor Code in ein Review geht, müssen folgende lokale Checks erfolgreich durchlaufen sein:
|
||||||
|
|
||||||
|
1. **Syntax Check:** `php -l`
|
||||||
|
2. **Statische Analyse:** `phpstan analyse` (Ziel Level 8/9). *Pragmatismus-Regel:* Wenn Typisierung die Lesbarkeit massiv zerstört $\rightarrow$ Review-Dialog statt "Force-Typing".
|
||||||
|
3. **Automatisierte Tests:** `phpunit` (Unit, Integration, Functional).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Die Golden Rules im Überblick:
|
||||||
|
1. **Kein Framework / Keine Vendor-Klassen im Domain.**
|
||||||
|
2. **Flache Entitäten / Referenzen über IDs.**
|
||||||
|
3. **Immutability by Default (`readonly`).**
|
||||||
|
4. **Keine assoziativen Arrays $\rightarrow$ Dedizierte Objekte.**
|
||||||
|
5. **Early Returns $\rightarrow$ Flacher Code.**
|
||||||
|
6. **Local Check: `php -l` $\rightarrow$ `phpstan` $\rightarrow$ `phpunit`.**
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
-- ==========================================
|
||||||
|
-- GardenPlan – PostgreSQL Init Script
|
||||||
|
-- Erstellt Keycloak-Datenbank & -Benutzer
|
||||||
|
-- ==========================================
|
||||||
|
|
||||||
|
CREATE DATABASE keycloak_db;
|
||||||
|
CREATE USER keycloak WITH PASSWORD 'keycloak_secret';
|
||||||
|
GRANT ALL PRIVILEGES ON DATABASE keycloak_db TO keycloak;
|
||||||
|
ALTER USER keycloak CREATEDB;
|
||||||
@@ -0,0 +1,377 @@
|
|||||||
|
{
|
||||||
|
"id": "gardenplan-realm",
|
||||||
|
"realm": "gardenplan",
|
||||||
|
"displayName": "GardenPlan",
|
||||||
|
"displayNameHtml": "<div class=\"kc-logo-text\"><span>GardenPlan</span></div>",
|
||||||
|
"notBefore": 0,
|
||||||
|
"defaultSignatureAlgorithm": "RS256",
|
||||||
|
"revokeRefreshToken": false,
|
||||||
|
"refreshTokenMaxReuse": 0,
|
||||||
|
"accessTokenLifespan": 3600,
|
||||||
|
"accessTokenLifespanForImplicitFlow": 900,
|
||||||
|
"ssoSessionIdleTimeout": 3600,
|
||||||
|
"ssoSessionMaxLifespan": 86400,
|
||||||
|
"ssoSessionIdleTimeoutRememberMe": 0,
|
||||||
|
"ssoSessionMaxLifespanRememberMe": 0,
|
||||||
|
"offlineSessionIdleTimeout": 2592000,
|
||||||
|
"offlineSessionMaxLifespanEnabled": false,
|
||||||
|
"offlineSessionMaxLifespan": 5184000,
|
||||||
|
"clientSessionIdleTimeout": 0,
|
||||||
|
"clientSessionMaxLifespan": 0,
|
||||||
|
"clientOfflineSessionIdleTimeout": 0,
|
||||||
|
"clientOfflineSessionMaxLifespan": 0,
|
||||||
|
"accessCodeLifespan": 60,
|
||||||
|
"accessCodeLifespanUserAction": 300,
|
||||||
|
"accessCodeLifespanLogin": 1800,
|
||||||
|
"actionTokenGeneratedByAdminLifespan": 43200,
|
||||||
|
"actionTokenGeneratedByUserLifespan": 3600,
|
||||||
|
"oauth2DeviceCodeLifespan": 600,
|
||||||
|
"oauth2DevicePollingInterval": 5,
|
||||||
|
"enabled": true,
|
||||||
|
"sslRequired": "external",
|
||||||
|
"registrationAllowed": true,
|
||||||
|
"registrationEmailAsUsername": false,
|
||||||
|
"rememberMe": false,
|
||||||
|
"verifyEmail": false,
|
||||||
|
"loginWithEmailAllowed": true,
|
||||||
|
"duplicateEmailsAllowed": false,
|
||||||
|
"resetPasswordAllowed": false,
|
||||||
|
"editUsernameAllowed": false,
|
||||||
|
"bruteForceProtected": false,
|
||||||
|
"permanentLockout": false,
|
||||||
|
"maxTemporaryLockouts": 0,
|
||||||
|
"maxFailureWaitSeconds": 900,
|
||||||
|
"minimumQuickLoginWaitSeconds": 60,
|
||||||
|
"waitIncrementSeconds": 60,
|
||||||
|
"quickLoginCheckMilliSlots": 144,
|
||||||
|
"quickLoginCheckMilliDetails": 200,
|
||||||
|
"bruteForceStrategy": "MULTI_USER",
|
||||||
|
"resetPasswordTimeout": 0,
|
||||||
|
"defaultRole": {
|
||||||
|
"id": "gardenplan-default-role",
|
||||||
|
"name": "default-roles-gardenplan",
|
||||||
|
"description": "${role_default-roles}",
|
||||||
|
"type": "DEFAULT",
|
||||||
|
"scope": "GLOBAL"
|
||||||
|
},
|
||||||
|
"requiredCredentials": [
|
||||||
|
"password"
|
||||||
|
],
|
||||||
|
"otpPolicyType": "totp",
|
||||||
|
"otpPolicyAlgorithm": "HmacSHA1",
|
||||||
|
"otpPolicyInitialCounter": 0,
|
||||||
|
"otpPolicyDigits": 6,
|
||||||
|
"otpPolicyLookAheadWindow": 1,
|
||||||
|
"otpPolicyPeriod": 30,
|
||||||
|
"otpSupportedApplications": [
|
||||||
|
"FreeOTP",
|
||||||
|
"Google Authenticator"
|
||||||
|
],
|
||||||
|
"localizationTexts": {},
|
||||||
|
"webAuthnChallengeUsage": "per-session",
|
||||||
|
"identityProviderAliases": {},
|
||||||
|
"users": [
|
||||||
|
{
|
||||||
|
"id": "test-user-001",
|
||||||
|
"username": "testuser",
|
||||||
|
"enabled": true,
|
||||||
|
"emailVerified": true,
|
||||||
|
"email": "testuser@gardenplan.local",
|
||||||
|
"credentials": [
|
||||||
|
{
|
||||||
|
"type": "password",
|
||||||
|
"value": "testpassword123!",
|
||||||
|
"temporary": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"roles": {
|
||||||
|
"client": {
|
||||||
|
"symfony-app": [
|
||||||
|
"uma_authorization"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"realm": [
|
||||||
|
"default-roles-gardenplan",
|
||||||
|
"offline_access",
|
||||||
|
"uma_authorization"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"clients": [
|
||||||
|
{
|
||||||
|
"id": "symfony-app-client",
|
||||||
|
"clientId": "symfony-app",
|
||||||
|
"name": "Symfony GardenPlan API",
|
||||||
|
"description": "Backend REST-API Client für GardenPlan",
|
||||||
|
"enabled": true,
|
||||||
|
"clientAuthenticatorType": "client-secret",
|
||||||
|
"secret": "changeme_client_secret",
|
||||||
|
"baseUrl": "http://localhost/",
|
||||||
|
"redirectUris": [
|
||||||
|
"http://localhost/*",
|
||||||
|
"http://127.0.0.1/*"
|
||||||
|
],
|
||||||
|
"webOrigins": [
|
||||||
|
"+"
|
||||||
|
],
|
||||||
|
"grantTypes": [
|
||||||
|
"authorization_code",
|
||||||
|
"refresh_token",
|
||||||
|
"client_credentials",
|
||||||
|
"implicit"
|
||||||
|
],
|
||||||
|
"standardFlowEnabled": true,
|
||||||
|
"implicitFlowEnabled": true,
|
||||||
|
"directAccessGrantsEnabled": true,
|
||||||
|
"serviceAccountsEnabled": true,
|
||||||
|
"publicClient": false,
|
||||||
|
"frontchannelLogout": true,
|
||||||
|
"protocol": "openid-connect",
|
||||||
|
"attributes": {
|
||||||
|
"client.secret.creation.time": "1700000000",
|
||||||
|
"oauth2.device.authorization.grant.enabled": "false",
|
||||||
|
"backchannel.logout.revoke.offline.tokens": "false",
|
||||||
|
"backchannel.logout.session.required": "true",
|
||||||
|
"client_credentials.use_refresh_token": "false",
|
||||||
|
"login.jansendata": "{}"
|
||||||
|
},
|
||||||
|
"authenticationFlowBindingOverrides": {},
|
||||||
|
"fullScopeAllowed": true,
|
||||||
|
"nodeReRegistrationTimeout": -1,
|
||||||
|
"defaultClientScopes": [
|
||||||
|
"web-origins",
|
||||||
|
"roles",
|
||||||
|
"profile",
|
||||||
|
"email"
|
||||||
|
],
|
||||||
|
"optionalClientScopes": [
|
||||||
|
"address",
|
||||||
|
"phone",
|
||||||
|
"offline_access",
|
||||||
|
"microprofile-jwt"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "frontend-client",
|
||||||
|
"clientId": "gardenplan-frontend",
|
||||||
|
"name": "GardenPlan Frontend (React/Vue)",
|
||||||
|
"description": "Frontend SPA Client für GardenPlan",
|
||||||
|
"enabled": true,
|
||||||
|
"publicClient": true,
|
||||||
|
"baseUrl": "http://localhost:3000/",
|
||||||
|
"redirectUris": [
|
||||||
|
"http://localhost:3000/*",
|
||||||
|
"http://127.0.0.1:3000/*"
|
||||||
|
],
|
||||||
|
"webOrigins": [
|
||||||
|
"http://localhost:3000",
|
||||||
|
"http://127.0.0.1:3000"
|
||||||
|
],
|
||||||
|
"grantTypes": [
|
||||||
|
"authorization_code",
|
||||||
|
"implicit"
|
||||||
|
],
|
||||||
|
"standardFlowEnabled": true,
|
||||||
|
"implicitFlowEnabled": false,
|
||||||
|
"directAccessGrantsEnabled": false,
|
||||||
|
"serviceAccountsEnabled": false,
|
||||||
|
"frontchannelLogout": true,
|
||||||
|
"protocol": "openid-connect",
|
||||||
|
"attributes": {
|
||||||
|
"oidc.ciba.grant.enabled": "false",
|
||||||
|
"oauth2.device.authorization.grant.enabled": "false",
|
||||||
|
"backchannel.logout.revoke.offline.tokens": "false",
|
||||||
|
"backchannel.logout.session.required": "true"
|
||||||
|
},
|
||||||
|
"authenticationFlowBindingOverrides": {},
|
||||||
|
"fullScopeAllowed": true,
|
||||||
|
"defaultClientScopes": [
|
||||||
|
"web-origins",
|
||||||
|
"roles",
|
||||||
|
"profile",
|
||||||
|
"email"
|
||||||
|
],
|
||||||
|
"optionalClientScopes": [
|
||||||
|
"address",
|
||||||
|
"phone",
|
||||||
|
"offline_access",
|
||||||
|
"microprofile-jwt"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"clientScopes": [
|
||||||
|
{
|
||||||
|
"id": "scope-email",
|
||||||
|
"name": "email",
|
||||||
|
"description": "OpenID Connect built-in scope: email",
|
||||||
|
"protocol": "openid-connect",
|
||||||
|
"attributes": {
|
||||||
|
"include.in.token.scope": "true",
|
||||||
|
"consent.screen.text": "${emailScopeConsentText}",
|
||||||
|
"display.on.consent.screen": "true"
|
||||||
|
},
|
||||||
|
"icons": {},
|
||||||
|
"protocolMappers": [
|
||||||
|
{
|
||||||
|
"id": "mapper-email",
|
||||||
|
"name": "email",
|
||||||
|
"protocol": "openid-connect",
|
||||||
|
"protocolMapper": "oidc-usermodel-property-mapper",
|
||||||
|
"consentRequired": false,
|
||||||
|
"config": {
|
||||||
|
"userinfo.token.claim": "true",
|
||||||
|
"user.attribute": "email",
|
||||||
|
"id.token.claim": "true",
|
||||||
|
"access.token.claim": "true",
|
||||||
|
"claim.name": "email",
|
||||||
|
"jsonType.label": "String",
|
||||||
|
"multivalued": "false"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "mapper-email-verified",
|
||||||
|
"name": "email verified",
|
||||||
|
"protocol": "openid-connect",
|
||||||
|
"protocolMapper": "oidc-usermodel-property-mapper",
|
||||||
|
"consentRequired": false,
|
||||||
|
"config": {
|
||||||
|
"userinfo.token.claim": "true",
|
||||||
|
"user.attribute": "emailVerified",
|
||||||
|
"id.token.claim": "true",
|
||||||
|
"access.token.claim": "true",
|
||||||
|
"claim.name": "email_verified",
|
||||||
|
"jsonType.label": "boolean",
|
||||||
|
"multivalued": "false"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "scope-profile",
|
||||||
|
"name": "profile",
|
||||||
|
"description": "OpenID Connect built-in scope: profile",
|
||||||
|
"protocol": "openid-connect",
|
||||||
|
"attributes": {
|
||||||
|
"include.in.token.scope": "true",
|
||||||
|
"consent.screen.text": "${profileScopeConsentText}",
|
||||||
|
"display.on.consent.screen": "true"
|
||||||
|
},
|
||||||
|
"icons": {},
|
||||||
|
"protocolMappers": [
|
||||||
|
{
|
||||||
|
"id": "mapper-profile-name",
|
||||||
|
"name": "full name",
|
||||||
|
"protocol": "openid-connect",
|
||||||
|
"protocolMapper": "oidc-full-name-mapper",
|
||||||
|
"consentRequired": false,
|
||||||
|
"config": {
|
||||||
|
"id.token.claim": "true",
|
||||||
|
"access.token.claim": "true",
|
||||||
|
"userinfo.token.claim": "true"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "mapper-username",
|
||||||
|
"name": "username",
|
||||||
|
"protocol": "openid-connect",
|
||||||
|
"protocolMapper": "oidc-usermodel-attribute-mapper",
|
||||||
|
"consentRequired": false,
|
||||||
|
"config": {
|
||||||
|
"user.attribute": "username",
|
||||||
|
"id.token.claim": "true",
|
||||||
|
"access.token.claim": "true",
|
||||||
|
"claim.name": "preferred_username",
|
||||||
|
"jsonType.label": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "scope-roles",
|
||||||
|
"name": "roles",
|
||||||
|
"description": "OpenID Connect scope for role mapping",
|
||||||
|
"protocol": "openid-connect",
|
||||||
|
"attributes": {
|
||||||
|
"include.in.token.scope": "false",
|
||||||
|
"display.on.consent.screen": "false"
|
||||||
|
},
|
||||||
|
"icons": {},
|
||||||
|
"protocolMappers": [
|
||||||
|
{
|
||||||
|
"id": "mapper-client-roles",
|
||||||
|
"name": "client roles",
|
||||||
|
"protocol": "openid-connect",
|
||||||
|
"protocolMapper": "oidc-usermodel-client-role-mapper",
|
||||||
|
"consentRequired": false,
|
||||||
|
"config": {
|
||||||
|
"multivalued": "true",
|
||||||
|
"user.attribute": "foo",
|
||||||
|
"access.token.claim": "true",
|
||||||
|
"claim.name": "resource_access.${client_id}.roles",
|
||||||
|
"jsonType.label": "String"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "mapper-realm-roles",
|
||||||
|
"name": "realm roles",
|
||||||
|
"protocol": "openid-connect",
|
||||||
|
"protocolMapper": "oidc-usermodel-realm-role-mapper",
|
||||||
|
"consentRequired": false,
|
||||||
|
"config": {
|
||||||
|
"multivalued": "true",
|
||||||
|
"user.attribute": "foo",
|
||||||
|
"access.token.claim": "true",
|
||||||
|
"claim.name": "realm_access.roles",
|
||||||
|
"jsonType.label": "String"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "scope-web-origins",
|
||||||
|
"name": "web-origins",
|
||||||
|
"description": "OpenID Connect scope for web origins",
|
||||||
|
"protocol": "openid-connect",
|
||||||
|
"attributes": {
|
||||||
|
"include.in.token.scope": "false",
|
||||||
|
"consent.screen.text": "",
|
||||||
|
"display.on.consent.screen": "false"
|
||||||
|
},
|
||||||
|
"icons": {},
|
||||||
|
"protocolMappers": [
|
||||||
|
{
|
||||||
|
"id": "mapper-web-origins",
|
||||||
|
"name": "allowed web origins",
|
||||||
|
"protocol": "openid-connect",
|
||||||
|
"protocolMapper": "oidc-allowed-origins-mapper",
|
||||||
|
"consentRequired": false,
|
||||||
|
"config": {}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "scope-offline-access",
|
||||||
|
"name": "offline_access",
|
||||||
|
"description": "OpenID Connect built-in scope: offline_access",
|
||||||
|
"protocol": "openid-connect",
|
||||||
|
"attributes": {
|
||||||
|
"consent.screen.text": "${offlineAccessScopeConsentText}",
|
||||||
|
"display.on.consent.screen": "true"
|
||||||
|
},
|
||||||
|
"icons": {}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"defaultDefaultClientScopes": [
|
||||||
|
"role_list",
|
||||||
|
"profile",
|
||||||
|
"email",
|
||||||
|
"roles",
|
||||||
|
"web-origins"
|
||||||
|
],
|
||||||
|
"defaultOptionalClientScopes": [
|
||||||
|
"offline_access",
|
||||||
|
"address",
|
||||||
|
"phone"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
# ==========================================
|
||||||
|
# GardenPlan – Nginx Konfiguration
|
||||||
|
# Symfony Reverse Proxy + API Endpoint
|
||||||
|
# ==========================================
|
||||||
|
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name _;
|
||||||
|
root /var/www/html/public;
|
||||||
|
|
||||||
|
# Performance-Settings
|
||||||
|
client_max_body_size 50M;
|
||||||
|
sendfile on;
|
||||||
|
tcp_nopush on;
|
||||||
|
tcp_nodelay on;
|
||||||
|
|
||||||
|
# Security Headers
|
||||||
|
add_header X-Frame-Options "SAMEORIGIN" always;
|
||||||
|
add_header X-Content-Type-Options "nosniff" always;
|
||||||
|
add_header X-XSS-Protection "1; mode=block" always;
|
||||||
|
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
|
||||||
|
|
||||||
|
# Default – Symfony Backend (REST API)
|
||||||
|
location / {
|
||||||
|
try_files $uri /index.php$is_args$args;
|
||||||
|
}
|
||||||
|
|
||||||
|
# PHP-FPM Verbindung
|
||||||
|
location ~ ^/index\.php(/|$) {
|
||||||
|
fastcgi_pass php-fpm:9000;
|
||||||
|
fastcgi_split_path_info ^(.+\.php)(/.*)$;
|
||||||
|
include fastcgi_params;
|
||||||
|
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
|
||||||
|
fastcgi_param HTTPS off;
|
||||||
|
|
||||||
|
# PHP-FPM Timeout-Settings
|
||||||
|
fastcgi_connect_timeout 600;
|
||||||
|
fastcgi_send_timeout 600;
|
||||||
|
fastcgi_read_timeout 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Verzeichnis-Zugriffe blockieren
|
||||||
|
location ~ /^.ht {
|
||||||
|
deny all;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Assets Cache-Control
|
||||||
|
location ~* \.(jpg|jpeg|gif|png|webp|svg|css|js|woff2?|ico)$ {
|
||||||
|
expires 30d;
|
||||||
|
add_header Cache-Control "public, immutable";
|
||||||
|
}
|
||||||
|
|
||||||
|
# Error Pages (optional)
|
||||||
|
error_page 404 /error.html;
|
||||||
|
error_page 500 502 503 504 /error.html;
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
# ==========================================
|
||||||
|
# GardenPlan – PHP-FPM Dockerfile
|
||||||
|
# Symfony 7 / PHP 8.2 + erforderliche Extensions
|
||||||
|
# ==========================================
|
||||||
|
|
||||||
|
FROM php:8.4-fpm-bookworm AS base
|
||||||
|
|
||||||
|
# System-Abhängigkeiten installieren
|
||||||
|
RUN apt-get update && apt-get install -y \
|
||||||
|
git \
|
||||||
|
curl \
|
||||||
|
libpng-dev \
|
||||||
|
libonig-dev \
|
||||||
|
libxml2-dev \
|
||||||
|
libpq-dev \
|
||||||
|
libzip-dev \
|
||||||
|
zip \
|
||||||
|
unzip \
|
||||||
|
libfreetype6-dev \
|
||||||
|
libjpeg62-turbo-dev \
|
||||||
|
libwebp-dev \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# PHP Extensions kompilieren & installieren
|
||||||
|
RUN docker-php-ext-configure gd --with-freetype --with-jpeg --with-webp \
|
||||||
|
&& docker-php-ext-install -j$(nproc) gd \
|
||||||
|
&& docker-php-ext-install pdo pdo_pgsql xml zip mbstring opcache
|
||||||
|
|
||||||
|
# Composer installieren
|
||||||
|
COPY --from=mlocati/php-extension-installer /usr/bin/install-php-extensions /usr/local/bin/
|
||||||
|
RUN install-php-extensions intl apcu
|
||||||
|
|
||||||
|
# Node.js & npm für Frontend-Build (optional in Backend)
|
||||||
|
RUN curl -fsSL https://deb.nodesource.com/setup_20.x | bash - \
|
||||||
|
&& apt-get install -y nodejs \
|
||||||
|
&& npm install -g pnpm
|
||||||
|
|
||||||
|
WORKDIR /var/www/html
|
||||||
|
|
||||||
|
# Symfony CLI installieren (optional, nützlich für Local-Tunnel etc.)
|
||||||
|
RUN curl -sS https://get.symfony.com/cli/installer | bash \
|
||||||
|
&& mv /root/.symfony5/bin/symfony /usr/local/bin/symfony
|
||||||
|
|
||||||
|
EXPOSE 9000
|
||||||
|
|
||||||
|
CMD ["php-fpm"]
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
# 🌿 GardenPlan – Gartenplanung MVP
|
||||||
|
|
||||||
|
> Symfony-Basises für die Multi-Garten-Planung mit Pflanzen, Aussaat, Ernte & Aufgaben.
|
||||||
|
|
||||||
|
## 🏗️ Architektur
|
||||||
|
|
||||||
|
Strikte Schichtenarchitektur (Clean / Hexagonal):
|
||||||
|
**UI** → **Logic** → **Data**
|
||||||
|
|
||||||
|
Details: [Architektur-Dokumentation](Doku/architektur.md)
|
||||||
|
|
||||||
|
## 📋 Anforderungsprofil
|
||||||
|
|
||||||
|
[Hier ansehen](Doku/anforderungsprofil.md)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 Schnellstart
|
||||||
|
|
||||||
|
### 1. DockerCompose-Setup starten
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
Startet folgende Services:
|
||||||
|
| Service | Port | Beschreibung |
|
||||||
|
|---------|------|--------------
|
||||||
|
| **nginx** | `80` / `443` | Webserver für Symfony REST-API
|
||||||
|
| **php-fpm** | (intern) | PHP 8.3 + Symfony Runtime
|
||||||
|
| **PostgreSQL** | `5432` | Datenbank (`gardenplan` + `keycloak_db`)
|
||||||
|
| **Keycloak** | `8080` | OIDC/JWT Identity Provider
|
||||||
|
|
||||||
|
### 🔑 Zugangsdaten (Dev)
|
||||||
|
|
||||||
|
| System | Benutzer | Passwort | URL |
|
||||||
|
|--------|----------|----------|-----|
|
||||||
|
| **Database** | `symfony` | `changeme` | `postgresql://database:5432/gardenplan` |
|
||||||
|
| **Keycloak Admin** | `admin` | `adminsecret` | http://localhost:8080 |
|
||||||
|
| **Test-Nutzer** | `testuser` | `testpassword123!` | (in Keycloak Realm `gardenplan`)
|
||||||
|
|
||||||
|
### 📦 Nach dem ersten Start
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# In den PHP-Container wechseln
|
||||||
|
docker compose exec php-fpm bash
|
||||||
|
|
||||||
|
# Symfony Projekt setup
|
||||||
|
cd /var/www/html
|
||||||
|
composer install
|
||||||
|
|
||||||
|
# Datenbankmigrationen ausführen
|
||||||
|
php bin/console doctrine:database:create
|
||||||
|
php bin/console doctrine:migrations:migrate --no-interaction
|
||||||
|
|
||||||
|
# JWT-Schlüssel für Lexik Bundle generieren
|
||||||
|
php bin/console lexik:jwt:generate-keypair
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📐 Ordnerstruktur
|
||||||
|
|
||||||
|
```
|
||||||
|
src/
|
||||||
|
├── UI/ # Controller, Commands, MessageHandlers
|
||||||
|
├── Logic/ # UseCases, Manager, Domain Services
|
||||||
|
│ ├── Domain/ # Models, DTOs, Validators, Policies
|
||||||
|
│ └── InfrastructureInterface/ # Provider / Processor Interfaces
|
||||||
|
└── Data/ # Doctrine Entities, Mappers, Implementations
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📖 Aufgabenplan
|
||||||
|
|
||||||
|
| Task | Beschreibung | Status |
|
||||||
|
|------|-------------|--------|
|
||||||
|
| [01 – Infrastruktur](Doku/Tasks/01-Infra.md) | Docker, DB, Keycloak Setup | ✅ In Arbeit |
|
||||||
|
| 02 – Datenbank-Schema | Entities + Migrationen | ⏳ Offen |
|
||||||
|
| 03 – Planning-Domain | GardenPlan Model, UseCases | ⏳ Offen |
|
||||||
|
| 04 – REST API | Controller + DTOs | ⏳ Offen |
|
||||||
|
| 05 – Auth & RBAC Keycloak Integration | ⏳ Offen |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔧 Entwicklung
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Logs einsehen
|
||||||
|
docker compose logs -f php-fpm
|
||||||
|
docker compose logs -f nginx
|
||||||
|
|
||||||
|
# Container neu starten
|
||||||
|
docker compose restart php-fpm
|
||||||
|
|
||||||
|
# Alles stoppen
|
||||||
|
docker compose down
|
||||||
|
```
|
||||||
|
|
||||||
|
## .env-Variabeln kopieren & anpassen. Siehe `.env`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**🌱 Happy Gardening!**
|
||||||
|
|||||||
@@ -0,0 +1,121 @@
|
|||||||
|
# ==========================================
|
||||||
|
# GardenPlan – Docker Compose (Development)
|
||||||
|
# ==========================================
|
||||||
|
|
||||||
|
services:
|
||||||
|
# --------------------------------------------------
|
||||||
|
# PHP-FPM – Symfony Backend
|
||||||
|
# --------------------------------------------------
|
||||||
|
php-fpm:
|
||||||
|
build:
|
||||||
|
context: ./Infra/php-fpm
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
container_name: gardenplan-php-fpm
|
||||||
|
restart: unless-stopped
|
||||||
|
working_dir: /var/www/html
|
||||||
|
volumes:
|
||||||
|
- ./Backend:/var/www/html:z
|
||||||
|
- phpsocket:/var/run/php
|
||||||
|
networks:
|
||||||
|
- gardenplan-network
|
||||||
|
depends_on:
|
||||||
|
database:
|
||||||
|
condition: service_healthy
|
||||||
|
|
||||||
|
# --------------------------------------------------
|
||||||
|
# Nginx – Webserver & Reverse Proxy
|
||||||
|
# --------------------------------------------------
|
||||||
|
nginx:
|
||||||
|
image: nginx:1.25-alpine
|
||||||
|
container_name: gardenplan-nginx
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- "80:80"
|
||||||
|
- "443:443"
|
||||||
|
volumes:
|
||||||
|
- ./Infra/nginx/default.conf:/etc/nginx/conf.d/default.conf:z
|
||||||
|
- ./Backend/public:/var/www/html/public:ro,z
|
||||||
|
- nginx_cache:/var/cache/nginx
|
||||||
|
networks:
|
||||||
|
- gardenplan-network
|
||||||
|
depends_on:
|
||||||
|
- php-fpm
|
||||||
|
|
||||||
|
# --------------------------------------------------
|
||||||
|
# PostgreSQL – Datenbank
|
||||||
|
# --------------------------------------------------
|
||||||
|
database:
|
||||||
|
image: postgres:16-alpine
|
||||||
|
container_name: gardenplan-database
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- "5432:5432"
|
||||||
|
environment:
|
||||||
|
POSTGRES_DB: ${POSTGRES_DB:-gardenplan}
|
||||||
|
POSTGRES_USER: ${POSTGRES_USER:-symfony}
|
||||||
|
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-changeme}
|
||||||
|
PGDATA: /var/lib/postgresql/data/pgdata
|
||||||
|
volumes:
|
||||||
|
- postgres_data:/var/lib/postgresql/data
|
||||||
|
- ./Infra/database/init:/docker-entrypoint-initdb.d:z
|
||||||
|
networks:
|
||||||
|
- gardenplan-network
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-symfony} -d ${POSTGRES_DB:-gardenplan}"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
start_period: 30s
|
||||||
|
|
||||||
|
# --------------------------------------------------
|
||||||
|
# Keycloak – Identity Provider (OIDC / JWT)
|
||||||
|
# --------------------------------------------------
|
||||||
|
keycloak:
|
||||||
|
image: quay.io/keycloak/keycloak:24.0
|
||||||
|
container_name: gardenplan-keycloak
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- "8080:8080"
|
||||||
|
environment:
|
||||||
|
KEYCLOAK_ADMIN: ${KEYCLOAK_ADMIN:-admin}
|
||||||
|
KEYCLOAK_ADMIN_PASSWORD: ${KEYCLOAK_ADMIN_PASSWORD:-adminsecret}
|
||||||
|
KC_DATABASE: postgres
|
||||||
|
KC_DATABASE_URL: jdbc:postgresql://database:5432/keycloak_db
|
||||||
|
KC_DATABASE_USERNAME: keycloak
|
||||||
|
KC_DATABASE_PASSWORD: keycloak_secret
|
||||||
|
KC_HTTP_RELATIVE_PATH: /auth
|
||||||
|
KC_HOSTNAME: localhost
|
||||||
|
KC_HOSTNAME_STRICT: "false"
|
||||||
|
KC_FEATURES: scripts
|
||||||
|
command: start-dev
|
||||||
|
volumes:
|
||||||
|
- ./Infra/keycloak/realm-export.json:/opt/keycloak/data/import/realm-export.json:z
|
||||||
|
networks:
|
||||||
|
- gardenplan-network
|
||||||
|
depends_on:
|
||||||
|
database:
|
||||||
|
condition: service_healthy
|
||||||
|
|
||||||
|
# --------------------------------------------------
|
||||||
|
# Keycloak Database (getrennt, falls gewünscht)
|
||||||
|
# Hier verwenden wir die Haupt-DB mit eigenem Schema
|
||||||
|
# Alternative: extra DB-Container – hier vereinfacht
|
||||||
|
# --------------------------------------------------
|
||||||
|
|
||||||
|
# ==========================================
|
||||||
|
# Volumes
|
||||||
|
# ==========================================
|
||||||
|
volumes:
|
||||||
|
postgres_data:
|
||||||
|
driver: local
|
||||||
|
phpsocket:
|
||||||
|
driver: local
|
||||||
|
nginx_cache:
|
||||||
|
driver: local
|
||||||
|
|
||||||
|
# ==========================================
|
||||||
|
# Networks
|
||||||
|
# ==========================================
|
||||||
|
networks:
|
||||||
|
gardenplan-network:
|
||||||
|
driver: bridge
|
||||||
Reference in New Issue
Block a user