Compare commits
7 Commits
2094b71335
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
| a5965c8673 | |||
| f5ee201e04 | |||
| 98e4abcfb0 | |||
| 468a08c5a3 | |||
| 3226205889 | |||
| 0f9913faa2 | |||
| ba87c9773c |
@@ -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');
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,7 @@
|
||||
"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.*",
|
||||
@@ -73,5 +74,8 @@
|
||||
"allow-contrib": false,
|
||||
"require": "8.1.*"
|
||||
}
|
||||
},
|
||||
"require-dev": {
|
||||
"phpstan/phpstan": "^2.0"
|
||||
}
|
||||
}
|
||||
|
||||
Generated
+238
-2
@@ -4,7 +4,7 @@
|
||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "e6d1737f343b3572fc4b55b50cdc67e2",
|
||||
"content-hash": "8cfcdf73fe44a155dd3b0835cadb01bd",
|
||||
"packages": [
|
||||
{
|
||||
"name": "doctrine/collections",
|
||||
@@ -92,6 +92,91 @@
|
||||
],
|
||||
"time": "2026-01-15T10:01:58+00:00"
|
||||
},
|
||||
{
|
||||
"name": "doctrine/data-fixtures",
|
||||
"version": "2.2.1",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/doctrine/data-fixtures.git",
|
||||
"reference": "bf7ac3a050b54b261cedfb3d0a44733819062275"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/doctrine/data-fixtures/zipball/bf7ac3a050b54b261cedfb3d0a44733819062275",
|
||||
"reference": "bf7ac3a050b54b261cedfb3d0a44733819062275",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"doctrine/persistence": "^3.1 || ^4.0",
|
||||
"php": "^8.1",
|
||||
"psr/log": "^1.1 || ^2 || ^3"
|
||||
},
|
||||
"conflict": {
|
||||
"doctrine/dbal": "<3.5 || >=5",
|
||||
"doctrine/orm": "<2.14 || >=4",
|
||||
"doctrine/phpcr-odm": "<1.3.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"doctrine/coding-standard": "^14",
|
||||
"doctrine/dbal": "^3.5 || ^4",
|
||||
"doctrine/mongodb-odm": "^1.3.0 || ^2.0.0",
|
||||
"doctrine/orm": "^2.14 || ^3",
|
||||
"doctrine/phpcr-odm": "^1.8 || ^2.0",
|
||||
"ext-sqlite3": "*",
|
||||
"fig/log-test": "^1",
|
||||
"jackalope/jackalope-fs": "*",
|
||||
"phpstan/phpstan": "2.1.46",
|
||||
"phpunit/phpunit": "10.5.63 || 12.5.12",
|
||||
"symfony/cache": "^6.4 || ^7 || ^8",
|
||||
"symfony/var-exporter": "^6.4 || ^7 || ^8"
|
||||
},
|
||||
"suggest": {
|
||||
"alcaeus/mongo-php-adapter": "For using MongoDB ODM 1.3 with PHP 7 (deprecated)",
|
||||
"doctrine/mongodb-odm": "For loading MongoDB ODM fixtures",
|
||||
"doctrine/orm": "For loading ORM fixtures",
|
||||
"doctrine/phpcr-odm": "For loading PHPCR ODM fixtures"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Doctrine\\Common\\DataFixtures\\": "src"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Jonathan Wage",
|
||||
"email": "jonwage@gmail.com"
|
||||
}
|
||||
],
|
||||
"description": "Data Fixtures for all Doctrine Object Managers",
|
||||
"homepage": "https://www.doctrine-project.org",
|
||||
"keywords": [
|
||||
"database"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/doctrine/data-fixtures/issues",
|
||||
"source": "https://github.com/doctrine/data-fixtures/tree/2.2.1"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://www.doctrine-project.org/sponsorship.html",
|
||||
"type": "custom"
|
||||
},
|
||||
{
|
||||
"url": "https://www.patreon.com/phpdoctrine",
|
||||
"type": "patreon"
|
||||
},
|
||||
{
|
||||
"url": "https://tidelift.com/funding/github/packagist/doctrine%2Fdata-fixtures",
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2026-04-01T13:56:01+00:00"
|
||||
},
|
||||
{
|
||||
"name": "doctrine/dbal",
|
||||
"version": "4.4.3",
|
||||
@@ -362,6 +447,92 @@
|
||||
],
|
||||
"time": "2026-06-09T19:11:55+00:00"
|
||||
},
|
||||
{
|
||||
"name": "doctrine/doctrine-fixtures-bundle",
|
||||
"version": "4.3.1",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/doctrine/DoctrineFixturesBundle.git",
|
||||
"reference": "9e013ed10d49bf7746b07204d336384a7d9b5a4d"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/doctrine/DoctrineFixturesBundle/zipball/9e013ed10d49bf7746b07204d336384a7d9b5a4d",
|
||||
"reference": "9e013ed10d49bf7746b07204d336384a7d9b5a4d",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"doctrine/data-fixtures": "^2.2",
|
||||
"doctrine/doctrine-bundle": "^2.2 || ^3.0",
|
||||
"doctrine/orm": "^2.14.0 || ^3.0",
|
||||
"doctrine/persistence": "^2.4 || ^3.0 || ^4.0",
|
||||
"php": "^8.1",
|
||||
"psr/log": "^2 || ^3",
|
||||
"symfony/config": "^6.4 || ^7.0 || ^8.0",
|
||||
"symfony/console": "^6.4 || ^7.0 || ^8.0",
|
||||
"symfony/dependency-injection": "^6.4 || ^7.0 || ^8.0",
|
||||
"symfony/deprecation-contracts": "^2.1 || ^3",
|
||||
"symfony/doctrine-bridge": "^6.4.16 || ^7.1.9 || ^8.0",
|
||||
"symfony/http-kernel": "^6.4 || ^7.0 || ^8.0"
|
||||
},
|
||||
"conflict": {
|
||||
"doctrine/dbal": "< 3"
|
||||
},
|
||||
"require-dev": {
|
||||
"doctrine/coding-standard": "14.0.0",
|
||||
"phpstan/phpstan": "2.1.11",
|
||||
"phpunit/phpunit": "^10.5.38 || 11.4.14"
|
||||
},
|
||||
"type": "symfony-bundle",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Doctrine\\Bundle\\FixturesBundle\\": "src"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Fabien Potencier",
|
||||
"email": "fabien@symfony.com"
|
||||
},
|
||||
{
|
||||
"name": "Doctrine Project",
|
||||
"homepage": "https://www.doctrine-project.org"
|
||||
},
|
||||
{
|
||||
"name": "Symfony Community",
|
||||
"homepage": "https://symfony.com/contributors"
|
||||
}
|
||||
],
|
||||
"description": "Symfony DoctrineFixturesBundle",
|
||||
"homepage": "https://www.doctrine-project.org",
|
||||
"keywords": [
|
||||
"Fixture",
|
||||
"persistence"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/doctrine/DoctrineFixturesBundle/issues",
|
||||
"source": "https://github.com/doctrine/DoctrineFixturesBundle/tree/4.3.1"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://www.doctrine-project.org/sponsorship.html",
|
||||
"type": "custom"
|
||||
},
|
||||
{
|
||||
"url": "https://www.patreon.com/phpdoctrine",
|
||||
"type": "patreon"
|
||||
},
|
||||
{
|
||||
"url": "https://tidelift.com/funding/github/packagist/doctrine%2Fdoctrine-fixtures-bundle",
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2025-12-03T16:05:42+00:00"
|
||||
},
|
||||
{
|
||||
"name": "doctrine/event-manager",
|
||||
"version": "2.1.1",
|
||||
@@ -4865,7 +5036,72 @@
|
||||
"time": "2026-05-29T05:06:50+00:00"
|
||||
}
|
||||
],
|
||||
"packages-dev": [],
|
||||
"packages-dev": [
|
||||
{
|
||||
"name": "phpstan/phpstan",
|
||||
"version": "2.2.2",
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/phpstan/phpstan/zipball/e5cc34d491a90e79c216d824f60fe21fd4d93bd6",
|
||||
"reference": "e5cc34d491a90e79c216d824f60fe21fd4d93bd6",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": "^7.4|^8.0"
|
||||
},
|
||||
"conflict": {
|
||||
"phpstan/phpstan-shim": "*"
|
||||
},
|
||||
"bin": [
|
||||
"phpstan",
|
||||
"phpstan.phar"
|
||||
],
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"files": [
|
||||
"bootstrap.php"
|
||||
]
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Ondřej Mirtes"
|
||||
},
|
||||
{
|
||||
"name": "Markus Staab"
|
||||
},
|
||||
{
|
||||
"name": "Vincent Langlet"
|
||||
}
|
||||
],
|
||||
"description": "PHPStan - PHP Static Analysis Tool",
|
||||
"keywords": [
|
||||
"dev",
|
||||
"static analysis"
|
||||
],
|
||||
"support": {
|
||||
"docs": "https://phpstan.org/user-guide/getting-started",
|
||||
"forum": "https://github.com/phpstan/phpstan/discussions",
|
||||
"issues": "https://github.com/phpstan/phpstan/issues",
|
||||
"security": "https://github.com/phpstan/phpstan/security/policy",
|
||||
"source": "https://github.com/phpstan/phpstan-src"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://github.com/ondrejmirtes",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/phpstan",
|
||||
"type": "github"
|
||||
}
|
||||
],
|
||||
"time": "2026-06-05T09:00:01+00:00"
|
||||
}
|
||||
],
|
||||
"aliases": [],
|
||||
"minimum-stability": "stable",
|
||||
"stability-flags": {},
|
||||
|
||||
@@ -5,4 +5,5 @@ return [
|
||||
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],
|
||||
];
|
||||
|
||||
@@ -17,9 +17,9 @@ doctrine:
|
||||
App:
|
||||
type: attribute
|
||||
is_bundle: false
|
||||
dir: '%kernel.project_dir%/src/Entity'
|
||||
prefix: 'App\Entity'
|
||||
alias: App
|
||||
dir: '%kernel.project_dir%/src/Data/Doctrine/Entity/Saatgut'
|
||||
prefix: 'App\Data\Doctrine\Entity\Saatgut'
|
||||
alias: Saatgut
|
||||
|
||||
when@test:
|
||||
doctrine:
|
||||
|
||||
@@ -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,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();
|
||||
}
|
||||
}
|
||||
@@ -22,6 +22,18 @@
|
||||
"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": {
|
||||
@@ -34,6 +46,9 @@
|
||||
"config/packages/lexik_jwt_authentication.yaml"
|
||||
]
|
||||
},
|
||||
"phpstan/phpstan": {
|
||||
"version": "2.2.2"
|
||||
},
|
||||
"symfony/console": {
|
||||
"version": "8.1",
|
||||
"recipe": {
|
||||
|
||||
@@ -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.
|
||||
@@ -0,0 +1,35 @@
|
||||
|
||||
## 1. Zielsetzung
|
||||
Bereitstellung einer REST-API zur Verwaltung des Saatgut-Inventars inklusive strikter Sicherheitsprüfungen und Logik-Validierungen.
|
||||
|
||||
## 2. Endpunkt-Definitionen
|
||||
|
||||
### 2.1 Katalog (Public/Read)
|
||||
| Endpunkt | Methode | Parameter | Antwort | AK / Testfall |
|
||||
| :--- | :--- | :--- | :--- | :--- |
|
||||
| `/api/kategorien` | GET | - | `200 OK` [JSON] | Liste ist nicht leer, wenn DB gefüllt ist. |
|
||||
| `/api/pflanzen` | GET | `?kategorie_id=X`, `?suche=Y` | `200 OK` [JSON] | Filterung reduziert Ergebnismenge korrekt. |
|
||||
|
||||
### 2.2 Inventar (Protected / JWT required)
|
||||
| Endpunkt | Methode | Parameter | Antwort | AK / Testfall |
|
||||
| :--- | :--- | :--- | :--- | :--- |
|
||||
| `/api/mein-saatgut` | GET | - | `200 OK` [JSON] | Nur Einträge der eigenen `nutzer_id` sichtbar. |
|
||||
| `/api/mein-saatgut` | POST | Body (JSON) | `201 Created` | Fehlende Pflichtfelder $\rightarrow$ `400 Bad Request`. |
|
||||
| `/api/mein-saatgut/{id}`| PATCH | Body (JSON) | `200 OK` | Fremde ID $\rightarrow$ `403 Forbidden`. |
|
||||
| `/api/mein-saatgut/{id}`| DELETE | - | `204 No Content`| Fremde ID $\rightarrow$ `403 Forbidden`. |
|
||||
|
||||
## 3. Logik-Schicht (Service-Layer) & Unit Tests
|
||||
Die Geschäftslogik wird in dedizierten Services implementiert und unabhängig vom HTTP-Kontext getestet.
|
||||
|
||||
### 3.1 Business Rules (AKs für Logik-Tests)
|
||||
- [ ] **Validierung Ablaufdatum:** Der Service muss eine Exception werfen, wenn das `kaufdatum` zeitlich nach dem `ablaufdatum` liegt.
|
||||
- [ ] **Katalog-Existenz:** Bevor ein Eintrag in `SaatgutBestand` erstellt wird, muss geprüft werden, ob die `pflanze_id` im Katalog existiert $\rightarrow$ sonst Fehler werfen.
|
||||
- [ ] **Besitzprüfung:** Die Methode `isOwner(Nutzer, SaatgutEintrag)` muss strikt prüfen, ob die IDs übereinstimmen.
|
||||
|
||||
### 3.2 Test-Matrix
|
||||
| Ebene | Tool | Fokus | Ziel |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| **Functional** | `WebTestCase` | End-to-End API Flow | HTTP Statuscodes & JSON Struktur prüfen. |
|
||||
| **Unit** | `PHPUnit` | Business Logic | Edge Cases (z.B. Datumsfehler) isoliert testen. |
|
||||
| **Integration**| JWT/Mock | Security / Isolation | Sicherstellen, dass Nutzer A niemals Daten von B sieht. |
|
||||
|
||||
+31
-21
@@ -5,9 +5,9 @@ Das Ziel dieser Architektur ist eine strikte Trennung zwischen technischer Infra
|
||||
|
||||
### Grundregeln:
|
||||
* **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.
|
||||
* **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.
|
||||
* **Atomarität:** Business-Operationen sind atomar. Entweder alles wird gespeichert oder nichts (Transaction Management).
|
||||
* **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 dient als primärer Datenträger zwischen den Schichten.
|
||||
* **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:**
|
||||
* Mapping von Input auf **DTOs**.
|
||||
* 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**.
|
||||
|
||||
### 🔵 Logic Layer (Business Core)
|
||||
Hier wird definiert, *was* das System tut. Die Logik ist in Orchestrierung und zustandslose Fachlogik unterteilt.
|
||||
|
||||
#### 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`.
|
||||
* **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.
|
||||
* **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**. 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)
|
||||
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.
|
||||
* **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.
|
||||
|
||||
#### 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.
|
||||
* **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)
|
||||
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.
|
||||
* **Entities:** Die technischen Repräsentationen für Doctrine (z.B. `GardenPlanEntity`).
|
||||
* **Mappers:** Explizite Klassen, die zwischen `Entity` $\leftrightarrow$ `Model` transformieren.
|
||||
* **Provider / Processor:** Implementierungen der Interfaces (`Provider` = Lesen | `Processor` = Schreiben).
|
||||
* **Entities:** Technische Repräsentationen für Doctrine (z.B. `GardenPlanEntity`).
|
||||
* **Mappers:** Explizite Klassen, die zwischen `Entity` $\leftrightarrow$ `Model` transformieren (KI-generiert/manuell).
|
||||
|
||||
---
|
||||
|
||||
## 3. Technischer Workflow & Pipeline
|
||||
|
||||
### 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)
|
||||
`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
|
||||
|
||||
### Transaktionssteuerung & Fehler
|
||||
* **Transaction Management:** Der UseCase nutzt ein `TransactionManagerInterface`, um sicherzustellen, dass alle Änderungen innerhalb eines Prozesses atomar erfolgen.
|
||||
* **Error Handling:** Eigene Domain-Exceptions in der Logic. Ein zentraler **Symfony ExceptionListener** mappt diese auf HTTP-Statuscodes (400, 403, 404, etc.).
|
||||
### Dependency Injection & Composition Root
|
||||
Um eine strikte Entkopplung zu gewährleisten, wird die `Logic`-Schicht niemals direkt auf konkrete Implementierungen referenzieren (kein hartcodiertes `#[Autowire(service: ...)]`).
|
||||
* **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)
|
||||
* **Inbound:** `Message` $\rightarrow$ `Handler` $\rightarrow$ `DTO` $\rightarrow$ `UseCase`.
|
||||
* **Outbound:** Nachrichten werden erst **nach** dem erfolgreichen Commit der Transaktion verschickt (Post-Commit Trigger), um "Ghost Notifications" zu vermeiden.
|
||||
### Fehlermanagement & Exception-Handling
|
||||
Ein zentrales System mappt fachliche Fehler auf HTTP-Standards:
|
||||
* **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.
|
||||
|
||||
---
|
||||
|
||||
@@ -105,7 +115,7 @@ src/
|
||||
│ │ ├── Model/[Feature]/ # z.B. /Planning/GardenPlan.php
|
||||
│ │ ├── DTO/[Feature]/ # z.B. /Planning/UpdatePlanDto.php
|
||||
│ │ └── Service/[Feature]/ # Calculator, Policy, Validator, Strategy
|
||||
│ └── InfrastructureInterface/ # Provider & Processor Interfaces
|
||||
│ └── Interface/ # Provider & Processor Interfaces
|
||||
│
|
||||
├── Data/
|
||||
│ ├── Doctrine/
|
||||
|
||||
+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`.**
|
||||
Reference in New Issue
Block a user