Compare commits

...

2 commits

Author SHA1 Message Date
afe2a9fe92 Add entity Rating 2026-06-04 19:31:25 +00:00
f0d17e9b44 Transform subject controller to API 2026-06-03 21:49:30 +00:00
11 changed files with 379 additions and 114 deletions

View file

@ -0,0 +1,38 @@
<?php
namespace App\Controller\Api;
use App\Entity\Restaurant;
use App\Repository\SubjectRepository;
use App\Services\OutputService;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Attribute\Route;
class SubjectController extends AbstractController
{
#[Route('/api/subjects', methods: ['GET'])]
public function index(
SubjectRepository $repo,
OutputService $output,
): JsonResponse {
return $this->json($output->success(['subjects' => $repo->findAll()]));
}
#[Route('/api/subjects', methods: ['POST'])]
public function create(
Request $request,
EntityManagerInterface $em,
): JsonResponse {
$subject = new Restaurant();
$subject->setName($request->getPayload()->get('name'));
$subject->setCreatedBy($this->getUser());
$em->persist($subject);
$em->flush();
return $this->json(null, 201);
}
}

View file

@ -1,55 +0,0 @@
<?php
namespace App\Controller;
use App\Entity\Restaurant;
use App\Repository\SubjectRepository;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
class SubjectController extends AbstractController
{
#[Route('/')]
public function index(
Request $request,
EntityManagerInterface $em,
SubjectRepository $repo,
): Response {
$subject = new Restaurant();
$subject->setName("MyRestaurant");
$form = $this->createFormBuilder($subject)
->add('name', TextType::class)
->add('save', SubmitType::class)
->getForm();
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$subject->setCreatedBy($this->getUser());
$em->persist($subject);
$em->flush();
return $this->redirectToRoute('app_subject_index');
}
$subjects = $repo->findAll();
return $this->render('index/page.html.twig', [
'form' => $form,
'subjects' => $subjects,
]);
}
#[Route('/', 'subject_new', methods: ["POST"])]
public function save(): RedirectResponse
{
return $this->redirect('/');
}
}

52
src/Entity/Rating.php Normal file
View file

@ -0,0 +1,52 @@
<?php
namespace App\Entity;
use App\Repository\RatingRepository;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity(repositoryClass: RatingRepository::class)]
class Rating
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
#[ORM\ManyToOne(inversedBy: 'ratings')]
#[ORM\JoinColumn(nullable: false)]
private ?Subject $subject_id = null;
#[ORM\Column(type: Types::SMALLINT, nullable: true)]
private ?int $rating = null;
public function getId(): ?int
{
return $this->id;
}
public function getSubjectId(): ?Subject
{
return $this->subject_id;
}
public function setSubjectId(?Subject $subject_id): static
{
$this->subject_id = $subject_id;
return $this;
}
public function getRating(): ?int
{
return $this->rating;
}
public function setRating(?int $rating): static
{
$this->rating = $rating;
return $this;
}
}

View file

@ -4,13 +4,15 @@ namespace App\Entity;
use App\Entity\Restaurant;
use App\Repository\SubjectRepository;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity(repositoryClass: SubjectRepository::class)]
#[ORM\InheritanceType('SINGLE_TABLE')]
#[ORM\DiscriminatorColumn(name: 'type', type: 'string')]
#[ORM\DiscriminatorMap(['restaurant' => Restaurant::class])]
class Subject
abstract class Subject
{
#[ORM\Id]
#[ORM\GeneratedValue]
@ -24,6 +26,17 @@ class Subject
#[ORM\JoinColumn(nullable: true)]
private ?User $createdBy = null;
/**
* @var Collection<int, Rating>
*/
#[ORM\OneToMany(targetEntity: Rating::class, mappedBy: 'subject_id', orphanRemoval: true)]
private Collection $ratings;
public function __construct()
{
$this->ratings = new ArrayCollection();
}
public function getId(): ?int
{
return $this->id;
@ -52,4 +65,34 @@ class Subject
return $this;
}
/**
* @return Collection<int, Rating>
*/
public function getRatings(): Collection
{
return $this->ratings;
}
public function addRating(Rating $rating): static
{
if (!$this->ratings->contains($rating)) {
$this->ratings->add($rating);
$rating->setSubjectId($this);
}
return $this;
}
public function removeRating(Rating $rating): static
{
if ($this->ratings->removeElement($rating)) {
// set the owning side to null (unless already changed)
if ($rating->getSubjectId() === $this) {
$rating->setSubjectId(null);
}
}
return $this;
}
}

View file

@ -0,0 +1,53 @@
<?php
namespace App\Factory;
use App\Entity\Rating;
use App\Repository\RatingRepository;
use Doctrine\ORM\EntityRepository;
use Zenstruck\Foundry\Persistence\PersistentObjectFactory;
use Zenstruck\Foundry\Persistence\Proxy;
use Zenstruck\Foundry\Persistence\ProxyRepositoryDecorator;
/**
* @extends PersistentObjectFactory<Rating>
*/
final class RatingFactory extends PersistentObjectFactory
{
/**
* @see https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#factories-as-services
*
* @todo inject services if required
*/
public function __construct() {}
#[\Override]
public static function class(): string
{
return Rating::class;
}
/**
* @see https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#model-factories
*
* @todo add your default values here
*/
#[\Override]
protected function defaults(): array|callable
{
return [
'subject_id' => SubjectFactory::new(),
];
}
/**
* @see https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#initialization
*/
#[\Override]
protected function initialize(): static
{
return $this
// ->afterInstantiate(function(Rating $rating): void {})
;
}
}

View file

@ -0,0 +1,54 @@
<?php
namespace App\Factory;
use App\Entity\Restaurant;
use App\Entity\Subject;
use App\Repository\SubjectRepository;
use Doctrine\ORM\EntityRepository;
use Zenstruck\Foundry\Persistence\PersistentObjectFactory;
use Zenstruck\Foundry\Persistence\Proxy;
use Zenstruck\Foundry\Persistence\ProxyRepositoryDecorator;
/**
* @extends PersistentObjectFactory<Subject>
*/
final class SubjectFactory extends PersistentObjectFactory
{
/**
* @see https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#factories-as-services
*
* @todo inject services if required
*/
public function __construct() {}
#[\Override]
public static function class(): string
{
return Restaurant::class;
}
/**
* @see https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#model-factories
*
* @todo add your default values here
*/
#[\Override]
protected function defaults(): array|callable
{
return [
'name' => self::faker()->text(255),
];
}
/**
* @see https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#initialization
*/
#[\Override]
protected function initialize(): static
{
return $this
// ->afterInstantiate(function(Subject $subject): void {})
;
}
}

View file

@ -0,0 +1,43 @@
<?php
namespace App\Repository;
use App\Entity\Rating;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
/**
* @extends ServiceEntityRepository<Rating>
*/
class RatingRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, Rating::class);
}
// /**
// * @return Rating[] Returns an array of Rating objects
// */
// public function findByExampleField($value): array
// {
// return $this->createQueryBuilder('r')
// ->andWhere('r.exampleField = :val')
// ->setParameter('val', $value)
// ->orderBy('r.id', 'ASC')
// ->setMaxResults(10)
// ->getQuery()
// ->getResult()
// ;
// }
// public function findOneBySomeField($value): ?Rating
// {
// return $this->createQueryBuilder('r')
// ->andWhere('r.exampleField = :val')
// ->setParameter('val', $value)
// ->getQuery()
// ->getOneOrNullResult()
// ;
// }
}

View file

@ -0,0 +1,24 @@
<?php
namespace App\Services;
class OutputService
{
public function success(array $output): array
{
return [
'status' => 'success',
'payload' => $output,
'errors' => null,
];
}
public function error(array $errors): array
{
return [
'status' => 'error',
'payload' => null,
'errors' => $errors,
];
}
}

View file

@ -1,38 +0,0 @@
<?php
namespace App\Tests\Integration\Subject;
use App\Entity\Restaurant;
use App\Factory\UserFactory;
use App\Repository\SubjectRepository;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
class AddSubjectTest extends WebTestCase
{
public function testAddingNewSubject(): void
{
$client = static::createClient();
$user = UserFactory::createOne();
$client->loginUser($user);
$client->request('GET', '/');
$client->submitForm('form_save', ['form[name]' => 'Riviera']);
$this->assertResponseRedirects();
$crawler = $client->followRedirect();
$subject = static::getContainer()
->get(SubjectRepository::class)
->findOneBy(['name' => 'Riviera']);
$this->assertNotNull($subject);
$this->assertInstanceOf(Restaurant::class, $subject);
$this->assertNotNull($subject->getCreatedBy());
$this->assertSame($user->getId(), $subject->getCreatedBy()->getId());
$this->assertSelectorTextContains('ul li', 'Riviera');
}
}

View file

@ -1,20 +0,0 @@
<?php
namespace App\Tests\Integration\Subject;
use App\Factory\RestaurantFactory;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
class ListSubjectsTest extends WebTestCase
{
public function testSubjectList(): void
{
$client = static::createClient();
$subject = RestaurantFactory::createOne();
$client->request('GET', '/');
$this->assertSelectorTextContains('ul li', $subject->getName());
}
}

View file

@ -0,0 +1,71 @@
<?php
namespace App\Tests\Integration\Subject;
use App\Entity\Restaurant;
use App\Factory\SubjectFactory;
use App\Factory\UserFactory;
use App\Repository\SubjectRepository;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
class SubjectControllerTest extends WebTestCase
{
public function testList(): void
{
$client = static::createClient();
$user = UserFactory::createOne();
$client->loginUser($user);
SubjectFactory::createMany(5, [
'createdBy' => $user,
]);
static::getContainer()
->get(SubjectRepository::class)
->findOneBy(['name' => 'Riviera']);
$client->request(
'GET',
'/api/subjects',
server: ['CONTENT_TYPE' => 'application/json'],
content: json_encode(['name' => 'Riviera']),
);
$data = json_decode($client->getResponse()->getContent(), true);
$this->assertResponseIsSuccessful();
$this->assertCount(5, $data['payload']['subjects']);
}
public function testAddingNewSubjectViaApi(): void
{
$client = static::createClient();
$repo = static::getContainer()->get(SubjectRepository::class);
$user = UserFactory::createOne();
$client->loginUser($user);
$this->assertEmpty($repo->findAll());
$client->request(
'POST',
'/api/subjects',
server: ['CONTENT_TYPE' => 'application/json'],
content: json_encode(['name' => 'Riviera']),
);
$this->assertResponseIsSuccessful();
$this->assertResponseStatusCodeSame(201);
$this->assertCount(1, $repo->findAll());
$subject = static::getContainer()
->get(SubjectRepository::class)
->findOneBy(['name' => 'Riviera']);
$this->assertNotNull($subject);
$this->assertInstanceOf(Restaurant::class, $subject);
$this->assertNotNull($subject->getCreatedBy());
$this->assertSame($user->getId(), $subject->getCreatedBy()->getId());
}
}