+ */
+ public function getAddresses(): Collection
+ {
+ return $this->addresses;
+ }
+
+ public function addAddress(Address $address): self
+ {
+ if (!$this->addresses->contains($address)) {
+ $this->addresses[] = $address;
+ $address->setCountry($this);
+ }
+
+ return $this;
+ }
+
+ public function removeAddress(Address $address): self
+ {
+ if ($this->addresses->removeElement($address)) {
+ // set the owning side to null (unless already changed)
+ if ($address->getCountry() === $this) {
+ $address->setCountry(null);
+ }
+ }
+
+ return $this;
+ }
+
+}
diff --git a/src/Form/AddressType.php b/src/Form/AddressType.php
new file mode 100644
index 0000000..d983b6f
--- /dev/null
+++ b/src/Form/AddressType.php
@@ -0,0 +1,138 @@
+addressRepository = $addressRepository;
+ $this->cityRepository = $cityRepository;
+ $this->countryRepository = $countryRepository;
+ }
+ public function buildForm(FormBuilderInterface $builder, array $options): void
+ {
+ $builder
+ ->add('country', EntityType::class, [
+ 'class' => Country::class,
+ 'placeholder' => 'Select Country',
+ 'choice_label' => 'name',
+ ])
+ ->add('city', EntityType::class, [
+ 'class' => City::class,
+ 'placeholder' => 'Select Country',
+ 'choices' => [],
+ 'choice_label' => 'name',
+ ])
+ ->add('streetName', TextareaType::class, [
+ 'required' => true,
+ ])
+ ->add('postcode', TextType::class, [
+ 'required' => true,
+ 'attr' => ['pattern' => '\d*', 'title'=>'Must be numeric' ],
+ ]);
+
+ $builder->addEventListener(FormEvents::PRE_SUBMIT, function (FormEvent $event) {
+ $form = $event->getForm();
+ $data = $event->getData();
+
+ $countryId = $data['country'] ?? null;
+
+ if ($countryId) {
+ $cities = $this->cityRepository->findBy([
+ 'country' => $countryId,
+ 'isDeleted' => false,
+ ]);
+
+ $form->add('city', EntityType::class, [
+ 'class' => City::class,
+ 'choices' => $cities,
+ 'placeholder' => 'Select City',
+ 'choice_label' => 'name',
+ ]);
+ }
+ });
+ $builder->addEventListener(FormEvents::PRE_SET_DATA, function (FormEvent $event) {
+ $form = $event->getForm();
+ $address = $event->getData();
+
+ if ($address && $address->getCountry()) {
+ $countryId = $address->getCountry()->getId();
+ $cities = $this->cityRepository->findBy([
+ 'country' => $countryId,
+ 'isDeleted' => false,
+ ]);
+
+ $form->add('city', EntityType::class, [
+ 'class' => City::class,
+ 'choices' => $cities,
+ 'placeholder' => 'Select City',
+ 'choice_label' => 'name',
+ ]);
+ }
+ });
+
+ $builder->addEventListener(FormEvents::POST_SUBMIT, function (FormEvent $event) {
+ $form = $event->getForm();
+ $data = $event->getData();
+
+ if (empty($data->getStreetName())) {
+ $form->get('streetName')->addError(new FormError('Street name cannot be empty.'));
+ }
+ if (empty($data->getPostcode())) {
+ $form->get('postcode')->addError(new FormError('Postcode cannot be empty.'));
+ }
+ if (!$data->getCity()) {
+ $form->get('city')->addError(new FormError('City must be selected.'));
+ }
+ if (!$data->getCountry()) {
+ $form->get('country')->addError(new FormError('Country must be selected.'));
+ }
+
+ $existingAddress = $this->addressRepository->findOneBy([
+ 'StreetName' => $data->getStreetName(),
+ 'postcode' => $data->getPostcode(),
+ 'city' => $data->getCity(),
+ 'country' => $data->getCountry(),
+ ]);
+ if($existingAddress) {
+ $form->get('streetName')->addError(new FormError('Address already exists.'));
+ }
+
+ if($data->getPostcode() && !preg_match('/^\d+$/', $data->getPostcode())) {
+ $form->get('postcode')->addError(new FormError('Postcode must be numeric.'));
+ }
+ if($data->getPostcode() && strlen($data->getPostcode()) != 6) {
+ $form->get('postcode')->addError(new FormError('Postcode must be 6 digits long.'));
+ }
+
+ });
+ }
+
+ public function configureOptions(OptionsResolver $resolver): void
+ {
+ $resolver->setDefaults([
+ 'data_class' => Address::class,
+ ]);
+ }
+}
diff --git a/src/Form/CityType.php b/src/Form/CityType.php
new file mode 100644
index 0000000..254f814
--- /dev/null
+++ b/src/Form/CityType.php
@@ -0,0 +1,81 @@
+cityRepository = $cityRepository;
+ }
+ public function buildForm(FormBuilderInterface $builder, array $options): void
+ {
+ $builder
+ ->add('name', TextType::class, [
+ 'required' => true,
+ 'label' => 'City Name',
+ ])
+ ->add('country', EntityType::class, [
+ 'class' => Country::class,
+ 'choice_label' => 'name',
+ 'placeholder' => 'Select Country',
+ 'required' => true,
+ ])
+ ->add('active', CheckboxType::class, [
+ 'label' => 'Active',
+ 'required' => false,
+ 'data' => true,
+ ]);
+
+ $builder->addEventListener(FormEvents::POST_SUBMIT, function (FormEvent $event) {
+ $data = $event->getData();
+ $form = $event->getForm();
+
+ // Validate that the city name is not empty
+ if (empty($data->getName())) {
+ $form->get('name')->addError(new FormError('City name cannot be empty.'));
+ }
+ if (null === $data->getCountry()) {
+ $form->get('country')->addError(new FormError('Please select a country.'));
+ }
+
+ if ($data->getName() && $data->getCountry()) {
+ $existingCity = $this->cityRepository->findOneBy([
+ 'name' => $data->getName(),
+ 'country' => $data->getCountry(),
+ ]);
+
+ // Skip current when checking for existing city
+ if ($existingCity && $existingCity->getId() !== $data->getId()) {
+ $form->get('name')->addError(new FormError('This city already exists in the selected country.'));
+ }
+ }
+ });
+ }
+
+
+
+ public function configureOptions(OptionsResolver $resolver): void
+ {
+ $resolver->setDefaults([
+ 'data_class' => City::class,
+ ]);
+ }
+
+
+}
diff --git a/src/Form/CountryType.php b/src/Form/CountryType.php
new file mode 100644
index 0000000..ab4622c
--- /dev/null
+++ b/src/Form/CountryType.php
@@ -0,0 +1,61 @@
+countryRepository = $countryRepository;
+ }
+ public function buildForm(FormBuilderInterface $builder, array $options): void
+ {
+
+
+ $builder
+ ->add('name', TextType::class, [
+ 'label' => 'Country Name',
+ 'attr' => [
+ 'class' => 'form-control',
+ 'placeholder' => 'Enter country name',
+ 'required' => true,
+ ],
+
+ ]);
+ $builder->addEventListener(FormEvents::POST_SUBMIT, function (FormEvent $event) {
+ $data = $event->getData();
+ $form = $event->getForm();
+
+ // Validate that the country name is not empty
+ if (empty($data->getName())) {
+ $form->get('name')->addError(new FormError('Country name cannot be empty.'));
+ }
+
+ // Check if the country name already exists
+ $existingCountry = $this->countryRepository->findOneBy(['name' => $data->getName()]);
+ if ($existingCountry) {
+ $form->get('name')->addError(new FormError('This country already exists.'));
+ }
+ });
+ }
+
+ public function configureOptions(OptionsResolver $resolver): void
+ {
+ $resolver->setDefaults([
+ 'data_class' => Country::class,
+ ]);
+ }
+}
diff --git a/src/Repository/.gitignore b/src/Repository/.gitignore
new file mode 100644
index 0000000..e69de29
diff --git a/src/Repository/AddressRepository.php b/src/Repository/AddressRepository.php
new file mode 100644
index 0000000..9639e04
--- /dev/null
+++ b/src/Repository/AddressRepository.php
@@ -0,0 +1,66 @@
+
+ *
+ * @method Address|null find($id, $lockMode = null, $lockVersion = null)
+ * @method Address|null findOneBy(array $criteria, array $orderBy = null)
+ * @method Address[] findAll()
+ * @method Address[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
+ */
+class AddressRepository extends ServiceEntityRepository
+{
+ public function __construct(ManagerRegistry $registry)
+ {
+ parent::__construct($registry, Address::class);
+ }
+
+ public function add(Address $entity, bool $flush = false): void
+ {
+ $this->getEntityManager()->persist($entity);
+
+ if ($flush) {
+ $this->getEntityManager()->flush();
+ }
+ }
+
+ public function remove(Address $entity, bool $flush = false): void
+ {
+ $this->getEntityManager()->remove($entity);
+
+ if ($flush) {
+ $this->getEntityManager()->flush();
+ }
+ }
+
+// /**
+// * @return Address[] Returns an array of Address objects
+// */
+// public function findByExampleField($value): array
+// {
+// return $this->createQueryBuilder('a')
+// ->andWhere('a.exampleField = :val')
+// ->setParameter('val', $value)
+// ->orderBy('a.id', 'ASC')
+// ->setMaxResults(10)
+// ->getQuery()
+// ->getResult()
+// ;
+// }
+
+// public function findOneBySomeField($value): ?Address
+// {
+// return $this->createQueryBuilder('a')
+// ->andWhere('a.exampleField = :val')
+// ->setParameter('val', $value)
+// ->getQuery()
+// ->getOneOrNullResult()
+// ;
+// }
+}
diff --git a/src/Repository/CityRepository.php b/src/Repository/CityRepository.php
new file mode 100644
index 0000000..5ba54c4
--- /dev/null
+++ b/src/Repository/CityRepository.php
@@ -0,0 +1,103 @@
+
+ *
+ * @method City|null find($id, $lockMode = null, $lockVersion = null)
+ * @method City|null findOneBy(array $criteria, array $orderBy = null)
+ * @method City[] findAll()
+ * @method City[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
+ */
+class CityRepository extends ServiceEntityRepository
+{
+ public function __construct(ManagerRegistry $registry)
+ {
+ parent::__construct($registry, City::class);
+ }
+
+ public function add(City $entity, bool $flush = false): void
+ {
+ $this->getEntityManager()->persist($entity);
+
+ if ($flush) {
+ $this->getEntityManager()->flush();
+ }
+ }
+
+ public function remove(City $entity, bool $flush = false): void
+ {
+ $this->getEntityManager()->remove($entity);
+
+ if ($flush) {
+ $this->getEntityManager()->flush();
+ }
+ }
+ public function findByNameLike(string $name): array
+ {
+ return $this->createQueryBuilder('c')
+ ->andWhere('c.name LIKE :name')
+ ->setParameter('name', '%' . $name . '%')
+ ->orderBy('c.id', 'ASC')
+ ->getQuery()
+ ->getResult();
+ }
+
+ public function filterCities(?string $search, ?string $countryId, ?string $status): array
+ {
+ $queryBuilder = $this->createQueryBuilder('c')
+ ->join('c.country', 'country')
+ ->addSelect('country');
+
+ if ($search) {
+ $queryBuilder->andWhere('c.name LIKE :search')
+ ->setParameter('search', '%' . $search . '%');
+ }
+
+ if ($countryId) {
+ $queryBuilder->andWhere('country.id = :countryId')
+ ->setParameter('countryId', $countryId);
+ }
+
+ if ($status !== null && $status !== '') {
+ $queryBuilder->andWhere('c.active = :status')
+ ->setParameter('status', (bool) $status);
+ }
+
+ return $queryBuilder->orderBy('c.name', 'ASC')
+ ->andWhere('c.isDeleted = false')
+ ->orderBy('c.id', 'ASC')
+ ->getQuery()
+ ->getResult();
+ }
+
+// /**
+// * @return City[] Returns an array of City objects
+// */
+// public function findByExampleField($value): array
+// {
+// return $this->createQueryBuilder('c')
+// ->andWhere('c.exampleField = :val')
+// ->setParameter('val', $value)
+// ->orderBy('c.id', 'ASC')
+// ->setMaxResults(10)
+// ->getQuery()
+// ->getResult()
+// ;
+// }
+
+// public function findOneBySomeField($value): ?City
+// {
+// return $this->createQueryBuilder('c')
+// ->andWhere('c.exampleField = :val')
+// ->setParameter('val', $value)
+// ->getQuery()
+// ->getOneOrNullResult()
+// ;
+// }
+}
diff --git a/src/Repository/CountryRepository.php b/src/Repository/CountryRepository.php
new file mode 100644
index 0000000..b200643
--- /dev/null
+++ b/src/Repository/CountryRepository.php
@@ -0,0 +1,76 @@
+
+ *
+ * @method Country|null find($id, $lockMode = null, $lockVersion = null)
+ * @method Country|null findOneBy(array $criteria, array $orderBy = null)
+ * @method Country[] findAll()
+ * @method Country[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
+ */
+class CountryRepository extends ServiceEntityRepository
+{
+ public function __construct(ManagerRegistry $registry)
+ {
+ parent::__construct($registry, Country::class);
+ }
+
+ public function add(Country $entity, bool $flush = false): void
+ {
+ $this->getEntityManager()->persist($entity);
+
+ if ($flush) {
+ $this->getEntityManager()->flush();
+ }
+ }
+
+ public function remove(Country $entity, bool $flush = false): void
+ {
+ $this->getEntityManager()->remove($entity);
+
+ if ($flush) {
+ $this->getEntityManager()->flush();
+ }
+ }
+
+ public function findByNameLike(string $search): array
+ {
+ return $this->createQueryBuilder('c')
+ ->where('c.name LIKE :search')
+ ->setParameter('search', '%' . $search . '%')
+ ->orderBy('c.name', 'ASC')
+ ->getQuery()
+ ->getResult();
+ }
+
+// /**
+// * @return Country[] Returns an array of Country objects
+// */
+// public function findByExampleField($value): array
+// {
+// return $this->createQueryBuilder('c')
+// ->andWhere('c.exampleField = :val')
+// ->setParameter('val', $value)
+// ->orderBy('c.id', 'ASC')
+// ->setMaxResults(10)
+// ->getQuery()
+// ->getResult()
+// ;
+// }
+
+// public function findOneBySomeField($value): ?Country
+// {
+// return $this->createQueryBuilder('c')
+// ->andWhere('c.exampleField = :val')
+// ->setParameter('val', $value)
+// ->getQuery()
+// ->getOneOrNullResult()
+// ;
+// }
+}
diff --git a/symfony.lock b/symfony.lock
new file mode 100644
index 0000000..d2a24be
--- /dev/null
+++ b/symfony.lock
@@ -0,0 +1,282 @@
+{
+ "doctrine/annotations": {
+ "version": "2.0",
+ "recipe": {
+ "repo": "github.com/symfony/recipes",
+ "branch": "main",
+ "version": "1.0",
+ "ref": "a2759dd6123694c8d901d0ec80006e044c2e6457"
+ },
+ "files": [
+ "config/routes/annotations.yaml"
+ ]
+ },
+ "doctrine/deprecations": {
+ "version": "1.1",
+ "recipe": {
+ "repo": "github.com/symfony/recipes",
+ "branch": "main",
+ "version": "1.0",
+ "ref": "87424683adc81d7dc305eefec1fced883084aab9"
+ }
+ },
+ "doctrine/doctrine-bundle": {
+ "version": "2.13",
+ "recipe": {
+ "repo": "github.com/symfony/recipes",
+ "branch": "main",
+ "version": "2.4",
+ "ref": "eaa0b7647c0ec3dbdcf24ade4625f381aa23c027"
+ },
+ "files": [
+ "config/packages/doctrine.yaml",
+ "src/Entity/.gitignore",
+ "src/Repository/.gitignore"
+ ]
+ },
+ "doctrine/doctrine-migrations-bundle": {
+ "version": "3.4",
+ "recipe": {
+ "repo": "github.com/symfony/recipes",
+ "branch": "main",
+ "version": "3.1",
+ "ref": "1d01ec03c6ecbd67c3375c5478c9a423ae5d6a33"
+ },
+ "files": [
+ "config/packages/doctrine_migrations.yaml",
+ "migrations/.gitignore"
+ ]
+ },
+ "phpunit/phpunit": {
+ "version": "9.6",
+ "recipe": {
+ "repo": "github.com/symfony/recipes",
+ "branch": "main",
+ "version": "9.6",
+ "ref": "6a9341aa97d441627f8bd424ae85dc04c944f8b4"
+ },
+ "files": [
+ ".env.test",
+ "phpunit.xml.dist",
+ "tests/bootstrap.php"
+ ]
+ },
+ "stof/doctrine-extensions-bundle": {
+ "version": "1.13",
+ "recipe": {
+ "repo": "github.com/symfony/recipes-contrib",
+ "branch": "main",
+ "version": "1.2",
+ "ref": "e805aba9eff5372e2d149a9ff56566769e22819d"
+ },
+ "files": [
+ "config/packages/stof_doctrine_extensions.yaml"
+ ]
+ },
+ "symfony/console": {
+ "version": "5.4",
+ "recipe": {
+ "repo": "github.com/symfony/recipes",
+ "branch": "main",
+ "version": "5.3",
+ "ref": "1781ff40d8a17d87cf53f8d4cf0c8346ed2bb461"
+ },
+ "files": [
+ "bin/console"
+ ]
+ },
+ "symfony/debug-bundle": {
+ "version": "5.4",
+ "recipe": {
+ "repo": "github.com/symfony/recipes",
+ "branch": "main",
+ "version": "5.3",
+ "ref": "5aa8aa48234c8eb6dbdd7b3cd5d791485d2cec4b"
+ },
+ "files": [
+ "config/packages/debug.yaml"
+ ]
+ },
+ "symfony/flex": {
+ "version": "1.22",
+ "recipe": {
+ "repo": "github.com/symfony/recipes",
+ "branch": "main",
+ "version": "1.0",
+ "ref": "146251ae39e06a95be0fe3d13c807bcf3938b172"
+ },
+ "files": [
+ ".env"
+ ]
+ },
+ "symfony/framework-bundle": {
+ "version": "5.4",
+ "recipe": {
+ "repo": "github.com/symfony/recipes",
+ "branch": "main",
+ "version": "5.4",
+ "ref": "3cd216a4d007b78d8554d44a5b1c0a446dab24fb"
+ },
+ "files": [
+ "config/packages/cache.yaml",
+ "config/packages/framework.yaml",
+ "config/preload.php",
+ "config/routes/framework.yaml",
+ "config/services.yaml",
+ "public/index.php",
+ "src/Controller/.gitignore",
+ "src/Kernel.php"
+ ]
+ },
+ "symfony/mailer": {
+ "version": "5.4",
+ "recipe": {
+ "repo": "github.com/symfony/recipes",
+ "branch": "main",
+ "version": "4.3",
+ "ref": "09051cfde49476e3c12cd3a0e44289ace1c75a4f"
+ },
+ "files": [
+ "config/packages/mailer.yaml"
+ ]
+ },
+ "symfony/maker-bundle": {
+ "version": "1.43",
+ "recipe": {
+ "repo": "github.com/symfony/recipes",
+ "branch": "main",
+ "version": "1.0",
+ "ref": "fadbfe33303a76e25cb63401050439aa9b1a9c7f"
+ }
+ },
+ "symfony/messenger": {
+ "version": "5.4",
+ "recipe": {
+ "repo": "github.com/symfony/recipes",
+ "branch": "main",
+ "version": "5.4",
+ "ref": "8bd5f27013fb1d7217191c548e340f0bdb11912c"
+ },
+ "files": [
+ "config/packages/messenger.yaml"
+ ]
+ },
+ "symfony/monolog-bundle": {
+ "version": "3.10",
+ "recipe": {
+ "repo": "github.com/symfony/recipes",
+ "branch": "main",
+ "version": "3.7",
+ "ref": "aff23899c4440dd995907613c1dd709b6f59503f"
+ },
+ "files": [
+ "config/packages/monolog.yaml"
+ ]
+ },
+ "symfony/notifier": {
+ "version": "5.4",
+ "recipe": {
+ "repo": "github.com/symfony/recipes",
+ "branch": "main",
+ "version": "5.0",
+ "ref": "178877daf79d2dbd62129dd03612cb1a2cb407cc"
+ },
+ "files": [
+ "config/packages/notifier.yaml"
+ ]
+ },
+ "symfony/phpunit-bridge": {
+ "version": "7.3",
+ "recipe": {
+ "repo": "github.com/symfony/recipes",
+ "branch": "main",
+ "version": "6.3",
+ "ref": "a411a0480041243d97382cac7984f7dce7813c08"
+ },
+ "files": [
+ ".env.test",
+ "bin/phpunit",
+ "phpunit.xml.dist",
+ "tests/bootstrap.php"
+ ]
+ },
+ "symfony/routing": {
+ "version": "5.4",
+ "recipe": {
+ "repo": "github.com/symfony/recipes",
+ "branch": "main",
+ "version": "5.3",
+ "ref": "85de1d8ae45b284c3c84b668171d2615049e698f"
+ },
+ "files": [
+ "config/packages/routing.yaml",
+ "config/routes.yaml"
+ ]
+ },
+ "symfony/security-bundle": {
+ "version": "5.4",
+ "recipe": {
+ "repo": "github.com/symfony/recipes",
+ "branch": "main",
+ "version": "5.3",
+ "ref": "98f1f2b0d635908c2b40f3675da2d23b1a069d30"
+ },
+ "files": [
+ "config/packages/security.yaml"
+ ]
+ },
+ "symfony/translation": {
+ "version": "5.4",
+ "recipe": {
+ "repo": "github.com/symfony/recipes",
+ "branch": "main",
+ "version": "5.3",
+ "ref": "e28e27f53663cc34f0be2837aba18e3a1bef8e7b"
+ },
+ "files": [
+ "config/packages/translation.yaml",
+ "translations/.gitignore"
+ ]
+ },
+ "symfony/twig-bundle": {
+ "version": "5.4",
+ "recipe": {
+ "repo": "github.com/symfony/recipes",
+ "branch": "main",
+ "version": "5.4",
+ "ref": "bb2178c57eee79e6be0b297aa96fc0c0def81387"
+ },
+ "files": [
+ "config/packages/twig.yaml",
+ "templates/base.html.twig"
+ ]
+ },
+ "symfony/validator": {
+ "version": "5.4",
+ "recipe": {
+ "repo": "github.com/symfony/recipes",
+ "branch": "main",
+ "version": "5.3",
+ "ref": "c32cfd98f714894c4f128bb99aa2530c1227603c"
+ },
+ "files": [
+ "config/packages/validator.yaml"
+ ]
+ },
+ "symfony/web-profiler-bundle": {
+ "version": "5.4",
+ "recipe": {
+ "repo": "github.com/symfony/recipes",
+ "branch": "main",
+ "version": "5.3",
+ "ref": "24bbc3d84ef2f427f82104f766014e799eefcc3e"
+ },
+ "files": [
+ "config/packages/web_profiler.yaml",
+ "config/routes/web_profiler.yaml"
+ ]
+ },
+ "twig/extra-bundle": {
+ "version": "v3.11.0"
+ }
+}
diff --git a/templates/address/add_address_form.html.twig b/templates/address/add_address_form.html.twig
new file mode 100644
index 0000000..16a13ea
--- /dev/null
+++ b/templates/address/add_address_form.html.twig
@@ -0,0 +1,84 @@
+{% extends 'base.html.twig' %}
+
+{% block title %}Add Address{% endblock %}
+
+{% block body %}
+
+
+
Add Address
+
+ {{ form_start(form) }}
+
+ {{ form_label(form.country) }}
+ {{ form_widget(form.country, { attr: { class: 'form-select' } }) }}
+ {% if (form_errors(form.country)) %}
+ {{ form_errors(form.country) }}
+ {% endif %}
+
+
+
+ {{ form_label(form.city) }}
+ {{ form_widget(form.city, { attr: { class: 'form-select' } }) }}
+ {% if (form_errors(form.city)) %}
+ {{ form_errors(form.city) }}
+ {% endif %}
+
+
+
+ {{ form_label(form.streetName) }}
+ {{ form_widget(form.streetName, { attr: { class: 'form-control', rows: 2 } }) }}
+ {% if (form_errors(form.streetName)) %}
+ {{ form_errors(form.streetName) }}
+ {% endif %}
+
+
+
+ {{ form_label(form.postcode) }}
+ {{ form_widget(form.postcode, { attr: { class: 'form-control', pattern: '\\d*' } }) }}
+ {% if (form_errors(form.postcode)) %}
+ {{ form_errors(form.postcode) }}
+ {% endif %}
+
+
+
Save
+
Cancel
+ {{ form_end(form) }}
+
+{% endblock %}
+
+{% block javascripts %}
+
+{% endblock %}
+
+
+
diff --git a/templates/address/address.html.twig b/templates/address/address.html.twig
new file mode 100644
index 0000000..3cedeb7
--- /dev/null
+++ b/templates/address/address.html.twig
@@ -0,0 +1,61 @@
+{% extends 'base.html.twig' %}
+
+{% block title %}Address{% endblock %}
+
+{% block body %}
+
+
+
Addresses
+
+ Add Address
+
+
+ {% for flash in app.flashes('success') %}
+
+ {{ flash }}
+
+ {% endfor %}
+
+ {% for flash in app.flashes('error') %}
+
+ {{ flash }}
+
+ {% endfor %}
+
+
+
+
+
+ ID
+ Street Name
+ Postcode
+ City
+ Country
+ Actions
+
+
+
+ {% for address in addresses %}
+
+ {{ address.id }}
+ {{ address.streetName }}
+ {{ address.postcode }}
+ {{ address.city.name }}
+ {{ address.country.name }}
+
+ Edit
+
+
+
+ {% else %}
+
+ No addresses found.
+
+ {% endfor %}
+
+
+
+
+{% endblock %}
diff --git a/templates/address/edit_address_form.html.twig b/templates/address/edit_address_form.html.twig
new file mode 100644
index 0000000..a66ae7f
--- /dev/null
+++ b/templates/address/edit_address_form.html.twig
@@ -0,0 +1,54 @@
+{% extends 'base.html.twig' %}
+
+{% block title %}Add Address{% endblock %}
+
+{% block body %}
+
+
+
Add Address
+
+ {{ form_start(form) }}
+
+ {{ form_label(form.country) }}
+ {{ form_widget(form.country, { attr: { class: 'form-select' } }) }}
+ {% if (form_errors(form.country)) %}
+ {{ form_errors(form.country) }}
+ {% endif %}
+
+
+
+ {{ form_label(form.city) }}
+ {{ form_widget(form.city, { attr: { class: 'form-select' } }) }}
+ {% if (form_errors(form.city)) %}
+ {{ form_errors(form.city) }}
+ {% endif %}
+
+
+
+ {{ form_label(form.streetName) }}
+ {{ form_widget(form.streetName, { attr: { class: 'form-control', rows: 2 } }) }}
+ {% if (form_errors(form.streetName)) %}
+ {{ form_errors(form.streetName) }}
+ {% endif %}
+
+
+
+ {{ form_label(form.postcode) }}
+ {{ form_widget(form.postcode, { attr: { class: 'form-control', pattern: '\\d*' } }) }}
+ {% if (form_errors(form.postcode)) %}
+ {{ form_errors(form.postcode) }}
+ {% endif %}
+
+
+
Save
+
Cancel
+ {{ form_end(form) }}
+
+{% endblock %}
+
+
diff --git a/templates/base.html.twig b/templates/base.html.twig
new file mode 100644
index 0000000..878f7c6
--- /dev/null
+++ b/templates/base.html.twig
@@ -0,0 +1,41 @@
+
+
+
+
+ {% block title %}Country CRUD{% endblock %}
+
+
+
+
+ {% block stylesheets %}
+
+ {% endblock %}
+
+
+
+ MyApp
+
+
+
+
+ {% block body %}{% endblock %}
+
+
+
+
+
+
+{% block javascripts %}{% endblock %}
+
+
diff --git a/templates/city/add_city_form.html.twig b/templates/city/add_city_form.html.twig
new file mode 100644
index 0000000..a7a2471
--- /dev/null
+++ b/templates/city/add_city_form.html.twig
@@ -0,0 +1,73 @@
+{% extends 'base.html.twig' %}
+
+{% block title %}Add City{% endblock %}
+
+{% block body %}
+
+
Add New City
+
+ {{ form_start(form, {'attr': {'class': 'needs-validation'}}) }}
+
+
+
+ {{ form_label(form.country, null, {'label_attr': {'class': 'form-label'}}) }}
+ {{ form_widget(form.country, {'attr': {'class': 'form-select'}}) }}
+ {% if (form_errors(form.country)) %}
+ {{ form_errors(form.country) }}
+ {% endif %}
+
+
+
+
+ {{ form_widget(form.name, {
+ 'attr': {
+ 'class': 'form-control',
+ 'name': 'cities[]',
+ 'placeholder': 'City Name',
+ 'required': true
+ }
+ }) }}
+
+ {% if (form_errors(form.name)) %}
+
{{ form_errors(form.name) }}
+ {% endif %}
+
+
+
+
+ Add more
+
+
+
+
+ {{ form_widget(form.active, {'attr': {'class': 'form-check-input'}}) }}
+ {{ form_label(form.active, null, {'label_attr': {'class': 'form-check-label'}}) }}
+
+
+
+
Save
+
Back
+
+ {{ form_end(form) }}
+
+{% endblock %}
+
+{% block javascripts %}
+
+
+{% endblock %}
diff --git a/templates/city/city.html.twig b/templates/city/city.html.twig
new file mode 100644
index 0000000..11a2e35
--- /dev/null
+++ b/templates/city/city.html.twig
@@ -0,0 +1,90 @@
+{% extends 'base.html.twig' %}
+
+{% block title %}City{% endblock %}
+
+{% block body %}
+
+
+
+ {% for flash in app.flashes('success') %}
+
+ {{ flash }}
+
+ {% endfor %}
+
+ {% for flash in app.flashes('error') %}
+
+ {{ flash }}
+
+ {% endfor %}
+
+
+
+
+
+
+
+
+ ID
+ Name
+ Country
+ Status
+ Action
+
+
+
+ {% for city in cities %}
+
+ {{ city.id }}
+ {{ city.name }}
+ {{ city.country.name }}
+
+ {% if city.active %}
+ Active
+ {% else %}
+ Not Active
+ {% endif %}
+
+
+ Edit
+
+
+
+ {% else %}
+
+ No cities found.
+
+ {% endfor %}
+
+
+
+
+{% endblock %}
diff --git a/templates/city/edit_city_form.html.twig b/templates/city/edit_city_form.html.twig
new file mode 100644
index 0000000..834e476
--- /dev/null
+++ b/templates/city/edit_city_form.html.twig
@@ -0,0 +1,50 @@
+{% extends 'base.html.twig' %}
+
+{% block title %}Edit City{% endblock %}
+
+{% block body %}
+
+
Edit City
+
+ {{ form_start(form, {'attr': {'class': 'needs-validation'}}) }}
+
+
+
+ {{ form_label(form.country, null, {'label_attr': {'class': 'form-label'}}) }}
+ {{ form_widget(form.country, {'attr': {'class': 'form-select'}}) }}
+ {% if (form_errors(form.country)) %}
+ {{ form_errors(form.country) }}
+ {% endif %}
+
+
+
+
+ {{ form_widget(form.name, {
+ 'attr': {
+ 'class': 'form-control',
+ 'name': 'cities[]',
+ 'placeholder': 'City Name',
+ 'required': true
+ }
+ }) }}
+
+ {% if (form_errors(form.name)) %}
+
{{ form_errors(form.name) }}
+ {% endif %}
+
+
+
+
+ {{ form_widget(form.active, {'attr': {'class': 'form-check-input'}}) }}
+ {{ form_label(form.active, null, {'label_attr': {'class': 'form-check-label'}}) }}
+
+
+
+
Save
+
Cancel
+
+ {{ form_end(form) }}
+
+{% endblock %}
+
+
diff --git a/templates/country/add_country_form.html.twig b/templates/country/add_country_form.html.twig
new file mode 100644
index 0000000..5796c53
--- /dev/null
+++ b/templates/country/add_country_form.html.twig
@@ -0,0 +1,18 @@
+{% extends 'base.html.twig' %}
+
+{% block title %}Add Country{% endblock %}
+
+{% block body %}
+ Add New Country
+
+ {{ form_start(form) }}
+
+ {{ form_widget(form.name) }}
+ {% if (form_errors(form.name)) %}
+ {{ form_errors(form.name) }}
+ {% endif %}
+
+ Save
+ Cancel
+ {{ form_end(form) }}
+{% endblock %}
diff --git a/templates/country/country.html.twig b/templates/country/country.html.twig
new file mode 100644
index 0000000..031ffa6
--- /dev/null
+++ b/templates/country/country.html.twig
@@ -0,0 +1,69 @@
+{% extends 'base.html.twig' %}
+
+{% block title %}Countries{% endblock %}
+
+{% block body %}
+
+
+
Country List
+
+ Add Country
+
+
+ {% for flash in app.flashes('success') %}
+
+ {{ flash }}
+
+ {% endfor %}
+
+ {% for flash in app.flashes('error') %}
+
+ {{ flash }}
+
+
+ {% endfor %}
+
+
+
+
+
+
+
+ ID
+ Name
+ Actions
+
+
+
+ {% for country in countries %}
+
+ {{ country.id }}
+ {{ country.name }}
+
+
+ Edit
+
+
+
+
+ {% else %}
+
+ No countries found.
+
+ {% endfor %}
+
+
+
+
+{% endblock %}
+
+{% block javascripts %}{% endblock %}
diff --git a/templates/country/edit_country_form.html.twig b/templates/country/edit_country_form.html.twig
new file mode 100644
index 0000000..f30f3cb
--- /dev/null
+++ b/templates/country/edit_country_form.html.twig
@@ -0,0 +1,18 @@
+{% extends 'base.html.twig' %}
+
+{% block title %}Edit Country{% endblock %}
+
+{% block body %}
+ Edit Country
+
+ {{ form_start(form) }}
+
+ {{ form_widget(form.name) }}
+ {% if (form_errors(form.name)) %}
+ {{ form_errors(form.name) }}
+ {% endif %}
+
+ Save
+ Cancel
+ {{ form_end(form) }}
+{% endblock %}
diff --git a/templates/index.html.twig b/templates/index.html.twig
new file mode 100644
index 0000000..da764ae
--- /dev/null
+++ b/templates/index.html.twig
@@ -0,0 +1,8 @@
+{% extends 'base.html.twig' %}
+
+{% block title %}Index{% endblock %}
+
+{% block body %}
+
+ Welcome to Country Crud .
+{% endblock %}
diff --git a/tests/bootstrap.php b/tests/bootstrap.php
new file mode 100644
index 0000000..8276338
--- /dev/null
+++ b/tests/bootstrap.php
@@ -0,0 +1,9 @@
+bootEnv(dirname(__DIR__).'/.env');
+}
diff --git a/translations/.gitignore b/translations/.gitignore
new file mode 100644
index 0000000..e69de29
diff --git a/webpack.config.js b/webpack.config.js
new file mode 100644
index 0000000..408012f
--- /dev/null
+++ b/webpack.config.js
@@ -0,0 +1,76 @@
+const Encore = require('@symfony/webpack-encore');
+
+// Manually configure the runtime environment if not already configured yet by the "encore" command.
+// It's useful when you use tools that rely on webpack.config.js file.
+if (!Encore.isRuntimeEnvironmentConfigured()) {
+ Encore.configureRuntimeEnvironment(process.env.NODE_ENV || 'dev');
+}
+
+Encore
+ // directory where compiled assets will be stored
+ .setOutputPath('public/build/')
+ // public path used by the web server to access the output path
+ .setPublicPath('/build')
+ // only needed for CDN's or subdirectory deploy
+ //.setManifestKeyPrefix('build/')
+
+ /*
+ * ENTRY CONFIG
+ *
+ * Each entry will result in one JavaScript file (e.g. app.js)
+ * and one CSS file (e.g. app.css) if your JavaScript imports CSS.
+ */
+ .addEntry('app', './assets/app.js')
+
+ // enables the Symfony UX Stimulus bridge (used in assets/bootstrap.js)
+ .enableStimulusBridge('./assets/controllers.json')
+
+ // When enabled, Webpack "splits" your files into smaller pieces for greater optimization.
+ .splitEntryChunks()
+
+ // will require an extra script tag for runtime.js
+ // but, you probably want this, unless you're building a single-page app
+ .enableSingleRuntimeChunk()
+
+ /*
+ * FEATURE CONFIG
+ *
+ * Enable & configure other features below. For a full
+ * list of features, see:
+ * https://symfony.com/doc/current/frontend.html#adding-more-features
+ */
+ .cleanupOutputBeforeBuild()
+ .enableBuildNotifications()
+ .enableSourceMaps(!Encore.isProduction())
+ // enables hashed filenames (e.g. app.abc123.css)
+ .enableVersioning(Encore.isProduction())
+
+ // configure Babel
+ // .configureBabel((config) => {
+ // config.plugins.push('@babel/a-babel-plugin');
+ // })
+
+ // enables and configure @babel/preset-env polyfills
+ .configureBabelPresetEnv((config) => {
+ config.useBuiltIns = 'usage';
+ config.corejs = '3.23';
+ })
+
+ // enables Sass/SCSS support
+ //.enableSassLoader()
+
+ // uncomment if you use TypeScript
+ //.enableTypeScriptLoader()
+
+ // uncomment if you use React
+ //.enableReactPreset()
+
+ // uncomment to get integrity="..." attributes on your script & link tags
+ // requires WebpackEncoreBundle 1.4 or higher
+ //.enableIntegrityHashes(Encore.isProduction())
+
+ // uncomment if you're having problems with a jQuery plugin
+ //.autoProvidejQuery()
+;
+
+module.exports = Encore.getWebpackConfig();