13 Commits
10 changed files with 468 additions and 13 deletions
+4
View File
@@ -17,3 +17,7 @@ modules:
dump: tests/_data/dump.sql dump: tests/_data/dump.sql
cleanup: true # reload dump between tests cleanup: true # reload dump between tests
populate: true # load dump before all tests populate: true # load dump before all tests
coverage:
enabled: true
include:
- src/*
+10 -2
View File
@@ -8,8 +8,16 @@ use Slim\Psr7\Request;
class HelloWorldController class HelloWorldController
{ {
/** @noinspection PhpUnusedParameterInspection */ /**
public function read(Request $request, Response $response, $args): Response * @param Request $request
* @param Response $response
* @param mixed[] $args
*
* @return Response
*
* @noinspection PhpUnusedParameterInspection
*/
public function read(Request $request, Response $response, array $args): Response
{ {
$payload = json_encode('Hallo World'); $payload = json_encode('Hallo World');
@@ -0,0 +1,117 @@
<?php
namespace TorstenHettstedt\TimekeepingApi\Repositories;
use DateInterval;
use DateTimeInterface;
use Exception;
use PDO;
use PDOException;
use TorstenHettstedt\TimekeepingApi\Models\PeriodDesignationEnum;
use TorstenHettstedt\TimekeepingApi\Models\WorkingHoursView;
/**
* @implements RepositoryReaderInterface<WorkingHoursView>
*/
abstract class AbstractWorkingHoursViewRepository implements RepositoryReaderInterface
{
protected const SQL_SELECT = '';
protected const SQL_WHERE = '';
protected PDO $database;
protected PeriodDesignationEnum $periodDesignation;
/**
* WorkingHoursRepository constructor.
*
* @param PDO $database
*/
public function __construct(PDO $database)
{
$this->database = $database;
$this->database->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
}
/**
*
* Gibt alle vorhandenen Einträge zurück
*
* @return WorkingHoursView[]
* @throws Exception
* @throws PDOException
*/
public function findAll(): array
{
$models = [];
$stmt = $this->database->prepare(static::SQL_SELECT);
$stmt->execute();
while ($row = $stmt->fetch()) {
$model = new WorkingHoursView(
$this->buildDateFromPeriod($row['period']),
$this->periodDesignation,
(int) $row['workingDays'],
$this->buildDateInterval($row['totalHours']),
$this->buildDateInterval($row['overtime'])
);
$models[] = $model;
}
return $models;
}
/**
* Gibt den Eintrag zurück, der durch die ID
*
* @param mixed $primary_key
*
* @return WorkingHoursView
* @throws RepositoryRecordNotFoundException
* @throws Exception
* @throws PDOException
*/
public function findByKey(mixed $primary_key): WorkingHoursView
{
$stmt = $this->database->prepare(static::SQL_SELECT . static::SQL_WHERE);
$stmt->bindParam(1, $primary_key);
$stmt->execute();
$row = $stmt->fetch();
if ($row === false) {
throw new RepositoryRecordNotFoundException();
}
return new WorkingHoursView(
$this->buildDateFromPeriod($row['period']),
$this->periodDesignation,
(int) $row['workingDays'],
$this->buildDateInterval($row['totalHours']),
$this->buildDateInterval($row['overtime'])
);
}
/**
* @param string $time
*
* @return DateInterval
* @throws Exception
*/
protected function buildDateInterval(string $time): DateInterval
{
$time_array = explode(':', $time);
$hours = (int)$time_array[0];
$minutes = (int)$time_array[1];
$seconds = (int)$time_array[2];
$interval = new DateInterval(sprintf('PT%dH%dM%dS', abs($hours), abs($minutes), abs($seconds)));
if (($hours < 0 || ($minutes < 0) || ($seconds < 0))) {
$interval->invert = 1;
}
return $interval;
}
/**
* @param string $period
*
* @return DateTimeInterface
* @throws Exception
*/
abstract protected function buildDateFromPeriod(string $period): DateTimeInterface;
}
@@ -0,0 +1,38 @@
<?php
namespace TorstenHettstedt\TimekeepingApi\Repositories;
use DateTime;
use DateTimeInterface;
use PDO;
use TorstenHettstedt\TimekeepingApi\Models\PeriodDesignationEnum;
class WorkingHoursMonthlyViewRepository extends AbstractWorkingHoursViewRepository
{
protected const SQL_SELECT = <<<SQL
select
"Monat" as "period",
"Gesamtarbeitszeit" as "totalHours",
"Arbeitstage" as "workingDays",
"Überstunden" as "overtime"
from "Arbeitszeiten - Monat"
SQL;
protected const SQL_WHERE = ' where "Monat" = ?';
protected PDO $database;
protected PeriodDesignationEnum $periodDesignation;
public function __construct(PDO $database)
{
parent::__construct($database);
$this->periodDesignation = PeriodDesignationEnum::MONTHLY();
}
protected function buildDateFromPeriod(string $period): DateTimeInterface
{
return new DateTime($period . '-01');
}
}
@@ -7,6 +7,7 @@ namespace TorstenHettstedt\TimekeepingApi\Repositories;
use DateInterval; use DateInterval;
use DateTime; use DateTime;
use Exception; use Exception;
use InvalidArgumentException;
use PDO; use PDO;
use PDOException; use PDOException;
use TorstenHettstedt\TimekeepingApi\Models\ModelInterface; use TorstenHettstedt\TimekeepingApi\Models\ModelInterface;
@@ -43,13 +44,13 @@ class WorkingHoursRepository implements RepositoryReaderInterface, RepositoryWri
public function findAll(): array public function findAll(): array
{ {
$models = []; $models = [];
$stmt = $this->database->prepare('SELECT "Datum" as workingDay, "Arbeitszeit" as workingTime FROM public."Arbeitszeiten"'); $stmt = $this->database->prepare('select "Datum" as "workingDay", "Arbeitszeit" as "workingTime" from public."Arbeitszeiten"');
$stmt->execute(); $stmt->execute();
while ($row = $stmt->fetch()) { while ($row = $stmt->fetch()) {
$model = new WorkingHours(); $model = new WorkingHours();
$model $model
->setWorkingDay(new DateTime($row['workingday'])) ->setWorkingDay(new DateTime($row['workingDay']))
->setWorkingTime(new DateInterval('P0000-00-00T'.$row['workingtime'])); ->setWorkingTime(new DateInterval('P0000-00-00T' . $row['workingTime']));
$models[] = $model; $models[] = $model;
} }
return $models; return $models;
@@ -67,7 +68,7 @@ class WorkingHoursRepository implements RepositoryReaderInterface, RepositoryWri
*/ */
public function findByKey(mixed $primary_key): WorkingHours public function findByKey(mixed $primary_key): WorkingHours
{ {
$stmt = $this->database->prepare('SELECT "Datum" as workingDay, "Arbeitszeit" as workingTime FROM public."Arbeitszeiten" WHERE "Datum" = ?'); $stmt = $this->database->prepare('select "Datum" as "workingDay", "Arbeitszeit" as "workingTime" from public."Arbeitszeiten" where "Datum" = ?');
$stmt->bindParam(1, $primary_key); $stmt->bindParam(1, $primary_key);
$stmt->execute(); $stmt->execute();
$row = $stmt->fetch(); $row = $stmt->fetch();
@@ -76,32 +77,77 @@ class WorkingHoursRepository implements RepositoryReaderInterface, RepositoryWri
} }
$model = new WorkingHours(); $model = new WorkingHours();
$model $model
->setWorkingDay(new DateTime($row['workingday'])) ->setWorkingDay(new DateTime($row['workingDay']))
->setWorkingTime(new DateInterval('P0000-00-00T'.$row['workingtime'])); ->setWorkingTime(new DateInterval('P0000-00-00T' . $row['workingTime']));
return $model; return $model;
} }
/** /**
* @param WorkingHours|ModelInterface $model * @param WorkingHours|ModelInterface $model
*
* @throws RepositoryModelAlreadyExists
* @throws Exception
*/ */
public function insert(mixed $model): void public function insert(mixed $model): void
{ {
// TODO: Implement insert() method. if ($model instanceof WorkingHours) {
$workingDay = $model->getWorkingDay()->format('Y-m-d');
$workingTime = $model->getWorkingTime()->format('%H:%I:%S');
} else {
throw new InvalidArgumentException('Es wird ein Modell der Klasse "' . WorkingHours::class . '" verlangt.');
}
try {
$this->findByKey($workingDay);
throw new RepositoryModelAlreadyExists();
} /** @noinspection PhpUnusedLocalVariableInspection */
catch (RepositoryRecordNotFoundException $exception) {
$stmt = $this->database->prepare('insert into public."Arbeitszeiten" ("Datum", "Arbeitszeit") values (?, ?) on conflict do nothing');
$stmt->bindParam(1, $workingDay);
$stmt->bindParam(2, $workingTime);
$stmt->execute();
}
} }
/** /**
* @param WorkingHours|ModelInterface $model * @param WorkingHours|ModelInterface $model
*
* @throws RepositoryRecordNotFoundException
*/ */
public function update(mixed $model): void public function update(mixed $model): void
{ {
// TODO: Implement update() method. if ($model instanceof WorkingHours) {
$workingDay = $model->getWorkingDay()->format('Y-m-d');
$workingTime = $model->getWorkingTime()->format('%H:%I:%S');
} else {
throw new InvalidArgumentException('Es wird ein Modell der Klasse "' . WorkingHours::class . '" verlangt.');
}
$stmt = $this->database->prepare('update public."Arbeitszeiten" set "Arbeitszeit" = ? where "Datum" = ?');
$stmt->bindParam(1, $workingTime);
$stmt->bindParam(2, $workingDay);
$stmt->execute();
if ($stmt->rowCount() < 1) {
throw new RepositoryRecordNotFoundException();
}
} }
/** /**
* @param WorkingHours|ModelInterface $model * @param WorkingHours|ModelInterface $model
*
* @throws RepositoryRecordNotFoundException
*/ */
public function delete(mixed $model): void public function delete(mixed $model): void
{ {
// TODO: Implement delete() method. if ($model instanceof WorkingHours) {
$workingDay = $model->getWorkingDay()->format('Y-m-d');
} else {
throw new InvalidArgumentException('Es wird ein Modell der Klasse "' . WorkingHours::class . '" verlangt.');
}
$stmt = $this->database->prepare('delete from public."Arbeitszeiten" where "Datum" = ?');
$stmt->bindParam(1, $workingDay);
$stmt->execute();
if ($stmt->rowCount() < 1) {
throw new RepositoryRecordNotFoundException();
}
} }
} }
@@ -0,0 +1,43 @@
<?php
namespace TorstenHettstedt\TimekeepingApi\Repositories;
use DateInterval;
use DateTime;
use DateTimeInterface;
use PDO;
use TorstenHettstedt\TimekeepingApi\Models\PeriodDesignationEnum;
class WorkingHoursWeeklyViewRepository extends AbstractWorkingHoursViewRepository
{
protected const SQL_SELECT = <<<SQL
select
"Woche" as "period",
"Gesamtarbeitszeit" as "totalHours",
"Arbeitstage" as "workingDays",
"Überstunden" as "overtime"
from "Arbeitszeiten - Woche"
SQL;
protected const SQL_WHERE = ' where "Woche" = ?';
protected PDO $database;
protected PeriodDesignationEnum $periodDesignation;
public function __construct(PDO $database)
{
parent::__construct($database);
$this->periodDesignation = PeriodDesignationEnum::WEEKLY();
}
protected function buildDateFromPeriod(string $period): DateTimeInterface
{
$time_array = explode('#', $period);
$year = (int)$time_array[0];
$week = (int)$time_array[1] - 1;
$date = new DateTime('first day of January ' . $year);
$date->modify('monday this week');
$date->add(new DateInterval(sprintf('P%dW', $week)));
return $date;
}
}
@@ -0,0 +1,43 @@
<?php
namespace TorstenHettstedt\TimekeepingApi\Repositories;
use DateTime;
use DateTimeInterface;
use Exception;
use PDO;
use TorstenHettstedt\TimekeepingApi\Models\PeriodDesignationEnum;
class WorkingHoursYearlyViewRepository extends AbstractWorkingHoursViewRepository
{
protected const SQL_SELECT = <<<SQL
select
"Jahr" as "period",
"Gesamtarbeitszeit" as "totalHours",
"Arbeitstage" as "workingDays",
"Überstunden" as "overtime"
from "Arbeitszeiten - Jahr"
SQL;
protected const SQL_WHERE = ' where "Jahr" = ?';
protected PDO $database;
protected PeriodDesignationEnum $periodDesignation;
public function __construct(PDO $database)
{
parent::__construct($database);
$this->periodDesignation = PeriodDesignationEnum::YEARLY();
}
/**
* @param string $period
*
* @return DateTimeInterface
* @throws Exception
*/
protected function buildDateFromPeriod(string $period): DateTimeInterface
{
return new DateTime($period . '-01-01');
}
}
+3 -2
View File
@@ -23,8 +23,9 @@ DROP VIEW IF EXISTS public."Arbeitszeiten - Jahr";
-- --
CREATE TABLE public."Arbeitszeiten" ( CREATE TABLE public."Arbeitszeiten" (
"Datum" date NOT NULL, "Datum" date not null,
"Arbeitszeit" interval "Arbeitszeit" interval(6) null,
constraint "Arbeitszeiten_pkey" primary key ("Datum")
); );
@@ -6,8 +6,10 @@ use Codeception\Test\Unit;
use DateInterval; use DateInterval;
use DateTime; use DateTime;
use Exception; use Exception;
use InvalidArgumentException;
use PDO; use PDO;
use PDOStatement; use PDOStatement;
use TorstenHettstedt\TimekeepingApi\Models\ModelInterface;
use TorstenHettstedt\TimekeepingApi\Models\WorkingHours; use TorstenHettstedt\TimekeepingApi\Models\WorkingHours;
use TorstenHettstedt\TimekeepingApi\Repositories\RepositoryModelAlreadyExists; use TorstenHettstedt\TimekeepingApi\Repositories\RepositoryModelAlreadyExists;
use TorstenHettstedt\TimekeepingApi\Repositories\RepositoryRecordNotFoundException; use TorstenHettstedt\TimekeepingApi\Repositories\RepositoryRecordNotFoundException;
@@ -26,10 +28,14 @@ class WorkingHoursRepositoryTest extends Unit
protected function _before(): void protected function _before(): void
{ {
/** @noinspection SpellCheckingInspection */
$this->pdoObject = new PDO('pgsql:host=psql.torsten-hettstedt.net;port=5432;dbname=testdb;user=bruce;password=mypass'); $this->pdoObject = new PDO('pgsql:host=psql.torsten-hettstedt.net;port=5432;dbname=testdb;user=bruce;password=mypass');
parent::_before(); parent::_before();
} }
/**
* @throws Exception
*/
public function testFindAll(): void public function testFindAll(): void
{ {
$repository = new WorkingHoursRepository($this->pdoObject); $repository = new WorkingHoursRepository($this->pdoObject);
@@ -58,6 +64,17 @@ class WorkingHoursRepositoryTest extends Unit
$repository->findByKey(self::NEW_DATE); $repository->findByKey(self::NEW_DATE);
} }
/**
* @throws Exception
*/
public function testInsertWithInvalidModel(): void
{
$repository = new WorkingHoursRepository($this->pdoObject);
$model = $this->makeEmpty(ModelInterface::class);
$this->expectException(InvalidArgumentException::class);
$repository->insert($model);
}
/** /**
* @throws RepositoryRecordNotFoundException * @throws RepositoryRecordNotFoundException
* @throws Exception * @throws Exception
@@ -91,6 +108,18 @@ class WorkingHoursRepositoryTest extends Unit
$repository->insert($model); $repository->insert($model);
} }
/**
* @throws RepositoryRecordNotFoundException
* @throws Exception
*/
public function testDeleteWithInvalidModel(): void
{
$repository = new WorkingHoursRepository($this->pdoObject);
$model = $this->makeEmpty(ModelInterface::class);
$this->expectException(InvalidArgumentException::class);
$repository->delete($model);
}
/** /**
* @throws RepositoryRecordNotFoundException * @throws RepositoryRecordNotFoundException
* @throws Exception * @throws Exception
@@ -107,6 +136,10 @@ class WorkingHoursRepositoryTest extends Unit
$repository->findByKey(self::EXISTING_DATE); $repository->findByKey(self::EXISTING_DATE);
} }
/**
* @throws RepositoryRecordNotFoundException
* @throws Exception
*/
public function testNotExistsDelete(): void public function testNotExistsDelete(): void
{ {
$repository = new WorkingHoursRepository($this->pdoObject); $repository = new WorkingHoursRepository($this->pdoObject);
@@ -118,6 +151,18 @@ class WorkingHoursRepositoryTest extends Unit
$repository->delete($model); $repository->delete($model);
} }
/**
* @throws RepositoryRecordNotFoundException
* @throws Exception
*/
public function testUpdateWithInvalidModel(): void
{
$repository = new WorkingHoursRepository($this->pdoObject);
$model = $this->makeEmpty(ModelInterface::class);
$this->expectException(InvalidArgumentException::class);
$repository->update($model);
}
/** /**
* @throws RepositoryRecordNotFoundException * @throws RepositoryRecordNotFoundException
* @throws Exception * @throws Exception
@@ -0,0 +1,110 @@
<?php
namespace TorstenHettstedt\TimekeepingApi\Tests\Unit\Repositories;
use Codeception\Test\Unit;
use DateTime;
use DateTimeZone;
use Exception;
use PDO;
use TorstenHettstedt\TimekeepingApi\Models\WorkingHoursView;
use TorstenHettstedt\TimekeepingApi\Repositories\RepositoryRecordNotFoundException;
use TorstenHettstedt\TimekeepingApi\Repositories\WorkingHoursWeeklyViewRepository;
use TorstenHettstedt\TimekeepingApi\Repositories\WorkingHoursMonthlyViewRepository;
use TorstenHettstedt\TimekeepingApi\Repositories\WorkingHoursYearlyViewRepository;
class WorkingHoursViewRepositoryTest extends Unit
{
protected PDO $pdoObject;
protected function _before(): void
{
/** @noinspection SpellCheckingInspection */
$this->pdoObject = new PDO('pgsql:host=psql.torsten-hettstedt.net;port=5432;dbname=testdb;user=bruce;password=mypass');
parent::_before();
}
/**
* @return array[]
*/
public function listObjectProvider(): array
{
return [
[WorkingHoursYearlyViewRepository::class, 1],
[WorkingHoursMonthlyViewRepository::class, 3],
[WorkingHoursWeeklyViewRepository::class, 13],
];
}
/**
* @return array[]
* @throws Exception
*/
public function existingObjectProvider(): array
{
return [
[WorkingHoursYearlyViewRepository::class, '2020', 61, new DateTime('2020-01-01', new DateTimeZone('UCT'))],
[WorkingHoursMonthlyViewRepository::class, '2020 February', 20, new DateTime('2020-02-01', new DateTimeZone('UCT'))],
[WorkingHoursWeeklyViewRepository::class, '2020#03', 5, new DateTime('2020-01-13', new DateTimeZone('UCT'))],
];
}
/**
* @return array[]
*/
public function notExistingObjectProvider(): array
{
return [
[WorkingHoursYearlyViewRepository::class, '2022',],
[WorkingHoursMonthlyViewRepository::class, '2020 May',],
[WorkingHoursWeeklyViewRepository::class, '2020#30',],
];
}
/**
* @param string $period_class
* @param int $count_records
*
* @dataProvider listObjectProvider
*/
public function testFindAll(string $period_class, int $count_records): void
{
$repository = new $period_class($this->pdoObject);
$models = $repository->findAll();
$this->assertIsArray($models);
$this->assertContainsOnly(WorkingHoursView::class, $models);
$this->assertCount($count_records, $models);
}
/**
*
* @dataProvider existingObjectProvider
*
* @param string $period_class
* @param string $search
* @param int $workingDays
* @param DateTime $period
*/
public function testExistingFindByKey(string $period_class, string $search, int $workingDays, DateTime $period): void
{
$repository = new $period_class($this->pdoObject);
$model = $repository->findByKey($search);
$this->assertInstanceOf(WorkingHoursView::class, $model);
$this->assertEquals($workingDays, $model->getWorkingDays());
$this->assertEquals($period->format('Ymd'), $model->getPeriod()->format('Ymd'));
}
/**
* @param string $period_class
* @param string $search
*
* @dataProvider notExistingObjectProvider
*
*/
public function testNotExistingFindByKey(string $period_class, string $search): void
{
$repository = new $period_class($this->pdoObject);
$this->expectException(RepositoryRecordNotFoundException::class);
$repository->findByKey($search);
}
}