Add persistence (with local db implementation)

This commit is contained in:
Daniel Siepmann 2020-10-20 22:52:46 +02:00
parent 6abb14b3bd
commit 1a0bbf1099
Signed by: Daniel Siepmann
GPG key ID: 33D6629915560EF4
19 changed files with 441 additions and 29 deletions

2
.env
View file

@ -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 ###

View file

@ -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

View file

@ -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",

59
composer.lock generated
View file

@ -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",

View file

@ -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],
];

View file

@ -0,0 +1,4 @@
dama_doctrine_test:
enable_static_connection: true
enable_static_meta_data_cache: true
enable_static_query_cache: true

View file

@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20201020204121 extends AbstractMigration
{
public function getDescription() : string
{
return '';
}
public function up(Schema $schema) : void
{
// this up() migration is auto-generated, please modify it to your needs
$this->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');
}
}

View file

@ -33,4 +33,8 @@
<listeners>
<listener class="Symfony\Bridge\PhpUnit\SymfonyTestsListener" />
</listeners>
<extensions>
<extension class="DAMA\DoctrineTestBundle\PHPUnit\PHPUnitExtension"/>
</extensions>
</phpunit>

View file

@ -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),
]);
}

View file

@ -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);
}
}

View file

@ -0,0 +1,40 @@
<?php
namespace App\Persistence;
/*
* Copyright (C) 2020 Daniel Siepmann <coding@daniel-siepmann.de>
*
* 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<Entry>
*/
public function get(int $limit): array;
}

View file

@ -0,0 +1,71 @@
<?php
namespace App\Persistence;
/*
* Copyright (C) 2020 Daniel Siepmann <coding@daniel-siepmann.de>
*
* 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;
}
}

View file

@ -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');
}

View file

@ -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": {

View file

@ -0,0 +1,12 @@
<section>
{% for entry in entries %}
<article>
<header>{{ entry.title }}</header>
<time>{{ entry.began|date('d.m.Y') }}</time>
<time>{{ entry.ended|date('H:i:s') }}</time>
-
<time>{{ entry.ended|date('H:i:s') }}</time>
<!-- // TODO: Add duration, e.g. by using a view model -->
</article>
{% endfor %}
</section>

View file

@ -4,4 +4,5 @@
<h1>Time entries</h1>
{{ include('time/timer.html.twig') }}
{{ include('time/entries.html.twig') }}
{% endblock %}

View file

@ -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');
}
}

View file

@ -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());
}
}

View file

@ -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();
}
}