first commit
This commit is contained in:
commit
9afba6461d
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
.env
|
||||
vendor/*
|
8
.idea/.gitignore
generated
vendored
Normal file
8
.idea/.gitignore
generated
vendored
Normal file
@ -0,0 +1,8 @@
|
||||
# Default ignored files
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
# Editor-based HTTP Client requests
|
||||
/httpRequests/
|
||||
# Datasource local storage ignored files
|
||||
/dataSources/
|
||||
/dataSources.local.xml
|
15
.idea/dataSources.xml
generated
Normal file
15
.idea/dataSources.xml
generated
Normal file
@ -0,0 +1,15 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="DataSourceManagerImpl" format="xml" multifile-model="true">
|
||||
<data-source source="LOCAL" name="tasklist@100.71.118.27" uuid="85ecee04-f3a3-4e8a-aaaa-5fb0275b3e95">
|
||||
<driver-ref>mariadb</driver-ref>
|
||||
<synchronize>true</synchronize>
|
||||
<jdbc-driver>org.mariadb.jdbc.Driver</jdbc-driver>
|
||||
<jdbc-url>jdbc:mariadb://100.71.118.27:3306/tasklist</jdbc-url>
|
||||
<jdbc-additional-properties>
|
||||
<property name="database.introspection.mysql.dbe5060" value="true" />
|
||||
</jdbc-additional-properties>
|
||||
<working-dir>$ProjectFileDir$</working-dir>
|
||||
</data-source>
|
||||
</component>
|
||||
</project>
|
14
.idea/deployment.xml
generated
Normal file
14
.idea/deployment.xml
generated
Normal file
@ -0,0 +1,14 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="PublishConfigData" remoteFilesAllowedToDisappearOnAutoupload="false">
|
||||
<serverData>
|
||||
<paths name="Webvoke Customer Portal">
|
||||
<serverdata>
|
||||
<mappings>
|
||||
<mapping local="$PROJECT_DIR$" web="/" />
|
||||
</mappings>
|
||||
</serverdata>
|
||||
</paths>
|
||||
</serverData>
|
||||
</component>
|
||||
</project>
|
7
.idea/discord.xml
generated
Normal file
7
.idea/discord.xml
generated
Normal file
@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="DiscordProjectSettings">
|
||||
<option name="show" value="PROJECT_FILES" />
|
||||
<option name="description" value="" />
|
||||
</component>
|
||||
</project>
|
8
.idea/laravel-idea.xml
generated
Normal file
8
.idea/laravel-idea.xml
generated
Normal file
@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="InertiaPackage">
|
||||
<option name="directoryPaths">
|
||||
<list />
|
||||
</option>
|
||||
</component>
|
||||
</project>
|
12
.idea/material_theme_project_new.xml
generated
Normal file
12
.idea/material_theme_project_new.xml
generated
Normal file
@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="MaterialThemeProjectNewConfig">
|
||||
<option name="metadata">
|
||||
<MTProjectMetadataState>
|
||||
<option name="migrated" value="true" />
|
||||
<option name="pristineConfig" value="false" />
|
||||
<option name="userId" value="993512f:1923a7b499f:-7ffe" />
|
||||
</MTProjectMetadataState>
|
||||
</option>
|
||||
</component>
|
||||
</project>
|
8
.idea/modules.xml
generated
Normal file
8
.idea/modules.xml
generated
Normal file
@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectModuleManager">
|
||||
<modules>
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/tasklist-api.iml" filepath="$PROJECT_DIR$/.idea/tasklist-api.iml" />
|
||||
</modules>
|
||||
</component>
|
||||
</project>
|
73
.idea/php.xml
generated
Normal file
73
.idea/php.xml
generated
Normal file
@ -0,0 +1,73 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="MessDetectorOptionsConfiguration">
|
||||
<option name="transferred" value="true" />
|
||||
</component>
|
||||
<component name="PHPCSFixerOptionsConfiguration">
|
||||
<option name="transferred" value="true" />
|
||||
</component>
|
||||
<component name="PHPCodeSnifferOptionsConfiguration">
|
||||
<option name="highlightLevel" value="WARNING" />
|
||||
<option name="transferred" value="true" />
|
||||
</component>
|
||||
<component name="PhpCodeSniffer">
|
||||
<phpcs_settings>
|
||||
<phpcs_by_interpreter asDefaultInterpreter="true" interpreter_id="e39e8ac2-c813-4ffd-9483-4fc00d471116" timeout="30000" />
|
||||
</phpcs_settings>
|
||||
</component>
|
||||
<component name="PhpIncludePathManager">
|
||||
<include_path>
|
||||
<path value="$PROJECT_DIR$/vendor/carbondate/carbon" />
|
||||
<path value="$PROJECT_DIR$/vendor/symfony/polyfill-mbstring" />
|
||||
<path value="$PROJECT_DIR$/vendor/symfony/polyfill-php80" />
|
||||
<path value="$PROJECT_DIR$/vendor/symfony/translation" />
|
||||
<path value="$PROJECT_DIR$/vendor/symfony/translation-contracts" />
|
||||
<path value="$PROJECT_DIR$/vendor/composer" />
|
||||
<path value="$PROJECT_DIR$/vendor/psr/clock" />
|
||||
<path value="$PROJECT_DIR$/vendor/nesbot/carbon" />
|
||||
<path value="$PROJECT_DIR$/vendor/symfony/deprecation-contracts" />
|
||||
<path value="$PROJECT_DIR$/vendor/carbonphp/carbon-doctrine-types" />
|
||||
<path value="$PROJECT_DIR$/vendor/symfony/polyfill-php83" />
|
||||
<path value="$PROJECT_DIR$/vendor/symfony/clock" />
|
||||
<path value="$PROJECT_DIR$/vendor/vlucas/phpdotenv" />
|
||||
<path value="$PROJECT_DIR$/vendor/symfony/polyfill-ctype" />
|
||||
<path value="$PROJECT_DIR$/vendor/phpoption/phpoption" />
|
||||
<path value="$PROJECT_DIR$/vendor/graham-campbell/result-type" />
|
||||
<path value="$PROJECT_DIR$/vendor/nikic/fast-route" />
|
||||
<path value="$PROJECT_DIR$/vendor/slim/slim" />
|
||||
<path value="$PROJECT_DIR$/vendor/psr/container" />
|
||||
<path value="$PROJECT_DIR$/vendor/psr/http-factory" />
|
||||
<path value="$PROJECT_DIR$/vendor/psr/http-message" />
|
||||
<path value="$PROJECT_DIR$/vendor/psr/log" />
|
||||
<path value="$PROJECT_DIR$/vendor/psr/http-server-middleware" />
|
||||
<path value="$PROJECT_DIR$/vendor/psr/http-server-handler" />
|
||||
<path value="$PROJECT_DIR$/vendor/laminas/laminas-diactoros" />
|
||||
<path value="$PROJECT_DIR$/vendor/laravel/serializable-closure" />
|
||||
<path value="$PROJECT_DIR$/vendor/php-di/invoker" />
|
||||
<path value="$PROJECT_DIR$/vendor/php-di/php-di" />
|
||||
<path value="$PROJECT_DIR$/vendor/php-di/slim-bridge" />
|
||||
</include_path>
|
||||
</component>
|
||||
<component name="PhpProjectSharedConfiguration" php_language_level="8.2" />
|
||||
<component name="PhpStan">
|
||||
<PhpStan_settings>
|
||||
<phpstan_by_interpreter asDefaultInterpreter="true" interpreter_id="e39e8ac2-c813-4ffd-9483-4fc00d471116" timeout="60000" />
|
||||
</PhpStan_settings>
|
||||
</component>
|
||||
<component name="PhpStanOptionsConfiguration">
|
||||
<option name="transferred" value="true" />
|
||||
</component>
|
||||
<component name="PhpUnit">
|
||||
<phpunit_settings>
|
||||
<PhpUnitSettings custom_loader_path="$PROJECT_DIR$/vendor/autoload.php" />
|
||||
</phpunit_settings>
|
||||
</component>
|
||||
<component name="Psalm">
|
||||
<Psalm_settings>
|
||||
<psalm_fixer_by_interpreter asDefaultInterpreter="true" interpreter_id="e39e8ac2-c813-4ffd-9483-4fc00d471116" timeout="60000" />
|
||||
</Psalm_settings>
|
||||
</component>
|
||||
<component name="PsalmOptionsConfiguration">
|
||||
<option name="transferred" value="true" />
|
||||
</component>
|
||||
</project>
|
10
.idea/phpspec.xml
generated
Normal file
10
.idea/phpspec.xml
generated
Normal file
@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="PHPSpec">
|
||||
<suites>
|
||||
<PhpSpecSuiteConfiguration>
|
||||
<option name="myPath" value="$PROJECT_DIR$" />
|
||||
</PhpSpecSuiteConfiguration>
|
||||
</suites>
|
||||
</component>
|
||||
</project>
|
6
.idea/sqldialects.xml
generated
Normal file
6
.idea/sqldialects.xml
generated
Normal file
@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="SqlDialectMappings">
|
||||
<file url="PROJECT" dialect="MariaDB" />
|
||||
</component>
|
||||
</project>
|
46
.idea/tasklist-api.iml
generated
Normal file
46
.idea/tasklist-api.iml
generated
Normal file
@ -0,0 +1,46 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="WEB_MODULE" version="4">
|
||||
<component name="NewModuleRootManager">
|
||||
<content url="file://$MODULE_DIR$">
|
||||
<sourceFolder url="file://$MODULE_DIR$/src/Config" isTestSource="false" packagePrefix="Config\" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/src/Database" isTestSource="false" packagePrefix="Database\" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/src/Interfaces" isTestSource="false" packagePrefix="Interfaces\" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/src/Models" isTestSource="false" packagePrefix="Models\" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/src/Repositories" isTestSource="false" packagePrefix="Repositories\" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/src/Utils" isTestSource="false" packagePrefix="Utils\" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/src/Controllers" isTestSource="false" packagePrefix="Controllers\" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/carbondate/carbon" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/carbonphp/carbon-doctrine-types" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/composer" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/graham-campbell/result-type" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/nesbot/carbon" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/phpoption/phpoption" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/psr/clock" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/clock" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/deprecation-contracts" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/polyfill-ctype" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/polyfill-mbstring" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/polyfill-php80" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/polyfill-php83" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/translation" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/translation-contracts" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/vlucas/phpdotenv" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/laminas/laminas-diactoros" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/laravel/serializable-closure" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/nikic/fast-route" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/php-di/invoker" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/php-di/php-di" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/php-di/slim-bridge" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/psr/container" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/psr/http-factory" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/psr/http-message" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/psr/http-server-handler" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/psr/http-server-middleware" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/psr/log" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/vendor/slim/slim" />
|
||||
</content>
|
||||
<orderEntry type="inheritedJdk" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
</module>
|
6
.idea/vcs.xml
generated
Normal file
6
.idea/vcs.xml
generated
Normal file
@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
22
composer.json
Normal file
22
composer.json
Normal file
@ -0,0 +1,22 @@
|
||||
{
|
||||
"require": {
|
||||
"ext-json": "*",
|
||||
"nesbot/carbon": "*",
|
||||
"vlucas/phpdotenv": "^5.6",
|
||||
"ext-pdo": "*",
|
||||
"slim/slim": "4.*",
|
||||
"laminas/laminas-diactoros": "^3.5",
|
||||
"php-di/slim-bridge": "^3.4"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Models\\": "src/Models/",
|
||||
"Interfaces\\": "src/Interfaces",
|
||||
"Database\\": "src/Database",
|
||||
"Config\\": "src/Config",
|
||||
"Repositories\\": "src/Repositories",
|
||||
"Utils\\": "src/Utils",
|
||||
"Controllers\\": "src/Controllers"
|
||||
}
|
||||
}
|
||||
}
|
1904
composer.lock
generated
Normal file
1904
composer.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
18
index.php
Normal file
18
index.php
Normal file
@ -0,0 +1,18 @@
|
||||
<?php declare(strict_types=1);
|
||||
|
||||
use Controllers\TaskController;
|
||||
use DI\Bridge\Slim\Bridge;
|
||||
use DI\Container;
|
||||
use Controllers\HomeFrontController;
|
||||
|
||||
require_once __DIR__ . '/vendor/autoload.php';
|
||||
|
||||
$container = new Container();
|
||||
$app = Bridge::create($container);
|
||||
$app->addBodyParsingMiddleware();
|
||||
|
||||
$app->get('/', [HomeFrontController::class, 'home']);
|
||||
|
||||
$app->post('/tasks', [TaskController::class, 'addTask']);
|
||||
|
||||
$app->run();
|
46
src/Config/Config.php
Normal file
46
src/Config/Config.php
Normal file
@ -0,0 +1,46 @@
|
||||
<?php declare(strict_types=1);
|
||||
|
||||
namespace Config;
|
||||
|
||||
use Dotenv\Dotenv;
|
||||
|
||||
class Config
|
||||
{
|
||||
|
||||
private Dotenv $env;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->env = Dotenv::createImmutable(dirname(__DIR__, 2));
|
||||
$this->env->load();
|
||||
}
|
||||
|
||||
public function getDbHost(): string {
|
||||
return $_ENV['DB_HOST'] ?? 'localhost';
|
||||
}
|
||||
|
||||
public function getDbUser(): string {
|
||||
return $_ENV['DB_USER'] ?? 'root';
|
||||
}
|
||||
|
||||
public function getDbPassword(): string {
|
||||
return $_ENV['DB_PASSWORD'];
|
||||
}
|
||||
|
||||
public function getDbName(): string {
|
||||
return $_ENV['DB_NAME'];
|
||||
}
|
||||
|
||||
public function getDbCharset(): string {
|
||||
return $_ENV['DB_CHARSET'] ?? 'utf8mb4';
|
||||
}
|
||||
|
||||
public function getDbCollation(): string {
|
||||
return $_ENV['DB_COLLATION'] ?? 'utf8mb4_unicode_ci';
|
||||
}
|
||||
|
||||
public function getDbPort(): int
|
||||
{
|
||||
return (int) $_ENV['DB_PORT'] ?? 3306;
|
||||
}
|
||||
}
|
46
src/Controllers/HomeFrontController.php
Normal file
46
src/Controllers/HomeFrontController.php
Normal file
@ -0,0 +1,46 @@
|
||||
<?php declare(strict_types=1);
|
||||
|
||||
namespace Controllers;
|
||||
|
||||
use Psr\Container\ContainerExceptionInterface;
|
||||
use Psr\Container\ContainerInterface;
|
||||
use Psr\Container\NotFoundExceptionInterface;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Utils\StructuredResponseBuilder;
|
||||
|
||||
class HomeFrontController
|
||||
{
|
||||
|
||||
private $container;
|
||||
|
||||
private $builder;
|
||||
|
||||
|
||||
/**
|
||||
* @param StructuredResponseBuilder $builder Response Builder interface.
|
||||
*/
|
||||
public function __construct(StructuredResponseBuilder $builder)
|
||||
{
|
||||
$this->builder = $builder;
|
||||
}
|
||||
|
||||
public function home(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface
|
||||
{
|
||||
$builderResponse = $this->builder
|
||||
->setError()
|
||||
->setErrorMessage("This is an API and is therefore not meant to handle requests with your browser.")
|
||||
->setErrorCode(400)
|
||||
->setPayload([])
|
||||
->build();
|
||||
|
||||
// ResponseInterface is immutable
|
||||
$newResponse = $response;
|
||||
$newResponse->withStatus(400, "Bad Request")->withHeader('Content-Type', 'application/json');
|
||||
|
||||
$newResponse->getBody()->write($builderResponse);
|
||||
|
||||
return $newResponse;
|
||||
}
|
||||
|
||||
}
|
124
src/Controllers/TaskController.php
Normal file
124
src/Controllers/TaskController.php
Normal file
@ -0,0 +1,124 @@
|
||||
<?php
|
||||
|
||||
namespace Controllers;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Carbon\Exceptions\InvalidFormatException;
|
||||
use Exceptions\InvalidTaskDateException;
|
||||
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 $repository, $builder;
|
||||
|
||||
// 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('tasks', $params)) {
|
||||
$this->builder->setError();
|
||||
$this->builder->setErrorMessage('Malformed request payload. Please try again.');
|
||||
$this->builder->setErrorCode(400);
|
||||
|
||||
$errorResponse = $response->withStatus(400, 'Invalid payload');
|
||||
$errorResponse->getBody()->write($this->builder->build());
|
||||
|
||||
return $errorResponse;
|
||||
}
|
||||
|
||||
$missingKeys = [];
|
||||
foreach ($requiredKeys as $key)
|
||||
{
|
||||
if (!array_key_exists($key, $params['tasks'])) {
|
||||
$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');
|
||||
$errorResponse->getBody()->write($this->builder->build());
|
||||
|
||||
return $errorResponse;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
$start = Carbon::parse($params['tasks']['start']);
|
||||
$end = Carbon::parse($params['tasks']['end']);
|
||||
|
||||
if ($start->gt($end) || $end->lt($start)) {
|
||||
throw new \DomainException('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);
|
||||
$errorResponse->getBody()->write($this->builder->build());
|
||||
|
||||
return $errorResponse;
|
||||
}
|
||||
|
||||
if (is_null(TaskStatus::tryFrom($params['tasks']['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);
|
||||
$errorResponse->getBody()->write($this->builder->build());
|
||||
|
||||
return $errorResponse;
|
||||
}
|
||||
|
||||
$task = new Task();
|
||||
$task->setTitle($params['tasks']['name'])
|
||||
->setStatus(TaskStatus::tryFrom((int) $params['tasks']['status']))
|
||||
->setDescription($params['tasks']['description'])
|
||||
->setStartDt($params['tasks']['start'])
|
||||
->setEndDt($params['tasks']['end']);
|
||||
|
||||
if (!$this->repository->create($task))
|
||||
{
|
||||
$this->builder->setError()
|
||||
->setErrorMessage('An unexpected error has occurred whilst trying to perform this operation.')
|
||||
->setErrorCode(500);
|
||||
|
||||
$errorResponse = $response->withStatus(500);
|
||||
$errorResponse->getBody()->write($this->builder->build());
|
||||
|
||||
return $errorResponse;
|
||||
}
|
||||
|
||||
$createdResponse = $response->withStatus(201);
|
||||
$this->builder->setOptionalMessage('Task created.')
|
||||
->setPayload($task->persist());
|
||||
|
||||
$createdResponse->getBody()->write($this->builder->build());
|
||||
return $createdResponse;
|
||||
}
|
||||
}
|
56
src/Database/Connection.php
Normal file
56
src/Database/Connection.php
Normal file
@ -0,0 +1,56 @@
|
||||
<?php declare(strict_types=1);
|
||||
|
||||
namespace Database;
|
||||
|
||||
use Config\Config;
|
||||
use Exceptions\DatabaseConnectionException;
|
||||
use PDO;
|
||||
|
||||
class Connection
|
||||
{
|
||||
|
||||
protected PDO $conn;
|
||||
private Config $configManager;
|
||||
|
||||
/**
|
||||
* @throws DatabaseConnectionException
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
$this->configManager = new Config();
|
||||
$this->connect();
|
||||
}
|
||||
|
||||
private function configure(): string
|
||||
{
|
||||
$hostname = $this->configManager->getDbHost();
|
||||
$dbName = $this->configManager->getDbName();
|
||||
$port = $this->configManager->getDbPort();
|
||||
|
||||
return "mysql:host=$hostname;port=$port;dbname=$dbName";
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws DatabaseConnectionException Thrown when the connection fails.
|
||||
*/
|
||||
public function connect(): void
|
||||
{
|
||||
|
||||
$username = $this->configManager->getDbUser();
|
||||
$password = $this->configManager->getDbPassword();
|
||||
|
||||
try {
|
||||
$this->conn = new \PDO($this->configure(), $username, $password);
|
||||
} catch (\PDOException $e) {
|
||||
throw new DatabaseConnectionException("A connection to the underlying backend could not be established. Please try again later", $e->getCode(), $e->getPrevious());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public function getConnection(): PDO
|
||||
{
|
||||
return $this->conn;
|
||||
}
|
||||
|
||||
}
|
11
src/Exceptions/DatabaseConnectionException.php
Normal file
11
src/Exceptions/DatabaseConnectionException.php
Normal file
@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
namespace Exceptions;
|
||||
|
||||
class DatabaseConnectionException extends \Exception
|
||||
{
|
||||
public function __construct(string $message = "Database connection error", int $code = 0, \Throwable $previous = null)
|
||||
{
|
||||
parent::__construct($message, $code, $previous);
|
||||
}
|
||||
}
|
11
src/Exceptions/InvalidTaskDateException.php
Normal file
11
src/Exceptions/InvalidTaskDateException.php
Normal file
@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
namespace Exceptions;
|
||||
|
||||
class InvalidTaskDateException extends \Exception
|
||||
{
|
||||
public function __construct(string $message = "Invalid task date.", int $code = 0, \Throwable $previous = null)
|
||||
{
|
||||
parent::__construct($message, $code, $previous);
|
||||
}
|
||||
}
|
8
src/Exceptions/TaskNotFoundException.php
Normal file
8
src/Exceptions/TaskNotFoundException.php
Normal file
@ -0,0 +1,8 @@
|
||||
<?php
|
||||
|
||||
namespace Exceptions;
|
||||
|
||||
class TaskNotFoundException extends \Exception
|
||||
{
|
||||
|
||||
}
|
8
src/Interfaces/Model.php
Normal file
8
src/Interfaces/Model.php
Normal file
@ -0,0 +1,8 @@
|
||||
<?php
|
||||
|
||||
namespace Interfaces;
|
||||
|
||||
interface Model
|
||||
{
|
||||
|
||||
}
|
23
src/Interfaces/TaskDao.php
Normal file
23
src/Interfaces/TaskDao.php
Normal file
@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace Interfaces;
|
||||
|
||||
use Models\Task;
|
||||
|
||||
interface TaskDao
|
||||
{
|
||||
public function create(Task $task): bool;
|
||||
|
||||
/**
|
||||
* Returns an array of Tasks
|
||||
* @return array
|
||||
*/
|
||||
public function readAll(): array;
|
||||
|
||||
public function readById($id): Task;
|
||||
|
||||
public function update(Task $task): bool;
|
||||
|
||||
public function delete(Task $task): bool;
|
||||
|
||||
}
|
8
src/Interfaces/Validator.php
Normal file
8
src/Interfaces/Validator.php
Normal file
@ -0,0 +1,8 @@
|
||||
<?php
|
||||
|
||||
namespace Interfaces;
|
||||
|
||||
interface Validator
|
||||
{
|
||||
public static function validate(): bool;
|
||||
}
|
124
src/Models/Task.php
Normal file
124
src/Models/Task.php
Normal file
@ -0,0 +1,124 @@
|
||||
<?php declare(strict_types=1);
|
||||
|
||||
namespace Models;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Utils\Enums\TaskStatus;
|
||||
|
||||
class Task
|
||||
{
|
||||
|
||||
private int $id;
|
||||
|
||||
private string $title;
|
||||
|
||||
private string $description;
|
||||
|
||||
private TaskStatus $status;
|
||||
|
||||
private string $endDt;
|
||||
|
||||
private string $startDt;
|
||||
|
||||
|
||||
public function __construct($status = TaskStatus::STARTED)
|
||||
{
|
||||
$this->status = $status;
|
||||
|
||||
}
|
||||
|
||||
public function getTitle(): string
|
||||
{
|
||||
return $this->title;
|
||||
}
|
||||
|
||||
public function setTitle(string $title): self
|
||||
{
|
||||
$this->title = $title;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getDescription(): string
|
||||
{
|
||||
return $this->description;
|
||||
}
|
||||
|
||||
public function setDescription(string $description): self
|
||||
{
|
||||
$this->description = $description;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getStartDt(): string
|
||||
{
|
||||
return $this->startDt;
|
||||
}
|
||||
|
||||
public function setStartDt(string $startDt): self
|
||||
{
|
||||
$this->startDt = Carbon::parse($startDt)->toDateTimeString();
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getEndDt(): string
|
||||
{
|
||||
return $this->endDt;
|
||||
}
|
||||
|
||||
public function setEndDt(string $endDt): self
|
||||
{
|
||||
$this->endDt = Carbon::parse($endDt)->toDateTimeString();
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getStatus(): TaskStatus
|
||||
{
|
||||
return $this->status;
|
||||
}
|
||||
|
||||
public function setStatus(TaskStatus $status): self
|
||||
{
|
||||
$this->status = $status;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getTaskId(): int
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
|
||||
public static function fromArray($taskData): Task
|
||||
{
|
||||
$task = new self();
|
||||
|
||||
$task->id = $taskData['id'] ?? null;
|
||||
$task->title = $taskData['name'] ?? '';
|
||||
$task->description = $taskData['description'] ?? '';
|
||||
$task->status = $taskData['status'] ?? 'started';
|
||||
$task->startDt = $taskData['start'] ?? '';
|
||||
$task->endDt = $taskData['end'] ?? '';
|
||||
|
||||
return $task;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the array representation of the database entity.
|
||||
* @return array
|
||||
*/
|
||||
public function persist(): array
|
||||
{
|
||||
return [
|
||||
'task_owner' => 1, // fake hard-coded user for now
|
||||
'created_at' => Carbon::now()->toDateTimeString(),
|
||||
'updated_at' => Carbon::now()->toDateTimeString(),
|
||||
'name' => $this->getTitle(),
|
||||
'description' => $this->getDescription(),
|
||||
'start' => $this->getStartDt(),
|
||||
'end' => $this->getEndDt(),
|
||||
'status_id' => $this->getStatus()->value
|
||||
|
||||
];
|
||||
}
|
||||
|
||||
}
|
79
src/Repositories/TaskRepository.php
Normal file
79
src/Repositories/TaskRepository.php
Normal file
@ -0,0 +1,79 @@
|
||||
<?php declare(strict_types=1);
|
||||
|
||||
namespace Repositories;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Database\Connection;
|
||||
use Interfaces\TaskDao;
|
||||
use Models\Task;
|
||||
use PDO;
|
||||
|
||||
class TaskRepository implements TaskDao
|
||||
{
|
||||
|
||||
private PDO $conn;
|
||||
|
||||
|
||||
public function __construct(Connection $connection)
|
||||
{
|
||||
$this->conn = $connection->getConnection();
|
||||
}
|
||||
|
||||
public function create(Task $task): bool
|
||||
{
|
||||
$data = $task->persist();
|
||||
$stmt = $this->conn->prepare(
|
||||
"INSERT INTO Tasks (task_owner, created_at, updated_at, name, description, start, end, status_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?)"
|
||||
);
|
||||
|
||||
$stmt->bindParam(1, $data['task_owner']);
|
||||
$stmt->bindParam(2, $data['created_at']);
|
||||
$stmt->bindParam(3, $data['updated_at']);
|
||||
$stmt->bindParam(4, $data['name']);
|
||||
$stmt->bindParam(5, $data['description']);
|
||||
$stmt->bindParam(6, $data['start']);
|
||||
$stmt->bindParam(7, $data['end']);
|
||||
$stmt->bindParam(8, $data['status_id']);
|
||||
|
||||
|
||||
return $stmt->execute();
|
||||
}
|
||||
|
||||
public function readAll(): array
|
||||
{
|
||||
$stmt = $this->conn->query('SELECT * FROM Tasks');
|
||||
return $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
}
|
||||
|
||||
public function readById($id): Task
|
||||
{
|
||||
$stmt = $this->conn->prepare('SELECT * FROM Tasks WHERE id = ?');
|
||||
$stmt->execute([$id]);
|
||||
|
||||
return Task::fromArray($stmt->fetch(PDO::FETCH_ASSOC));
|
||||
}
|
||||
|
||||
public function update(Task $task): bool
|
||||
{
|
||||
$stmt = $this->conn->prepare(
|
||||
"UPDATE Tasks SET task_owner = ?, updated_at = ?, name = ?, description = ?, start = ?, end = ?, status_id = 0 WHERE id = ?"
|
||||
);
|
||||
|
||||
return $stmt->execute([
|
||||
1, // Mock user ID for now, we haven't implemented authentication yet
|
||||
Carbon::now(),
|
||||
$task->getTitle(),
|
||||
$task->getDescription(),
|
||||
$task->getStartDt(),
|
||||
$task->getEndDt(),
|
||||
$task->getStatus(),
|
||||
$task->getTaskId()
|
||||
]);
|
||||
}
|
||||
|
||||
public function delete(Task $task): bool
|
||||
{
|
||||
$stmt = $this->conn->prepare('DELETE FROM Tasks WHERE id = ?');
|
||||
return $stmt->execute([$task->getTaskId()]);
|
||||
}
|
||||
}
|
27
src/Utils/Enums/TaskStatus.php
Normal file
27
src/Utils/Enums/TaskStatus.php
Normal file
@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace Utils\Enums;
|
||||
|
||||
enum TaskStatus: int
|
||||
{
|
||||
/**
|
||||
* Used when a task has just been started.
|
||||
*/
|
||||
case STARTED = 1;
|
||||
|
||||
/**
|
||||
* Used when a task is currently being worked on.
|
||||
*/
|
||||
case IN_PROGRESS = 2;
|
||||
|
||||
|
||||
/**
|
||||
* Used when a task is currently blocking this task.
|
||||
*/
|
||||
case BLOCKED = 3;
|
||||
|
||||
/**
|
||||
* Used when a task is currently completed.
|
||||
*/
|
||||
case COMPLETED = 4;
|
||||
}
|
134
src/Utils/StructuredResponseBuilder.php
Normal file
134
src/Utils/StructuredResponseBuilder.php
Normal file
@ -0,0 +1,134 @@
|
||||
<?php declare(strict_types=1);
|
||||
|
||||
namespace Utils;
|
||||
|
||||
class StructuredResponseBuilder
|
||||
{
|
||||
|
||||
/**
|
||||
* @var bool If true, the response payload is automatically formated as an error
|
||||
*/
|
||||
private bool $isErrorResponse = false;
|
||||
|
||||
/**
|
||||
* @var int Only used if isErrorResponse is True. Sets the HTTP response code for the error.
|
||||
*/
|
||||
private int $isErrorResponseCode;
|
||||
|
||||
/**
|
||||
* @var string Only used if isErrorResponse is True. Sets the HTTP error message for the error.
|
||||
*/
|
||||
private string $isErrorMessage;
|
||||
|
||||
/**
|
||||
* @var string Sets the optional message to be sent within the payload.
|
||||
*/
|
||||
private string $optionalMessage;
|
||||
|
||||
/**
|
||||
* @var array Sets the payload to be sent within the response.
|
||||
*/
|
||||
private array $payload = [];
|
||||
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function isError(): bool
|
||||
{
|
||||
return $this->isErrorResponse;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param bool $isErrorResponse
|
||||
* @return $this
|
||||
*/
|
||||
public function setError(bool $isErrorResponse = true): self
|
||||
{
|
||||
$this->isErrorResponse = $isErrorResponse;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function getErrorCode(): int
|
||||
{
|
||||
return $this->isErrorResponseCode;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $isErrorResponseCode
|
||||
* @return $this
|
||||
*/
|
||||
public function setErrorCode(int $isErrorResponseCode): self
|
||||
{
|
||||
$this->isErrorResponseCode = $isErrorResponseCode;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getErrorMessage(): string
|
||||
{
|
||||
return $this->isErrorMessage;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $isErrorMessage
|
||||
* @return $this
|
||||
*/
|
||||
public function setErrorMessage(string $isErrorMessage): self
|
||||
{
|
||||
$this->isErrorMessage = $isErrorMessage;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getOptionalMessage(): string
|
||||
{
|
||||
return $this->optionalMessage;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $optionalMessage
|
||||
* @return $this
|
||||
*/
|
||||
public function setOptionalMessage(string $optionalMessage): self
|
||||
{
|
||||
$this->optionalMessage = $optionalMessage;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function getPayload(): array
|
||||
{
|
||||
return $this->payload;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $payload
|
||||
* @return $this
|
||||
*/
|
||||
public function setPayload(array $payload): self
|
||||
{
|
||||
$this->payload = $payload;
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
||||
public function build(): string
|
||||
{
|
||||
return json_encode([
|
||||
"error" => $this->isError(),
|
||||
"code" => (!$this->isError()) ? 'NaN' : $this->getErrorCode(),
|
||||
"message" => ($this->isError()) ? $this->getErrorMessage() : $this->getOptionalMessage(),
|
||||
"payload" => $this->getPayload()
|
||||
]);
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user