feat: add update endpoint

This commit is contained in:
Miguel Nogueira 2025-04-16 15:33:13 +01:00
parent f8467ada09
commit c2e621859e
3 changed files with 127 additions and 0 deletions

View File

@ -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']);

View File

@ -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;
}
}
}

View File

@ -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 = ?');