diff --git a/.env b/.env
index 8ccf894..11d946d 100644
--- a/.env
+++ b/.env
@@ -29,5 +29,5 @@ APP_SECRET=5ea76603566babff85ae4fa3e7d44071
# For an SQLite database, use: "sqlite:///%kernel.project_dir%/var/data.db"
# For a PostgreSQL database, use: "postgresql://db_user:db_password@127.0.0.1:5432/db_name?serverVersion=11&charset=utf8"
# IMPORTANT: You MUST configure your server version, either here or in config/packages/doctrine.yaml
-DATABASE_URL=mysql://db_user:db_password@127.0.0.1:3306/db_name?serverVersion=5.7
+DATABASE_URL=sqlite:///%kernel.project_dir%/var/data.db
###< doctrine/doctrine-bundle ###
diff --git a/.env.test b/.env.test
index d048686..58ce8ef 100644
--- a/.env.test
+++ b/.env.test
@@ -3,3 +3,4 @@ KERNEL_CLASS='App\Kernel'
APP_SECRET='$ecretf0rt3st'
SYMFONY_DEPRECATIONS_HELPER=999999
PANTHER_APP_ENV=panther
+DATABASE_URL=sqlite:///%kernel.project_dir%/var/test.db
diff --git a/composer.json b/composer.json
index 5e07b41..ca8800f 100644
--- a/composer.json
+++ b/composer.json
@@ -42,6 +42,7 @@
"twig/twig": "^2.12|^3.0"
},
"require-dev": {
+ "dama/doctrine-test-bundle": "^6.3",
"symfony/browser-kit": "^5.1",
"symfony/css-selector": "^5.1",
"symfony/debug-bundle": "^5.1",
diff --git a/composer.lock b/composer.lock
index fe21e51..ff2c8ab 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": "42c5e66445821e40a4194fe5a0a8a7b7",
+ "content-hash": "a571222fe89b2d15a25ad09857c44cc6",
"packages": [
{
"name": "composer/package-versions-deprecated",
@@ -7256,6 +7256,63 @@
}
],
"packages-dev": [
+ {
+ "name": "dama/doctrine-test-bundle",
+ "version": "v6.3.3",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/dmaicher/doctrine-test-bundle.git",
+ "reference": "a364cfee35acb7d37698c4749f7dd34d04646535"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/dmaicher/doctrine-test-bundle/zipball/a364cfee35acb7d37698c4749f7dd34d04646535",
+ "reference": "a364cfee35acb7d37698c4749f7dd34d04646535",
+ "shasum": ""
+ },
+ "require": {
+ "doctrine/dbal": "^2.9,>=2.9.3",
+ "doctrine/doctrine-bundle": "^1.11 || ^2.0",
+ "php": "^7.1",
+ "symfony/framework-bundle": "^3.4 || ^4.3 || ^5.0"
+ },
+ "require-dev": {
+ "phpstan/phpstan": "^0.12",
+ "phpunit/phpunit": "^7.0 || ^8.0 || ^9.0",
+ "symfony/phpunit-bridge": "^4.3 || ^5.0",
+ "symfony/yaml": "^3.4 || ^4.3 || ^5.0"
+ },
+ "type": "symfony-bundle",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "7.0.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "DAMA\\DoctrineTestBundle\\": "src/DAMA/DoctrineTestBundle"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "David Maicher",
+ "email": "mail@dmaicher.de"
+ }
+ ],
+ "description": "Symfony bundle to isolate doctrine database tests and improve test performance",
+ "keywords": [
+ "doctrine",
+ "isolation",
+ "performance",
+ "symfony",
+ "tests"
+ ],
+ "time": "2020-09-04T05:50:21+00:00"
+ },
{
"name": "nikic/php-parser",
"version": "v4.10.2",
diff --git a/config/bundles.php b/config/bundles.php
index 70fae29..e222496 100644
--- a/config/bundles.php
+++ b/config/bundles.php
@@ -12,4 +12,5 @@ return [
Symfony\Bundle\MonologBundle\MonologBundle::class => ['all' => true],
Symfony\Bundle\DebugBundle\DebugBundle::class => ['dev' => true, 'test' => true],
Symfony\Bundle\MakerBundle\MakerBundle::class => ['dev' => true],
+ DAMA\DoctrineTestBundle\DAMADoctrineTestBundle::class => ['test' => true],
];
diff --git a/config/packages/test/dama_doctrine_test_bundle.yaml b/config/packages/test/dama_doctrine_test_bundle.yaml
new file mode 100644
index 0000000..80b0091
--- /dev/null
+++ b/config/packages/test/dama_doctrine_test_bundle.yaml
@@ -0,0 +1,4 @@
+dama_doctrine_test:
+ enable_static_connection: true
+ enable_static_meta_data_cache: true
+ enable_static_query_cache: true
diff --git a/migrations/Version20201020204121.php b/migrations/Version20201020204121.php
new file mode 100644
index 0000000..0965b6c
--- /dev/null
+++ b/migrations/Version20201020204121.php
@@ -0,0 +1,31 @@
+addSql('CREATE TABLE entry (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, title VARCHAR(255) NOT NULL, start DATETIME NOT NULL, "end" DATETIME NOT NULL)');
+ }
+
+ public function down(Schema $schema) : void
+ {
+ // this down() migration is auto-generated, please modify it to your needs
+ $this->addSql('DROP TABLE entry');
+ }
+}
diff --git a/phpunit.xml.dist b/phpunit.xml.dist
index 29f86af..378ddb6 100644
--- a/phpunit.xml.dist
+++ b/phpunit.xml.dist
@@ -33,4 +33,8 @@
+
+
+
+
diff --git a/src/Controller/TimeController.php b/src/Controller/TimeController.php
index 895ae0a..be05e0b 100644
--- a/src/Controller/TimeController.php
+++ b/src/Controller/TimeController.php
@@ -21,6 +21,7 @@ namespace App\Controller;
* 02110-1301, USA.
*/
+use App\Persistence\Entries;
use App\Service\ActiveEntry;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
@@ -33,15 +34,24 @@ class TimeController extends AbstractController
*/
private $activeEntry;
- public function __construct(ActiveEntry $activeEntry)
- {
+ /**
+ * @var Entries
+ */
+ private $entries;
+
+ public function __construct(
+ ActiveEntry $activeEntry,
+ Entries $entries
+ ) {
$this->activeEntry = $activeEntry;
+ $this->entries = $entries;
}
public function index(): Response
{
return $this->render('time/index.html.twig', [
'entry' => $this->activeEntry->get(),
+ 'entries' => $this->entries->get(10),
]);
}
diff --git a/src/Entity/Entry.php b/src/Entity/Entry.php
index 5a31ef9..903152c 100644
--- a/src/Entity/Entry.php
+++ b/src/Entity/Entry.php
@@ -21,22 +21,37 @@ namespace App\Entity;
* 02110-1301, USA.
*/
+use Doctrine\ORM\Mapping as ORM;
+
+/**
+ * @ORM\Entity()
+ */
class Entry
{
+ /**
+ * @ORM\Id()
+ * @ORM\GeneratedValue()
+ * @ORM\Column(type="integer")
+ */
+ private $id;
+
/**
* @var string
+ * @ORM\Column(type="string", length=255)
*/
private $title = '';
/**
* @var \DateTimeImmutable|null
+ * @ORM\Column(type="datetime")
*/
private $start;
/**
* @var \DateTimeImmutable|null
+ * @ORM\Column(type="datetime")
*/
- private $stop;
+ private $end;
/**
* @var bool
@@ -49,6 +64,20 @@ class Entry
$this->title = $title;
}
+ public static function fromPersistence(
+ string $title,
+ \DateTimeImmutable $start,
+ \DateTimeImmutable $end
+ ): self {
+ $entry = new static();
+
+ $entry->title = $title;
+ $entry->start = $start;
+ $entry->end = $end;
+
+ return $entry;
+ }
+
public function start(): void
{
$this->start = new \DateTimeImmutable();
@@ -57,7 +86,7 @@ class Entry
public function stop(): void
{
- $this->stop = new \DateTimeImmutable();
+ $this->end = new \DateTimeImmutable();
$this->running = false;
}
@@ -70,4 +99,19 @@ class Entry
{
return $this->title;
}
+
+ public function getBegan(): ?\DateTimeImmutable
+ {
+ return $this->start;
+ }
+
+ public function getEnded(): ?\DateTimeImmutable
+ {
+ return $this->end;
+ }
+
+ public function getDuration(): \DateInterval
+ {
+ return $this->start->diff($this->end);
+ }
}
diff --git a/src/Persistence/Entries.php b/src/Persistence/Entries.php
new file mode 100644
index 0000000..8c6989b
--- /dev/null
+++ b/src/Persistence/Entries.php
@@ -0,0 +1,40 @@
+
+ *
+ * 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;
+
+interface Entries
+{
+ /**
+ * Adds the given entry to the persistence.
+ * Those entries don't have identifiers and are always new.
+ */
+ public function add(Entry $entry): void;
+
+ /**
+ * Get amount of recent entries from persistence.
+ *
+ * @return array
+ */
+ public function get(int $limit): array;
+}
diff --git a/src/Persistence/LocalDatabase.php b/src/Persistence/LocalDatabase.php
new file mode 100644
index 0000000..cd61139
--- /dev/null
+++ b/src/Persistence/LocalDatabase.php
@@ -0,0 +1,71 @@
+
+ *
+ * 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 Doctrine\DBAL\Connection;
+
+class LocalDatabase implements Entries
+{
+ /**
+ * @var Connection
+ */
+ private $connection;
+
+ public function __construct(
+ Connection $connection
+ ) {
+ $this->connection = $connection;
+ }
+
+ public function add(Entry $entry): void
+ {
+ $this->connection->insert('entry', [
+ 'title' => $entry->getTitle(),
+ 'start' => $entry->getBegan()->format('U'),
+ 'end' => $entry->getEnded()->format('U'),
+ ]);
+ }
+
+ public function get(int $limit): array
+ {
+ $queryBuilder = $this->connection->createQueryBuilder();
+
+ $queryBuilder->select('*');
+ $queryBuilder->from('entry');
+ $queryBuilder->setMaxResults($limit);
+ $queryBuilder->orderBy('id', 'desc');
+
+ $entries = [];
+
+ $result = $queryBuilder->execute();
+ while ($entry = $result->fetch()) {
+ $entries[] = Entry::fromPersistence(
+ $entry['title'],
+ new \DateTimeImmutable('@' . $entry['start']),
+ new \DateTimeImmutable('@' . $entry['end'])
+ );
+ }
+
+ return $entries;
+ }
+}
diff --git a/src/Service/ActiveEntry.php b/src/Service/ActiveEntry.php
index 22bf33e..4b48a89 100644
--- a/src/Service/ActiveEntry.php
+++ b/src/Service/ActiveEntry.php
@@ -22,6 +22,7 @@ namespace App\Service;
*/
use App\Entity\Entry;
+use App\Persistence\Entries;
use Symfony\Component\HttpFoundation\Session\SessionInterface;
class ActiveEntry
@@ -31,9 +32,17 @@ class ActiveEntry
*/
private $session;
- public function __construct(SessionInterface $session)
- {
+ /**
+ * @var Entries
+ */
+ private $persistence;
+
+ public function __construct(
+ SessionInterface $session,
+ Entries $persistence
+ ) {
$this->session = $session;
+ $this->persistence = $persistence;
}
public function startNew(
@@ -51,7 +60,12 @@ class ActiveEntry
public function stopRunning(): void
{
- $this->get()->stop();
+ $entry = $this->get();
+
+ $entry->stop();
+
+ $this->persistence->add($entry);
+
$this->session->remove('runningEntry');
}
diff --git a/symfony.lock b/symfony.lock
index 995a50f..9b5322d 100644
--- a/symfony.lock
+++ b/symfony.lock
@@ -2,6 +2,18 @@
"composer/package-versions-deprecated": {
"version": "1.11.99"
},
+ "dama/doctrine-test-bundle": {
+ "version": "4.0",
+ "recipe": {
+ "repo": "github.com/symfony/recipes-contrib",
+ "branch": "master",
+ "version": "4.0",
+ "ref": "56eaa387b5e48ebcc7c95a893b47dfa1ad51449c"
+ },
+ "files": [
+ "config/packages/test/dama_doctrine_test_bundle.yaml"
+ ]
+ },
"doctrine/annotations": {
"version": "1.0",
"recipe": {
diff --git a/templates/time/entries.html.twig b/templates/time/entries.html.twig
new file mode 100644
index 0000000..7088208
--- /dev/null
+++ b/templates/time/entries.html.twig
@@ -0,0 +1,12 @@
+
+ {% for entry in entries %}
+
+
+
+
+ -
+
+
+
+ {% endfor %}
+
diff --git a/templates/time/index.html.twig b/templates/time/index.html.twig
index 88fa861..b58c0e4 100644
--- a/templates/time/index.html.twig
+++ b/templates/time/index.html.twig
@@ -4,4 +4,5 @@
Time entries
{{ include('time/timer.html.twig') }}
+ {{ include('time/entries.html.twig') }}
{% endblock %}
diff --git a/tests/Functional/Controller/TimeControllerTest.php b/tests/Functional/Controller/TimeControllerTest.php
index f5d4af4..7945de9 100644
--- a/tests/Functional/Controller/TimeControllerTest.php
+++ b/tests/Functional/Controller/TimeControllerTest.php
@@ -25,6 +25,7 @@ use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
/**
* @covers App\Controller\TimeController
+ * @covers App\Persistence\LocalDatabase
* @uses App\Entity\Entry
* @uses App\Service\ActiveEntry
*/
@@ -33,7 +34,7 @@ class TimeControllerTest extends WebTestCase
/**
* @test
*/
- public function rootCanBeOpened()
+ public function rootCanBeOpened(): void
{
$client = static::createClient();
$crawler = $client->request('GET', '/');
@@ -45,7 +46,7 @@ class TimeControllerTest extends WebTestCase
/**
* @test
*/
- public function newEntryCanBeStarted()
+ public function newEntryCanBeStarted(): void
{
$client = static::createClient();
@@ -62,7 +63,7 @@ class TimeControllerTest extends WebTestCase
/**
* @test
*/
- public function runningEntryCanBeStopped()
+ public function runningEntryCanBeStopped(): void
{
$client = static::createClient();
@@ -83,7 +84,7 @@ class TimeControllerTest extends WebTestCase
/**
* @test
*/
- public function noneRunningEntryCanBeStopped()
+ public function noneRunningEntryCanBeStopped(): void
{
$client = static::createClient();
@@ -95,4 +96,26 @@ class TimeControllerTest extends WebTestCase
$this->assertInputValueSame('title', '');
$this->assertSelectorTextSame('button', 'Start');
}
+
+ /**
+ * @test
+ */
+ public function previousEntriesAreShownOnIndex(): void
+ {
+ $client = static::createClient();
+
+ $client->request('GET', '/');
+ $client->submitForm('Start', ['title' => 'Test Entry 1']);
+ $client->request('GET', '/');
+ $client->submitForm('Stop');
+
+ $client->request('GET', '/');
+ $client->submitForm('Start', ['title' => 'Test Entry 2']);
+ $client->request('GET', '/');
+ $client->submitForm('Stop');
+
+ $crawler = $client->request('GET', '/');
+ $this->assertSelectorTextContains('article:nth-of-type(1)', 'Test Entry 2');
+ $this->assertSelectorTextContains('article:nth-of-type(2)', 'Test Entry 1');
+ }
}
diff --git a/tests/Unit/Entity/EntryTest.php b/tests/Unit/Entity/EntryTest.php
index cca49ff..8045c18 100644
--- a/tests/Unit/Entity/EntryTest.php
+++ b/tests/Unit/Entity/EntryTest.php
@@ -32,7 +32,7 @@ class EntryTest extends TestCase
/**
* @test
*/
- public function canBeCreated()
+ public function canBeCreatedViaConstructor(): void
{
$subject = new Entry();
@@ -42,7 +42,25 @@ class EntryTest extends TestCase
/**
* @test
*/
- public function canBeCreatedWithTitle()
+ public function canBeCreatedFromPersistence(): void
+ {
+ $subject = Entry::fromPersistence(
+ 'Example Title',
+ new \DateTimeImmutable('2020-10-29 17:10:05'),
+ new \DateTimeImmutable('2020-10-29 18:12:15')
+ );
+
+ static::assertInstanceOf(Entry::class, $subject);
+ static::assertSame('Example Title', $subject->getTitle());
+ static::assertSame('2020-10-29 17:10:05', $subject->getBegan()->format('Y-m-d H:i:s'));
+ static::assertSame('2020-10-29 18:12:15', $subject->getEnded()->format('Y-m-d H:i:s'));
+ static::assertSame('01:02:10', $subject->getDuration()->format('%r%H:%I:%S'));
+ }
+
+ /**
+ * @test
+ */
+ public function canBeCreatedWithTitle(): void
{
$subject = new Entry('Example title');
@@ -52,7 +70,7 @@ class EntryTest extends TestCase
/**
* @test
*/
- public function returnsProvidedTitle()
+ public function returnsProvidedTitle(): void
{
$subject = new Entry('Example title');
@@ -62,7 +80,7 @@ class EntryTest extends TestCase
/**
* @test
*/
- public function canBeStarted()
+ public function canBeStarted(): void
{
$subject = new Entry();
@@ -74,7 +92,7 @@ class EntryTest extends TestCase
/**
* @test
*/
- public function canBeStopped()
+ public function canBeStopped(): void
{
$subject = new Entry();
@@ -87,7 +105,7 @@ class EntryTest extends TestCase
/**
* @test
*/
- public function canBeStoppedEvenIfNotRunning()
+ public function canBeStoppedEvenIfNotRunning(): void
{
$subject = new Entry();
@@ -95,4 +113,59 @@ class EntryTest extends TestCase
static::assertFalse($subject->isRunning());
}
+
+ /**
+ * @test
+ */
+ public function returnsNullForBeganBeforeStarted(): void
+ {
+ $subject = new Entry();
+
+ static::assertNull($subject->getBegan());
+ }
+
+ /**
+ * @test
+ */
+ public function returnsBeganAfterStarted(): void
+ {
+ $subject = new Entry();
+ $subject->start();
+
+ static::assertInstanceOf(\DateTimeImmutable::class, $subject->getBegan());
+ }
+
+ /**
+ * @test
+ */
+ public function returnsNullForEndedBeforeStarted(): void
+ {
+ $subject = new Entry();
+
+ static::assertNull($subject->getEnded());
+ }
+
+ /**
+ * @test
+ */
+ public function returnsEndedAfterStoped(): void
+ {
+ $subject = new Entry();
+ $subject->start();
+ $subject->stop();
+
+ static::assertInstanceOf(\DateTimeImmutable::class, $subject->getEnded());
+ }
+
+ /**
+ * @test
+ */
+ public function returnsDurationAfterStopped(): void
+ {
+ $subject = new Entry();
+ $subject->start();
+ $subject->stop();
+
+ static::assertInstanceOf(\DateInterval::class, $subject->getDuration());
+ }
}
diff --git a/tests/Unit/Service/ActiveEntryTest.php b/tests/Unit/Service/ActiveEntryTest.php
index e024407..6bdebb9 100644
--- a/tests/Unit/Service/ActiveEntryTest.php
+++ b/tests/Unit/Service/ActiveEntryTest.php
@@ -22,12 +22,14 @@ namespace App\Tests\Service;
*/
use App\Entity\Entry;
+use App\Persistence\Entries;
use App\Service\ActiveEntry;
use PHPUnit\Framework\TestCase;
use Prophecy\Argument;
use Symfony\Component\HttpFoundation\Session\SessionInterface;
+
/**
* @covers App\Service\ActiveEntry
*/
@@ -36,11 +38,13 @@ class ActiveEntryTest extends TestCase
/**
* @test
*/
- public function canBeCreated()
+ public function canBeCreated(): void
{
$session = $this->prophesize(SessionInterface::class);
+ $persistence = $this->prophesize(Entries::class);
$subject = new ActiveEntry(
- $session->reveal()
+ $session->reveal(),
+ $persistence->reveal()
);
static::assertInstanceOf(ActiveEntry::class, $subject);
@@ -49,11 +53,13 @@ class ActiveEntryTest extends TestCase
/**
* @test
*/
- public function returnsEmptyEntryAsDefault()
+ public function returnsEmptyEntryAsDefault(): void
{
$session = $this->prophesize(SessionInterface::class);
+ $persistence = $this->prophesize(Entries::class);
$subject = new ActiveEntry(
- $session->reveal()
+ $session->reveal(),
+ $persistence->reveal()
);
$entry = $subject->get();
@@ -66,11 +72,13 @@ class ActiveEntryTest extends TestCase
/**
* @test
*/
- public function canStartANewEntry()
+ public function canStartANewEntry(): void
{
$session = $this->prophesize(SessionInterface::class);
+ $persistence = $this->prophesize(Entries::class);
$subject = new ActiveEntry(
- $session->reveal()
+ $session->reveal(),
+ $persistence->reveal()
);
$subject->startNew('Example title');
@@ -85,12 +93,14 @@ class ActiveEntryTest extends TestCase
/**
* @test
*/
- public function returnsStartedEntry()
+ public function returnsStartedEntry(): void
{
$session = $this->prophesize(SessionInterface::class);
+ $persistence = $this->prophesize(Entries::class);
$entry = $this->prophesize(Entry::class);
$subject = new ActiveEntry(
- $session->reveal()
+ $session->reveal(),
+ $persistence->reveal()
);
$session->set('runningEntry', Argument::type(Entry::class))->shouldBeCalled();
@@ -104,12 +114,14 @@ class ActiveEntryTest extends TestCase
/**
* @test
*/
- public function stopsRunningEntry()
+ public function stopsRunningEntry(): void
{
$session = $this->prophesize(SessionInterface::class);
+ $persistence = $this->prophesize(Entries::class);
$entry = $this->prophesize(Entry::class);
$subject = new ActiveEntry(
- $session->reveal()
+ $session->reveal(),
+ $persistence->reveal()
);
$session->set('runningEntry', Argument::type(Entry::class))->shouldBeCalled();
@@ -119,5 +131,6 @@ class ActiveEntryTest extends TestCase
$subject->stopRunning();
$session->remove('runningEntry')->shouldBeCalled();
+ $persistence->add($entry->reveal())->shouldBeCalled();
}
}