Feat: Make some entities soft-deletable

This commit is contained in:
Björn Fromme
2023-10-20 17:17:21 +02:00
parent 168a69c103
commit d7bbb4bb29
27 changed files with 213 additions and 63 deletions
+49
View File
@@ -0,0 +1,49 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20231020145208 extends AbstractMigration
{
public function getDescription(): string
{
return '';
}
public function up(Schema $schema): void
{
// this up() migration is auto-generated, please modify it to your needs
$this->addSql('ALTER TABLE assignment ADD deleted_at DATETIME DEFAULT NULL COMMENT \'(DC2Type:datetime_immutable)\'');
$this->addSql('ALTER TABLE availability ADD deleted_at DATETIME DEFAULT NULL COMMENT \'(DC2Type:datetime_immutable)\'');
$this->addSql('ALTER TABLE destination ADD created_at DATETIME NOT NULL COMMENT \'(DC2Type:datetime_immutable)\', ADD updated_at DATETIME DEFAULT NULL COMMENT \'(DC2Type:datetime_immutable)\', ADD deleted_at DATETIME DEFAULT NULL COMMENT \'(DC2Type:datetime_immutable)\'');
$this->addSql('ALTER TABLE disposition ADD deleted_at DATETIME DEFAULT NULL COMMENT \'(DC2Type:datetime_immutable)\'');
$this->addSql('ALTER TABLE fee ADD deleted_at DATETIME DEFAULT NULL COMMENT \'(DC2Type:datetime_immutable)\'');
$this->addSql('ALTER TABLE job_profile ADD deleted_at DATETIME DEFAULT NULL COMMENT \'(DC2Type:datetime_immutable)\'');
$this->addSql('ALTER TABLE training ADD deleted_at DATETIME DEFAULT NULL COMMENT \'(DC2Type:datetime_immutable)\'');
}
public function postUp(Schema $schema): void
{
$timestamp = (new \DateTimeImmutable())->format('Y-m-d H:i:s');
$this->connection->executeQuery('UPDATE destination set created_at=?', [$timestamp]);
}
public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql('ALTER TABLE disposition DROP deleted_at');
$this->addSql('ALTER TABLE assignment DROP deleted_at');
$this->addSql('ALTER TABLE destination DROP created_at, DROP updated_at, DROP deleted_at');
$this->addSql('ALTER TABLE job_profile DROP deleted_at');
$this->addSql('ALTER TABLE training DROP deleted_at');
$this->addSql('ALTER TABLE fee DROP deleted_at');
$this->addSql('ALTER TABLE availability DROP deleted_at');
}
}
+2
View File
@@ -94,6 +94,7 @@ class BpnImportCommand extends Command
'product' => $product,
'pickups' => json_encode($datePickups),
'country' => $hotel->getCountry(),
'updated_at' => (new \DateTimeImmutable())->format('Y-m-d H:i:s')
], [
'id' => $id,
'hotel_bus_pro_id' => $hotelBusProId,
@@ -114,6 +115,7 @@ class BpnImportCommand extends Command
'hotel_bus_pro_id' => $hotelBusProId,
'pickups' => json_encode($datePickups),
'country' => $hotel->getCountry(),
'created_at' => (new \DateTimeImmutable())->format('Y-m-d H:i:s')
])
;
++$addedCount;
@@ -0,0 +1,36 @@
<?php
namespace App\Controller\Admin\Assignment;
use App\Entity\Assignment;
use Doctrine\ORM\EntityManagerInterface;
use Psr\Log\LoggerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Http\Attribute\IsGranted;
class DeleteController extends AbstractController
{
public function __construct(
private readonly EntityManagerInterface $entityManager,
private readonly LoggerInterface $logger
) {
}
#[Route('/admin/assignment/delete/{uuid}', name: 'app_admin_assignment_delete')]
#[IsGranted('ROLE_ADMINISTRATIVE')]
public function index(Assignment $assignment): Response
{
$assignment->setDeleted();
$this->entityManager->flush();
$this->logger->info('Delete assignment', [
'assignment' => $assignment->getUuid(),
]);
$this->addFlash('success', 'Der Einsatz wurde gelöscht');
return $this->redirectToRoute('app_admin_assignment_index');
}
}
@@ -22,7 +22,7 @@ class DeleteController extends AbstractController
#[IsGranted('ROLE_ADMIN')]
public function index(Availability $availability): Response
{
$this->entityManager->remove($availability);
$availability->setDeleted();
$this->entityManager->flush();
$this->addFlash('success', 'Die Verfügbarkeit wurde gelöscht');
@@ -22,7 +22,7 @@ class DeleteController extends AbstractController
#[IsGranted('ROLE_ADMIN')]
public function index(Fee $fee): Response
{
$this->entityManager->remove($fee);
$fee->setDeleted();
$this->entityManager->flush();
$this->addFlash('success', 'Das Honorar wurde gelöscht');
@@ -17,7 +17,7 @@ class IndexController extends AbstractController
#[IsGranted('ROLE_ADMIN')]
public function index(): Response
{
$fees = $this->feeRepository->findBy([], ['name' => 'ASC']);
$fees = $this->feeRepository->getList();
return $this->render('admin/system/fee/index.html.twig', [
'fees' => $fees,
@@ -22,7 +22,7 @@ class DeleteController extends AbstractController
#[IsGranted('ROLE_ADMIN')]
public function index(JobProfile $jobProfile): Response
{
$this->entityManager->remove($jobProfile);
$jobProfile->setDeleted();
$this->entityManager->flush();
$this->addFlash('success', 'Das Job-Profil wurde gelöscht');
@@ -22,7 +22,7 @@ class DeleteController extends AbstractController
#[IsGranted('ROLE_ADMIN')]
public function index(Training $training): Response
{
$this->entityManager->remove($training);
$training->setDeleted();
$this->entityManager->flush();
$this->addFlash('success', 'Die Fortbildung wurde gelöscht');
+3 -1
View File
@@ -3,6 +3,7 @@
namespace App\Entity;
use App\Entity\Traits\BlameableEntity;
use App\Entity\Traits\SoftDeletableEntity;
use App\Entity\Traits\TimestampableEntity;
use App\Repository\AssignmentRepository;
use Carbon\CarbonPeriod;
@@ -14,10 +15,11 @@ use Symfony\Component\Uid\Uuid;
use Symfony\Component\Validator\Constraints as Assert;
#[ORM\Entity(repositoryClass: AssignmentRepository::class)]
class Assignment implements BlameableEntityInterface, TimestampableEntityInterface
class Assignment implements BlameableEntityInterface, TimestampableEntityInterface, SoftDeletableEntityInterface
{
use BlameableEntity;
use TimestampableEntity;
use SoftDeletableEntity;
#[ORM\Id]
#[ORM\GeneratedValue]
+3 -1
View File
@@ -3,6 +3,7 @@
namespace App\Entity;
use App\Entity\Traits\BlameableEntity;
use App\Entity\Traits\SoftDeletableEntity;
use App\Entity\Traits\TimestampableEntity;
use App\Repository\AvailabilityRepository;
use Doctrine\Common\Collections\ArrayCollection;
@@ -12,10 +13,11 @@ use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;
#[ORM\Entity(repositoryClass: AvailabilityRepository::class)]
class Availability implements BlameableEntityInterface, TimestampableEntityInterface
class Availability implements BlameableEntityInterface, TimestampableEntityInterface, SoftDeletableEntityInterface
{
use BlameableEntity;
use TimestampableEntity;
use SoftDeletableEntity;
#[ORM\Id]
#[ORM\GeneratedValue]
+6 -1
View File
@@ -2,13 +2,18 @@
namespace App\Entity;
use App\Entity\Traits\SoftDeletableEntity;
use App\Entity\Traits\TimestampableEntity;
use App\Repository\DestinationRepository;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity(repositoryClass: DestinationRepository::class)]
class Destination
class Destination implements TimestampableEntityInterface, SoftDeletableEntityInterface
{
use TimestampableEntity;
use SoftDeletableEntity;
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
+3 -1
View File
@@ -3,6 +3,7 @@
namespace App\Entity;
use App\Entity\Traits\BlameableEntity;
use App\Entity\Traits\SoftDeletableEntity;
use App\Entity\Traits\TimestampableEntity;
use App\Repository\DispositionRepository;
use Doctrine\Common\Collections\ArrayCollection;
@@ -12,10 +13,11 @@ use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Uid\Uuid;
#[ORM\Entity(repositoryClass: DispositionRepository::class)]
class Disposition implements BlameableEntityInterface, TimestampableEntityInterface
class Disposition implements BlameableEntityInterface, TimestampableEntityInterface, SoftDeletableEntityInterface
{
use BlameableEntity;
use TimestampableEntity;
use SoftDeletableEntity;
public const STATUS_NEW = 'new';
public const STATUS_CHECKING_CONTRACT = 'checking_contract';
+3 -1
View File
@@ -3,16 +3,18 @@
namespace App\Entity;
use App\Entity\Traits\BlameableEntity;
use App\Entity\Traits\SoftDeletableEntity;
use App\Entity\Traits\TimestampableEntity;
use App\Repository\FeeRepository;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;
#[ORM\Entity(repositoryClass: FeeRepository::class)]
class Fee implements BlameableEntityInterface, TimestampableEntityInterface
class Fee implements BlameableEntityInterface, TimestampableEntityInterface, SoftDeletableEntityInterface
{
use BlameableEntity;
use TimestampableEntity;
use SoftDeletableEntity;
#[ORM\Id]
#[ORM\GeneratedValue]
+3 -1
View File
@@ -3,6 +3,7 @@
namespace App\Entity;
use App\Entity\Traits\BlameableEntity;
use App\Entity\Traits\SoftDeletableEntity;
use App\Entity\Traits\TimestampableEntity;
use App\Repository\JobProfileRepository;
use Doctrine\Common\Collections\ArrayCollection;
@@ -13,10 +14,11 @@ use Symfony\Component\Uid\Uuid;
use Symfony\Component\Validator\Constraints as Assert;
#[ORM\Entity(repositoryClass: JobProfileRepository::class)]
class JobProfile implements BlameableEntityInterface, TimestampableEntityInterface
class JobProfile implements BlameableEntityInterface, TimestampableEntityInterface, SoftDeletableEntityInterface
{
use BlameableEntity;
use TimestampableEntity;
use SoftDeletableEntity;
#[ORM\Id]
#[ORM\GeneratedValue]
@@ -0,0 +1,10 @@
<?php
namespace App\Entity;
interface SoftDeletableEntityInterface
{
public function getDeletedAt(): ?\DateTimeImmutable;
public function setDeletedAt(\DateTimeImmutable $createdAt): static;
}
+4 -4
View File
@@ -4,11 +4,11 @@ namespace App\Entity;
interface TimestampableEntityInterface
{
public function getCreatedAt();
public function getCreatedAt(): \DateTimeImmutable;
public function setCreatedAt(\DateTimeImmutable $createdAt);
public function setCreatedAt(\DateTimeImmutable $createdAt): static;
public function getUpdatedAt();
public function getUpdatedAt(): ?\DateTimeImmutable;
public function setUpdatedAt(\DateTimeImmutable $updatedAt);
public function setUpdatedAt(\DateTimeImmutable $updatedAt): static;
}
+3 -1
View File
@@ -3,6 +3,7 @@
namespace App\Entity;
use App\Entity\Traits\BlameableEntity;
use App\Entity\Traits\SoftDeletableEntity;
use App\Entity\Traits\TimestampableEntity;
use App\Repository\TrainingRepository;
use Doctrine\Common\Collections\ArrayCollection;
@@ -10,10 +11,11 @@ use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity(repositoryClass: TrainingRepository::class)]
class Training implements BlameableEntityInterface, TimestampableEntityInterface
class Training implements BlameableEntityInterface, TimestampableEntityInterface, SoftDeletableEntityInterface
{
use BlameableEntity;
use TimestampableEntity;
use SoftDeletableEntity;
#[ORM\Id]
#[ORM\GeneratedValue]
+35
View File
@@ -0,0 +1,35 @@
<?php
namespace App\Entity\Traits;
use Doctrine\ORM\Mapping as ORM;
trait SoftDeletableEntity
{
#[ORM\Column(nullable: true)]
private ?\DateTimeImmutable $deletedAt = null;
public function getDeletedAt(): \DateTimeImmutable
{
return $this->deletedAt;
}
public function setDeletedAt(\DateTimeImmutable $deletedAt): static
{
$this->deletedAt = $deletedAt;
return $this;
}
public function setDeleted(): static
{
$this->setDeletedAt(new \DateTimeImmutable());
return $this;
}
public function isDeleted(): bool
{
return null !== $this->getDeletedAt();
}
}
+4
View File
@@ -78,6 +78,10 @@ class AssignmentType extends AbstractType
return $fee->getName();
},
'query_builder' => function (EntityRepository $repository) {
$qb = $repository->createQueryBuilder('fee');
return $qb->where($qb->expr()->isNull('fee.deletedAt'));
},
'multiple' => true,
'expanded' => true,
])
+5
View File
@@ -5,6 +5,7 @@ namespace App\Form;
use App\Entity\JobProfile;
use App\Entity\Teamer;
use App\Entity\Training;
use Doctrine\ORM\EntityRepository;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
@@ -49,6 +50,10 @@ class TeamerJobProfileType extends AbstractType
return ['disabled' => 'disabled'];
},
'query_builder' => function (EntityRepository $repository) {
$qb = $repository->createQueryBuilder('job_profile');
return $qb->where($qb->expr()->isNull('job_profile.deletedAt'));
},
'expanded' => true,
'multiple' => true,
])
+5
View File
@@ -32,6 +32,7 @@ class AssignmentRepository extends ServiceEntityRepository
->select('assignment', 'destination', 'job_profile', 'application', 'disposition')
->innerJoin('assignment.destination', 'destination')
->innerJoin('assignment.jobProfile', 'job_profile')
->where($qb->expr()->isNull('assignment.deletedAt'))
;
if (null !== $teamer) {
@@ -61,6 +62,7 @@ class AssignmentRepository extends ServiceEntityRepository
->innerJoin('assignment.teamers', 'teamer', Join::WITH, 'teamer = :teamer')
->leftJoin('assignment.applications', 'application', Join::WITH, 'application.teamer = :teamer')
->leftJoin('assignment.dispositions', 'disposition', Join::WITH, 'disposition.teamer = :teamer')
->where($qb->expr()->isNull('assignment.deletedAt'))
->setParameter('teamer', $teamer)
;
@@ -76,6 +78,7 @@ class AssignmentRepository extends ServiceEntityRepository
$result = $qb
->select('MIN(destination.dateFrom) minDate', 'MAX(destination.dateTo) maxDate')
->innerJoin('assignment.destination', 'destination')
->where($qb->expr()->isNull('assignment.deletedAt'))
->getQuery()
->getSingleResult()
;
@@ -89,6 +92,7 @@ class AssignmentRepository extends ServiceEntityRepository
->select('job_profile.id', 'job_profile.name')
->innerJoin('assignment.jobProfile', 'job_profile')
->orderBy('job_profile.name', 'ASC')
->where($qb->expr()->isNull('assignment.deletedAt'))
->getQuery()
->getArrayResult()
;
@@ -102,6 +106,7 @@ class AssignmentRepository extends ServiceEntityRepository
$result = $qb
->select('destination.id id', 'destination.product product', 'destination.hotel hotel')
->innerJoin('assignment.destination', 'destination')
->where($qb->expr()->isNull('assignment.deletedAt'))
->orderBy('destination.product', 'ASC')
->groupBy('destination.id')
->getQuery()
+4 -2
View File
@@ -30,7 +30,8 @@ class AvailabilityRepository extends ServiceEntityRepository
return $qb
->where($qb->expr()->andX(
$qb->expr()->gt('availability.dateFrom', ':today'),
$qb->expr()->isNull('availability.owner')
$qb->expr()->isNull('availability.owner'),
$qb->expr()->isNull('availability.deletedAt')
))
->orderBy('availability.dateFrom', 'ASC')
->setParameter('today', new \DateTimeImmutable())
@@ -53,7 +54,8 @@ class AvailabilityRepository extends ServiceEntityRepository
$qb->expr()->orX(
$qb->expr()->isNull('availability.owner'),
$qb->expr()->eq('availability.owner', ':owner')
)
),
$qb->expr()->isNull('availability.deletedAt')
))
->orderBy('availability.dateFrom', 'ASC')
->setParameter('teamer', $teamer)
+10 -6
View File
@@ -29,12 +29,16 @@ class DestinationRepository extends ServiceEntityRepository
$qb = $this->createQueryBuilder('destination');
$dates = $qb
->where($qb->expr()->orX(
$qb->expr()->like('destination.product', ':search'),
$qb->expr()->like('destination.hotel', ':search'),
$qb->expr()->like('DATE_FORMAT(destination.dateFrom, \'%d.%m.%y\')', ':search'),
$qb->expr()->like('DATE_FORMAT(destination.dateTo, \'%d.%m.%y\')', ':search')
))
->where(
$qb->expr()->andX(
$qb->expr()->isNull('destination.deletedAt'),
$qb->expr()->orX(
$qb->expr()->like('destination.product', ':search'),
$qb->expr()->like('destination.hotel', ':search'),
$qb->expr()->like('DATE_FORMAT(destination.dateFrom, \'%d.%m.%y\')', ':search'),
$qb->expr()->like('DATE_FORMAT(destination.dateTo, \'%d.%m.%y\')', ':search')
))
)
->setParameter('search', '%'.$this->escapeLikeWildcards($search).'%')
->orderBy('destination.dateFrom', 'ASC')
->addOrderBy('destination.product', 'ASC')
+8 -39
View File
@@ -21,46 +21,15 @@ class FeeRepository extends ServiceEntityRepository
parent::__construct($registry, Fee::class);
}
public function save(Fee $entity, bool $flush = false): void
public function getList(): array
{
$this->getEntityManager()->persist($entity);
$qb = $this->createQueryBuilder('fee');
if ($flush) {
$this->getEntityManager()->flush();
}
return $qb
->where($qb->expr()->isNull('fee.deletedAt'))
->orderBy('fee.name', 'ASC')
->getQuery()
->getResult()
;
}
public function remove(Fee $entity, bool $flush = false): void
{
$this->getEntityManager()->remove($entity);
if ($flush) {
$this->getEntityManager()->flush();
}
}
// /**
// * @return Fee[] Returns an array of Fee objects
// */
// public function findByExampleField($value): array
// {
// return $this->createQueryBuilder('f')
// ->andWhere('f.exampleField = :val')
// ->setParameter('val', $value)
// ->orderBy('f.id', 'ASC')
// ->setMaxResults(10)
// ->getQuery()
// ->getResult()
// ;
// }
// public function findOneBySomeField($value): ?Fee
// {
// return $this->createQueryBuilder('f')
// ->andWhere('f.exampleField = :val')
// ->setParameter('val', $value)
// ->getQuery()
// ->getOneOrNullResult()
// ;
// }
}
+1
View File
@@ -28,6 +28,7 @@ class JobProfileRepository extends ServiceEntityRepository
return $qb
->select('job_profile', 'required_training')
->leftJoin('job_profile.requiredTrainings', 'required_training')
->where($qb->expr()->isNull('job_profile.deletedAt'))
->orderBy('job_profile.name', 'ASC')
->getQuery()
->getResult()
+1
View File
@@ -28,6 +28,7 @@ class TrainingRepository extends ServiceEntityRepository
return $qb
->select('training', 'training_attendance')
->leftJoin('training.trainingAttendances', 'training_attendance')
->where($qb->expr()->isNull('training.deletedAt'))
->orderBy('training.name', 'ASC')
->getQuery()
->getResult()
@@ -65,6 +65,16 @@
</td>
<td>
<div class="flex items-center space-x-1 justify-end">
<button type="button"
class="text-red-500"
{{ stimulus_controller('modal-button', [], [], {'confirmation-modal': '#confirmation-modal'}) }}
{{ stimulus_action('modal-button', 'confirmation', null, {
'title': 'Bist du sicher?',
'content': 'Möchtest du den Einsatz wirklich löschen?',
'target-url': path('app_admin_assignment_delete', { 'uuid': assignment.uuid })
}) }}>
{{ icon('delete') }}
</button>
<a href="{{ path('app_admin_assignment_duplicate', { 'uuid': assignment.uuid, 'r': return_url() }) }}">
{{ icon('copy', 'w-5 h-5') }}
</a>