diff --git a/composer.json b/composer.json
index b10979b..5e07b41 100644
--- a/composer.json
+++ b/composer.json
@@ -13,7 +13,7 @@
"doctrine/doctrine-migrations-bundle": "^3.0",
"doctrine/orm": "^2.7",
"phpdocumentor/reflection-docblock": "^5.2",
- "sensio/framework-extra-bundle": "^5.1",
+ "sensio/framework-extra-bundle": "^5.6",
"symfony/asset": "5.1.*",
"symfony/console": "5.1.*",
"symfony/dotenv": "5.1.*",
diff --git a/composer.lock b/composer.lock
index e972a66..fe21e51 100644
--- a/composer.lock
+++ b/composer.lock
@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
- "content-hash": "619de2ebf9720ae1643b2e798c5a7355",
+ "content-hash": "42c5e66445821e40a4194fe5a0a8a7b7",
"packages": [
{
"name": "composer/package-versions-deprecated",
@@ -7855,7 +7855,7 @@
"prefer-stable": false,
"prefer-lowest": false,
"platform": {
- "php": ">=7.2.5",
+ "php": "^7.3.0",
"ext-ctype": "*",
"ext-iconv": "*"
},
diff --git a/config/packages/framework.yaml b/config/packages/framework.yaml
index cad7f78..7804996 100644
--- a/config/packages/framework.yaml
+++ b/config/packages/framework.yaml
@@ -7,7 +7,8 @@ framework:
# Enables session support. Note that the session will ONLY be started if you read or write from it.
# Remove or comment this section to explicitly disable session support.
session:
- handler_id: null
+ handler_id: 'session.handler.native_file'
+ # save_path: '%kernel.project_dir%/var/sessions/%kernel.environment%'
cookie_secure: auto
cookie_samesite: lax
diff --git a/config/routes.yaml b/config/routes.yaml
index c3283aa..7f4c926 100644
--- a/config/routes.yaml
+++ b/config/routes.yaml
@@ -1,3 +1,13 @@
-#index:
-# path: /
-# controller: App\Controller\DefaultController::index
+---
+index:
+ path: /
+ controller: App\Controller\TimeController::index
+
+startNewEntry:
+ path: /start
+ controller: App\Controller\TimeController::start
+ methods: POST
+stopRunningEntry:
+ path: /stop
+ controller: App\Controller\TimeController::stop
+ methods: POST
diff --git a/phpunit.xml.dist b/phpunit.xml.dist
index d81f0c3..29f86af 100644
--- a/phpunit.xml.dist
+++ b/phpunit.xml.dist
@@ -24,6 +24,9 @@
src
+
+ src/Kernel.php
+
diff --git a/src/Controller/.gitignore b/src/Controller/.gitignore
deleted file mode 100644
index e69de29..0000000
diff --git a/src/Controller/TimeController.php b/src/Controller/TimeController.php
new file mode 100644
index 0000000..895ae0a
--- /dev/null
+++ b/src/Controller/TimeController.php
@@ -0,0 +1,67 @@
+
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+ * 02110-1301, USA.
+ */
+
+use App\Service\ActiveEntry;
+use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpFoundation\Response;
+
+class TimeController extends AbstractController
+{
+ /**
+ * @var ActiveEntry
+ */
+ private $activeEntry;
+
+ public function __construct(ActiveEntry $activeEntry)
+ {
+ $this->activeEntry = $activeEntry;
+ }
+
+ public function index(): Response
+ {
+ return $this->render('time/index.html.twig', [
+ 'entry' => $this->activeEntry->get(),
+ ]);
+ }
+
+ public function start(Request $request): Response
+ {
+ $this->activeEntry->startNew(
+ $request->request->get('title')
+ );
+
+ return $this->redirectToRoute('index');
+ }
+
+ public function stop(): Response
+ {
+ if ($this->activeEntry->get()->isRunning() === false) {
+ return $this->redirectToRoute('index');
+ }
+
+ $this->activeEntry->stopRunning();
+
+ return $this->redirectToRoute('index');
+ }
+}
diff --git a/src/Entity/Entry.php b/src/Entity/Entry.php
new file mode 100644
index 0000000..5a31ef9
--- /dev/null
+++ b/src/Entity/Entry.php
@@ -0,0 +1,73 @@
+
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+ * 02110-1301, USA.
+ */
+
+class Entry
+{
+ /**
+ * @var string
+ */
+ private $title = '';
+
+ /**
+ * @var \DateTimeImmutable|null
+ */
+ private $start;
+
+ /**
+ * @var \DateTimeImmutable|null
+ */
+ private $stop;
+
+ /**
+ * @var bool
+ */
+ private $running = false;
+
+ public function __construct(
+ string $title = ''
+ ) {
+ $this->title = $title;
+ }
+
+ public function start(): void
+ {
+ $this->start = new \DateTimeImmutable();
+ $this->running = true;
+ }
+
+ public function stop(): void
+ {
+ $this->stop = new \DateTimeImmutable();
+ $this->running = false;
+ }
+
+ public function isRunning(): bool
+ {
+ return $this->running;
+ }
+
+ public function getTitle(): string
+ {
+ return $this->title;
+ }
+}
diff --git a/src/Service/ActiveEntry.php b/src/Service/ActiveEntry.php
new file mode 100644
index 0000000..22bf33e
--- /dev/null
+++ b/src/Service/ActiveEntry.php
@@ -0,0 +1,62 @@
+
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+ * 02110-1301, USA.
+ */
+
+use App\Entity\Entry;
+use Symfony\Component\HttpFoundation\Session\SessionInterface;
+
+class ActiveEntry
+{
+ /**
+ * @var SessionInterface
+ */
+ private $session;
+
+ public function __construct(SessionInterface $session)
+ {
+ $this->session = $session;
+ }
+
+ public function startNew(
+ string $title
+ ): Entry {
+ $entry = new Entry(
+ $title
+ );
+ $entry->start();
+
+ $this->session->set('runningEntry', $entry);
+
+ return $entry;
+ }
+
+ public function stopRunning(): void
+ {
+ $this->get()->stop();
+ $this->session->remove('runningEntry');
+ }
+
+ public function get(): Entry
+ {
+ return $this->session->get('runningEntry') ?? new Entry();
+ }
+}
diff --git a/templates/base.html.twig b/templates/base.html.twig
index 043f42d..fb2a3c0 100644
--- a/templates/base.html.twig
+++ b/templates/base.html.twig
@@ -1,12 +1,17 @@
-
-
+
+
-
- {% block title %}Welcome!{% endblock %}
+
+ Timetracking
+
{% block stylesheets %}{% endblock %}
+
- {% block body %}{% endblock %}
+ {% block body %}
+ {% block content %}
+ {% endblock %}
+ {% endblock %}
{% block javascripts %}{% endblock %}
diff --git a/templates/time/index.html.twig b/templates/time/index.html.twig
new file mode 100644
index 0000000..88fa861
--- /dev/null
+++ b/templates/time/index.html.twig
@@ -0,0 +1,7 @@
+{% extends 'base.html.twig' %}
+
+{% block content %}
+ Time entries
+
+ {{ include('time/timer.html.twig') }}
+{% endblock %}
diff --git a/templates/time/timer.html.twig b/templates/time/timer.html.twig
new file mode 100644
index 0000000..ba6b8b1
--- /dev/null
+++ b/templates/time/timer.html.twig
@@ -0,0 +1,11 @@
+{% if entry.isRunning %}
+
+{% else %}
+
+{% endif %}
diff --git a/tests/Functional/Controller/TimeControllerTest.php b/tests/Functional/Controller/TimeControllerTest.php
new file mode 100644
index 0000000..f5d4af4
--- /dev/null
+++ b/tests/Functional/Controller/TimeControllerTest.php
@@ -0,0 +1,98 @@
+
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+ * 02110-1301, USA.
+ */
+
+use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
+
+/**
+ * @covers App\Controller\TimeController
+ * @uses App\Entity\Entry
+ * @uses App\Service\ActiveEntry
+ */
+class TimeControllerTest extends WebTestCase
+{
+ /**
+ * @test
+ */
+ public function rootCanBeOpened()
+ {
+ $client = static::createClient();
+ $crawler = $client->request('GET', '/');
+
+ $this->assertResponseIsSuccessful();
+ $this->assertSelectorTextContains('h1', 'Time entries');
+ }
+
+ /**
+ * @test
+ */
+ public function newEntryCanBeStarted()
+ {
+ $client = static::createClient();
+
+ $client->request('GET', '/');
+
+ $client->submitForm('Start', ['title' => 'Test Entry']);
+ $this->assertResponseRedirects('/');
+
+ $client->request('GET', '/');
+ $this->assertResponseIsSuccessful();
+ $this->assertInputValueSame('title', 'Test Entry');
+ }
+
+ /**
+ * @test
+ */
+ public function runningEntryCanBeStopped()
+ {
+ $client = static::createClient();
+
+ $client->request('GET', '/');
+
+ $client->submitForm('Start', ['title' => 'Test Entry']);
+
+ $client->request('GET', '/');
+ $client->submitForm('Stop');
+ $this->assertResponseRedirects('/');
+
+ $client->request('GET', '/');
+ $this->assertResponseIsSuccessful();
+ $this->assertInputValueSame('title', '');
+ $this->assertSelectorTextSame('button', 'Start');
+ }
+
+ /**
+ * @test
+ */
+ public function noneRunningEntryCanBeStopped()
+ {
+ $client = static::createClient();
+
+ $client->request('POST', '/stop');
+ $this->assertResponseRedirects('/');
+
+ $client->request('GET', '/');
+ $this->assertResponseIsSuccessful();
+ $this->assertInputValueSame('title', '');
+ $this->assertSelectorTextSame('button', 'Start');
+ }
+}
diff --git a/tests/Unit/Entity/EntryTest.php b/tests/Unit/Entity/EntryTest.php
new file mode 100644
index 0000000..cca49ff
--- /dev/null
+++ b/tests/Unit/Entity/EntryTest.php
@@ -0,0 +1,98 @@
+
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+ * 02110-1301, USA.
+ */
+
+use App\Entity\Entry;
+use PHPUnit\Framework\TestCase;
+
+/**
+ * @covers App\Entity\Entry
+ */
+class EntryTest extends TestCase
+{
+ /**
+ * @test
+ */
+ public function canBeCreated()
+ {
+ $subject = new Entry();
+
+ static::assertInstanceOf(Entry::class, $subject);
+ }
+
+ /**
+ * @test
+ */
+ public function canBeCreatedWithTitle()
+ {
+ $subject = new Entry('Example title');
+
+ static::assertInstanceOf(Entry::class, $subject);
+ }
+
+ /**
+ * @test
+ */
+ public function returnsProvidedTitle()
+ {
+ $subject = new Entry('Example title');
+
+ static::assertSame('Example title', $subject->getTitle());
+ }
+
+ /**
+ * @test
+ */
+ public function canBeStarted()
+ {
+ $subject = new Entry();
+
+ $subject->start();
+
+ static::assertTrue($subject->isRunning());
+ }
+
+ /**
+ * @test
+ */
+ public function canBeStopped()
+ {
+ $subject = new Entry();
+
+ $subject->start();
+ $subject->stop();
+
+ static::assertFalse($subject->isRunning());
+ }
+
+ /**
+ * @test
+ */
+ public function canBeStoppedEvenIfNotRunning()
+ {
+ $subject = new Entry();
+
+ $subject->stop();
+
+ static::assertFalse($subject->isRunning());
+ }
+}
diff --git a/tests/Unit/Service/ActiveEntryTest.php b/tests/Unit/Service/ActiveEntryTest.php
new file mode 100644
index 0000000..e024407
--- /dev/null
+++ b/tests/Unit/Service/ActiveEntryTest.php
@@ -0,0 +1,123 @@
+
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+ * 02110-1301, USA.
+ */
+
+use App\Entity\Entry;
+use App\Service\ActiveEntry;
+use PHPUnit\Framework\TestCase;
+use Prophecy\Argument;
+use Symfony\Component\HttpFoundation\Session\SessionInterface;
+
+
+/**
+ * @covers App\Service\ActiveEntry
+ */
+class ActiveEntryTest extends TestCase
+{
+ /**
+ * @test
+ */
+ public function canBeCreated()
+ {
+ $session = $this->prophesize(SessionInterface::class);
+ $subject = new ActiveEntry(
+ $session->reveal()
+ );
+
+ static::assertInstanceOf(ActiveEntry::class, $subject);
+ }
+
+ /**
+ * @test
+ */
+ public function returnsEmptyEntryAsDefault()
+ {
+ $session = $this->prophesize(SessionInterface::class);
+ $subject = new ActiveEntry(
+ $session->reveal()
+ );
+
+ $entry = $subject->get();
+
+ static::assertInstanceOf(Entry::class, $entry);
+ static::assertSame('', $entry->getTitle());
+ static::assertFalse($entry->isRunning());
+ }
+
+ /**
+ * @test
+ */
+ public function canStartANewEntry()
+ {
+ $session = $this->prophesize(SessionInterface::class);
+ $subject = new ActiveEntry(
+ $session->reveal()
+ );
+
+ $subject->startNew('Example title');
+
+ $session->set('runningEntry', Argument::that(function (Entry $entry) {
+ return $entry->getTitle() === 'Example title'
+ && $entry->isRunning()
+ ;
+ }))->shouldBeCalled();
+ }
+
+ /**
+ * @test
+ */
+ public function returnsStartedEntry()
+ {
+ $session = $this->prophesize(SessionInterface::class);
+ $entry = $this->prophesize(Entry::class);
+ $subject = new ActiveEntry(
+ $session->reveal()
+ );
+
+ $session->set('runningEntry', Argument::type(Entry::class))->shouldBeCalled();
+ $session->get('runningEntry')->willReturn($entry->reveal())->shouldBeCalled();
+
+ $subject->startNew('Example title');
+
+ static::assertSame($entry->reveal(), $subject->get());
+ }
+
+ /**
+ * @test
+ */
+ public function stopsRunningEntry()
+ {
+ $session = $this->prophesize(SessionInterface::class);
+ $entry = $this->prophesize(Entry::class);
+ $subject = new ActiveEntry(
+ $session->reveal()
+ );
+
+ $session->set('runningEntry', Argument::type(Entry::class))->shouldBeCalled();
+ $session->get('runningEntry')->willReturn($entry->reveal())->shouldBeCalled();
+
+ $subject->startNew('Example title');
+ $subject->stopRunning();
+
+ $session->remove('runningEntry')->shouldBeCalled();
+ }
+}