tasklist-backend/src/Controllers/TaskController.php

296 lines
11 KiB
PHP

<?php
namespace Controllers;
use Carbon\Carbon;
use Carbon\Exceptions\InvalidFormatException;
use Exceptions\InvalidTaskDateException;
use Exceptions\TaskNotFoundException;
use Models\Task;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Repositories\TaskRepository;
use Utils\Enums\TaskStatus;
use Utils\StructuredResponseBuilder;
class TaskController
{
private StructuredResponseBuilder $builder;
private TaskRepository $repository;
// instead of requesting the builder here, we could simply make it static
public function __construct(TaskRepository $taskRepository, StructuredResponseBuilder $builder)
{
$this->repository = $taskRepository;
$this->builder = $builder;
}
// response is immutable. psr-7 impl makes copies of stuff.
// TODO: There's a LOT of validation here. We need to build a validator, use a library, or refactor this method.
public function addTask(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface
{
$params = $request->getParsedBody();
$requiredKeys = ['name', 'description', 'status', 'start', 'end'];
if (!array_key_exists('payload', $params)) {
$this->builder->setError();
$this->builder->setErrorMessage('Malformed request payload. Please try again.');
$this->builder->setErrorCode(400);
$errorResponse = $response->withStatus(400, 'Invalid payload')->withHeader('Content-Type', 'application/json');
$errorResponse->getBody()->write($this->builder->build());
return $errorResponse;
}
$missingKeys = [];
foreach ($requiredKeys as $key)
{
if (!array_key_exists($key, $params['payload'])) {
$missingKeys[] = $key;
}
}
if (!empty($missingKeys)) {
$this->builder->setError();
$this->builder->setErrorMessage("The following parameters are missing: " . implode(', ', $missingKeys));
$this->builder->setErrorCode(400);
$errorResponse = $response->withStatus('400', 'Missing arguments')->withHeader('Content-Type', 'application/json');
$errorResponse->getBody()->write($this->builder->build());
return $errorResponse;
}
try
{
$start = Carbon::parse($params['payload']['start']);
$end = Carbon::parse($params['payload']['end']);
if ($start->gt($end) || $end->lt($start)) {
throw new InvalidTaskDateException('Invalid task date. Start date must not be after end date.');
}
} catch (InvalidFormatException | \DomainException $exception)
{
$this->builder->setError();
$this->builder->setErrorMessage($exception->getMessage());
$this->builder->setErrorCode(400);
$errorResponse = $response->withStatus(400)->withHeader('Content-Type', 'application/json');
$errorResponse->getBody()->write($this->builder->build());
return $errorResponse;
}
if (is_null(TaskStatus::tryFrom($params['payload']['status']))) {
$this->builder->setError();
$this->builder->setErrorCode(400);
$this->builder->setErrorMessage('Invalid task status code. Status must range from 1-4 (started, in progress, blocked, completed).');
$errorResponse = $response->withStatus(400)->withHeader('Content-Type', 'application/json');
$errorResponse->getBody()->write($this->builder->build());
return $errorResponse;
}
$task = new Task();
$task->setName($params['payload']['name'])
->setStatusId(TaskStatus::tryFrom((int) $params['payload']['status']))
->setDescription($params['payload']['description'])
->setStart($params['payload']['start'])
->setEnd($params['payload']['end']);
$createdTask = $this->repository->create($task);
if ($createdTask == 0)
{
$this->builder->setError()
->setErrorMessage('An unexpected error has occurred whilst trying to perform this operation.')
->setErrorCode(500);
$errorResponse = $response->withStatus(500)->withHeader('Content-Type', 'application/json');
$errorResponse->getBody()->write($this->builder->build());
return $errorResponse;
}
$createdResponse = $response->withStatus(201)->withHeader('Content-Type', 'application/json');
$this->builder->setOptionalMessage('Task created.')
->setPayload($task->setId($createdTask)->persist());
$createdResponse->getBody()->write($this->builder->build());
return $createdResponse;
}
public function getTasks(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface
{
$tasks = $this->repository->readAll();
$payload['tasks'] = $tasks;
$this->builder->setPayload($payload)->setOptionalMessage('Listing all tasks: ');
$response->getBody()->write($this->builder->build());
return $response->withHeader('Content-Type', 'application/json');
}
public function getTask(ServerRequestInterface $request, ResponseInterface $response, string $id): ResponseInterface
{
try {
$payload['task'] = $this->repository->readById((int)$id)->persist();
$this->builder->setPayload($payload)->setOptionalMessage("Task retrieved.");
$response->getBody()->write($this->builder->build());
return $response->withHeader('Content-Type', 'application/json');
} catch (TaskNotFoundException $e) {
$this->builder->setError()->setErrorMessage($e->getMessage())->setErrorCode(404);
$errorResponse = $response->withStatus(404, $e->getMessage())->withHeader('Content-Type', 'application/json');
$response->getBody()->write($this->builder->build());
return $errorResponse;
}
}
public function delete(ServerRequestInterface $request, ResponseInterface $response, $id): ResponseInterface
{
try
{
$task = $this->repository->readById((int) $id);
$this->repository->delete($task);
$this->builder->setOptionalMessage('Task deleted.');
$response->getBody()->write($this->builder->build());
return $response->withHeader('Content-Type', 'application/json');
} catch (TaskNotFoundException $exception)
{
$this->builder->setError()->setErrorMessage($exception->getMessage())->setErrorCode(404);
$errorResponse = $response->withStatus(404);
$errorResponse->getBody()->write($this->builder->build());
return $errorResponse->withHeader('Content-Type', 'application/json');
}
}
public function update(ServerRequestInterface $request, ResponseInterface $response, $id): ResponseInterface
{
$payloadWarnings = [];
$toUpdate = $request->getParsedBody()['payload']['update'];
$toUpdate['updated_at'] = Carbon::now()->toDateTimeString();
$acceptableFields = [
'task_owner',
'name',
'description',
'start',
'end'
];
foreach ($toUpdate as $updateField => $value)
{
if($updateField !== 'updated_at' && !in_array($updateField, $acceptableFields))
{
$payloadWarnings[] = "W: An invalid, unknown, or locked field has been truncated: " . $updateField;
unset($toUpdate[$updateField]);
}
}
try
{
$task = $this->repository->readById($id);
// if we used the dirty implementation, we could skip all this code
foreach($toUpdate as $property => $value)
{
// ignore updated_at
if($property !== 'updated_at')
{
$task->{'set' . ucfirst(snakeToCamel($property))}($value);
}
}
$finalFields = array_keys($toUpdate);
$result = $this->repository->updateSelective($task, $finalFields);
if ($result && empty($payloadWarnings))
{
return $response->withStatus(204);
}
elseif($result)
{
$this->builder->setOptionalMessage('Task updated successfully with warnings.')
->setPayload([
'updated' => $toUpdate,
'warnings' => $payloadWarnings,
]);
$response->getBody()->write($this->builder->build());
return $response;
}
else
{
$this->builder->setError()
->setErrorMessage('An unexpected error occurred while updating the resource.')
->setErrorCode(500);
$errorResponse = $response->withStatus(500)->withHeader('Content-Type', 'application/json');
$errorResponse->getBody()->write($this->builder->build());
return $errorResponse;
}
}
catch (TaskNotFoundException $e)
{
$this->builder->setError()
->setErrorMessage($e->getMessage())
->setErrorCode(404);
$errorResponse = $response->withStatus(404)->withHeader('Content-Type', 'application/json');
$errorResponse->getBody()->write($this->builder->build());
return $errorResponse;
}
}
public function updateStatus(ServerRequestInterface $request, ResponseInterface $response, $id): ResponseInterface
{
try
{
$task = $this->repository->readById($id);
$newCode = $request->getParsedBody()['payload']['status_id'];
if (is_null(TaskStatus::tryFrom($newCode)))
{
$this->builder->setError()->setErrorCode(400)->setErrorMessage('Invalid task status, must be between 1 to 4 (started, in progress, blocked, completed).');
$response->getBody()->write($this->builder->build());
return $response->withStatus(400);
}
$task->setStatusId(TaskStatus::from($newCode));
$this->repository->updateSelective($task, ['status_id']);
return $response->withStatus(204);
} catch (TaskNotFoundException $e)
{
$this->builder->setError()->setErrorCode(404)->setErrorMessage($e->getMessage());
$response->getBody()->write($this->builder->build());
return $response->withStatus(404);
}
}
}