Good morning everyone! This is my first question, and also my first project with Symfony (student project), so please be indulgent! 😀
I’m trying to create an application with Symfony, which aims to allow a user to manage his budget, by creating, removing and editing transactions. I have created my project, and also my entities with Doctrine, everything is well for now, the project perfectly works with Crud and database.
But, I have a problem (of course, if I hadn’t, I wouldn’t be writing this question!). As you can see on the wollowing picture, a new transaction is created with a form, with the following inputs: a name, an amount, a type and a category. A type is either a debit or a credit, and the category input represents the usage of the transaction (salary, bills, shopping, etc.)
My problem is that I would like to adapt the options of the Category select dynamically, depending on the value of the Type select (for example, if credit is chosed, it shows salary, and if it’s debit, then the options will be bills and shopping). I know that the best way to proceed is to use AJAX, but I have some problems implementing it. Indeed, I already developed the adaptation of the Category options depeding on the value setted for the Type select, but on load of the webpage. Now, I would like to trigger this event on change with AJAX, and this is where I struggle… 🙁 Here is my code:
templatestransactionnew.html.twig
{% extends 'base.html.twig' %}
{% block title %}New Transaction{% endblock %}
{% block body %}
<h1>Create new Transaction</h1>
{{ form(form)}}
<button type="submit" class="btn" formonvalidate>Valider</button>
<a href="{{ path('app_transaction_index') }}">back to list</a>
{% endblock %}
templatesbase.html.twig
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>{% block title %}Welcome!{% endblock %}</title>
<link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 128 128%22><text y=%221.2em%22 font-size=%2296%22>⚫️</text></svg>">
{# Run `composer require symfony/webpack-encore-bundle` to start using Symfony UX #}
{% block stylesheets %}
{{ encore_entry_link_tags('app') }}
{% endblock %}
</head>
<body>
{% block body %}{% endblock %}
{% block javascripts %}{% endblock %}
</body>
</html>
srcControllerHomeController.php
<?php
namespace AppController;
use SymfonyBundleFrameworkBundleControllerAbstractController;
use SymfonyComponentHttpFoundationResponse;
use SymfonyComponentRoutingAnnotationRoute;
class HomeController extends AbstractController
{
#[Route('/home', name: 'app_home')]
public function index(): Response
{
return $this->render('home/index.html.twig', [
'controller_name' => 'HomeController',
]);
}
}
srcRepositoryCategoryRepository.php
<?php
namespace AppRepository;
use AppEntityCategory;
use DoctrineBundleDoctrineBundleRepositoryServiceEntityRepository;
use DoctrinePersistenceManagerRegistry;
use AppEntityType;
/**
* @extends ServiceEntityRepository<Category>
*
* @method Category|null find($id, $lockMode = null, $lockVersion = null)
* @method Category|null findOneBy(array $criteria, array $orderBy = null)
* @method Category[] findAll()
* @method Category[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
*/
class CategoryRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, Category::class);
}
public function add(Category $entity, bool $flush = false): void
{
$this->getEntityManager()->persist($entity);
if ($flush) {
$this->getEntityManager()->flush();
}
}
public function remove(Category $entity, bool $flush = false): void
{
$this->getEntityManager()->remove($entity);
if ($flush) {
$this->getEntityManager()->flush();
}
}
public function findByTypeOrderedByAscName(Type $type): array
{
return $this->createQueryBuilder('c')
->andWhere('c.type = :type')
->setParameter('type', $type)
->orderBy('c.title', 'ASC')
->getQuery()
->getResult()
;
}
}
srcFormTransactionType.php
<?php
namespace AppForm;
use AppEntityTransaction;
use SymfonyComponentFormAbstractType;
use SymfonyComponentFormFormBuilderInterface;
use SymfonyComponentOptionsResolverOptionsResolver;
use SymfonyComponentFormFormEvents;
use SymfonyComponentFormFormEvent;
use SymfonyBridgeDoctrineFormTypeEntityType;
use AppRepositoryTypeRepository;
use AppRepositoryCategoryRepository;
class TransactionType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder
->add('name')
->add('montant')
->add('type')
->add('category')
;
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'data_class' => Transaction::class,
]);
}
}
srcControllerTransactionController.php
<?php
namespace AppController;
use AppEntityTransaction;
use AppFormTransactionType;
use AppRepositoryTransactionRepository;
use SymfonyBundleFrameworkBundleControllerAbstractController;
use SymfonyComponentHttpFoundationRequest;
use SymfonyComponentHttpFoundationResponse;
use SymfonyComponentRoutingAnnotationRoute;
use SymfonyComponentFormFormEvents;
use SymfonyComponentFormFormEvent;
use SymfonyBridgeDoctrineFormTypeEntityType;
use AppRepositoryTypeRepository;
use AppRepositoryCategoryRepository;
use SymfonyComponentValidatorConstraintsNotBlank;
#[Route('/transaction')]
class TransactionController extends AbstractController
{
#[Route('/', name: 'app_transaction_index', methods: ['GET'])]
public function index(TransactionRepository $transactionRepository): Response
{
return $this->render('transaction/index.html.twig', [
'transactions' => $transactionRepository->findAll(),
]);
}
#[Route('/new', name: 'app_transaction_new', methods: ['GET', 'POST'])]
public function new(Request $request, TypeRepository $typeRepository, CategoryRepository $categoryRepository): Response
{
$form = $this->createFormBuilder(['type' => $typeRepository->find(0)])
->addEventListener(FormEvents::PRE_SET_DATA, function(FormEvent $event) use ($categoryRepository) {
$type = $event->getData()['type'] ?? null;
$categories = $type === null ? [] : $categoryRepository->findByTypeOrderedByAscName($type);
$event->getForm()->add('category', EntityType::class, [
'class' => 'AppEntityCategory',
'choice_label' => 'title',
'choices' => $categories,
'disabled' => $type === null,
'placeholder' => "Sélectionnez une catégorie",
'constraints' => new NotBlank(['message' => 'Sélectionnez une catégorie'])
]);
})
->add('name')
->add('montant')
->add('type', EntityType::class, [
'class' => 'AppEntityType',
'choice_label' => 'title',
'placeholder' => "Sélectionnez un type",
'constraints' => new NotBlank(['message' => 'Sélectionnez un type'])
])
->getForm();
return $this->renderForm('transaction/new.html.twig', compact('form'));
}
#[Route('/{id}', name: 'app_transaction_show', methods: ['GET'])]
public function show(Transaction $transaction): Response
{
return $this->render('transaction/show.html.twig', [
'transaction' => $transaction,
]);
}
#[Route('/{id}/edit', name: 'app_transaction_edit', methods: ['GET', 'POST'])]
public function edit(Request $request, Transaction $transaction, TransactionRepository $transactionRepository): Response
{
$form = $this->createForm(TransactionType::class, $transaction);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$transactionRepository->add($transaction, true);
return $this->redirectToRoute('app_transaction_index', [], Response::HTTP_SEE_OTHER);
}
return $this->renderForm('transaction/edit.html.twig', [
'transaction' => $transaction,
'form' => $form,
]);
}
#[Route('/{id}', name: 'app_transaction_delete', methods: ['POST'])]
public function delete(Request $request, Transaction $transaction, TransactionRepository $transactionRepository): Response
{
if ($this->isCsrfTokenValid('delete'.$transaction->getId(), $request->request->get('_token'))) {
$transactionRepository->remove($transaction, true);
}
return $this->redirectToRoute('app_transaction_index', [], Response::HTTP_SEE_OTHER);
}
}
srcEntityTransaction.php
<?php
namespace AppEntity;
use AppRepositoryTransactionRepository;
use DoctrineORMMapping as ORM;
use MonologDateTimeImmutable;
#[ORMEntity(repositoryClass: TransactionRepository::class)]
class Transaction
{
#[ORMId]
#[ORMGeneratedValue]
#[ORMColumn(type: 'integer')]
private $id;
#[ORMColumn(type: 'string', length: 255)]
private $name;
#[ORMColumn(type: 'float')]
private $montant;
#[ORMColumn(type: 'datetime_immutable')]
private $createdAt;
#[ORMManyToOne(targetEntity: Type::class, inversedBy: 'transactions')]
#[ORMJoinColumn(nullable: true)]
private $type;
#[ORMManyToOne(targetEntity: User::class, inversedBy: 'transactions')]
#[ORMJoinColumn(nullable: false)]
private $user;
#[ORMManyToOne(targetEntity: Category::class, inversedBy: 'transactions')]
#[ORMJoinColumn(nullable: false)]
private $category;
public function getId(): ?int
{
return $this->id;
}
public function getName(): ?string
{
return $this->name;
}
public function setName(string $name): self
{
$this->name = $name;
return $this;
}
public function getMontant(): ?float
{
return $this->montant;
}
public function setMontant(float $montant): self
{
$this->montant = $montant;
return $this;
}
public function getCreatedAt(): ?DateTimeImmutable
{
return $this->createdAt;
}
public function setCreatedAt(DateTimeImmutable $createdAt): self
{
$this->createdAt = $createdAt;
return $this;
}
public function getType(): ?Type
{
return $this->type;
}
public function setType(?Type $type): self
{
$this->type = $type;
return $this;
}
public function getUser(): ?User
{
return $this->user;
}
public function setUser(?User $user): self
{
$this->user = $user;
return $this;
}
public function getCategory(): ?Category
{
return $this->category;
}
public function setCategory(?Category $category): self
{
$this->category = $category;
return $this;
}
public function __construct() {
$this->createdAt = new DateTimeImmutable(date('d-m-Y H:i:s'), new DateTimeZone("Europe/Paris"));
}
public function __toString() {
return $this->getName();
}
}
srcEntityType.php
<?php
namespace AppEntity;
use AppRepositoryTypeRepository;
use DoctrineCommonCollectionsArrayCollection;
use DoctrineCommonCollectionsCollection;
use DoctrineORMMapping as ORM;
#[ORMEntity(repositoryClass: TypeRepository::class)]
class Type
{
#[ORMId]
#[ORMGeneratedValue]
#[ORMColumn(type: 'integer')]
private $id;
#[ORMColumn(type: 'string', length: 50)]
private $title;
#[ORMColumn(type: 'integer')]
private $coefficient;
#[ORMOneToMany(mappedBy: 'type', targetEntity: Category::class, orphanRemoval: true)]
private $categories;
#[ORMOneToMany(mappedBy: 'type', targetEntity: Transaction::class)]
private $transactions;
public function __construct()
{
$this->categories = new ArrayCollection();
$this->transactions = new ArrayCollection();
}
public function getId(): ?int
{
return $this->id;
}
public function getTitle(): ?string
{
return $this->title;
}
public function setTitle(string $title): self
{
$this->title = $title;
return $this;
}
public function getCoefficient(): ?int
{
return $this->coefficient;
}
public function setCoefficient(int $coefficient): self
{
$this->coefficient = $coefficient;
return $this;
}
/**
* @return Collection<int, Category>
*/
public function getCategories(): Collection
{
return $this->categories;
}
public function addCategory(Category $category): self
{
if (!$this->categories->contains($category)) {
$this->categories[] = $category;
$category->setType($this);
}
return $this;
}
public function removeCategory(Category $category): self
{
if ($this->categories->removeElement($category)) {
// set the owning side to null (unless already changed)
if ($category->getType() === $this) {
$category->setType(null);
}
}
return $this;
}
/**
* @return Collection<int, Transaction>
*/
public function getTransactions(): Collection
{
return $this->transactions;
}
public function addTransaction(Transaction $transaction): self
{
if (!$this->transactions->contains($transaction)) {
$this->transactions[] = $transaction;
$transaction->setType($this);
}
return $this;
}
public function removeTransaction(Transaction $transaction): self
{
if ($this->transactions->removeElement($transaction)) {
// set the owning side to null (unless already changed)
if ($transaction->getType() === $this) {
$transaction->setType(null);
}
}
return $this;
}
public function __toString() {
return $this->getTitle();
}
}
srcEntityCategory.php
<?php
namespace AppEntity;
use AppRepositoryCategoryRepository;
use DoctrineCommonCollectionsArrayCollection;
use DoctrineCommonCollectionsCollection;
use DoctrineORMMapping as ORM;
#[ORMEntity(repositoryClass: CategoryRepository::class)]
class Category
{
#[ORMId]
#[ORMGeneratedValue]
#[ORMColumn(type: 'integer')]
private $id;
#[ORMColumn(type: 'string', length: 50)]
private $title;
#[ORMColumn(type: 'text', nullable: true)]
private $description;
#[ORMManyToOne(targetEntity: Type::class, inversedBy: 'categories')]
#[ORMJoinColumn(nullable: false)]
private $type;
#[ORMOneToMany(mappedBy: 'category', targetEntity: Transaction::class, orphanRemoval: true)]
private $transactions;
public function __construct()
{
$this->transactions = new ArrayCollection();
}
public function getId(): ?int
{
return $this->id;
}
public function getTitle(): ?string
{
return $this->title;
}
public function setTitle(string $title): self
{
$this->title = $title;
return $this;
}
public function getDescription(): ?string
{
return $this->description;
}
public function setDescription(?string $description): self
{
$this->description = $description;
return $this;
}
public function getType(): ?Type
{
return $this->type;
}
public function setType(?Type $type): self
{
$this->type = $type;
return $this;
}
/**
* @return Collection<int, Transaction>
*/
public function getTransactions(): Collection
{
return $this->transactions;
}
public function addTransaction(Transaction $transaction): self
{
if (!$this->transactions->contains($transaction)) {
$this->transactions[] = $transaction;
$transaction->setCategory($this);
}
return $this;
}
public function removeTransaction(Transaction $transaction): self
{
if ($this->transactions->removeElement($transaction)) {
// set the owning side to null (unless already changed)
if ($transaction->getCategory() === $this) {
$transaction->setCategory(null);
}
}
return $this;
}
public function __toString() {
return $this->getTitle();
}
}
I hope that someone will be able to help me! Thanks in advance, and have a nice day! 😀
PS: I would also like the Category select to be disabled when the Type select’s placeholder is selected, and the Category select to be enabled when a value is selected with the Type select.