diff --git a/api/composer.json b/api/composer.json index 208d1db..47725b1 100644 --- a/api/composer.json +++ b/api/composer.json @@ -20,6 +20,7 @@ "symfony/http-client": "6.3.*", "symfony/mailer": "6.3.*", "symfony/runtime": "6.3.*", + "symfony/validator": "6.3.*", "symfony/yaml": "6.3.*" }, "require-dev": { diff --git a/api/composer.lock b/api/composer.lock index 12ad2f2..4fc8260 100644 --- a/api/composer.lock +++ b/api/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": "59fda4b1008b9f7bcabb6a38e5669bfb", + "content-hash": "2d9f653274f1c6a69af9aa7ed8872559", "packages": [ { "name": "doctrine/cache", @@ -4164,6 +4164,180 @@ ], "time": "2023-11-09T08:28:21+00:00" }, + { + "name": "symfony/translation-contracts", + "version": "v3.4.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/translation-contracts.git", + "reference": "dee0c6e5b4c07ce851b462530088e64b255ac9c5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/translation-contracts/zipball/dee0c6e5b4c07ce851b462530088e64b255ac9c5", + "reference": "dee0c6e5b4c07ce851b462530088e64b255ac9c5", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.4-dev" + }, + "thanks": { + "name": "symfony/contracts", + "url": "https://github.com/symfony/contracts" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\Translation\\": "" + }, + "exclude-from-classmap": [ + "/Test/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to translation", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/translation-contracts/tree/v3.4.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-07-25T15:08:44+00:00" + }, + { + "name": "symfony/validator", + "version": "v6.3.8", + "source": { + "type": "git", + "url": "https://github.com/symfony/validator.git", + "reference": "f75b40e088d095db1e788b81605a76f4563cb80e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/validator/zipball/f75b40e088d095db1e788b81605a76f4563cb80e", + "reference": "f75b40e088d095db1e788b81605a76f4563cb80e", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-mbstring": "~1.0", + "symfony/polyfill-php83": "^1.27", + "symfony/translation-contracts": "^2.5|^3" + }, + "conflict": { + "doctrine/annotations": "<1.13", + "doctrine/lexer": "<1.1", + "symfony/dependency-injection": "<5.4", + "symfony/expression-language": "<5.4", + "symfony/http-kernel": "<5.4", + "symfony/intl": "<5.4", + "symfony/property-info": "<5.4", + "symfony/translation": "<5.4", + "symfony/yaml": "<5.4" + }, + "require-dev": { + "doctrine/annotations": "^1.13|^2", + "egulias/email-validator": "^2.1.10|^3|^4", + "symfony/cache": "^5.4|^6.0", + "symfony/config": "^5.4|^6.0", + "symfony/console": "^5.4|^6.0", + "symfony/dependency-injection": "^5.4|^6.0", + "symfony/expression-language": "^5.4|^6.0", + "symfony/finder": "^5.4|^6.0", + "symfony/http-client": "^5.4|^6.0", + "symfony/http-foundation": "^5.4|^6.0", + "symfony/http-kernel": "^5.4|^6.0", + "symfony/intl": "^5.4|^6.0", + "symfony/mime": "^5.4|^6.0", + "symfony/property-access": "^5.4|^6.0", + "symfony/property-info": "^5.4|^6.0", + "symfony/translation": "^5.4|^6.0", + "symfony/yaml": "^5.4|^6.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Validator\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides tools to validate values", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/validator/tree/v6.3.8" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-11-07T10:17:15+00:00" + }, { "name": "symfony/var-dumper", "version": "v6.3.8", diff --git a/api/config/packages/validator.yaml b/api/config/packages/validator.yaml new file mode 100644 index 0000000..0201281 --- /dev/null +++ b/api/config/packages/validator.yaml @@ -0,0 +1,13 @@ +framework: + validation: + email_validation_mode: html5 + + # 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 diff --git a/api/src/Controller/IndexController.php b/api/src/Controller/IndexController.php index c998164..50bf585 100644 --- a/api/src/Controller/IndexController.php +++ b/api/src/Controller/IndexController.php @@ -5,12 +5,20 @@ use App\Pagination\Paginator; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\JsonResponse; +use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Request; use App\Repository\FruitRepository; use Symfony\Component\Routing\Annotation\Route; +use App\Entity\Fruit; +use Symfony\Component\Validator\Validator\ValidatorInterface; +use Doctrine\ORM\EntityManagerInterface; final class IndexController extends AbstractController { + public function __construct(private EntityManagerInterface $em) + { + } + #[Route('/', name: 'fruits', methods: ['GET'])] public function all(Request $request, FruitRepository $fruits): JsonResponse { @@ -30,9 +38,39 @@ public function all(Request $request, FruitRepository $fruits): JsonResponse } #[Route('/fruit', name: 'fruit_add', methods: ['POST', 'PUT'])] - public function fruitNew(Request $request) + public function fruitNew(Request $request, ValidatorInterface $validator) { - // dd($request); + $data = json_decode($request->getContent(), true); + + $fruit = new Fruit(); + $fruit->setName($data['name'] ?? '') + ->setGenus($data['genus'] ?? '') + ->setFamily($data['family'] ?? '') + ->setFruitOrder($data['fruit_order'] ?? '') + ->setCarbohydrates($data['carbohydrates'] ?? 0) + ->setFat($data['fat'] ?? 0) + ->setProtein($data['protein'] ?? 0) + ->setSugar($data['sugar'] ?? 0) + ->setCalories($data['calories'] ?? 0) + ->setCreatedAt(new \DateTime('now')) + ->setUpdatedAt(new \DateTime('now')) + ->setSource(Fruit::SOURCE_FROM_APP); + + $violations = $validator->validate($fruit); + + // @INFO: Return an error upon validator errors + if (count($violations) > 0) { + $errors = []; + foreach ($violations as $violation) { + $errors[$violation->getPropertyPath()][] = $violation->getMessage(); + } + + return $this->json(['errors' => $errors], Response::HTTP_UNPROCESSABLE_ENTITY); + } + $this->em->persist($fruit); + $this->em->flush(); + + return new JsonResponse($fruit->toArray()); } #[Route('/fruit/{id}', name: 'fruit', methods: ['GET', 'POST', 'PUT'])] diff --git a/api/src/Entity/Fruit.php b/api/src/Entity/Fruit.php index 59ef0c8..e9deea1 100644 --- a/api/src/Entity/Fruit.php +++ b/api/src/Entity/Fruit.php @@ -4,6 +4,7 @@ use App\Repository\FruitRepository; use Doctrine\ORM\Mapping as ORM; +use Symfony\Component\Validator\Constraints as Assert; #[ORM\Entity(repositoryClass: FruitRepository::class)] #[ORM\Table(name: 'fruits')] @@ -25,15 +26,43 @@ class Fruit private ?int $id = null; #[ORM\Column(length: 50)] + #[Assert\NotBlank(message: "Name cannot be blank")] + #[Assert\Length( + min: 4, + max: 50, + minMessage: "Name must be at least 4 characters long.", + maxMessage: "Name cannot be longer than 50 characters." + )] private ?string $name = null; #[ORM\Column(length: 20)] + #[Assert\NotBlank(message: "Genus cannot be blank")] + #[Assert\Length( + min: 4, + max: 20, + minMessage: "Genus must be at least 4 characters long.", + maxMessage: "Genus cannot be longer than 20 characters." + )] private ?string $genus = null; #[ORM\Column(length: 50)] + #[Assert\NotBlank(message: "Family cannot be blank")] + #[Assert\Length( + min: 4, + max: 50, + minMessage: "Family must be at least 4 characters long.", + maxMessage: "Family cannot be longer than 50 characters." + )] private ?string $family = null; #[ORM\Column(length: 30)] + #[Assert\NotBlank(message: "Fruit order cannot be blank")] + #[Assert\Length( + min: 4, + max: 30, + minMessage: "Fruit order must be at least 4 characters long.", + maxMessage: "Fruit order cannot be longer than 30 characters." + )] private ?string $fruitOrder = null; #[ORM\Column(nullable: true)] @@ -221,4 +250,9 @@ public function setTimestampsOnUpdate(): void { $this->updatedAt = new \DateTime('now'); } + + public function toArray(): array + { + return get_object_vars($this); + } } diff --git a/api/symfony.lock b/api/symfony.lock index 9e5224f..07bb66d 100644 --- a/api/symfony.lock +++ b/api/symfony.lock @@ -167,5 +167,17 @@ "config/packages/routing.yaml", "config/routes.yaml" ] + }, + "symfony/validator": { + "version": "6.3", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "5.3", + "ref": "c32cfd98f714894c4f128bb99aa2530c1227603c" + }, + "files": [ + "config/packages/validator.yaml" + ] } }