diff --git a/Backend/Migrations/Version20250617184300_create_saatgut_tables.php b/Backend/Migrations/Version20250617184300_create_saatgut_tables.php new file mode 100644 index 0000000..eb73fba --- /dev/null +++ b/Backend/Migrations/Version20250617184300_create_saatgut_tables.php @@ -0,0 +1,58 @@ +addSql(<<addSql(<<addSql(<<addSql('DROP TABLE IF EXISTS saatgut_bestand'); + $this->addSql('DROP TABLE IF EXISTS pflanze'); + $this->addSql('DROP TABLE IF EXISTS kategorie'); + } +} diff --git a/Backend/composer.json b/Backend/composer.json index 7787984..f00cb12 100644 --- a/Backend/composer.json +++ b/Backend/composer.json @@ -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" } } diff --git a/Backend/composer.lock b/Backend/composer.lock index b5e3188..556ce3f 100644 --- a/Backend/composer.lock +++ b/Backend/composer.lock @@ -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": {}, diff --git a/Backend/config/bundles.php b/Backend/config/bundles.php index 4fa1f41..0086e24 100644 --- a/Backend/config/bundles.php +++ b/Backend/config/bundles.php @@ -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], ]; diff --git a/Backend/config/packages/doctrine.yaml b/Backend/config/packages/doctrine.yaml index 290611c..8831fb1 100644 --- a/Backend/config/packages/doctrine.yaml +++ b/Backend/config/packages/doctrine.yaml @@ -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: diff --git a/Backend/phpstan.neon b/Backend/phpstan.neon new file mode 100644 index 0000000..6d84ee4 --- /dev/null +++ b/Backend/phpstan.neon @@ -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#' diff --git a/Backend/src/Data/Doctrine/Entity/Saatgut/KategorieEntity.php b/Backend/src/Data/Doctrine/Entity/Saatgut/KategorieEntity.php new file mode 100644 index 0000000..997a677 --- /dev/null +++ b/Backend/src/Data/Doctrine/Entity/Saatgut/KategorieEntity.php @@ -0,0 +1,48 @@ +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; + } +} diff --git a/Backend/src/Data/Doctrine/Entity/Saatgut/PflanzeEntity.php b/Backend/src/Data/Doctrine/Entity/Saatgut/PflanzeEntity.php new file mode 100644 index 0000000..706a8d1 --- /dev/null +++ b/Backend/src/Data/Doctrine/Entity/Saatgut/PflanzeEntity.php @@ -0,0 +1,84 @@ +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; + } +} diff --git a/Backend/src/Data/Doctrine/Entity/Saatgut/SaatgutBestandEntity.php b/Backend/src/Data/Doctrine/Entity/Saatgut/SaatgutBestandEntity.php new file mode 100644 index 0000000..6fbcc87 --- /dev/null +++ b/Backend/src/Data/Doctrine/Entity/Saatgut/SaatgutBestandEntity.php @@ -0,0 +1,118 @@ +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; + } +} diff --git a/Backend/src/Data/Doctrine/Repository/Saatgut/KategorieRepository.php b/Backend/src/Data/Doctrine/Repository/Saatgut/KategorieRepository.php new file mode 100644 index 0000000..906f687 --- /dev/null +++ b/Backend/src/Data/Doctrine/Repository/Saatgut/KategorieRepository.php @@ -0,0 +1,29 @@ + + */ +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() + ); + } +} diff --git a/Backend/src/Data/Doctrine/Repository/Saatgut/PflanzeRepository.php b/Backend/src/Data/Doctrine/Repository/Saatgut/PflanzeRepository.php new file mode 100644 index 0000000..dbd1d5b --- /dev/null +++ b/Backend/src/Data/Doctrine/Repository/Saatgut/PflanzeRepository.php @@ -0,0 +1,46 @@ + + */ +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(); + } +} diff --git a/Backend/src/Data/Doctrine/Repository/Saatgut/SaatgutBestandRepository.php b/Backend/src/Data/Doctrine/Repository/Saatgut/SaatgutBestandRepository.php new file mode 100644 index 0000000..0a6800a --- /dev/null +++ b/Backend/src/Data/Doctrine/Repository/Saatgut/SaatgutBestandRepository.php @@ -0,0 +1,47 @@ + + */ +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(); + } +} diff --git a/Backend/src/DataFixtures/AppFixtures.php b/Backend/src/DataFixtures/AppFixtures.php new file mode 100644 index 0000000..987f6fe --- /dev/null +++ b/Backend/src/DataFixtures/AppFixtures.php @@ -0,0 +1,17 @@ +persist($product); + + $manager->flush(); + } +} diff --git a/Backend/src/DataFixtures/KategorieFixtures.php b/Backend/src/DataFixtures/KategorieFixtures.php new file mode 100644 index 0000000..828de5f --- /dev/null +++ b/Backend/src/DataFixtures/KategorieFixtures.php @@ -0,0 +1,32 @@ + '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(); + } +} diff --git a/Backend/src/DataFixtures/PflanzeFixtures.php b/Backend/src/DataFixtures/PflanzeFixtures.php new file mode 100644 index 0000000..e55f125 --- /dev/null +++ b/Backend/src/DataFixtures/PflanzeFixtures.php @@ -0,0 +1,47 @@ +[] + */ + 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(); + } +} diff --git a/Backend/symfony.lock b/Backend/symfony.lock index e303a63..c653066 100644 --- a/Backend/symfony.lock +++ b/Backend/symfony.lock @@ -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": {