From aa66d1f53d8c3fc079d6edc1da809ddbefdccb1c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torsten=20L=C3=BCcke?= Date: Thu, 1 May 2025 17:51:41 +0200 Subject: [PATCH 01/15] Erweiterung auf PHP 8.1 --- api/Dockerfile | 2 +- api/composer.json | 17 +++++++++-------- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/api/Dockerfile b/api/Dockerfile index 4d8c4ea..9cb0a73 100644 --- a/api/Dockerfile +++ b/api/Dockerfile @@ -1,4 +1,4 @@ -FROM php:8.0-apache +FROM php:8.1-apache-bookworm RUN apt update && apt install -y libpq-dev RUN docker-php-ext-install -j$(nproc) pdo pdo_pgsql diff --git a/api/composer.json b/api/composer.json index d4a5b86..b6401c7 100644 --- a/api/composer.json +++ b/api/composer.json @@ -16,21 +16,22 @@ } ], "require": { + "php": "8.1.*", "slim/slim": "^4.7.1", - "vlucas/phpdotenv": "^4.2", - "slim/psr7": "^1.3", + "vlucas/phpdotenv": "^v5.6.1", + "slim/psr7": "^1.7.0", "php-di/slim-bridge": "^3.1.0", "jetbrains/phpstorm-attributes": "^1.0.0", "myclabs/php-enum": "^1.8.0", "ext-pdo": "*" }, "require-dev": { - "phpstan/phpstan": "^0.12.80", - "codeception/codeception": "^4.1.18", - "codeception/module-phpbrowser": "^1.0.0", - "codeception/module-asserts": "^1.0.0", - "codeception/module-db": "^1.1.0", - "codeception/module-rest": "^1.2.8" + "phpstan/phpstan": "^2.1.12", + "codeception/codeception": "^5.2.1", + "codeception/module-phpbrowser": "^3.0.1", + "codeception/module-asserts": "^3.1.0", + "codeception/module-db": "^3.2.2", + "codeception/module-rest": "^3.4.1" }, "autoload": { "psr-4": { From dff3c7f34d003abd597af1b8e63853fb5e11718b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torsten=20L=C3=BCcke?= Date: Thu, 1 May 2025 17:52:39 +0200 Subject: [PATCH 02/15] Anpassungen an das aktuelle *Postges* --- api/tests/_data/dump.sql | 128 +++++++++++++++++++-------------------- 1 file changed, 64 insertions(+), 64 deletions(-) diff --git a/api/tests/_data/dump.sql b/api/tests/_data/dump.sql index 0dd1931..ffc1978 100644 --- a/api/tests/_data/dump.sql +++ b/api/tests/_data/dump.sql @@ -13,10 +13,10 @@ SET xmloption = content; SET client_min_messages = warning; SET row_security = off; -DROP TABLE IF EXISTS public."Arbeitszeiten"; DROP VIEW IF EXISTS public."Arbeitszeiten - Woche"; DROP VIEW IF EXISTS public."Arbeitszeiten - Monat"; DROP VIEW IF EXISTS public."Arbeitszeiten - Jahr"; +DROP TABLE IF EXISTS public."Arbeitszeiten"; -- -- Name: Arbeitszeiten; Type: TABLE; Schema: public; Owner: torsten @@ -83,66 +83,66 @@ ALTER TABLE public."Arbeitszeiten - Woche" OWNER TO bruce; -- Data for Name: Arbeitszeiten; Type: TABLE DATA; Schema: public; Owner: torsten -- -COPY public."Arbeitszeiten" ("Datum", "Arbeitszeit") FROM stdin; -2020-01-07 08:06:00 -2020-01-08 08:16:00 -2020-01-09 07:49:00 -2020-01-10 07:30:00 -2020-01-13 08:06:00 -2020-01-14 08:04:00 -2020-01-15 08:20:00 -2020-01-16 07:13:00 -2020-01-17 07:57:00 -2020-01-20 07:53:00 -2020-01-21 07:49:00 -2020-01-22 07:56:00 -2020-01-23 07:55:00 -2020-01-24 07:08:00 -2020-01-27 08:23:00 -2020-01-28 07:20:00 -2020-01-29 08:13:00 -2020-01-30 08:43:00 -2020-01-31 07:21:00 -2020-02-03 07:56:00 -2020-02-04 08:05:00 -2020-02-05 07:58:00 -2020-02-06 08:01:00 -2020-02-07 07:59:00 -2020-02-10 07:50:00 -2020-02-11 08:01:00 -2020-02-12 08:14:00 -2020-02-13 08:12:00 -2020-02-14 07:51:00 -2020-02-17 07:59:00 -2020-02-18 08:05:00 -2020-02-19 07:34:00 -2020-02-20 07:33:00 -2020-02-21 07:56:00 -2020-02-24 08:02:00 -2020-02-25 08:13:00 -2020-02-26 08:42:00 -2020-02-27 07:49:00 -2020-02-28 08:16:00 -2020-03-02 08:22:00 -2020-03-03 08:20:00 -2020-03-04 08:12:00 -2020-03-05 08:32:00 -2020-03-06 04:41:00 -2020-03-09 07:05:00 -2020-03-10 07:34:00 -2020-03-11 07:39:00 -2020-03-12 07:50:00 -2020-03-13 08:26:00 -2020-03-16 07:51:00 -2020-03-17 07:50:00 -2020-03-18 07:19:00 -2020-03-19 07:55:00 -2020-03-20 05:43:00 -2020-03-23 08:05:00 -2020-03-24 08:21:00 -2020-03-25 08:10:00 -2020-03-26 10:31:00 -2020-03-27 04:55:00 -2020-03-30 08:21:00 -2020-03-31 08:01:00 -\. +INSERT INTO public."Arbeitszeiten" ("Datum", "Arbeitszeit") VALUES +('2020-01-07','08:06:00'), +('2020-01-08','08:16:00'), +('2020-01-09','07:49:00'), +('2020-01-10','07:30:00'), +('2020-01-13','08:06:00'), +('2020-01-14','08:04:00'), +('2020-01-15','08:20:00'), +('2020-01-16','07:13:00'), +('2020-01-17','07:57:00'), +('2020-01-20','07:53:00'), +('2020-01-21','07:49:00'), +('2020-01-22','07:56:00'), +('2020-01-23','07:55:00'), +('2020-01-24','07:08:00'), +('2020-01-27','08:23:00'), +('2020-01-28','07:20:00'), +('2020-01-29','08:13:00'), +('2020-01-30','08:43:00'), +('2020-01-31','07:21:00'), +('2020-02-03','07:56:00'), +('2020-02-04','08:05:00'), +('2020-02-05','07:58:00'), +('2020-02-06','08:01:00'), +('2020-02-07','07:59:00'), +('2020-02-10','07:50:00'), +('2020-02-11','08:01:00'), +('2020-02-12','08:14:00'), +('2020-02-13','08:12:00'), +('2020-02-14','07:51:00'), +('2020-02-17','07:59:00'), +('2020-02-18','08:05:00'), +('2020-02-19','07:34:00'), +('2020-02-20','07:33:00'), +('2020-02-21','07:56:00'), +('2020-02-24','08:02:00'), +('2020-02-25','08:13:00'), +('2020-02-26','08:42:00'), +('2020-02-27','07:49:00'), +('2020-02-28','08:16:00'), +('2020-03-02','08:22:00'), +('2020-03-03','08:20:00'), +('2020-03-04','08:12:00'), +('2020-03-05','08:32:00'), +('2020-03-06','04:41:00'), +('2020-03-09','07:05:00'), +('2020-03-10','07:34:00'), +('2020-03-11','07:39:00'), +('2020-03-12','07:50:00'), +('2020-03-13','08:26:00'), +('2020-03-16','07:51:00'), +('2020-03-17','07:50:00'), +('2020-03-18','07:19:00'), +('2020-03-19','07:55:00'), +('2020-03-20','05:43:00'), +('2020-03-23','08:05:00'), +('2020-03-24','08:21:00'), +('2020-03-25','08:10:00'), +('2020-03-26','10:31:00'), +('2020-03-27','04:55:00'), +('2020-03-30','08:21:00'), +('2020-03-31','08:01:00') +; \ No newline at end of file From b094cf818013f8796f5ab54b4505ec75083d40fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torsten=20L=C3=BCcke?= Date: Thu, 1 May 2025 17:53:02 +0200 Subject: [PATCH 03/15] Kleine Korrekturen wegen PHP 8.1 --- api/src/Controller/HttpConflictRequestException.php | 9 ++------- api/tests/unit/Controller/WorkingHoursControllerTest.php | 1 + .../unit/Controller/WorkingHoursViewControllerTest.php | 1 + api/tests/unit/Middleware/ErrorHandlerTest.php | 1 + 4 files changed, 5 insertions(+), 7 deletions(-) diff --git a/api/src/Controller/HttpConflictRequestException.php b/api/src/Controller/HttpConflictRequestException.php index f995249..8315fe4 100644 --- a/api/src/Controller/HttpConflictRequestException.php +++ b/api/src/Controller/HttpConflictRequestException.php @@ -8,13 +8,8 @@ class HttpConflictRequestException extends HttpSpecializedException { /** @var int */ protected $code = 409; - /** @var string */ protected $message = 'Conflict.'; - - /** @var string */ - protected $title = '409 Conflict'; - - /** @var string */ - protected $description = 'The 409 (Conflict) status code indicates that the request could not be completed due to a conflict with the current state of the target resource.'; + protected string $title = '409 Conflict'; + protected string $description = 'The 409 (Conflict) status code indicates that the request could not be completed due to a conflict with the current state of the target resource.'; } diff --git a/api/tests/unit/Controller/WorkingHoursControllerTest.php b/api/tests/unit/Controller/WorkingHoursControllerTest.php index 40d67e3..a44846d 100644 --- a/api/tests/unit/Controller/WorkingHoursControllerTest.php +++ b/api/tests/unit/Controller/WorkingHoursControllerTest.php @@ -51,6 +51,7 @@ class WorkingHoursControllerTest extends Unit 'write' => function (mixed $data) { $this->assertIsString($data); $this->assertJson($data); + return strlen($data); }, ]), 'withHeader' => $this->makeEmpty(Response::class, [ diff --git a/api/tests/unit/Controller/WorkingHoursViewControllerTest.php b/api/tests/unit/Controller/WorkingHoursViewControllerTest.php index fa2ca2f..e7c4768 100644 --- a/api/tests/unit/Controller/WorkingHoursViewControllerTest.php +++ b/api/tests/unit/Controller/WorkingHoursViewControllerTest.php @@ -35,6 +35,7 @@ class WorkingHoursViewControllerTest extends Unit 'write' => function (mixed $data) { $this->assertIsString($data); $this->assertJson($data); + return strlen($data); }, ]), 'withHeader' => $this->makeEmpty(Response::class, [ diff --git a/api/tests/unit/Middleware/ErrorHandlerTest.php b/api/tests/unit/Middleware/ErrorHandlerTest.php index 036fd8b..e317c0a 100644 --- a/api/tests/unit/Middleware/ErrorHandlerTest.php +++ b/api/tests/unit/Middleware/ErrorHandlerTest.php @@ -38,6 +38,7 @@ class ErrorHandlerTest extends Unit 'write' => function (mixed $data) { $this->assertIsString($data); $this->assertJson($data); + return strlen($data); }, ]), ]), From 82aad9766e412b9c1b15cadfcf5e1a1fe319f179 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torsten=20L=C3=BCcke?= Date: Thu, 1 May 2025 18:17:45 +0200 Subject: [PATCH 04/15] Erweiterung auf PHP 8.4 --- api/Dockerfile | 2 +- api/composer.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/api/Dockerfile b/api/Dockerfile index 9cb0a73..e269c7e 100644 --- a/api/Dockerfile +++ b/api/Dockerfile @@ -1,4 +1,4 @@ -FROM php:8.1-apache-bookworm +FROM php:8.4-apache-bookworm RUN apt update && apt install -y libpq-dev RUN docker-php-ext-install -j$(nproc) pdo pdo_pgsql diff --git a/api/composer.json b/api/composer.json index b6401c7..b1712b5 100644 --- a/api/composer.json +++ b/api/composer.json @@ -16,7 +16,7 @@ } ], "require": { - "php": "8.1.*", + "php": "8.4.*", "slim/slim": "^4.7.1", "vlucas/phpdotenv": "^v5.6.1", "slim/psr7": "^1.7.0", From 41a3c79c9188c4b1ba54295c6759c1d4c4be45a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torsten=20L=C3=BCcke?= Date: Sat, 3 May 2025 09:27:39 +0200 Subject: [PATCH 05/15] Refactor Tests --- .../Controller/AbstractControllerTest.php | 52 +++++++++ .../Controller/WorkingHoursControllerTest.php | 108 ++++++++---------- .../WorkingHoursViewControllerTest.php | 52 ++++----- .../unit/Middleware/ErrorHandlerTest.php | 7 +- api/tests/unit/Models/WorkingHoursTest.php | 18 ++- .../unit/Models/WorkingHoursViewTest.php | 71 +++++++----- .../WorkingHoursRepositoryTest.php | 74 ++++++------ .../WorkingHoursViewRepositoryTest.php | 51 +++------ 8 files changed, 230 insertions(+), 203 deletions(-) create mode 100644 api/tests/unit/Controller/AbstractControllerTest.php diff --git a/api/tests/unit/Controller/AbstractControllerTest.php b/api/tests/unit/Controller/AbstractControllerTest.php new file mode 100644 index 0000000..0f8627c --- /dev/null +++ b/api/tests/unit/Controller/AbstractControllerTest.php @@ -0,0 +1,52 @@ +container = $this->makeEmpty(ContainerInterface::class, [ + 'has' => true, + 'get' => new PDO( + 'pgsql:host=psql.torsten-hettstedt.net;port=5432;dbname=testdb;user=bruce;password=mypass' + ), + ]); + $this->response = $this->createMock(Response::class); + $this->response->expects($this->any())->method('getStatusCode')->willReturn(self::FICTIONAL_STATUS_CODE); + $this->response->expects($this->any())->method('getBody')->willReturn( + $this->makeEmpty(StreamInterface::class, [ + 'write' => function (mixed $data) { + $this->assertIsString($data); + $this->assertJson($data); + return strlen($data); + }, + 'getContents' => 'abc', + ]) + ); + $this->response->expects($this->any())->method('withHeader')->willReturn($this->response); + $this->response->expects($this->any())->method('withStatus')->willReturn($this->response); + } +} diff --git a/api/tests/unit/Controller/WorkingHoursControllerTest.php b/api/tests/unit/Controller/WorkingHoursControllerTest.php index a44846d..a9a3167 100644 --- a/api/tests/unit/Controller/WorkingHoursControllerTest.php +++ b/api/tests/unit/Controller/WorkingHoursControllerTest.php @@ -2,14 +2,13 @@ namespace TorstenHettstedt\TimekeepingApi\Tests\Unit\Controller; -use Codeception\Example; -use Codeception\Test\Unit; +use Codeception\Attribute\DataProvider; use Exception; -use PDO; +use PHPUnit\Framework\MockObject\MockObject; +use Psr\Container\ContainerExceptionInterface; use Psr\Container\ContainerInterface; -use Psr\Http\Message\StreamInterface; +use Psr\Container\NotFoundExceptionInterface; use Slim\Exception\HttpBadRequestException; -use Slim\Exception\HttpInternalServerErrorException; use Slim\Exception\HttpNotFoundException; use Slim\Psr7\Request; use Slim\Psr7\Response; @@ -17,51 +16,38 @@ use TorstenHettstedt\TimekeepingApi\Controller\HttpConflictRequestException; use TorstenHettstedt\TimekeepingApi\Controller\NotDatabasesException; use TorstenHettstedt\TimekeepingApi\Controller\WorkingHoursController; -class WorkingHoursControllerTest extends Unit +class WorkingHoursControllerTest extends AbstractControllerTest { - const EXISTING_DATE = '2020-01-28'; - const EXISTING_INTERVAL = '07:20:00'; - const NEW_DATE = '2020-04-01'; - const NEW_INTERVAL = '07:59:00'; + protected const FICTIONAL_STATUS_CODE = 666; + public const EXISTING_DATE = '2020-01-28'; + public const EXISTING_INTERVAL = '07:20:00'; + public const NEW_DATE = '2020-04-01'; + public const NEW_INTERVAL = '07:59:00'; - protected ContainerInterface $container; - protected Request $request; - protected Response $response; + protected ContainerInterface $container; + protected Request $request; + protected Response|MockObject $response; /** * @throws Exception + * @throws \PHPUnit\Framework\MockObject\Exception */ protected function _before(): void { parent::_before(); - /** @noinspection SpellCheckingInspection */ - $this->container = $this->makeEmpty(ContainerInterface::class, [ - 'has' => true, - 'get' => new PDO('pgsql:host=psql.torsten-hettstedt.net;port=5432;dbname=testdb;user=bruce;password=mypass'), - ]); $this->request = $this->makeEmpty(Request::class, [ 'getParsedBody' => [ 'workingDay' => self::EXISTING_DATE, 'workingTime' => self::EXISTING_INTERVAL, ], ]); - $this->response = $this->makeEmpty(Response::class, [ - 'getBody' => $this->makeEmpty(StreamInterface::class, [ - 'write' => function (mixed $data) { - $this->assertIsString($data); - $this->assertJson($data); - return strlen($data); - }, - ]), - 'withHeader' => $this->makeEmpty(Response::class, [ - 'withStatus' => $this->makeEmpty(Response::class), - ]), - ]); } /** * @throws NotDatabasesException + * @throws ContainerExceptionInterface + * @throws NotFoundExceptionInterface * @throws Exception */ public function testConstructWithNonDatabase(): void @@ -104,12 +90,12 @@ class WorkingHoursControllerTest extends Unit /** * @param array $queryParams * - * @dataProvider valideFilterDataProvider - * - * @throws HttpInternalServerErrorException + * @throws ContainerExceptionInterface * @throws NotDatabasesException + * @throws NotFoundExceptionInterface * @throws Exception */ + #[DataProvider('valideFilterDataProvider')] public function testBrowseWithValideParameter(array $queryParams): void { $this->request = $this->makeEmpty(Request::class, [ @@ -117,7 +103,8 @@ class WorkingHoursControllerTest extends Unit ]); $controller = new WorkingHoursController($this->container); $response = $controller->browse($this->request, $this->response, []); - $this->assertInstanceOf(Response::class, $response); + $this->assertEquals(self::FICTIONAL_STATUS_CODE, $response->getStatusCode()); + $this->assertNotEmpty($response->getBody()->getContents()); } /** @@ -247,12 +234,12 @@ class WorkingHoursControllerTest extends Unit /** * @param array $queryParams * - * @dataProvider invalideFilterDataProvider - * - * @throws HttpInternalServerErrorException + * @throws ContainerExceptionInterface * @throws NotDatabasesException + * @throws NotFoundExceptionInterface * @throws Exception */ + #[DataProvider('invalideFilterDataProvider')] public function testBrowseWithInvalideParameter(array $queryParams): void { $this->request = $this->makeEmpty(Request::class, [ @@ -264,9 +251,10 @@ class WorkingHoursControllerTest extends Unit } /** + * @throws ContainerExceptionInterface * @throws NotDatabasesException - * @throws HttpBadRequestException - * @throws HttpNotFoundException + * @throws NotFoundExceptionInterface + * @throws Exception */ public function testUpdateExistRecord(): void { @@ -274,13 +262,15 @@ class WorkingHoursControllerTest extends Unit $response = $controller->update($this->request, $this->response, [ 'id' => self::EXISTING_DATE, ]); - $this->assertInstanceOf(Response::class, $response); + $this->assertEquals(self::FICTIONAL_STATUS_CODE, $response->getStatusCode()); + $this->assertNotEmpty($response->getBody()->getContents()); } /** + * @throws ContainerExceptionInterface * @throws NotDatabasesException - * @throws HttpBadRequestException - * @throws HttpNotFoundException + * @throws NotFoundExceptionInterface + * @throws Exception */ public function testUpdateNotExistRecord(): void { @@ -312,14 +302,12 @@ class WorkingHoursControllerTest extends Unit * @param string $date * @param string $time * - * @dataProvider invalidCreatDataProvider - * - * @throws HttpBadRequestException - * @throws HttpConflictRequestException - * @throws HttpInternalServerErrorException + * @throws ContainerExceptionInterface * @throws NotDatabasesException + * @throws NotFoundExceptionInterface * @throws Exception */ + #[DataProvider('invalidCreatDataProvider')] public function testCreatInvalidData(string $date, string $time): void { $this->request = $this->makeEmpty(Request::class, [ @@ -334,10 +322,9 @@ class WorkingHoursControllerTest extends Unit } /** - * @throws HttpBadRequestException + * @throws ContainerExceptionInterface * @throws NotDatabasesException - * @throws HttpInternalServerErrorException - * @throws HttpConflictRequestException + * @throws NotFoundExceptionInterface * @throws Exception */ public function testCreatNewRecord(): void @@ -350,14 +337,14 @@ class WorkingHoursControllerTest extends Unit ]); $controller = new WorkingHoursController($this->container); $response = $controller->creat($this->request, $this->response, []); - $this->assertInstanceOf(Response::class, $response); + $this->assertEquals(self::FICTIONAL_STATUS_CODE, $response->getStatusCode()); + $this->assertNotEmpty($response->getBody()->getContents()); } /** - * @throws HttpBadRequestException - * @throws HttpConflictRequestException - * @throws HttpInternalServerErrorException + * @throws ContainerExceptionInterface * @throws NotDatabasesException + * @throws NotFoundExceptionInterface * @throws Exception */ public function testCreatExistRecord(): void @@ -374,9 +361,10 @@ class WorkingHoursControllerTest extends Unit } /** - * @throws HttpInternalServerErrorException - * @throws HttpNotFoundException + * @throws ContainerExceptionInterface * @throws NotDatabasesException + * @throws NotFoundExceptionInterface + * @throws Exception */ public function testReadExistRecord(): void { @@ -384,13 +372,15 @@ class WorkingHoursControllerTest extends Unit $response = $controller->read($this->request, $this->response, [ 'id' => self::EXISTING_DATE, ]); - $this->assertInstanceOf(Response::class, $response); + $this->assertEquals(self::FICTIONAL_STATUS_CODE, $response->getStatusCode()); + $this->assertNotEmpty($response->getBody()->getContents()); } /** - * @throws HttpInternalServerErrorException - * @throws HttpNotFoundException + * @throws ContainerExceptionInterface * @throws NotDatabasesException + * @throws NotFoundExceptionInterface + * @throws Exception */ public function testReadNotExistRecord(): void { diff --git a/api/tests/unit/Controller/WorkingHoursViewControllerTest.php b/api/tests/unit/Controller/WorkingHoursViewControllerTest.php index e7c4768..2f5e2da 100644 --- a/api/tests/unit/Controller/WorkingHoursViewControllerTest.php +++ b/api/tests/unit/Controller/WorkingHoursViewControllerTest.php @@ -2,50 +2,30 @@ namespace TorstenHettstedt\TimekeepingApi\Tests\Unit\Controller; -use Codeception\Test\Unit; use Exception; -use PDO; +use Psr\Container\ContainerExceptionInterface; use Psr\Container\ContainerInterface; -use Psr\Http\Message\StreamInterface; +use Psr\Container\NotFoundExceptionInterface; use Slim\Psr7\Request; -use Slim\Psr7\Response; use TorstenHettstedt\TimekeepingApi\Controller\NotDatabasesException; use TorstenHettstedt\TimekeepingApi\Controller\WorkingHoursViewController; -class WorkingHoursViewControllerTest extends Unit +class WorkingHoursViewControllerTest extends AbstractControllerTest { - protected ContainerInterface $container; - protected Request $request; - protected Response $response; - /** * @throws Exception + * @throws \PHPUnit\Framework\MockObject\Exception */ protected function _before(): void { parent::_before(); - /** @noinspection SpellCheckingInspection */ - $this->container = $this->makeEmpty(ContainerInterface::class, [ - 'has' => true, - 'get' => new PDO('pgsql:host=psql.torsten-hettstedt.net;port=5432;dbname=testdb;user=bruce;password=mypass') - ]); - $this->request = $this->makeEmpty(Request::class, []); - $this->response = $this->makeEmpty(Response::class, [ - 'getBody' => $this->makeEmpty(StreamInterface::class, [ - 'write' => function (mixed $data) { - $this->assertIsString($data); - $this->assertJson($data); - return strlen($data); - }, - ]), - 'withHeader' => $this->makeEmpty(Response::class, [ - 'withStatus' => $this->makeEmpty(Response::class), - ]), - ]); + $this->request = $this->makeEmpty(Request::class); } /** * @throws NotDatabasesException + * @throws ContainerExceptionInterface + * @throws NotFoundExceptionInterface * @throws Exception */ public function testConstructWithNonDatabase(): void @@ -58,32 +38,44 @@ class WorkingHoursViewControllerTest extends Unit } /** + * @throws NotDatabasesException + * @throws ContainerExceptionInterface + * @throws NotFoundExceptionInterface * @throws Exception */ public function testBrowseMonthly(): void { $controller = new WorkingHoursViewController($this->container); $response = $controller->browseMonthly($this->request, $this->response, []); - $this->assertInstanceOf(Response::class, $response); + $this->assertEquals(self::FICTIONAL_STATUS_CODE, $response->getStatusCode()); + $this->assertNotEmpty($response->getBody()->getContents()); } /** + * @throws NotDatabasesException + * @throws ContainerExceptionInterface + * @throws NotFoundExceptionInterface * @throws Exception */ public function testBrowseYearly(): void { $controller = new WorkingHoursViewController($this->container); $response = $controller->browseYearly($this->request, $this->response, []); - $this->assertInstanceOf(Response::class, $response); + $this->assertEquals(self::FICTIONAL_STATUS_CODE, $response->getStatusCode()); + $this->assertNotEmpty($response->getBody()->getContents()); } /** + * @throws NotDatabasesException + * @throws ContainerExceptionInterface + * @throws NotFoundExceptionInterface * @throws Exception */ public function testBrowseWeekly(): void { $controller = new WorkingHoursViewController($this->container); $response = $controller->browseWeekly($this->request, $this->response, []); - $this->assertInstanceOf(Response::class, $response); + $this->assertEquals(self::FICTIONAL_STATUS_CODE, $response->getStatusCode()); + $this->assertNotEmpty($response->getBody()->getContents()); } } diff --git a/api/tests/unit/Middleware/ErrorHandlerTest.php b/api/tests/unit/Middleware/ErrorHandlerTest.php index e317c0a..779ade7 100644 --- a/api/tests/unit/Middleware/ErrorHandlerTest.php +++ b/api/tests/unit/Middleware/ErrorHandlerTest.php @@ -5,6 +5,7 @@ namespace TorstenHettstedt\TimekeepingApi\Tests\Unit\Middleware; use Codeception\Stub\Expected; use Codeception\Test\Unit; use Exception; +use Psr\Container\ContainerInterface; use Psr\Http\Message\ResponseFactoryInterface; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; @@ -20,6 +21,7 @@ use TorstenHettstedt\TimekeepingApi\Middleware\ErrorHandler; class ErrorHandlerTest extends Unit { + /** @var App */ protected App $app; protected ServerRequestInterface $request; protected Exception $exception; @@ -88,11 +90,10 @@ class ErrorHandlerTest extends Unit 'file' => '/path(to/file', 'getTitle' => Expected::once('The Title'), ]); - $this->logger = $this->makeEmpty(LoggerInterface::class, []); + $this->logger = $this->makeEmpty(LoggerInterface::class); $middleWare = new ErrorHandler($this->app); $middleWare($this->request, $this->exception, true, true, true, $this->logger); - } /** @@ -105,7 +106,7 @@ class ErrorHandlerTest extends Unit 'code' => 400, 'file' => '/path(to/file', ]); - $this->logger = $this->makeEmpty(LoggerInterface::class, []); + $this->logger = $this->makeEmpty(LoggerInterface::class); $middleWare = new ErrorHandler($this->app); $middleWare($this->request, $this->exception, true, true, true, $this->logger); diff --git a/api/tests/unit/Models/WorkingHoursTest.php b/api/tests/unit/Models/WorkingHoursTest.php index 200f997..b1612ff 100644 --- a/api/tests/unit/Models/WorkingHoursTest.php +++ b/api/tests/unit/Models/WorkingHoursTest.php @@ -2,6 +2,7 @@ namespace TorstenHettstedt\TimekeepingApi\Tests\Unit\Models; +use Codeception\Attribute\DataProvider; use Codeception\Test\Unit; use DateInterval; use DateTime; @@ -22,7 +23,7 @@ class WorkingHoursTest extends Unit } /** - * @return array[] + * @return array>> */ public function workingHoursProvider(): array { @@ -49,17 +50,13 @@ class WorkingHoursTest extends Unit /** * @param DateTime $date * @param DateInterval $interval - * @param array $should_json - * - * @dataProvider workingHoursProvider + * @param array $shouldJson */ - public function testWorkingHoursWithContent(DateTime $date, DateInterval $interval, array $should_json): void + #[DataProvider('workingHoursProvider')] + public function testWorkingHoursWithContent(DateTime $date, DateInterval $interval, array $shouldJson): void { - $obj = new WorkingHours(); - $obj - ->setWorkingDay($date) - ->setWorkingTime($interval); + $obj->setWorkingDay($date)->setWorkingTime($interval); $this->assertNotNull($obj->getWorkingDay()); $this->assertInstanceOf(DateTime::class, $obj->getWorkingDay()); @@ -70,7 +67,6 @@ class WorkingHoursTest extends Unit $this->assertEquals($interval, $obj->getWorkingTime()); $json = $obj->jsonSerialize(); - $this->assertIsArray($json); - $this->assertEquals($should_json, $json); + $this->assertEquals($shouldJson, $json); } } diff --git a/api/tests/unit/Models/WorkingHoursViewTest.php b/api/tests/unit/Models/WorkingHoursViewTest.php index cf0a0db..ed79216 100644 --- a/api/tests/unit/Models/WorkingHoursViewTest.php +++ b/api/tests/unit/Models/WorkingHoursViewTest.php @@ -2,6 +2,7 @@ namespace TorstenHettstedt\TimekeepingApi\Tests\Unit\Models; +use Codeception\Attribute\DataProvider; use Codeception\Test\Unit; use DateInterval; use DateTime; @@ -13,7 +14,7 @@ class WorkingHoursViewTest extends Unit { /** - * @return array[] + * @return array}> */ public function workingHoursProvider(): array { @@ -25,7 +26,11 @@ class WorkingHoursViewTest extends Unit $absence_time->invert = 1; return [ [ - $date, PeriodDesignationEnum::WEEKLY(), $work_days, $total_hours, $overtime, + $date, + PeriodDesignationEnum::WEEKLY, + $work_days, + $total_hours, + $overtime, [ 'period' => '2020#49', 'periodDesignation' => 'weekly', @@ -35,7 +40,11 @@ class WorkingHoursViewTest extends Unit ], ], [ - $date, PeriodDesignationEnum::WEEKLY(), $work_days, $total_hours, $absence_time, + $date, + PeriodDesignationEnum::WEEKLY, + $work_days, + $total_hours, + $absence_time, [ 'period' => '2020#49', 'periodDesignation' => 'weekly', @@ -45,7 +54,11 @@ class WorkingHoursViewTest extends Unit ], ], [ - $date, PeriodDesignationEnum::MONTHLY(), $work_days, $total_hours, $overtime, + $date, + PeriodDesignationEnum::MONTHLY, + $work_days, + $total_hours, + $overtime, [ 'period' => '2020-11', 'periodDesignation' => 'monthly', @@ -55,7 +68,11 @@ class WorkingHoursViewTest extends Unit ], ], [ - $date, PeriodDesignationEnum::MONTHLY(), $work_days, $total_hours, $absence_time, + $date, + PeriodDesignationEnum::MONTHLY, + $work_days, + $total_hours, + $absence_time, [ 'period' => '2020-11', 'periodDesignation' => 'monthly', @@ -65,7 +82,12 @@ class WorkingHoursViewTest extends Unit ], ], [ - $date, PeriodDesignationEnum::YEARLY(), $work_days, $total_hours, $overtime, [ + $date, + PeriodDesignationEnum::YEARLY, + $work_days, + $total_hours, + $overtime, + [ 'period' => '2020', 'periodDesignation' => 'yearly', 'workingDays' => 15, @@ -74,7 +96,12 @@ class WorkingHoursViewTest extends Unit ], ], [ - $date, PeriodDesignationEnum::YEARLY(), $work_days, $total_hours, $absence_time, [ + $date, + PeriodDesignationEnum::YEARLY, + $work_days, + $total_hours, + $absence_time, + [ 'period' => '2020', 'periodDesignation' => 'yearly', 'workingDays' => 15, @@ -86,50 +113,40 @@ class WorkingHoursViewTest extends Unit } /** - * @param DateTimeInterface $period - * @param PeriodDesignationEnum $periodDesignation - * @param int $workingDays - * @param DateInterval $totalHours - * @param DateInterval $overtime - * @param array $should_json - * - * @dataProvider workingHoursProvider + * @param DateTimeInterface $period + * @param PeriodDesignationEnum $periodDesignation + * @param int $workingDays + * @param DateInterval $totalHours + * @param DateInterval $overtime + * @param array $shouldJson */ + #[DataProvider('workingHoursProvider')] public function testWorkingHoursViewWithContent( DateTimeInterface $period, PeriodDesignationEnum $periodDesignation, int $workingDays, DateInterval $totalHours, DateInterval $overtime, - array $should_json - ): void - { - + array $shouldJson + ): void { $obj = new WorkingHoursView($period, $periodDesignation, $workingDays, $totalHours, $overtime); - $this->assertNotNull($obj->getPeriod()); $this->assertInstanceOf(DateTimeInterface::class, $obj->getPeriod()); $this->assertEquals($period, $obj->getPeriod()); - $this->assertNotNull($obj->getPeriodDesignation()); $this->assertInstanceOf(PeriodDesignationEnum::class, $obj->getPeriodDesignation()); $this->assertEquals($periodDesignation, $obj->getPeriodDesignation()); - $this->assertNotNull($obj->getWorkingDays()); - $this->assertIsInt($obj->getWorkingDays()); $this->assertEquals($workingDays, $obj->getWorkingDays()); - $this->assertNotNull($obj->getTotalHours()); $this->assertInstanceOf(DateInterval::class, $obj->getTotalHours()); $this->assertEquals($totalHours, $obj->getTotalHours()); - $this->assertNotNull($obj->getOvertime()); $this->assertInstanceOf(DateInterval::class, $obj->getOvertime()); $this->assertEquals($overtime, $obj->getOvertime()); $json = $obj->jsonSerialize(); - $this->assertIsArray($json); - $this->assertEquals($should_json, $json); + $this->assertEquals($shouldJson, $json); } } diff --git a/api/tests/unit/Repositories/WorkingHoursRepositoryTest.php b/api/tests/unit/Repositories/WorkingHoursRepositoryTest.php index 7080ee9..0dbef52 100644 --- a/api/tests/unit/Repositories/WorkingHoursRepositoryTest.php +++ b/api/tests/unit/Repositories/WorkingHoursRepositoryTest.php @@ -2,7 +2,7 @@ namespace TorstenHettstedt\TimekeepingApi\Tests\Unit\Repositories; -use Codeception\Example; +use Codeception\Attribute\DataProvider; use Codeception\Test\Unit; use DateInterval; use DateTime; @@ -18,18 +18,20 @@ use TorstenHettstedt\TimekeepingApi\Repositories\WorkingHoursRepository; class WorkingHoursRepositoryTest extends Unit { - const EXISTING_DATE = '2020-01-28'; - const EXISTING_INTERVAL = 'PT7H20M'; - const NEW_DATE = '2020-04-01'; - const NEW_INTERVAL = 'PT7H59M'; - const RECORDS_COUNT = 61; + public const EXISTING_DATE = '2020-01-28'; + public const EXISTING_INTERVAL = 'PT7H20M'; + public const NEW_DATE = '2020-04-01'; + public const NEW_INTERVAL = 'PT7H59M'; + public const RECORDS_COUNT = 61; 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'); + $this->pdoObject = new PDO( + 'pgsql:host=psql.torsten-hettstedt.net;port=5432;dbname=testdb;user=bruce;password=mypass' + ); parent::_before(); } @@ -40,8 +42,8 @@ class WorkingHoursRepositoryTest extends Unit { $repository = new WorkingHoursRepository($this->pdoObject); $models = $repository->findAll(); - $this->assertIsArray($models); - $this->assertContainsOnly(WorkingHours::class, $models); + + $this->assertContainsOnlyInstancesOf(WorkingHours::class, $models); $this->assertCount(self::RECORDS_COUNT, $models); } @@ -51,13 +53,13 @@ class WorkingHoursRepositoryTest extends Unit public function valideFilterParameter(): array { return [ - [null, null, 61,], - ['2020-04-01', null, 0,], - ['2020-04-01', '2020-04-30', 0,], - [null, '2020-04-30', 61,], - ['2020-03-01', null, 22,], + [null, null, 61,], + ['2020-04-01', null, 0,], + ['2020-04-01', '2020-04-30', 0,], + [null, '2020-04-30', 61,], + ['2020-03-01', null, 22,], ['2020-03-01', '2020-03-31', 22,], - ['2020-03-01', '2020-03-07', 5,], + ['2020-03-01', '2020-03-07', 5,], ]; } @@ -66,16 +68,15 @@ class WorkingHoursRepositoryTest extends Unit * @param string|null $ende * @param int $count * - * @dataProvider valideFilterParameter - * * @throws Exception */ + #[DataProvider('valideFilterParameter')] public function testFindAllWithValideParameter(?string $start, ?string $ende, int $count): void { $repository = new WorkingHoursRepository($this->pdoObject); $models = $repository->findFiltered($start, $ende); - $this->assertIsArray($models); - $this->assertContainsOnly(WorkingHours::class, $models); + + $this->assertContainsOnlyInstancesOf(WorkingHours::class, $models); $this->assertCount($count, $models); } @@ -85,23 +86,23 @@ class WorkingHoursRepositoryTest extends Unit public function invalideFilterParameter(): array { return [ - ['2020-04-31',null], - [null, '2020-04-31'], - ['2020-04-00','2020-04-31'], - ['2020-04-01','2020-04-31'], - ['2020-04-00','2020-04-30'], + ['2020-04-31', null], + [null, '2020-04-31'], + ['2020-04-00', '2020-04-31'], + ['2020-04-01', '2020-04-31'], + ['2020-04-00', '2020-04-30'], // - ['2020-04', null], - [null, '2020-04'], - ['2020-04', '2020-04'], - ['2020-04-01','2020-04'], - ['2020-04', '2020-04-30'], + ['2020-04', null], + [null, '2020-04'], + ['2020-04', '2020-04'], + ['2020-04-01', '2020-04'], + ['2020-04', '2020-04-30'], // - ['2020', null], - [null, '2020'], - ['2020', '2020'], - ['2020-04-01','2020'], - ['2020', '2020-04-30'], + ['2020', null], + [null, '2020'], + ['2020', '2020'], + ['2020-04-01', '2020'], + ['2020', '2020-04-30'], ]; } @@ -109,10 +110,9 @@ class WorkingHoursRepositoryTest extends Unit * @param string|null $start * @param string|null $ende * - * @dataProvider invalideFilterParameter - * * @throws Exception */ + #[DataProvider('invalideFilterParameter')] public function testFindAllWithInvalideParameter(?string $start, ?string $ende): void { $repository = new WorkingHoursRepository($this->pdoObject); @@ -127,7 +127,6 @@ class WorkingHoursRepositoryTest extends Unit { $repository = new WorkingHoursRepository($this->pdoObject); $model = $repository->findByKey(self::EXISTING_DATE); - $this->assertInstanceOf(WorkingHours::class, $model); $this->assertEquals(new DateTime(self::EXISTING_DATE), $model->getWorkingDay()); $this->assertEquals(new DateInterval(self::EXISTING_INTERVAL), $model->getWorkingTime()); } @@ -163,7 +162,6 @@ class WorkingHoursRepositoryTest extends Unit ]); $repository->insert($model); $model = $repository->findByKey(self::NEW_DATE); - $this->assertInstanceOf(WorkingHours::class, $model); $this->assertEquals(new DateTime(self::NEW_DATE), $model->getWorkingDay()); $this->assertEquals(new DateInterval(self::NEW_INTERVAL), $model->getWorkingTime()); $this->assertCount(self::RECORDS_COUNT + 1, $repository->findAll()); diff --git a/api/tests/unit/Repositories/WorkingHoursViewRepositoryTest.php b/api/tests/unit/Repositories/WorkingHoursViewRepositoryTest.php index 9297162..8211985 100644 --- a/api/tests/unit/Repositories/WorkingHoursViewRepositoryTest.php +++ b/api/tests/unit/Repositories/WorkingHoursViewRepositoryTest.php @@ -2,12 +2,12 @@ namespace TorstenHettstedt\TimekeepingApi\Tests\Unit\Repositories; +use Codeception\Attribute\DataProvider; use Codeception\Test\Unit; use DateTime; use DateTimeZone; use Exception; -use PDO; -use TorstenHettstedt\TimekeepingApi\Models\WorkingHoursView; +use PDO;use TorstenHettstedt\TimekeepingApi\Models\WorkingHoursView; use TorstenHettstedt\TimekeepingApi\Repositories\RepositoryRecordNotFoundException; use TorstenHettstedt\TimekeepingApi\Repositories\WorkingHoursWeeklyViewRepository; use TorstenHettstedt\TimekeepingApi\Repositories\WorkingHoursMonthlyViewRepository; @@ -25,7 +25,7 @@ class WorkingHoursViewRepositoryTest extends Unit } /** - * @return array[] + * @return array */ public function listObjectProvider(): array { @@ -37,7 +37,7 @@ class WorkingHoursViewRepositoryTest extends Unit } /** - * @return array[] + * @return array * @throws Exception */ public function existingObjectProvider(): array @@ -50,7 +50,7 @@ class WorkingHoursViewRepositoryTest extends Unit } /** - * @return array[] + * @return array */ public function notExistingObjectProvider(): array { @@ -61,49 +61,30 @@ class WorkingHoursViewRepositoryTest extends Unit ]; } - /** - * @param string $period_class - * @param int $count_records - * - * @dataProvider listObjectProvider - */ - public function testFindAll(string $period_class, int $count_records): void + #[DataProvider(('listObjectProvider'))] + public function testFindAll(string $periodClass, int $countRecords): void { - $repository = new $period_class($this->pdoObject); + $repository = new $periodClass($this->pdoObject); $models = $repository->findAll(); $this->assertIsArray($models); - $this->assertContainsOnly(WorkingHoursView::class, $models); - $this->assertCount($count_records, $models); + $this->assertContainsOnlyInstancesOf(WorkingHoursView::class, $models); + $this->assertCount($countRecords, $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 + #[DataProvider(('existingObjectProvider'))] + public function testExistingFindByKey(string $periodClass, string $search, int $workingDays, DateTime $period): void { - $repository = new $period_class($this->pdoObject); + $repository = new $periodClass($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 + #[DataProvider(('notExistingObjectProvider'))] + public function testNotExistingFindByKey(string $periodClass, string $search): void { - $repository = new $period_class($this->pdoObject); + $repository = new $periodClass($this->pdoObject); $this->expectException(RepositoryRecordNotFoundException::class); $repository->findByKey($search); } From 6fa6b28af6f285826d869e425ea2b770830b0f08 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torsten=20L=C3=BCcke?= Date: Sat, 3 May 2025 09:29:03 +0200 Subject: [PATCH 06/15] Refactor Classes --- api/src/Controller/AbstractController.php | 7 ++- api/src/Controller/PreflightController.php | 1 + api/src/Controller/WorkingHoursController.php | 54 ++++++++++++------- .../Controller/WorkingHoursViewController.php | 36 +++++++------ api/src/Middleware/ErrorHandler.php | 14 +++-- .../Middleware/JsonBodyParserMiddleware.php | 2 +- .../Middleware/OriginAccessControlHandler.php | 7 ++- api/src/Models/PeriodDesignationEnum.php | 19 +++---- api/src/Models/WorkingHours.php | 24 ++------- api/src/Models/WorkingHoursView.php | 47 +++++----------- .../AbstractWorkingHoursViewRepository.php | 10 ++-- .../RepositoryReaderInterface.php | 4 +- .../RepositoryWriterInterface.php | 4 +- .../WorkingHoursMonthlyViewRepository.php | 9 ++-- .../Repositories/WorkingHoursRepository.php | 20 +++---- .../WorkingHoursWeeklyViewRepository.php | 9 ++-- .../WorkingHoursYearlyViewRepository.php | 9 ++-- 17 files changed, 125 insertions(+), 151 deletions(-) diff --git a/api/src/Controller/AbstractController.php b/api/src/Controller/AbstractController.php index c0cf8a9..b9af4ce 100644 --- a/api/src/Controller/AbstractController.php +++ b/api/src/Controller/AbstractController.php @@ -4,7 +4,9 @@ namespace TorstenHettstedt\TimekeepingApi\Controller; use JsonSerializable; use PDO; +use Psr\Container\ContainerExceptionInterface; use Psr\Container\ContainerInterface; +use Psr\Container\NotFoundExceptionInterface; use Slim\Psr7\Response; abstract class AbstractController @@ -18,6 +20,8 @@ abstract class AbstractController * @param ContainerInterface $container * * @throws NotDatabasesException + * @throws ContainerExceptionInterface + * @throws NotFoundExceptionInterface */ public function __construct(ContainerInterface $container) { @@ -34,7 +38,8 @@ abstract class AbstractController * * @return Response */ - protected function printResponse(Response $response, mixed $data, int $status_code): Response { + protected function printResponse(Response $response, mixed $data, int $status_code): Response + { $payload = json_encode($data); $response->getBody()->write($payload); diff --git a/api/src/Controller/PreflightController.php b/api/src/Controller/PreflightController.php index 1ac6bf9..d017f7a 100644 --- a/api/src/Controller/PreflightController.php +++ b/api/src/Controller/PreflightController.php @@ -11,6 +11,7 @@ use TorstenHettstedt\TimekeepingApi\Middleware\OriginAccessControlHandler; class PreflightController { + /** @noinspection PhpUnused */ public function preflight(Request $request, Response $response): ResponseInterface { return (new OriginAccessControlHandler())->originAccessControl($request, $response); diff --git a/api/src/Controller/WorkingHoursController.php b/api/src/Controller/WorkingHoursController.php index be291db..7b14b97 100644 --- a/api/src/Controller/WorkingHoursController.php +++ b/api/src/Controller/WorkingHoursController.php @@ -21,9 +21,9 @@ class WorkingHoursController extends AbstractController { /** - * @param Request $request - * @param Response $response - * @param mixed[] $args + * @param Request $request + * @param Response $response + * @param array $args * * @return Response * @@ -44,9 +44,9 @@ class WorkingHoursController extends AbstractController } /** - * @param Request $request - * @param Response $response - * @param mixed[] $args + * @param Request $request + * @param Response $response + * @param array $args * * @return Response * @@ -59,21 +59,29 @@ class WorkingHoursController extends AbstractController $repository = new WorkingHoursRepository($this->databases); $queryParams = $request->getQueryParams(); try { - return $this->printResponse($response, $repository->findFiltered( - $queryParams['start-date'] ?? null, - $queryParams['end-date'] ?? null - ), StatusCodeInterface::STATUS_OK); + return $this->printResponse( + $response, + $repository->findFiltered( + $queryParams['start-date'] ?? null, + $queryParams['end-date'] ?? null + ), + StatusCodeInterface::STATUS_OK + ); } catch (RepositoryBadWhereDataException $exception) { throw new HttpBadRequestException($request, 'Ein Wert für das Datum im Query ist ungültig', $exception); } catch (Exception $exception) { - throw new HttpInternalServerErrorException($request, 'Der Wert für das Datum oder die Zeit ist falsch in der Datenbank', $exception); + throw new HttpInternalServerErrorException( + $request, + 'Der Wert für das Datum oder die Zeit ist falsch in der Datenbank', + $exception + ); } } /** - * @param Request $request - * @param Response $response - * @param mixed[] $args + * @param Request $request + * @param Response $response + * @param array $args * * @return Response * @@ -88,14 +96,18 @@ class WorkingHoursController extends AbstractController } catch (RepositoryRecordNotFoundException $exception) { throw new HttpNotFoundException($request, 'Der Eintrag ist nicht vorhanden.', $exception); } catch (Exception $exception) { - throw new HttpInternalServerErrorException($request, 'Der Wert für das Datum oder die Zeit ist falsch in der Datenbank', $exception); + throw new HttpInternalServerErrorException( + $request, + 'Der Wert für das Datum oder die Zeit ist falsch in der Datenbank', + $exception + ); } } /** - * @param Request $request - * @param Response $response - * @param mixed[] $args + * @param Request $request + * @param Response $response + * @param array $args * * @return Response * @@ -114,7 +126,11 @@ class WorkingHoursController extends AbstractController } catch (RepositoryRecordAlreadyExistException $exception) { throw new HttpConflictRequestException($request, 'Der Eintrag ist schon vorhanden.', $exception); } catch (Exception $exception) { - throw new HttpInternalServerErrorException($request, 'Der Wert für das Datum oder die Zeit ist falsch in der Datenbank', $exception); + throw new HttpInternalServerErrorException( + $request, + 'Der Wert für das Datum oder die Zeit ist falsch in der Datenbank', + $exception + ); } return $this->printResponse($response, $model, StatusCodeInterface::STATUS_CREATED); } diff --git a/api/src/Controller/WorkingHoursViewController.php b/api/src/Controller/WorkingHoursViewController.php index 917b163..ff26ca0 100644 --- a/api/src/Controller/WorkingHoursViewController.php +++ b/api/src/Controller/WorkingHoursViewController.php @@ -15,9 +15,9 @@ use TorstenHettstedt\TimekeepingApi\Repositories\WorkingHoursYearlyViewRepositor class WorkingHoursViewController extends AbstractController { /** - * @param Request $request - * @param Response $response - * @param mixed[] $args + * @param Request $request + * @param Response $response + * @param array $args * * @return Response * @@ -30,15 +30,17 @@ class WorkingHoursViewController extends AbstractController try { return $this->printResponse($response, $repository->findAll(), StatusCodeInterface::STATUS_OK); } catch (Exception $exception) { - throw new HttpInternalServerErrorException($request, - 'Der Wert für das Datum oder die Zeit ist falsch in der Datenbank', $exception); + throw new HttpInternalServerErrorException( + $request, + 'Der Wert für das Datum oder die Zeit ist falsch in der Datenbank', $exception + ); } } /** - * @param Request $request - * @param Response $response - * @param mixed[] $args + * @param Request $request + * @param Response $response + * @param array $args * * @return Response * @@ -51,15 +53,17 @@ class WorkingHoursViewController extends AbstractController try { return $this->printResponse($response, $repository->findAll(), StatusCodeInterface::STATUS_OK); } catch (Exception $exception) { - throw new HttpInternalServerErrorException($request, - 'Der Wert für das Datum oder die Zeit ist falsch in der Datenbank', $exception); + throw new HttpInternalServerErrorException( + $request, + 'Der Wert für das Datum oder die Zeit ist falsch in der Datenbank', $exception + ); } } /** - * @param Request $request - * @param Response $response - * @param mixed[] $args + * @param Request $request + * @param Response $response + * @param array $args * * @return Response * @@ -72,8 +76,10 @@ class WorkingHoursViewController extends AbstractController try { return $this->printResponse($response, $repository->findAll(), StatusCodeInterface::STATUS_OK); } catch (Exception $exception) { - throw new HttpInternalServerErrorException($request, - 'Der Wert für das Datum oder die Zeit ist falsch in der Datenbank', $exception); + throw new HttpInternalServerErrorException( + $request, + 'Der Wert für das Datum oder die Zeit ist falsch in der Datenbank', $exception + ); } } diff --git a/api/src/Middleware/ErrorHandler.php b/api/src/Middleware/ErrorHandler.php index 80d5f15..38985df 100644 --- a/api/src/Middleware/ErrorHandler.php +++ b/api/src/Middleware/ErrorHandler.php @@ -1,10 +1,12 @@ - + */ protected App $app; /** * ErrorHandler constructor. * - * @param App $app + * @param App $app */ public function __construct(App $app) { @@ -29,7 +34,7 @@ class ErrorHandler /** * @param Throwable $exception * - * @return mixed[] + * @return array */ #[ArrayShape([ 'timestamp' => "false|string", @@ -71,8 +76,7 @@ class ErrorHandler bool $logErrors, bool $logErrorDetails, ?LoggerInterface $logger = null - ): ResponseInterface - { + ): ResponseInterface { if ($logger !== null && $logErrors === true) { $logger->error($exception->getMessage()); } diff --git a/api/src/Middleware/JsonBodyParserMiddleware.php b/api/src/Middleware/JsonBodyParserMiddleware.php index 1c020e6..e09d5ec 100644 --- a/api/src/Middleware/JsonBodyParserMiddleware.php +++ b/api/src/Middleware/JsonBodyParserMiddleware.php @@ -13,7 +13,7 @@ class JsonBodyParserMiddleware implements MiddlewareInterface { $contentType = $request->getHeaderLine('Content-Type'); - if (strstr($contentType, 'application/json')) { + if (str_contains($contentType, 'application/json')) { $contents = json_decode(file_get_contents('php://input'), true); if (json_last_error() === JSON_ERROR_NONE) { $request = $request->withParsedBody($contents); diff --git a/api/src/Middleware/OriginAccessControlHandler.php b/api/src/Middleware/OriginAccessControlHandler.php index a62de79..6eb301c 100644 --- a/api/src/Middleware/OriginAccessControlHandler.php +++ b/api/src/Middleware/OriginAccessControlHandler.php @@ -28,15 +28,14 @@ class OriginAccessControlHandler implements MiddlewareInterface public function originAccessControl(Request $request, Response $response): Response { - $routeContext = RouteContext::fromRequest($request); $routingResults = $routeContext->getRoutingResults(); $methods = $routingResults->getAllowedMethods(); $requestHeaders = $request->getHeaderLine('Access-Control-Request-Headers'); if ($this->testRoute($request)) { - // Stunden Minuten Sekunden - $accessControlMaxAge = 24 * 60 * 60; + // Stunden Minuten Sekunden + $accessControlMaxAge = 24 * 60 * 60; $response = $response->withHeader('Access-Control-Allow-Origin', $this->buildOrigin($request)); $response = $response->withHeader('Access-Control-Allow-Methods', implode(',', $methods)); $response = $response->withHeader('Access-Control-Allow-Headers', $requestHeaders); @@ -53,6 +52,6 @@ class OriginAccessControlHandler implements MiddlewareInterface protected function buildOrigin(Request $request): string { - return $request->getHeaderLine('Origin') ?? ''; + return $request->getHeaderLine('Origin'); } } \ No newline at end of file diff --git a/api/src/Models/PeriodDesignationEnum.php b/api/src/Models/PeriodDesignationEnum.php index 8efffb6..b0d93a7 100644 --- a/api/src/Models/PeriodDesignationEnum.php +++ b/api/src/Models/PeriodDesignationEnum.php @@ -1,23 +1,16 @@ - - * - * @method static PeriodDesignationEnum WEEKLY() - * @method static PeriodDesignationEnum MONTHLY() - * @method static PeriodDesignationEnum YEARLY() */ -class PeriodDesignationEnum extends Enum +enum PeriodDesignationEnum: string { - private const WEEKLY = 'Y#W'; - private const MONTHLY = 'Y-m'; - private const YEARLY = 'Y'; + case WEEKLY = 'Y#W'; + case MONTHLY = 'Y-m'; + case YEARLY = 'Y'; } \ No newline at end of file diff --git a/api/src/Models/WorkingHours.php b/api/src/Models/WorkingHours.php index e485f31..e55e5b4 100644 --- a/api/src/Models/WorkingHours.php +++ b/api/src/Models/WorkingHours.php @@ -10,42 +10,26 @@ use JetBrains\PhpStorm\ArrayShape; class WorkingHours implements ModelInterface { - protected ?DateTime $workingDay = null; + protected ?DateTime $workingDay = null; protected ?DateInterval $workingTime = null; - /** - * @return DateTime|null - */ public function getWorkingDay(): ?DateTime { return $this->workingDay; } - /** - * @param DateTime $workingDay - * - * @return WorkingHours - */ - public function setWorkingDay(DateTime $workingDay): WorkingHours + public function setWorkingDay(DateTime $workingDay): static { $this->workingDay = $workingDay; return $this; } - /** - * @return DateInterval|null - */ public function getWorkingTime(): ?DateInterval { return $this->workingTime; } - /** - * @param DateInterval $workingTime - * - * @return WorkingHours - */ - public function setWorkingTime(DateInterval $workingTime): WorkingHours + public function setWorkingTime(DateInterval $workingTime): static { $this->workingTime = $workingTime; return $this; @@ -58,7 +42,7 @@ class WorkingHours implements ModelInterface public function jsonSerialize(): array { return [ - 'workingDay' => $this->getWorkingDay()->format('Y-m-d'), + 'workingDay' => $this->getWorkingDay()->format('Y-m-d'), 'workingTime' => $this->getWorkingTime()->format('%H:%I:%S') ]; } diff --git a/api/src/Models/WorkingHoursView.php b/api/src/Models/WorkingHoursView.php index b229ec8..bdeef5c 100644 --- a/api/src/Models/WorkingHoursView.php +++ b/api/src/Models/WorkingHoursView.php @@ -10,21 +10,12 @@ use JetBrains\PhpStorm\ArrayShape; class WorkingHoursView implements ModelInterface { - protected DateTimeInterface $period; + protected DateTimeInterface $period; protected PeriodDesignationEnum $periodDesignation; - protected int $workingDays; - protected DateInterval $totalHours; - protected DateInterval $overtime; + protected int $workingDays; + protected DateInterval $totalHours; + protected DateInterval $overtime; - /** - * WorkingHoursView constructor. - * - * @param DateTimeInterface $period - * @param PeriodDesignationEnum $periodDesignation - * @param int $workingDays - * @param DateInterval $totalHours - * @param DateInterval $overtime - */ public function __construct( DateTimeInterface $period, PeriodDesignationEnum $periodDesignation, @@ -39,41 +30,26 @@ class WorkingHoursView implements ModelInterface $this->overtime = $overtime; } - /** - * @return DateTimeInterface - */ public function getPeriod(): DateTimeInterface { return $this->period; } - /** - * @return PeriodDesignationEnum - */ public function getPeriodDesignation(): PeriodDesignationEnum { return $this->periodDesignation; } - /** - * @return int - */ public function getWorkingDays(): int { return $this->workingDays; } - /** - * @return DateInterval - */ public function getTotalHours(): DateInterval { return $this->totalHours; } - /** - * @return DateInterval - */ public function getOvertime(): DateInterval { return $this->overtime; @@ -84,17 +60,18 @@ class WorkingHoursView implements ModelInterface * * @return array */ - #[ArrayShape(['period' => "string", - 'periodDesignation' => "string", - 'totalHours' => "string", - 'workingDays' => "int", - 'overtime' => "string" + #[ArrayShape([ + 'period' => "string", + 'periodDesignation' => "string", + 'totalHours' => "string", + 'workingDays' => "int", + 'overtime' => "string" ])] public function jsonSerialize(): array { $formatOvertime = (($this->getOvertime()->invert === 1) ? '-' : '') . '%H:%I:%S'; return [ - 'period' => $this->getPeriod()->format($this->getPeriodDesignation()->getValue()), - 'periodDesignation' => strtolower($this->getPeriodDesignation()->getKey()), + 'period' => $this->getPeriod()->format($this->getPeriodDesignation()->value), + 'periodDesignation' => strtolower($this->getPeriodDesignation()->name), 'workingDays' => $this->getWorkingDays(), 'totalHours' => $this->getTotalHours()->format('%H:%I:%S'), 'overtime' => $this->getOvertime()->format($formatOvertime) diff --git a/api/src/Repositories/AbstractWorkingHoursViewRepository.php b/api/src/Repositories/AbstractWorkingHoursViewRepository.php index d8a757b..fbeb182 100644 --- a/api/src/Repositories/AbstractWorkingHoursViewRepository.php +++ b/api/src/Repositories/AbstractWorkingHoursViewRepository.php @@ -18,9 +18,8 @@ abstract class AbstractWorkingHoursViewRepository implements RepositoryReaderInt { protected const SQL_SELECT = ''; - protected const SQL_WHERE = ''; + protected const SQL_WHERE = ''; - protected PDO $database; protected PeriodDesignationEnum $periodDesignation; /** @@ -28,9 +27,8 @@ abstract class AbstractWorkingHoursViewRepository implements RepositoryReaderInt * * @param PDO $database */ - public function __construct(PDO $database) + public function __construct(protected PDO $database) { - $this->database = $database; $this->database->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); } @@ -51,7 +49,7 @@ abstract class AbstractWorkingHoursViewRepository implements RepositoryReaderInt $model = new WorkingHoursView( $this->buildDateFromPeriod($row['period']), $this->periodDesignation, - (int) $row['workingDays'], + (int)$row['workingDays'], $this->buildDateInterval($row['totalHours']), $this->buildDateInterval($row['overtime']) ); @@ -82,7 +80,7 @@ abstract class AbstractWorkingHoursViewRepository implements RepositoryReaderInt return new WorkingHoursView( $this->buildDateFromPeriod($row['period']), $this->periodDesignation, - (int) $row['workingDays'], + (int)$row['workingDays'], $this->buildDateInterval($row['totalHours']), $this->buildDateInterval($row['overtime']) ); diff --git a/api/src/Repositories/RepositoryReaderInterface.php b/api/src/Repositories/RepositoryReaderInterface.php index 00aa1dc..97eb4df 100644 --- a/api/src/Repositories/RepositoryReaderInterface.php +++ b/api/src/Repositories/RepositoryReaderInterface.php @@ -3,8 +3,10 @@ namespace TorstenHettstedt\TimekeepingApi\Repositories; +use TorstenHettstedt\TimekeepingApi\Models\ModelInterface; + /** - * @template T of \TorstenHettstedt\TimekeepingApi\Models\ModelInterface + * @template T of ModelInterface */ interface RepositoryReaderInterface { diff --git a/api/src/Repositories/RepositoryWriterInterface.php b/api/src/Repositories/RepositoryWriterInterface.php index bc5f8fc..8c7241d 100644 --- a/api/src/Repositories/RepositoryWriterInterface.php +++ b/api/src/Repositories/RepositoryWriterInterface.php @@ -3,8 +3,10 @@ namespace TorstenHettstedt\TimekeepingApi\Repositories; +use TorstenHettstedt\TimekeepingApi\Models\ModelInterface; + /** - * @template T of \TorstenHettstedt\TimekeepingApi\Models\ModelInterface + * @template T of ModelInterface */ interface RepositoryWriterInterface { diff --git a/api/src/Repositories/WorkingHoursMonthlyViewRepository.php b/api/src/Repositories/WorkingHoursMonthlyViewRepository.php index eb73c7e..a307076 100644 --- a/api/src/Repositories/WorkingHoursMonthlyViewRepository.php +++ b/api/src/Repositories/WorkingHoursMonthlyViewRepository.php @@ -13,22 +13,19 @@ class WorkingHoursMonthlyViewRepository extends AbstractWorkingHoursViewReposito { protected const SQL_SELECT = <<periodDesignation = PeriodDesignationEnum::MONTHLY(); + $this->periodDesignation = PeriodDesignationEnum::MONTHLY; } protected function buildDateFromPeriod(string $period): DateTimeInterface diff --git a/api/src/Repositories/WorkingHoursRepository.php b/api/src/Repositories/WorkingHoursRepository.php index 82c81d1..3434753 100644 --- a/api/src/Repositories/WorkingHoursRepository.php +++ b/api/src/Repositories/WorkingHoursRepository.php @@ -21,16 +21,8 @@ use TorstenHettstedt\TimekeepingApi\Models\WorkingHours; class WorkingHoursRepository implements RepositoryReaderInterface, RepositoryWriterInterface { - protected PDO $database; - - /** - * WorkingHoursRepository constructor. - * - * @param PDO $database - */ - public function __construct(PDO $database) + public function __construct(protected PDO $database) { - $this->database = $database; $this->database->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); } @@ -106,7 +98,7 @@ class WorkingHoursRepository implements RepositoryReaderInterface, RepositoryWri if (is_string($end)) { $stmt->bindParam(':end', $end); } - return $stmt ?? null; + return $stmt ?: null; } /** @@ -121,7 +113,9 @@ class WorkingHoursRepository implements RepositoryReaderInterface, RepositoryWri */ 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->execute(); $row = $stmt->fetch(); @@ -155,7 +149,9 @@ class WorkingHoursRepository implements RepositoryReaderInterface, RepositoryWri throw new RepositoryRecordAlreadyExistException(); } /** @noinspection PhpUnusedLocalVariableInspection */ catch (RepositoryRecordNotFoundException $exception) { - $stmt = $this->database->prepare('insert into public."Arbeitszeiten" ("Datum", "Arbeitszeit") values (?, ?) on conflict do nothing'); + $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(); diff --git a/api/src/Repositories/WorkingHoursWeeklyViewRepository.php b/api/src/Repositories/WorkingHoursWeeklyViewRepository.php index d1043e1..e0f383b 100644 --- a/api/src/Repositories/WorkingHoursWeeklyViewRepository.php +++ b/api/src/Repositories/WorkingHoursWeeklyViewRepository.php @@ -12,22 +12,19 @@ class WorkingHoursWeeklyViewRepository extends AbstractWorkingHoursViewRepositor { protected const SQL_SELECT = <<periodDesignation = PeriodDesignationEnum::WEEKLY(); + $this->periodDesignation = PeriodDesignationEnum::WEEKLY; } protected function buildDateFromPeriod(string $period): DateTimeInterface diff --git a/api/src/Repositories/WorkingHoursYearlyViewRepository.php b/api/src/Repositories/WorkingHoursYearlyViewRepository.php index 1efb106..522e37b 100644 --- a/api/src/Repositories/WorkingHoursYearlyViewRepository.php +++ b/api/src/Repositories/WorkingHoursYearlyViewRepository.php @@ -12,22 +12,19 @@ class WorkingHoursYearlyViewRepository extends AbstractWorkingHoursViewRepositor { protected const SQL_SELECT = <<periodDesignation = PeriodDesignationEnum::YEARLY(); + $this->periodDesignation = PeriodDesignationEnum::YEARLY; } /** From 85d22d5c06e0bd1681d137b6da07fbea04fd8fbc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torsten=20L=C3=BCcke?= Date: Sat, 10 May 2025 12:42:05 +0200 Subject: [PATCH 07/15] Refactor Classes for PHP 8.4 --- api/html/index.php | 2 +- .../unit/Controller/WorkingHoursControllerTest.php | 10 +++++----- .../unit/Repositories/WorkingHoursRepositoryTest.php | 10 +++++----- .../Repositories/WorkingHoursViewRepositoryTest.php | 8 +++----- 4 files changed, 14 insertions(+), 16 deletions(-) diff --git a/api/html/index.php b/api/html/index.php index cbd9aca..addc91c 100644 --- a/api/html/index.php +++ b/api/html/index.php @@ -17,7 +17,7 @@ use TorstenHettstedt\TimekeepingApi\Middleware\OriginAccessControlHandler; $container = new Container(); $container->set('databases', function () { - $dsn = "pgsql:host=${_ENV['DATABASES_HOST']};port=5432;dbname=${_ENV['DATABASES_NAME']}"; + $dsn = "pgsql:host={$_ENV['DATABASES_HOST']};port=5432;dbname={$_ENV['DATABASES_NAME']}"; return new PDO($dsn, $_ENV['DATABASES_USER'], $_ENV['DATABASES_PASS']); }); diff --git a/api/tests/unit/Controller/WorkingHoursControllerTest.php b/api/tests/unit/Controller/WorkingHoursControllerTest.php index a9a3167..17ff3b8 100644 --- a/api/tests/unit/Controller/WorkingHoursControllerTest.php +++ b/api/tests/unit/Controller/WorkingHoursControllerTest.php @@ -19,11 +19,11 @@ use TorstenHettstedt\TimekeepingApi\Controller\WorkingHoursController; class WorkingHoursControllerTest extends AbstractControllerTest { - protected const FICTIONAL_STATUS_CODE = 666; - public const EXISTING_DATE = '2020-01-28'; - public const EXISTING_INTERVAL = '07:20:00'; - public const NEW_DATE = '2020-04-01'; - public const NEW_INTERVAL = '07:59:00'; + protected const int FICTIONAL_STATUS_CODE = 666; + public const string EXISTING_DATE = '2020-01-28'; + public const string EXISTING_INTERVAL = '07:20:00'; + public const string NEW_DATE = '2020-04-01'; + public const string NEW_INTERVAL = '07:59:00'; protected ContainerInterface $container; protected Request $request; diff --git a/api/tests/unit/Repositories/WorkingHoursRepositoryTest.php b/api/tests/unit/Repositories/WorkingHoursRepositoryTest.php index 0dbef52..326caff 100644 --- a/api/tests/unit/Repositories/WorkingHoursRepositoryTest.php +++ b/api/tests/unit/Repositories/WorkingHoursRepositoryTest.php @@ -18,11 +18,11 @@ use TorstenHettstedt\TimekeepingApi\Repositories\WorkingHoursRepository; class WorkingHoursRepositoryTest extends Unit { - public const EXISTING_DATE = '2020-01-28'; - public const EXISTING_INTERVAL = 'PT7H20M'; - public const NEW_DATE = '2020-04-01'; - public const NEW_INTERVAL = 'PT7H59M'; - public const RECORDS_COUNT = 61; + public const string EXISTING_DATE = '2020-01-28'; + public const string EXISTING_INTERVAL = 'PT7H20M'; + public const string NEW_DATE = '2020-04-01'; + public const string NEW_INTERVAL = 'PT7H59M'; + public const int RECORDS_COUNT = 61; protected PDO $pdoObject; diff --git a/api/tests/unit/Repositories/WorkingHoursViewRepositoryTest.php b/api/tests/unit/Repositories/WorkingHoursViewRepositoryTest.php index 8211985..14ebb54 100644 --- a/api/tests/unit/Repositories/WorkingHoursViewRepositoryTest.php +++ b/api/tests/unit/Repositories/WorkingHoursViewRepositoryTest.php @@ -6,7 +6,6 @@ use Codeception\Attribute\DataProvider; 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; @@ -38,14 +37,13 @@ class WorkingHoursViewRepositoryTest extends Unit /** * @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'))], + [WorkingHoursYearlyViewRepository::class, '2020', 61, date_create('2020-01-01', new DateTimeZone('UCT'))], + [WorkingHoursMonthlyViewRepository::class, '2020 February', 20, date_create('2020-02-01', new DateTimeZone('UCT'))], + [WorkingHoursWeeklyViewRepository::class, '2020#03', 5, date_create('2020-01-13', new DateTimeZone('UCT'))], ]; } From b89a3c744f353742cac6e529d7728bc183582526 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torsten=20L=C3=BCcke?= Date: Wed, 4 Jun 2025 18:07:10 +0200 Subject: [PATCH 08/15] =?UTF-8?q?Nutze=20neueste=20Version=20f=C3=BCr=20Sv?= =?UTF-8?q?elte5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ignoriere die Pagination der Tabellen und das Bearbeiten der Einträge. --- .gitignore | 3 + ui/{public => }/less/global.less | 0 ui/{public => }/less/lesslib/basics.less | 0 ui/{public => }/less/lesslib/colors.less | 0 ui/{public => }/less/lesslib/fonts.less | 0 ui/{public => }/less/lesslib/layout.less | 146 ++++++++++------ .../less/lesslib/media_queries.less | 12 +- .../less/lesslib/mixins.framework.less | 0 ui/{public => }/less/lesslib/mixins.less | 0 .../less/lesslib/print/basics.less | 2 +- .../less/lesslib/print/colors.less | 0 .../less/lesslib/print/layout.less | 4 +- ui/{public => }/less/print.less | 0 ui/package.json | 63 ++++--- ui/public/index.html | 22 --- ui/rollup.config.js | 51 ------ ui/src/app.html | 23 +++ ui/src/components/App.svelte | 57 ------ ui/src/components/Nav.svelte | 15 -- ui/src/components/Repositories.svelte | 110 ------------ ui/src/index.js | 5 - ui/src/lib/Models.svelte.js | 17 ++ ui/src/lib/Repositories.svelte | 165 ++++++++++++++++++ ui/src/lib/Views.svelte | 63 +++++++ ui/src/routes/+layout.svelte | 19 ++ ui/src/routes/+page.server.js | 9 + ui/src/routes/+page.svelte | 7 + ui/src/routes/Home.svelte | 6 - ui/src/routes/MonthlyViews.svelte | 11 -- ui/src/routes/Views.svelte | 81 --------- ui/src/routes/WeeklyViews.svelte | 11 -- ui/src/routes/WorkingHours.svelte | 33 ---- ui/src/routes/YearlyViews.svelte | 11 -- ui/src/routes/views/monthly/+page.server.js | 9 + ui/src/routes/views/monthly/+page.svelte | 7 + ui/src/routes/views/weekly/+page.server.js | 9 + ui/src/routes/views/weekly/+page.svelte | 7 + ui/src/routes/views/yearly/+page.server.js | 9 + ui/src/routes/views/yearly/+page.svelte | 7 + ui/{public => static}/favicon.png | Bin ui/{public => static}/fonts/Lobster_1.3.otf | Bin ui/{public => static}/fonts/opensans-bold.eot | Bin ui/{public => static}/fonts/opensans-bold.svg | 0 ui/{public => static}/fonts/opensans-bold.ttf | Bin .../fonts/opensans-bold.woff | Bin .../fonts/opensans-bolditalic.eot | Bin .../fonts/opensans-bolditalic.svg | 0 .../fonts/opensans-bolditalic.ttf | Bin .../fonts/opensans-bolditalic.woff | Bin .../fonts/opensans-italic.eot | Bin .../fonts/opensans-italic.svg | 0 .../fonts/opensans-italic.ttf | Bin .../fonts/opensans-italic.woff | Bin .../fonts/opensans-regular.eot | Bin .../fonts/opensans-regular.svg | 0 .../fonts/opensans-regular.ttf | Bin .../fonts/opensans-regular.woff | Bin ui/{public => static}/meyer_resets.css | 0 ui/svelte.config.js | 6 + ui/tsconfig.json | 19 ++ ui/vite.config.js | 6 + 61 files changed, 524 insertions(+), 501 deletions(-) rename ui/{public => }/less/global.less (100%) rename ui/{public => }/less/lesslib/basics.less (100%) rename ui/{public => }/less/lesslib/colors.less (100%) rename ui/{public => }/less/lesslib/fonts.less (100%) rename ui/{public => }/less/lesslib/layout.less (63%) rename ui/{public => }/less/lesslib/media_queries.less (98%) rename ui/{public => }/less/lesslib/mixins.framework.less (100%) rename ui/{public => }/less/lesslib/mixins.less (100%) rename ui/{public => }/less/lesslib/print/basics.less (99%) rename ui/{public => }/less/lesslib/print/colors.less (100%) rename ui/{public => }/less/lesslib/print/layout.less (98%) rename ui/{public => }/less/print.less (100%) delete mode 100644 ui/public/index.html delete mode 100644 ui/rollup.config.js create mode 100644 ui/src/app.html delete mode 100644 ui/src/components/App.svelte delete mode 100644 ui/src/components/Nav.svelte delete mode 100644 ui/src/components/Repositories.svelte delete mode 100644 ui/src/index.js create mode 100644 ui/src/lib/Models.svelte.js create mode 100644 ui/src/lib/Repositories.svelte create mode 100644 ui/src/lib/Views.svelte create mode 100644 ui/src/routes/+layout.svelte create mode 100644 ui/src/routes/+page.server.js create mode 100644 ui/src/routes/+page.svelte delete mode 100644 ui/src/routes/Home.svelte delete mode 100644 ui/src/routes/MonthlyViews.svelte delete mode 100644 ui/src/routes/Views.svelte delete mode 100644 ui/src/routes/WeeklyViews.svelte delete mode 100644 ui/src/routes/WorkingHours.svelte delete mode 100644 ui/src/routes/YearlyViews.svelte create mode 100644 ui/src/routes/views/monthly/+page.server.js create mode 100644 ui/src/routes/views/monthly/+page.svelte create mode 100644 ui/src/routes/views/weekly/+page.server.js create mode 100644 ui/src/routes/views/weekly/+page.svelte create mode 100644 ui/src/routes/views/yearly/+page.server.js create mode 100644 ui/src/routes/views/yearly/+page.svelte rename ui/{public => static}/favicon.png (100%) rename ui/{public => static}/fonts/Lobster_1.3.otf (100%) rename ui/{public => static}/fonts/opensans-bold.eot (100%) rename ui/{public => static}/fonts/opensans-bold.svg (100%) rename ui/{public => static}/fonts/opensans-bold.ttf (100%) rename ui/{public => static}/fonts/opensans-bold.woff (100%) rename ui/{public => static}/fonts/opensans-bolditalic.eot (100%) rename ui/{public => static}/fonts/opensans-bolditalic.svg (100%) rename ui/{public => static}/fonts/opensans-bolditalic.ttf (100%) rename ui/{public => static}/fonts/opensans-bolditalic.woff (100%) rename ui/{public => static}/fonts/opensans-italic.eot (100%) rename ui/{public => static}/fonts/opensans-italic.svg (100%) rename ui/{public => static}/fonts/opensans-italic.ttf (100%) rename ui/{public => static}/fonts/opensans-italic.woff (100%) rename ui/{public => static}/fonts/opensans-regular.eot (100%) rename ui/{public => static}/fonts/opensans-regular.svg (100%) rename ui/{public => static}/fonts/opensans-regular.ttf (100%) rename ui/{public => static}/fonts/opensans-regular.woff (100%) rename ui/{public => static}/meyer_resets.css (100%) create mode 100644 ui/svelte.config.js create mode 100644 ui/tsconfig.json create mode 100644 ui/vite.config.js diff --git a/.gitignore b/.gitignore index 9ffe02b..a07b7ca 100644 --- a/.gitignore +++ b/.gitignore @@ -109,3 +109,6 @@ Temporary Items /api/tests/_* /api/tests/*.suite.yml /api/.env +/ui/static/print.css* +/ui/static/global.css* +/ui/static/bundle.css* diff --git a/ui/public/less/global.less b/ui/less/global.less similarity index 100% rename from ui/public/less/global.less rename to ui/less/global.less diff --git a/ui/public/less/lesslib/basics.less b/ui/less/lesslib/basics.less similarity index 100% rename from ui/public/less/lesslib/basics.less rename to ui/less/lesslib/basics.less diff --git a/ui/public/less/lesslib/colors.less b/ui/less/lesslib/colors.less similarity index 100% rename from ui/public/less/lesslib/colors.less rename to ui/less/lesslib/colors.less diff --git a/ui/public/less/lesslib/fonts.less b/ui/less/lesslib/fonts.less similarity index 100% rename from ui/public/less/lesslib/fonts.less rename to ui/less/lesslib/fonts.less diff --git a/ui/public/less/lesslib/layout.less b/ui/less/lesslib/layout.less similarity index 63% rename from ui/public/less/lesslib/layout.less rename to ui/less/lesslib/layout.less index d0bd78a..0c280e1 100644 --- a/ui/public/less/lesslib/layout.less +++ b/ui/less/lesslib/layout.less @@ -5,50 +5,66 @@ html { body { background: @mainColorLight; - > header, > footer { - color: @mainColorDark; - background: @mainColor; - font-family: serif; - h1 { - font-size: 3rem; - text-align: center; - } - p { - text-indent: inherit; - &:first-letter { - font-size: inherit; + div { + > header, > footer { + color: @mainColorDark; + background: @mainColor; + font-family: serif; + + h1 { + font-size: 3rem; + text-align: center; + } + + p { + text-indent: inherit; + + &:first-letter { + font-size: inherit; + } + + font-style: italic; } - font-style: italic; } - } - > header h1 { - margin: 1rem auto; - } - > footer { - color: contrast(@mainColorDark); - } - & > nav { - ul { - background: darken(@mainColorLight,33%,relativ); - li { - font-weight: bold; - &:not(:first-child) { - border-left: @mainColorDark solid thin; - } - a { - text-decoration: none; - color: @mainColorDark; - } - &:hover { - background: lighten(@mainColor,20%,relativ); - a { - color: lighten(@mainColor,66%,relativ); + + > header h1 { + margin: 1rem auto; + } + + > footer { + color: contrast(@mainColorDark); + } + + & > nav { + ul { + background: darken(@mainColorLight, 33%, relativ); + + li { + font-weight: bold; + + &:not(:first-child) { + border-left: @mainColorDark solid thin; } - } - &.selected, &.selected:hover { - background: lighten(@mainColor,10%,relativ); + a { - color: lighten(@mainColor,66%,relativ); + text-decoration: none; + color: @mainColorDark; + } + + &:hover { + background: lighten(@mainColor, 20%, relativ); + + a { + color: lighten(@mainColor, 66%, relativ); + } + } + + &.selected, &.selected:hover { + background: lighten(@mainColor, 10%, relativ); + + a { + color: lighten(@mainColor, 66%, relativ); + } } } } @@ -63,15 +79,19 @@ main { border-color: @mainColorDark; border-width: medium; } + section { @sectionMargin: 1rem; + h1, h2 { margin: @sectionMargin; } + p, ul, ol, dl { margin: @sectionMargin; text-indent: @sectionMargin * 2; } + table { thead { tr:last-child { @@ -79,7 +99,9 @@ main { border-bottom: @mainColor solid thin; } } - }; + } + ; + tbody { td.absence-time { color: @redColor; @@ -87,15 +109,23 @@ main { text-decoration-style: dashed; } } + tfoot { + * { + border: initial; + } + } } + form { @formLabelWidth: 10rem; @formItemWidth: 40rem; + nav.menu { width: @formItemWidth + @formLabelWidth + 4rem; border-color: @mainColor; border-bottom-style: solid; margin: 1rem auto; + span { border-color: @mainColor; border-style: solid solid none; @@ -103,73 +133,89 @@ main { display: inline-block; font-weight: bolder; margin-right: 1rem; + &:first-child { margin-left: 1.5rem; } + &:hover { - background-color: darken(@mainColorLight,10%,relativ); + background-color: darken(@mainColorLight, 10%, relativ); } + &.active { - background-color: darken(@mainColorLight,20%,relativ); + background-color: darken(@mainColorLight, 20%, relativ); } } } + fieldset { width: @formItemWidth + @formLabelWidth + 2rem; margin: 1rem auto; - border-style: solid ; + border-style: solid; border-color: @mainColor; padding: @sectionMargin; + &.tasten { border: none; background-color: @mainColorLight; } + legend { margin: @sectionMargin; margin-left: @sectionMargin * 2; } + div { - & > label { + & > label { width: @formLabelWidth; display: inline-block; margin: 0.5rem; } + input { display: inline-block; width: @formItemWidth; padding: 0.25rem; margin-bottom: 1.0rem; - border-style: solid; + border-style: solid; border-width: thin; } } } } + button { - .button(@mainColor, spin(lighten(@mainColor,66%,relativ),@subColor)); + .button(@mainColor, spin(lighten(@mainColor, 66%, relativ), @subColor)); + &.sub-button { - .button(lighten(@mainColor,20%,relativ), spin(@mainColor,@subColor)); + .button(lighten(@mainColor, 20%, relativ), spin(@mainColor, @subColor)); } } + footer { text-align: right; padding: 0; + p { padding: 0.5rem; display: inline-block; margin: inherit; text-indent: inherit; + &:first-letter { font-size: inherit; } - background: @mainColorLight ; - color: @mainColor ; + + background: @mainColorLight; + color: @mainColor; font-style: italic; font-size: 85%; } + nav li { padding: 0.5rem; } + border-bottom-style: solid; } } diff --git a/ui/public/less/lesslib/media_queries.less b/ui/less/lesslib/media_queries.less similarity index 98% rename from ui/public/less/lesslib/media_queries.less rename to ui/less/lesslib/media_queries.less index 0b57d01..78d7375 100644 --- a/ui/public/less/lesslib/media_queries.less +++ b/ui/less/lesslib/media_queries.less @@ -5,7 +5,7 @@ font-size: 10pt; } header, footer, main, nav, aside { - body > & { + body div > & { width: 19cm; margin: auto; .border-box(); @@ -15,7 +15,7 @@ padding: 0.5rem; ; } - body { + body div { & > header { font-size: 200%; font-weight: bolder; @@ -71,7 +71,7 @@ @media screen and (min-width: 831px) { header, footer, main, nav, aside { - body > & { + body div > & { width: @breiteMainBereich; margin: auto; .border-box(); @@ -80,7 +80,7 @@ header, footer{ padding: 0.5rem; } - body { + body div { & > header { font-size: 200%; font-weight: bolder; @@ -139,7 +139,7 @@ } @media screen and (max-width: 830px) { - html body { + html body div { @boxHeight: 5rem; & > header { height: @boxHeight; @@ -267,7 +267,7 @@ } } @media screen and (max-width: 720px) { - html body { + html body div { @boxHeight: 2.5rem; & > header { height: @boxHeight; diff --git a/ui/public/less/lesslib/mixins.framework.less b/ui/less/lesslib/mixins.framework.less similarity index 100% rename from ui/public/less/lesslib/mixins.framework.less rename to ui/less/lesslib/mixins.framework.less diff --git a/ui/public/less/lesslib/mixins.less b/ui/less/lesslib/mixins.less similarity index 100% rename from ui/public/less/lesslib/mixins.less rename to ui/less/lesslib/mixins.less diff --git a/ui/public/less/lesslib/print/basics.less b/ui/less/lesslib/print/basics.less similarity index 99% rename from ui/public/less/lesslib/print/basics.less rename to ui/less/lesslib/print/basics.less index 63acfce..f464f04 100644 --- a/ui/public/less/lesslib/print/basics.less +++ b/ui/less/lesslib/print/basics.less @@ -2,7 +2,7 @@ html { font-size: 100%; } -body { +body div { color: #000; font-size: 1em; font-family: OpenSans, "Lucida Grande", "Lucida Sans Unicode", Verdana, Helvetica, Arial, sans-serif; diff --git a/ui/public/less/lesslib/print/colors.less b/ui/less/lesslib/print/colors.less similarity index 100% rename from ui/public/less/lesslib/print/colors.less rename to ui/less/lesslib/print/colors.less diff --git a/ui/public/less/lesslib/print/layout.less b/ui/less/lesslib/print/layout.less similarity index 98% rename from ui/public/less/lesslib/print/layout.less rename to ui/less/lesslib/print/layout.less index 0864ddf..864ebe6 100644 --- a/ui/public/less/lesslib/print/layout.less +++ b/ui/less/lesslib/print/layout.less @@ -1,5 +1,5 @@ html { - body { + body div { > header, > footer { background: none; font-family: serif; @@ -59,7 +59,7 @@ main { } } } - tbody { + tbody div { th { font-weight: bold; text-align: center; diff --git a/ui/public/less/print.less b/ui/less/print.less similarity index 100% rename from ui/public/less/print.less rename to ui/less/print.less diff --git a/ui/package.json b/ui/package.json index 567b320..72fecdd 100644 --- a/ui/package.json +++ b/ui/package.json @@ -1,31 +1,36 @@ { - "private": true, - "name": "svelte-demo", - "scripts": { - "build": "rollup -c", - "autobuild": "rollup -c -w", - "dev": "run-p start:dev autobuild", - "start": "sirv public --single", - "start:dev": "sirv public --dev --single" - }, - "dependencies": { - "ganalytics": "^3.1.2", - "navaid": "^1.0.2", - "open-iconic": "^1.1.1" - }, - "devDependencies": { - "@iconify-icons/oi": "^1.1.0", - "@iconify/svelte": "^1.0.4", - "@rollup/plugin-commonjs": "^15.0.0", - "@rollup/plugin-node-resolve": "^9.0.0", - "@rollup/plugin-replace": "^2.3.0", - "dotenv": "^8.2.0", - "npm-run-all": "^4.1.3", - "rollup": "^2.30.0", - "rollup-plugin-svelte": "^6.0.0", - "rollup-plugin-terser": "^7.0.0", - "sirv-cli": "^1.0.8", - "svelte": "^3.4.4", - "svelte-paginate": "^0.1.0" - } + "private": true, + "name": "svelte-demo", + "type": "module", + "scripts": { + "dev": "vite dev", + "build": "vite build", + "preview": "vite preview", + "prepare": "svelte-kit sync || echo ''", + "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", + "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch" + }, + "dependencies": { + "ganalytics": "^3.1.2", + "navaid": "^1.0.2", + "open-iconic": "^1.1.1" + }, + "devDependencies": { + "@sveltejs/adapter-auto": "^6.0.0", + "@sveltejs/kit": "^2.16.0", + "@sveltejs/vite-plugin-svelte": "^5.0.0", + "svelte": "^5.0.0", + "svelte-check": "^4.0.0", + "typescript": "^5.0.0", + "vite": "^6.2.6", + "dotenv": "^8.2.0", + "wx-svelte-grid": "^2.1.5", + "@iconify-icons/oi": "^1.1.0", + "@iconify/svelte": "^5.0.0" + }, + "exports": { + ".": { + "svelte": "./puplic/index.js" + } + } } diff --git a/ui/public/index.html b/ui/public/index.html deleted file mode 100644 index a48646e..0000000 --- a/ui/public/index.html +++ /dev/null @@ -1,22 +0,0 @@ - - - - - - - - - - Svelte app - - - - - - - - - - - - diff --git a/ui/rollup.config.js b/ui/rollup.config.js deleted file mode 100644 index 7b165f5..0000000 --- a/ui/rollup.config.js +++ /dev/null @@ -1,51 +0,0 @@ -import svelte from 'rollup-plugin-svelte'; -import replace from '@rollup/plugin-replace'; -import resolve from '@rollup/plugin-node-resolve'; -import commonjs from '@rollup/plugin-commonjs'; -import { terser } from 'rollup-plugin-terser'; -import {config} from 'dotenv'; - -const production = !process.env.ROLLUP_WATCH; - -export default { - input: 'src/index.js', - output: { - name: 'app', - format: 'esm', - sourcemap: true, - dir: 'public', - }, - preserveEntrySignatures: false, - plugins: [ - svelte({ - // enable run-time checks when not in production - dev: !production, - // we'll extract any component CSS out into - // a separate file — better for performance - css: css => { - css.write('bundle.css'); - } - }), - - // If you have external dependencies installed from - // npm, you'll most likely need these plugins. In - // some cases you'll need additional configuration — - // consult the documentation for details: - // https://github.com/rollup/rollup-plugin-commonjs - resolve(), - commonjs(), - - replace({ - 'process.env.NODE_ENV': JSON.stringify(production ? 'production' : 'development'), - 'env': JSON.stringify({...config().parsed /* attached the .env config*/}) - }), - - // If we're building for production (npm run build - // instead of npm run dev), minify - production && terser() - ], - - watch: { - clearScreen: false - } -}; diff --git a/ui/src/app.html b/ui/src/app.html new file mode 100644 index 0000000..025cb9c --- /dev/null +++ b/ui/src/app.html @@ -0,0 +1,23 @@ + + + + + + + + Svelte app + + + + + %sveltekit.head% + + + + + +
+ %sveltekit.body% +
+ + diff --git a/ui/src/components/App.svelte b/ui/src/components/App.svelte deleted file mode 100644 index 0577043..0000000 --- a/ui/src/components/App.svelte +++ /dev/null @@ -1,57 +0,0 @@ -
- Arbeitszeiten -
-