lifecycle events for projects (#2013)
This commit is contained in:
@@ -20,7 +20,7 @@ services:
|
||||
# this creates a service per class whose id is the fully-qualified class name
|
||||
App\:
|
||||
resource: '../src/*'
|
||||
exclude: '../src/{DependencyInjection,Entity,Migrations,Tests,Kernel.php,Constants.php}'
|
||||
exclude: '../src/{DependencyInjection,Entity,Event,Migrations,Tests,Kernel.php,Constants.php}'
|
||||
|
||||
# controllers are imported separately to make sure services can be injected
|
||||
# as action arguments even if you don't extend any base controller class
|
||||
|
||||
@@ -17,6 +17,7 @@ use App\Entity\User;
|
||||
use App\Event\ProjectMetaDefinitionEvent;
|
||||
use App\Form\API\ProjectApiEditForm;
|
||||
use App\Form\API\ProjectRateApiForm;
|
||||
use App\Project\ProjectService;
|
||||
use App\Repository\ProjectRateRepository;
|
||||
use App\Repository\ProjectRepository;
|
||||
use App\Repository\Query\ProjectQuery;
|
||||
@@ -64,13 +65,18 @@ class ProjectController extends BaseApiController
|
||||
* @var ProjectRateRepository
|
||||
*/
|
||||
private $projectRateRepository;
|
||||
/**
|
||||
* @var ProjectService
|
||||
*/
|
||||
private $projectService;
|
||||
|
||||
public function __construct(ViewHandlerInterface $viewHandler, ProjectRepository $repository, EventDispatcherInterface $dispatcher, ProjectRateRepository $projectRateRepository)
|
||||
public function __construct(ViewHandlerInterface $viewHandler, ProjectRepository $repository, EventDispatcherInterface $dispatcher, ProjectRateRepository $projectRateRepository, ProjectService $projectService)
|
||||
{
|
||||
$this->viewHandler = $viewHandler;
|
||||
$this->repository = $repository;
|
||||
$this->dispatcher = $dispatcher;
|
||||
$this->projectRateRepository = $projectRateRepository;
|
||||
$this->projectService = $projectService;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -216,10 +222,7 @@ class ProjectController extends BaseApiController
|
||||
throw new AccessDeniedHttpException('User cannot create projects');
|
||||
}
|
||||
|
||||
$project = new Project();
|
||||
|
||||
$event = new ProjectMetaDefinitionEvent($project);
|
||||
$this->dispatcher->dispatch($event);
|
||||
$project = $this->projectService->createNewProject();
|
||||
|
||||
$form = $this->createForm(ProjectApiEditForm::class, $project, [
|
||||
'date_format' => self::DATE_FORMAT,
|
||||
@@ -229,7 +232,7 @@ class ProjectController extends BaseApiController
|
||||
$form->submit($request->request->all());
|
||||
|
||||
if ($form->isValid()) {
|
||||
$this->repository->saveProject($project);
|
||||
$this->projectService->saveNewProject($project);
|
||||
|
||||
$view = new View($project, 200);
|
||||
$view->getContext()->setGroups(self::GROUPS_ENTITY);
|
||||
@@ -301,7 +304,7 @@ class ProjectController extends BaseApiController
|
||||
return $this->viewHandler->handle($view);
|
||||
}
|
||||
|
||||
$this->repository->saveProject($project);
|
||||
$this->projectService->updateProject($project);
|
||||
|
||||
$view = new View($project, Response::HTTP_OK);
|
||||
$view->getContext()->setGroups(self::GROUPS_ENTITY);
|
||||
@@ -354,7 +357,7 @@ class ProjectController extends BaseApiController
|
||||
|
||||
$meta->setValue($value);
|
||||
|
||||
$this->repository->saveProject($project);
|
||||
$this->projectService->updateProject($project);
|
||||
|
||||
$view = new View($project, 200);
|
||||
$view->getContext()->setGroups(self::GROUPS_ENTITY);
|
||||
|
||||
@@ -29,6 +29,7 @@ use App\Form\ProjectTeamPermissionForm;
|
||||
use App\Form\Toolbar\ProjectToolbarForm;
|
||||
use App\Form\Type\ProjectType;
|
||||
use App\Project\ProjectDuplicationService;
|
||||
use App\Project\ProjectService;
|
||||
use App\Repository\ActivityRepository;
|
||||
use App\Repository\ProjectRateRepository;
|
||||
use App\Repository\ProjectRepository;
|
||||
@@ -40,9 +41,7 @@ use Pagerfanta\Pagerfanta;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\Form\FormInterface;
|
||||
use Symfony\Component\HttpFoundation\RedirectResponse;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
|
||||
/**
|
||||
@@ -65,12 +64,17 @@ final class ProjectController extends AbstractController
|
||||
* @var EventDispatcherInterface
|
||||
*/
|
||||
private $dispatcher;
|
||||
/**
|
||||
* @var ProjectService
|
||||
*/
|
||||
private $projectService;
|
||||
|
||||
public function __construct(ProjectRepository $repository, FormConfiguration $configuration, EventDispatcherInterface $dispatcher)
|
||||
public function __construct(ProjectRepository $repository, FormConfiguration $configuration, EventDispatcherInterface $dispatcher, ProjectService $projectService)
|
||||
{
|
||||
$this->repository = $repository;
|
||||
$this->configuration = $configuration;
|
||||
$this->dispatcher = $dispatcher;
|
||||
$this->projectService = $projectService;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -129,7 +133,7 @@ final class ProjectController extends AbstractController
|
||||
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
try {
|
||||
$this->repository->saveProject($project);
|
||||
$this->projectService->updateProject($project);
|
||||
$this->flashSuccess('action.update.success');
|
||||
|
||||
return $this->redirectToRoute('admin_project');
|
||||
@@ -151,13 +155,33 @@ final class ProjectController extends AbstractController
|
||||
*/
|
||||
public function createAction(Request $request, ?Customer $customer = null)
|
||||
{
|
||||
$project = new Project();
|
||||
$project = $this->projectService->createNewProject($customer);
|
||||
|
||||
if (null !== $customer) {
|
||||
$project->setCustomer($customer);
|
||||
$editForm = $this->createEditForm($project);
|
||||
$editForm->handleRequest($request);
|
||||
|
||||
if ($editForm->isSubmitted() && $editForm->isValid()) {
|
||||
try {
|
||||
$this->projectService->saveNewProject($project);
|
||||
$this->flashSuccess('action.update.success');
|
||||
|
||||
if ($editForm->has('create_more') && $editForm->get('create_more')->getData() === true) {
|
||||
$newProject = $this->projectService->createNewProject($project->getCustomer());
|
||||
$editForm = $this->createEditForm($newProject);
|
||||
$editForm->get('create_more')->setData(true);
|
||||
$project = $newProject;
|
||||
} else {
|
||||
return $this->redirectToRoute('project_details', ['id' => $project->getId()]);
|
||||
}
|
||||
} catch (\Exception $ex) {
|
||||
$this->flashUpdateException($ex);
|
||||
}
|
||||
}
|
||||
|
||||
return $this->renderProjectForm($project, $request);
|
||||
return $this->render('project/edit.html.twig', [
|
||||
'project' => $project,
|
||||
'form' => $editForm->createView()
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -356,7 +380,24 @@ final class ProjectController extends AbstractController
|
||||
*/
|
||||
public function editAction(Project $project, Request $request)
|
||||
{
|
||||
return $this->renderProjectForm($project, $request);
|
||||
$editForm = $this->createEditForm($project);
|
||||
$editForm->handleRequest($request);
|
||||
|
||||
if ($editForm->isSubmitted() && $editForm->isValid()) {
|
||||
try {
|
||||
$this->projectService->updateProject($project);
|
||||
$this->flashSuccess('action.update.success');
|
||||
|
||||
return $this->redirectToRoute('project_details', ['id' => $project->getId()]);
|
||||
} catch (\Exception $ex) {
|
||||
$this->flashUpdateException($ex);
|
||||
}
|
||||
}
|
||||
|
||||
return $this->render('project/edit.html.twig', [
|
||||
'project' => $project,
|
||||
'form' => $editForm->createView()
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -449,41 +490,6 @@ final class ProjectController extends AbstractController
|
||||
return $writer->getFileResponse($spreadsheet);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Project $project
|
||||
* @param Request $request
|
||||
* @return RedirectResponse|Response
|
||||
*/
|
||||
private function renderProjectForm(Project $project, Request $request)
|
||||
{
|
||||
$editForm = $this->createEditForm($project);
|
||||
$editForm->handleRequest($request);
|
||||
|
||||
if ($editForm->isSubmitted() && $editForm->isValid()) {
|
||||
try {
|
||||
$this->repository->saveProject($project);
|
||||
$this->flashSuccess('action.update.success');
|
||||
|
||||
if ($editForm->has('create_more') && $editForm->get('create_more')->getData() === true) {
|
||||
$newProject = new Project();
|
||||
$newProject->setCustomer($project->getCustomer());
|
||||
$editForm = $this->createEditForm($newProject);
|
||||
$editForm->get('create_more')->setData(true);
|
||||
$project = $newProject;
|
||||
} else {
|
||||
return $this->redirectToRoute('project_details', ['id' => $project->getId()]);
|
||||
}
|
||||
} catch (\Exception $ex) {
|
||||
$this->flashUpdateException($ex);
|
||||
}
|
||||
}
|
||||
|
||||
return $this->render('project/edit.html.twig', [
|
||||
'project' => $project,
|
||||
'form' => $editForm->createView()
|
||||
]);
|
||||
}
|
||||
|
||||
protected function getToolbarForm(ProjectQuery $query): FormInterface
|
||||
{
|
||||
return $this->createForm(ProjectToolbarForm::class, $query, [
|
||||
|
||||
34
src/Event/AbstractProjectEvent.php
Normal file
34
src/Event/AbstractProjectEvent.php
Normal file
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Event;
|
||||
|
||||
use App\Entity\Project;
|
||||
use Symfony\Contracts\EventDispatcher\Event;
|
||||
|
||||
/**
|
||||
* Base event class to used with project manipulations.
|
||||
*/
|
||||
abstract class AbstractProjectEvent extends Event
|
||||
{
|
||||
/**
|
||||
* @var Project
|
||||
*/
|
||||
private $project;
|
||||
|
||||
public function __construct(Project $project)
|
||||
{
|
||||
$this->project = $project;
|
||||
}
|
||||
|
||||
public function getProject(): Project
|
||||
{
|
||||
return $this->project;
|
||||
}
|
||||
}
|
||||
17
src/Event/ProjectCreateEvent.php
Normal file
17
src/Event/ProjectCreateEvent.php
Normal file
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Event;
|
||||
|
||||
/**
|
||||
* Triggered for new project instances, which might or might not be saved.
|
||||
*/
|
||||
final class ProjectCreateEvent extends AbstractProjectEvent
|
||||
{
|
||||
}
|
||||
17
src/Event/ProjectCreatePostEvent.php
Normal file
17
src/Event/ProjectCreatePostEvent.php
Normal file
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Event;
|
||||
|
||||
/**
|
||||
* Triggered for project instances, which were just saved.
|
||||
*/
|
||||
final class ProjectCreatePostEvent extends AbstractProjectEvent
|
||||
{
|
||||
}
|
||||
17
src/Event/ProjectCreatePreEvent.php
Normal file
17
src/Event/ProjectCreatePreEvent.php
Normal file
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Event;
|
||||
|
||||
/**
|
||||
* Triggered for project instances, which are just about to being saved.
|
||||
*/
|
||||
final class ProjectCreatePreEvent extends AbstractProjectEvent
|
||||
{
|
||||
}
|
||||
17
src/Event/ProjectUpdatePostEvent.php
Normal file
17
src/Event/ProjectUpdatePostEvent.php
Normal file
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Event;
|
||||
|
||||
/**
|
||||
* Triggered for project instances, which were just updated.
|
||||
*/
|
||||
final class ProjectUpdatePostEvent extends AbstractProjectEvent
|
||||
{
|
||||
}
|
||||
17
src/Event/ProjectUpdatePreEvent.php
Normal file
17
src/Event/ProjectUpdatePreEvent.php
Normal file
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Event;
|
||||
|
||||
/**
|
||||
* Triggered for project instances, which are just about to being updated.
|
||||
*/
|
||||
final class ProjectUpdatePreEvent extends AbstractProjectEvent
|
||||
{
|
||||
}
|
||||
@@ -15,14 +15,13 @@ use App\Entity\ProjectRate;
|
||||
use App\Repository\ActivityRateRepository;
|
||||
use App\Repository\ActivityRepository;
|
||||
use App\Repository\ProjectRateRepository;
|
||||
use App\Repository\ProjectRepository;
|
||||
|
||||
final class ProjectDuplicationService
|
||||
{
|
||||
/**
|
||||
* @var ProjectRepository
|
||||
* @var ProjectService
|
||||
*/
|
||||
private $projectRepository;
|
||||
private $projectService;
|
||||
/**
|
||||
* @var ActivityRepository
|
||||
*/
|
||||
@@ -37,12 +36,12 @@ final class ProjectDuplicationService
|
||||
private $activityRateRepository;
|
||||
|
||||
public function __construct(
|
||||
ProjectRepository $projectRepository,
|
||||
ProjectService $projectService,
|
||||
ActivityRepository $activityRepository,
|
||||
ProjectRateRepository $projectRateRepository,
|
||||
ActivityRateRepository $activityRateRepository
|
||||
) {
|
||||
$this->projectRepository = $projectRepository;
|
||||
$this->projectService = $projectService;
|
||||
$this->activityRepository = $activityRepository;
|
||||
$this->projectRateRepository = $projectRateRepository;
|
||||
$this->activityRateRepository = $activityRateRepository;
|
||||
@@ -68,7 +67,7 @@ final class ProjectDuplicationService
|
||||
$newProject->setEnd(null);
|
||||
}
|
||||
|
||||
$this->projectRepository->saveProject($newProject);
|
||||
$this->projectService->saveNewProject($newProject);
|
||||
|
||||
foreach ($this->projectRateRepository->getRatesForProject($project) as $rate) {
|
||||
/** @var ProjectRate $newRate */
|
||||
|
||||
104
src/Project/ProjectService.php
Normal file
104
src/Project/ProjectService.php
Normal file
@@ -0,0 +1,104 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Project;
|
||||
|
||||
use App\Entity\Customer;
|
||||
use App\Entity\Project;
|
||||
use App\Event\ProjectCreateEvent;
|
||||
use App\Event\ProjectCreatePostEvent;
|
||||
use App\Event\ProjectCreatePreEvent;
|
||||
use App\Event\ProjectMetaDefinitionEvent;
|
||||
use App\Event\ProjectUpdatePostEvent;
|
||||
use App\Event\ProjectUpdatePreEvent;
|
||||
use App\Repository\ProjectRepository;
|
||||
use App\Validator\ValidationFailedException;
|
||||
use InvalidArgumentException;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\Validator\Validator\ValidatorInterface;
|
||||
|
||||
final class ProjectService
|
||||
{
|
||||
/**
|
||||
* @var ProjectRepository
|
||||
*/
|
||||
private $repository;
|
||||
/**
|
||||
* @var EventDispatcherInterface
|
||||
*/
|
||||
private $dispatcher;
|
||||
/**
|
||||
* @var ValidatorInterface
|
||||
*/
|
||||
private $validator;
|
||||
|
||||
public function __construct(
|
||||
ProjectRepository $projectRepository,
|
||||
EventDispatcherInterface $dispatcher,
|
||||
ValidatorInterface $validator
|
||||
) {
|
||||
$this->repository = $projectRepository;
|
||||
$this->dispatcher = $dispatcher;
|
||||
$this->validator = $validator;
|
||||
}
|
||||
|
||||
public function createNewProject(?Customer $customer = null): Project
|
||||
{
|
||||
$project = new Project();
|
||||
|
||||
if ($customer !== null) {
|
||||
$project->setCustomer($customer);
|
||||
}
|
||||
|
||||
$this->dispatcher->dispatch(new ProjectMetaDefinitionEvent($project));
|
||||
$this->dispatcher->dispatch(new ProjectCreateEvent($project));
|
||||
|
||||
return $project;
|
||||
}
|
||||
|
||||
public function saveNewProject(Project $project): Project
|
||||
{
|
||||
if (null !== $project->getId()) {
|
||||
throw new InvalidArgumentException('Cannot create project, already persisted');
|
||||
}
|
||||
|
||||
$this->validateProject($project);
|
||||
|
||||
$this->dispatcher->dispatch(new ProjectCreatePreEvent($project));
|
||||
$this->repository->saveProject($project);
|
||||
$this->dispatcher->dispatch(new ProjectCreatePostEvent($project));
|
||||
|
||||
return $project;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Project $project
|
||||
* @param string[] $groups
|
||||
* @throws ValidationFailedException
|
||||
*/
|
||||
private function validateProject(Project $project, array $groups = []): void
|
||||
{
|
||||
$errors = $this->validator->validate($project, null, $groups);
|
||||
|
||||
if ($errors->count() > 0) {
|
||||
throw new ValidationFailedException($errors, 'Validation Failed');
|
||||
}
|
||||
}
|
||||
|
||||
public function updateProject(Project $project): Project
|
||||
{
|
||||
$this->validateProject($project);
|
||||
|
||||
$this->dispatcher->dispatch(new ProjectUpdatePreEvent($project));
|
||||
$this->repository->saveProject($project);
|
||||
$this->dispatcher->dispatch(new ProjectUpdatePostEvent($project));
|
||||
|
||||
return $project;
|
||||
}
|
||||
}
|
||||
16
src/Security/AccessDeniedException.php
Normal file
16
src/Security/AccessDeniedException.php
Normal file
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Security;
|
||||
|
||||
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
|
||||
|
||||
class AccessDeniedException extends AccessDeniedHttpException
|
||||
{
|
||||
}
|
||||
@@ -26,12 +26,12 @@ use App\Event\TimesheetUpdateMultiplePreEvent;
|
||||
use App\Event\TimesheetUpdatePostEvent;
|
||||
use App\Event\TimesheetUpdatePreEvent;
|
||||
use App\Repository\TimesheetRepository;
|
||||
use App\Security\AccessDeniedException;
|
||||
use App\Validator\ValidationException;
|
||||
use App\Validator\ValidationFailedException;
|
||||
use InvalidArgumentException;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
|
||||
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
|
||||
use Symfony\Component\Validator\Validator\ValidatorInterface;
|
||||
|
||||
@@ -117,7 +117,7 @@ final class TimesheetService
|
||||
* @param Timesheet $copyFrom
|
||||
* @throws ValidationFailedException for invalid timesheets or running timesheets that should be stopped
|
||||
* @throws InvalidArgumentException for already persisted timesheets
|
||||
* @throws AccessDeniedHttpException if user is not allowed to start timesheet
|
||||
* @throws AccessDeniedException if user is not allowed to start timesheet
|
||||
*/
|
||||
public function restartTimesheet(Timesheet $timesheet, Timesheet $copyFrom): Timesheet
|
||||
{
|
||||
@@ -133,7 +133,7 @@ final class TimesheetService
|
||||
* @return Timesheet
|
||||
* @throws ValidationFailedException for invalid timesheets or running timesheets that should be stopped
|
||||
* @throws InvalidArgumentException for already persisted timesheets
|
||||
* @throws AccessDeniedHttpException if user is not allowed to start timesheet
|
||||
* @throws AccessDeniedException if user is not allowed to start timesheet
|
||||
*/
|
||||
public function saveNewTimesheet(Timesheet $timesheet): Timesheet
|
||||
{
|
||||
@@ -142,7 +142,7 @@ final class TimesheetService
|
||||
}
|
||||
|
||||
if (null === $timesheet->getEnd() && !$this->auth->isGranted('start', $timesheet)) {
|
||||
throw new AccessDeniedHttpException('You are not allowed to start this timesheet record');
|
||||
throw new AccessDeniedException('You are not allowed to start this timesheet record');
|
||||
}
|
||||
|
||||
$this->validateTimesheet($timesheet);
|
||||
|
||||
29
tests/Event/AbstractProjectEventTest.php
Normal file
29
tests/Event/AbstractProjectEventTest.php
Normal file
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Tests\Event;
|
||||
|
||||
use App\Entity\Project;
|
||||
use App\Event\AbstractProjectEvent;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Contracts\EventDispatcher\Event;
|
||||
|
||||
abstract class AbstractProjectEventTest extends TestCase
|
||||
{
|
||||
abstract protected function createProjectEvent(Project $project): AbstractProjectEvent;
|
||||
|
||||
public function testGetterAndSetter()
|
||||
{
|
||||
$project = new Project();
|
||||
$sut = $this->createProjectEvent($project);
|
||||
|
||||
self::assertInstanceOf(Event::class, $sut);
|
||||
self::assertSame($project, $sut->getProject());
|
||||
}
|
||||
}
|
||||
26
tests/Event/ProjectCreateEventTest.php
Normal file
26
tests/Event/ProjectCreateEventTest.php
Normal file
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Tests\Event;
|
||||
|
||||
use App\Entity\Project;
|
||||
use App\Event\AbstractProjectEvent;
|
||||
use App\Event\ProjectCreateEvent;
|
||||
|
||||
/**
|
||||
* @covers \App\Event\AbstractProjectEvent
|
||||
* @covers \App\Event\ProjectCreateEvent
|
||||
*/
|
||||
class ProjectCreateEventTest extends AbstractProjectEventTest
|
||||
{
|
||||
protected function createProjectEvent(Project $project): AbstractProjectEvent
|
||||
{
|
||||
return new ProjectCreateEvent($project);
|
||||
}
|
||||
}
|
||||
26
tests/Event/ProjectCreatePostEventTest.php
Normal file
26
tests/Event/ProjectCreatePostEventTest.php
Normal file
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Tests\Event;
|
||||
|
||||
use App\Entity\Project;
|
||||
use App\Event\AbstractProjectEvent;
|
||||
use App\Event\ProjectCreatePostEvent;
|
||||
|
||||
/**
|
||||
* @covers \App\Event\AbstractProjectEvent
|
||||
* @covers \App\Event\ProjectCreatePostEvent
|
||||
*/
|
||||
class ProjectCreatePostEventTest extends AbstractProjectEventTest
|
||||
{
|
||||
protected function createProjectEvent(Project $project): AbstractProjectEvent
|
||||
{
|
||||
return new ProjectCreatePostEvent($project);
|
||||
}
|
||||
}
|
||||
26
tests/Event/ProjectCreatePreEventTest.php
Normal file
26
tests/Event/ProjectCreatePreEventTest.php
Normal file
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Tests\Event;
|
||||
|
||||
use App\Entity\Project;
|
||||
use App\Event\AbstractProjectEvent;
|
||||
use App\Event\ProjectCreatePreEvent;
|
||||
|
||||
/**
|
||||
* @covers \App\Event\AbstractProjectEvent
|
||||
* @covers \App\Event\ProjectCreatePreEvent
|
||||
*/
|
||||
class ProjectCreatePreEventTest extends AbstractProjectEventTest
|
||||
{
|
||||
protected function createProjectEvent(Project $project): AbstractProjectEvent
|
||||
{
|
||||
return new ProjectCreatePreEvent($project);
|
||||
}
|
||||
}
|
||||
26
tests/Event/ProjectUpdatePostEventTest.php
Normal file
26
tests/Event/ProjectUpdatePostEventTest.php
Normal file
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Tests\Event;
|
||||
|
||||
use App\Entity\Project;
|
||||
use App\Event\AbstractProjectEvent;
|
||||
use App\Event\ProjectUpdatePostEvent;
|
||||
|
||||
/**
|
||||
* @covers \App\Event\AbstractProjectEvent
|
||||
* @covers \App\Event\ProjectUpdatePostEvent
|
||||
*/
|
||||
class ProjectUpdatePostEventTest extends AbstractProjectEventTest
|
||||
{
|
||||
protected function createProjectEvent(Project $project): AbstractProjectEvent
|
||||
{
|
||||
return new ProjectUpdatePostEvent($project);
|
||||
}
|
||||
}
|
||||
26
tests/Event/ProjectUpdatePreEventTest.php
Normal file
26
tests/Event/ProjectUpdatePreEventTest.php
Normal file
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Tests\Event;
|
||||
|
||||
use App\Entity\Project;
|
||||
use App\Event\AbstractProjectEvent;
|
||||
use App\Event\ProjectUpdatePreEvent;
|
||||
|
||||
/**
|
||||
* @covers \App\Event\AbstractProjectEvent
|
||||
* @covers \App\Event\ProjectUpdatePreEvent
|
||||
*/
|
||||
class ProjectUpdatePreEventTest extends AbstractProjectEventTest
|
||||
{
|
||||
protected function createProjectEvent(Project $project): AbstractProjectEvent
|
||||
{
|
||||
return new ProjectUpdatePreEvent($project);
|
||||
}
|
||||
}
|
||||
157
tests/Project/ProjectServiceTest.php
Normal file
157
tests/Project/ProjectServiceTest.php
Normal file
@@ -0,0 +1,157 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Tests\Project;
|
||||
|
||||
use App\Entity\Customer;
|
||||
use App\Entity\Project;
|
||||
use App\Event\ProjectCreateEvent;
|
||||
use App\Event\ProjectCreatePostEvent;
|
||||
use App\Event\ProjectCreatePreEvent;
|
||||
use App\Event\ProjectMetaDefinitionEvent;
|
||||
use App\Event\ProjectUpdatePostEvent;
|
||||
use App\Event\ProjectUpdatePreEvent;
|
||||
use App\Project\ProjectService;
|
||||
use App\Repository\ProjectRepository;
|
||||
use App\Validator\ValidationFailedException;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\Validator\ConstraintViolation;
|
||||
use Symfony\Component\Validator\ConstraintViolationList;
|
||||
use Symfony\Component\Validator\Validator\ValidatorInterface;
|
||||
|
||||
/**
|
||||
* @covers \App\Project\ProjectService
|
||||
*/
|
||||
class ProjectServiceTest extends TestCase
|
||||
{
|
||||
private function getSut(
|
||||
?EventDispatcherInterface $dispatcher = null,
|
||||
?ValidatorInterface $validator = null,
|
||||
?ProjectRepository $repository = null
|
||||
): ProjectService {
|
||||
if ($repository === null) {
|
||||
$repository = $this->createMock(ProjectRepository::class);
|
||||
}
|
||||
|
||||
if ($dispatcher === null) {
|
||||
$dispatcher = $this->createMock(EventDispatcherInterface::class);
|
||||
}
|
||||
|
||||
if ($validator === null) {
|
||||
$validator = $this->createMock(ValidatorInterface::class);
|
||||
$validator->method('validate')->willReturn(new ConstraintViolationList());
|
||||
}
|
||||
|
||||
$service = new ProjectService($repository, $dispatcher, $validator);
|
||||
|
||||
return $service;
|
||||
}
|
||||
|
||||
public function testCannotSavePersistedProjectAsNew()
|
||||
{
|
||||
$project = $this->createMock(Project::class);
|
||||
$project->expects($this->once())->method('getId')->willReturn(1);
|
||||
|
||||
$sut = $this->getSut();
|
||||
|
||||
$this->expectException(\InvalidArgumentException::class);
|
||||
$this->expectExceptionMessage('Cannot create project, already persisted');
|
||||
|
||||
$sut->saveNewProject($project);
|
||||
}
|
||||
|
||||
public function testSaveNewProjectHasValidationError()
|
||||
{
|
||||
$constraints = new ConstraintViolationList();
|
||||
$constraints->add(new ConstraintViolation('toooo many tests', 'abc.def', [], '$root', 'begin', 4, null, null, null, '$cause'));
|
||||
|
||||
$validator = $this->createMock(ValidatorInterface::class);
|
||||
$validator->method('validate')->willReturn($constraints);
|
||||
|
||||
$sut = $this->getSut(null, $validator);
|
||||
|
||||
$this->expectException(ValidationFailedException::class);
|
||||
$this->expectExceptionMessage('Validation Failed');
|
||||
|
||||
$sut->saveNewProject(new Project());
|
||||
}
|
||||
|
||||
public function testUpdateDispatchesEvents()
|
||||
{
|
||||
$project = $this->createMock(Project::class);
|
||||
$project->method('getId')->willReturn(1);
|
||||
|
||||
$dispatcher = $this->createMock(EventDispatcherInterface::class);
|
||||
$dispatcher->expects($this->exactly(2))->method('dispatch')->willReturnCallback(function ($event) use ($project) {
|
||||
if ($event instanceof ProjectUpdatePostEvent) {
|
||||
self::assertSame($project, $event->getProject());
|
||||
} elseif ($event instanceof ProjectUpdatePreEvent) {
|
||||
self::assertSame($project, $event->getProject());
|
||||
} else {
|
||||
$this->fail('Invalid event received');
|
||||
}
|
||||
});
|
||||
|
||||
$sut = $this->getSut($dispatcher);
|
||||
|
||||
$sut->updateProject($project);
|
||||
}
|
||||
|
||||
public function testCreateNewProjectDispatchesEvents()
|
||||
{
|
||||
$dispatcher = $this->createMock(EventDispatcherInterface::class);
|
||||
$dispatcher->expects($this->exactly(2))->method('dispatch')->willReturnCallback(function ($event) {
|
||||
if ($event instanceof ProjectMetaDefinitionEvent) {
|
||||
self::assertInstanceOf(Project::class, $event->getEntity());
|
||||
} elseif ($event instanceof ProjectCreateEvent) {
|
||||
self::assertInstanceOf(Project::class, $event->getProject());
|
||||
} else {
|
||||
$this->fail('Invalid event received');
|
||||
}
|
||||
});
|
||||
|
||||
$sut = $this->getSut($dispatcher);
|
||||
|
||||
$customer = new Customer();
|
||||
$project = $sut->createNewProject($customer);
|
||||
|
||||
self::assertSame($customer, $project->getCustomer());
|
||||
}
|
||||
|
||||
public function testSaveNewProjectDispatchesEvents()
|
||||
{
|
||||
$dispatcher = $this->createMock(EventDispatcherInterface::class);
|
||||
$dispatcher->expects($this->exactly(2))->method('dispatch')->willReturnCallback(function ($event) {
|
||||
if ($event instanceof ProjectCreatePreEvent) {
|
||||
self::assertInstanceOf(Project::class, $event->getProject());
|
||||
} elseif ($event instanceof ProjectCreatePostEvent) {
|
||||
self::assertInstanceOf(Project::class, $event->getProject());
|
||||
} else {
|
||||
$this->fail('Invalid event received');
|
||||
}
|
||||
});
|
||||
|
||||
$sut = $this->getSut($dispatcher);
|
||||
|
||||
$project = new Project();
|
||||
$sut->saveNewProject($project);
|
||||
}
|
||||
|
||||
public function testCreateNewProjectWithoutCustomer()
|
||||
{
|
||||
$sut = $this->getSut();
|
||||
|
||||
$project = $sut->createNewProject();
|
||||
self::assertNull($project->getCustomer());
|
||||
|
||||
$project = $sut->createNewProject();
|
||||
self::assertNull($project->getCustomer());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user