From c2e621859e45bd06149a136c8032e2f9c99e9324 Mon Sep 17 00:00:00 2001 From: Miguel Nogueira Date: Wed, 16 Apr 2025 15:33:13 +0100 Subject: [PATCH] feat: add update endpoint --- public/index.php | 1 + src/Controllers/TaskController.php | 83 +++++++++++++++++++++++++++++ src/Repositories/TaskRepository.php | 43 +++++++++++++++ 3 files changed, 127 insertions(+) diff --git a/public/index.php b/public/index.php index 908dacc..c019b98 100644 --- a/public/index.php +++ b/public/index.php @@ -22,6 +22,7 @@ $app->get('/', [HomeFrontController::class, 'home']); $app->get('/tasks/{id}', [TaskController::class, 'getTask']); $app->delete('/tasks/{id}', [TaskController::class, 'delete']); +$app->patch('/tasks/{id}', [TaskController::class, 'update']); $app->get('/tasks', [TaskController::class, 'getTasks']); $app->post('/tasks', [TaskController::class, 'addTask']); diff --git a/src/Controllers/TaskController.php b/src/Controllers/TaskController.php index fe3bebb..4f6342a 100644 --- a/src/Controllers/TaskController.php +++ b/src/Controllers/TaskController.php @@ -178,4 +178,87 @@ class TaskController return $errorResponse; } } + + + 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; + } + } } \ No newline at end of file diff --git a/src/Repositories/TaskRepository.php b/src/Repositories/TaskRepository.php index 907fd7d..e35a274 100644 --- a/src/Repositories/TaskRepository.php +++ b/src/Repositories/TaskRepository.php @@ -83,6 +83,49 @@ class TaskRepository implements TaskDao ]); } + // fields = ['name', 'description'] + + // TODO: Abstract this implementation + // TODO: Check for injection vectors; fields might be used to pollute the query. Fields aren't bindParam'able. + public function updateSelective(Task $task, array $fields): bool + { + if (empty($fields)) + { + return false; + } + + $setClause = []; + $params = []; + + + foreach ($fields as $field) + { + // TODO: use the Dirty implementation. We can chop off the $fields param and still not trust that data. + // Consider using a Trait to control this behavior (dirty or dynamic get: e.g Uses DirtyFields or Uses ManualUpdates) + // we're discarding invalid properties here; if they give us --name;, this check will fail. + // essentially, the Model is serving as our array of allowed fields. + if (property_exists($task, snakeToCamel($field))) + { + $setClause[] = camel_to_snake($field) . " = ?"; // SET name = ?, SET updatedAt = ? (fixed by camel_to_snake) + $params[] = $task->{'get' . ucfirst(snakeToCamel($field))}(); + + } + } + + if (empty($setClause)) + { + return false; + } + + // we might have to update this and set the id later directly from the model + $params[] = $task->getTaskId(); + + $query = "UPDATE Tasks SET " . implode(', ', $setClause) . " WHERE id = ?"; + $stmt = $this->conn->prepare($query); + + return $stmt->execute($params); + } + public function delete(Task $task): bool { $stmt = $this->conn->prepare('DELETE FROM Tasks WHERE id = ?');