Skip to content

Commit

Permalink
feat: project category relation crud (#27)
Browse files Browse the repository at this point in the history
* feat(ProjectHelper): add getProjectAndCategoriesInfos

- Help to show and format project unarchived categories

see: #19

* feat(ProjectHandler): add project unarchived categories infos to handleGetProject

see: #19

* fix(Helper): use nullsafe operator in projectExists/categoryExists

* feat(categoryHelper): add getCategoryAndProjectsInfos

see: #19

* feat(CategoryHandler): add category unarchived projects infos to handleGetCategory

see: #19

* chore: keep feat wip

see: #19

* fix(ProjectMapper): fromEntityToDTO circular reference

* feat(projectValidator): add project categories validation methods

see: #19

* fix(Entities): removal when entity is still linked elsewhere

see: #19

* refac(ProjectHandler): use Mapper::FromEntityToJson instead of Helper::getProjectAndCategoriesInfos

see: #19

* feat(ProjectPersister): persist Category relations

see: #19

* feat(CategoryHelper): add getProjectsArrayFromCategory

see: #19

* refac(CategoryHandler): use Mapper:FromEntityToJson instead of Helper::getCategoryAndProjectsnfos

see: #19

* feat(Category): add projects to DTO/Mapper

* chore: format

* fix(ProjectValidator): CATEGORY_SLUG_INVALID typo

* feat(CategoryValidator): add category projects validation

see: #19

* feat(CategoryPersister): add Project relations

see: #19

* chore(deps): upgrade js dependencies

closes: #21

* feat(CategoryArchiver): remove category from related project

see: #19

* feat(ProjectArchiver): remove project from related category

close: #19
  • Loading branch information
n3wborn authored Nov 1, 2023
1 parent c6488f2 commit 2f8c2a0
Show file tree
Hide file tree
Showing 18 changed files with 457 additions and 209 deletions.
2 changes: 1 addition & 1 deletion src/Controller/ProjectController.php
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ public function getAllProjects(): JsonResponse
}

#[Route('/project/{slug}', name: self::ROUTE_ARCHIVE, methods: Request::METHOD_DELETE)]
public function classeArchive(?Project $project): JsonResponse
public function archiveProject(?Project $project): JsonResponse
{
return $this->handler->handleArchiveProject($project);
}
Expand Down
2 changes: 1 addition & 1 deletion src/Entity/Category.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ class Category
#[ORM\Column(nullable: true, type: 'datetime_immutable')]
private ?\DateTimeImmutable $updatedAt = null;

#[ORM\ManyToMany(targetEntity: Project::class, inversedBy: 'categories', cascade: ['persist'], fetch: 'EAGER', orphanRemoval: true)]
#[ORM\ManyToMany(targetEntity: Project::class, inversedBy: 'categories', fetch: 'EAGER')]
private Collection $projects;

public function __construct()
Expand Down
2 changes: 1 addition & 1 deletion src/Entity/Project.php
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ class Project
#[ORM\Column(nullable: true, type: 'datetime_immutable')]
private ?\DateTimeImmutable $updatedAt = null;

#[ORM\ManyToMany(targetEntity: Category::class, mappedBy: 'projects', cascade: ['persist'], fetch: 'EAGER', orphanRemoval: true)]
#[ORM\ManyToMany(targetEntity: Category::class, mappedBy: 'projects', fetch: 'EAGER')]
private Collection $categories;

public function __construct()
Expand Down
8 changes: 5 additions & 3 deletions src/Service/Category/CategoryArchiver.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +14,19 @@
final class CategoryArchiver
{
public function __construct(
private EntityManagerInterface $entityManager,
private EntityManagerInterface $em,
private CategoryValidator $validator,
private CategoryMapper $mapper,
private ExceptionLogger $logger,
private CategoryPersister $persister,
) {
}

public function softDelete(Category $category): void
{
$this->entityManager->persist($category->setArchivedAt(new \DateTimeImmutable()));
$this->entityManager->flush();
$this->persister->removeCategoryFromProjects($category);
$this->em->persist($category->setArchivedAt(new \DateTimeImmutable()));
$this->em->flush();
}

public function process(?Category $category): JsonResponse
Expand Down
13 changes: 13 additions & 0 deletions src/Service/Category/CategoryDTO.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ final class CategoryDTO
public function __construct(
private string $name = '',
private ?string $slug = null,
private array $projects = [],
) {
}

Expand All @@ -33,4 +34,16 @@ public function setSlug(string $slug): self

return $this;
}

public function getProjects(): array
{
return $this->projects;
}

public function setProjects(array $projects): self
{
$this->projects = $projects;

return $this;
}
}
5 changes: 3 additions & 2 deletions src/Service/Category/CategoryHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,9 @@ public function handleGetCategory(?Category $category): JsonResponse
null === $category
&& throw new NotFoundException(ApiMessages::translate(ApiMessages::CATEGORY_NOT_FOUND));

$result = $this->finder->get($category);
$response = new ApiResponse(CategoryMapper::fromEntityToJson($result));
$categoryInfos = $this->finder->get($category);
$result = CategoryMapper::fromEntityToJson($categoryInfos);
$response = new ApiResponse($result);
} catch (NotFoundException $exception) {
$this->logger->logNotice($exception);
$response = ApiResponse::createWarningMessage($exception->getMessage());
Expand Down
17 changes: 14 additions & 3 deletions src/Service/Category/CategoryHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ public static function isEditRoute(Request $request): bool
return CategoryController::ROUTE_EDIT === $request->get('_route');
}

/** @throws NotFoundException */
/** @throws NotFoundException */
public function editSlugParamExists(Request $request): ?Category
{
return isset($request->get('_route_params')['slug'])
Expand All @@ -38,7 +38,7 @@ public static function generateEditSuccessMessage(Request $request): string
: ApiMessages::CATEGORY_CREATE_SUCCESS_MESSAGE;
}

/** @throws NotFoundException|BadDataException */
/** @throws NotFoundException|BadDataException */
public function validateRequestResource(Request $request, category $category): void
{
(self::isEditRoute($request) && (null === $this->editSlugParamExists($request)))
Expand All @@ -58,6 +58,17 @@ public static function findNotArchivedFromProject(Project $project): array

public static function categoryExists(?Category $category): bool
{
return (!$category->isArchived()) && (null !== $category);
return (!$category?->isArchived()) && (null !== $category);
}

public static function getProjectsArrayFromCategory(Category $category): array
{
return array_map(
static fn (Project $project) => [
'name' => $project->getName(),
'slug' => $project->getSlug(),
],
$category->getProjects()->toArray()
);
}
}
1 change: 1 addition & 0 deletions src/Service/Category/CategoryMapper.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ public static function fromEntityToDTO(Category $category): CategoryDTO
return new CategoryDTO(
$category->getName(),
$category->getSlug(),
CategoryHelper::getProjectsArrayFromCategory($category)
);
}
}
37 changes: 33 additions & 4 deletions src/Service/Category/CategoryPersister.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace App\Service\Category;

use App\Entity\Category;
use App\Entity\Project;
use App\Exception\BadDataException;
use App\Exception\NotFoundException;
use App\Helper\ApiMessages;
Expand Down Expand Up @@ -44,12 +45,40 @@ public function processRequest(Category $category, CategoryDTO $dto, Request $re
return $response;
}

/** @throws NotFoundException */
public function persist(?Category $project, CategoryDTO $dto): void
/** @throws NotFoundException */
public function persist(Category $category, CategoryDTO $dto): void
{
$project->setName($dto->getName());
$category->setName($dto->getName());

/** @var Project $project */
foreach ($category->getProjects() as $project) {
$category->removeProject($project);
$project->removeCategory($category);
}

foreach ($dto->getProjects() as $dtoProject) {
/** @var Project $project */
$project = $this->em->getRepository(Project::class)->findOneBySlug($dtoProject['slug']);
$category->addProject($project);
$project->addCategory($category);
}

$this->em->persist($category);
$this->em->flush();
}

public function removeCategoryFromProjects(Category $category): void
{
array_map(
function (Project $project) use ($category) {
$project->removeCategory($category);
$category->removeProject($project);
$this->em->persist($project);
$this->em->persist($category);
},
$category->getProjects()->toArray()
);

$this->em->persist($project);
$this->em->flush();
}
}
77 changes: 75 additions & 2 deletions src/Service/Category/CategoryValidator.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,28 +7,101 @@
use App\Exception\NotFoundException;
use App\Helper\ApiMessages;
use App\Repository\CategoryRepository;
use App\Repository\ProjectRepository;

final class CategoryValidator
{
public const CATEGORY_ALREADY_ARCHIVED = 'La catégorie est inexistante ou a déja été supprimée';
public const NAME_SHOULD_BE_UNIQUE = 'La catégorie doit avoir un nom unique';
public const NAME_SHOULD_NOT_BE_EMPTY = 'Le champ Nom ne peut être vide';
public const PROJECT_SLUG_INVALID = 'Au moins un des projets est invalide';

public function __construct(
private CategoryRepository $categoryRepository,
private ProjectRepository $projectRepository,
) {
}

/** @throws BadDataException*/
/** @throws BadDataException */
public function validate(CategoryDTO $dto, bool $isEditRoute = true): void
{
$this->validateNameNotEmpty($dto->getName());
$this
->validateNameNotEmpty($dto->getName())
->validateProjects($dto)
;

$isEditRoute
? $this->validateEditionName($dto)
: $this->validateCreationName($dto);
}

/** @throws BadDataException */
private function validateProjects(CategoryDTO $dto): self
{
foreach ($dto->getProjects() as $project) {
$this->validateProject($project);
}

return $this;
}

/** @throws BadDataException */
private function validateProject(mixed $data): self
{
$this
->validateProjectFormat($data)
->validateProjectIsNotEmptyString($data)
->validateProjectSlugIsValidEntity($data);

return $this;
}

private function validateProjectFormat(mixed $data): self
{
(!is_array($data) || !array_key_exists('slug', $data))
&& throw new BadDataException(self::PROJECT_SLUG_INVALID);

return $this;
}

private function validateProjectIsNotEmptyString(mixed $projectSlug): self
{
(empty($projectSlug['slug']) || !is_string($projectSlug['slug']))
&& throw new BadDataException(self::PROJECT_SLUG_INVALID);

return $this;
}

/** @throws BadDataException */
private function validateProjectSlugIsValidEntity(mixed $data): self
{
$slug = $data['slug'];

$this
->validateProjectExists($slug)
->validateProjectIsNotArchived($slug);

return $this;
}

/** @throws BadDataException */
private function validateProjectExists(string $slug): self
{
(null === $this->projectRepository->findOneBy(['slug' => $slug]))
&& throw new NotFoundException(self::PROJECT_SLUG_INVALID);

return $this;
}

/** @throws BadDataException */
private function validateProjectIsNotArchived(string $slug): self
{
(null !== $this->projectRepository->findOneBy(['slug' => $slug])->getArchivedAt())
&& throw new NotFoundException(self::PROJECT_SLUG_INVALID);

return $this;
}

/** @throws BadDataException */
public function validateCreationName(CategoryDTO $dto): self
{
Expand Down
2 changes: 2 additions & 0 deletions src/Service/Project/ProjectArchiver.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,13 @@ public function __construct(
private ProjectValidator $validator,
private ProjectMapper $mapper,
private ExceptionLogger $logger,
private ProjectPersister $persister,
) {
}

public function softDelete(Project $project): void
{
$this->persister->removeProjectFromCategories($project);
$this->entityManager->persist($project->setArchivedAt(new \DateTimeImmutable()));
$this->entityManager->flush();
}
Expand Down
13 changes: 13 additions & 0 deletions src/Service/Project/ProjectDTO.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ public function __construct(
private string $name = '',
private string $description = '',
private ?string $slug = null,
private array $categories = [],
) {
}

Expand Down Expand Up @@ -46,4 +47,16 @@ public function setDescription(string $description): self

return $this;
}

public function getCategories(): array
{
return $this->categories;
}

public function setCategories(array $categories): self
{
$this->categories = $categories;

return $this;
}
}
5 changes: 3 additions & 2 deletions src/Service/Project/ProjectHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,9 @@ public function handleGetProject(?Project $project): JsonResponse
null === $project
&& throw new NotFoundException(ApiMessages::translate(ApiMessages::PROJECT_NOT_FOUND));

$result = $this->finder->get($project);
$response = new ApiResponse(ProjectMapper::fromEntityToJson($result));
$projectInfos = $this->finder->get($project);
$result = ProjectMapper::fromEntityToJson($projectInfos);
$response = new ApiResponse($result);
} catch (NotFoundException $exception) {
$this->logger->logNotice($exception);
$response = ApiResponse::createWarningMessage($exception->getMessage());
Expand Down
18 changes: 15 additions & 3 deletions src/Service/Project/ProjectHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace App\Service\Project;

use App\Controller\ProjectController;
use App\Entity\Category;
use App\Entity\Project;
use App\Exception\BadDataException;
use App\Exception\NotFoundException;
Expand All @@ -22,7 +23,7 @@ public static function isEditRoute(Request $request): bool
return ProjectController::ROUTE_EDIT === $request->get('_route');
}

/** @throws NotFoundException */
/** @throws NotFoundException */
public function editSlugParamExists(Request $request): ?Project
{
return isset($request->get('_route_params')['slug'])
Expand All @@ -37,7 +38,7 @@ public static function generateEditSuccessMessage(Request $request): string
: ApiMessages::PROJECT_CREATE_SUCCESS_MESSAGE;
}

/** @throws NotFoundException|BadDataException */
/** @throws NotFoundException|BadDataException */
public function validateRequestResource(Request $request, Project $project): void
{
(self::isEditRoute($request) && (null === $this->editSlugParamExists($request)))
Expand All @@ -49,6 +50,17 @@ public function validateRequestResource(Request $request, Project $project): voi

public static function projectExists(?Project $project): bool
{
return (!$project->isArchived()) && (null !== $project);
return (!$project?->isArchived()) && (null !== $project);
}

public static function getCategoriesArrayFromProject(Project $project): array
{
return array_map(
static fn (Category $category) => [
'name' => $category->getName(),
'slug' => $category->getSlug(),
],
$project->getCategories()->toArray()
);
}
}
1 change: 1 addition & 0 deletions src/Service/Project/ProjectMapper.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ public static function fromEntityToDTO(Project $project): ProjectDTO
$project->getName(),
$project->getDescription(),
$project->getSlug(),
ProjectHelper::getCategoriesArrayFromProject($project)
);
}
}
Loading

0 comments on commit 2f8c2a0

Please sign in to comment.