Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: project category relation crud #27

Merged
merged 21 commits into from
Nov 1, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
3792b82
feat(ProjectHelper): add getProjectAndCategoriesInfos
n3wborn Sep 26, 2023
3752b53
feat(ProjectHandler): add project unarchived categories infos to hand…
n3wborn Sep 26, 2023
1a724b0
fix(Helper): use nullsafe operator in projectExists/categoryExists
n3wborn Oct 2, 2023
f151df4
feat(categoryHelper): add getCategoryAndProjectsInfos
n3wborn Oct 2, 2023
22ecd59
feat(CategoryHandler): add category unarchived projects infos to hand…
n3wborn Oct 2, 2023
0c3550b
chore: keep feat wip
n3wborn Oct 9, 2023
6d82cf2
fix(ProjectMapper): fromEntityToDTO circular reference
n3wborn Oct 12, 2023
ba48796
feat(projectValidator): add project categories validation methods
n3wborn Oct 12, 2023
59915c7
fix(Entities): removal when entity is still linked elsewhere
n3wborn Oct 17, 2023
e7d915d
refac(ProjectHandler): use Mapper::FromEntityToJson instead of Helper…
n3wborn Oct 17, 2023
fd7e538
feat(ProjectPersister): persist Category relations
n3wborn Oct 17, 2023
041a486
feat(CategoryHelper): add getProjectsArrayFromCategory
n3wborn Oct 17, 2023
7945981
refac(CategoryHandler): use Mapper:FromEntityToJson instead of Helper…
n3wborn Oct 17, 2023
bdcc0e9
feat(Category): add projects to DTO/Mapper
n3wborn Oct 17, 2023
ab59e5d
chore: format
n3wborn Oct 17, 2023
f524bb8
fix(ProjectValidator): CATEGORY_SLUG_INVALID typo
n3wborn Oct 17, 2023
b075052
feat(CategoryValidator): add category projects validation
n3wborn Oct 17, 2023
945c87c
feat(CategoryPersister): add Project relations
n3wborn Oct 17, 2023
ed1d1dc
chore(deps): upgrade js dependencies
n3wborn Oct 19, 2023
dc71d51
feat(CategoryArchiver): remove category from related project
n3wborn Oct 19, 2023
6025dfe
feat(ProjectArchiver): remove project from related category
n3wborn Oct 25, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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