Skip to content

Commit ccbe5ae

Browse files
committed
Add first tests
1 parent db928fc commit ccbe5ae

15 files changed

+485
-50
lines changed

composer.json

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@
1717
],
1818
"require": {
1919
"php": "^7.2",
20-
"psr/container": "^1.0"
20+
"psr/container": "^1.0",
21+
"ext-pdo": "*"
2122
},
2223
"require-dev": {
2324
"simply/container": "^0.2.1",
@@ -32,7 +33,8 @@
3233
},
3334
"autoload-dev": {
3435
"psr-4": {
35-
"Simply\\Database\\": "tests/tests/"
36+
"Simply\\Database\\": "tests/tests/",
37+
"Simply\\Database\\Test\\": "tests/helpers/"
3638
}
3739
}
3840
}

phpunit.xml

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<phpunit bootstrap="tests/bootstrap.php">
3+
<testsuites>
4+
<testsuite name="Default">
5+
<directory suffix="Test.php">tests/tests/</directory>
6+
</testsuite>
7+
</testsuites>
8+
<filter>
9+
<whitelist processUncoveredFilesFromWhitelist="true">
10+
<directory suffix=".php">src/</directory>
11+
</whitelist>
12+
</filter>
13+
<php>
14+
<env name="phpunit_mysql_hostname" value="localhost" />
15+
<env name="phpunit_mysql_database" value="phpunit_tests" />
16+
<env name="phpunit_mysql_username" value="root" />
17+
<env name="phpunit_mysql_password" value="toor" />
18+
</php>
19+
</phpunit>

src/Connection/Connection.php

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ interface Connection
1414
public const ORDER_DESCENDING = 2;
1515

1616
public function getConnection(): \PDO;
17-
public function getLastInsertId();
1817
public function insert(string $table, array $values, string & $primaryKey = null): \PDOStatement;
1918
public function select(array $fields, string $table, array $where, array $orderBy = [], int $limit = null): \PDOStatement;
2019
public function update(string $table, array $values, array $where): \PDOStatement;

src/Connection/MySqlConnection.php

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,22 +10,21 @@
1010
*/
1111
class MySqlConnection implements Connection
1212
{
13-
private $initializer;
13+
private $lazyLoader;
1414
private $pdo;
1515

16-
public function __construct($hostname, $database, $username, $password)
16+
public function __construct(string $hostname, string $database, string $username, string $password)
1717
{
18-
$this->lastId = false;
19-
$this->initializer = function () use ($hostname, $database, $username, $password) {
18+
$this->lazyLoader = function () use ($hostname, $database, $username, $password): \PDO {
2019
return new \PDO($this->getDataSource($hostname, $database), $username, $password, [
2120
\PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION,
2221
\PDO::ATTR_EMULATE_PREPARES => false,
23-
\PDO::MYSQL_ATTR_INIT_COMMAND => sprintf("SET timezone = '%s'", date('P')),
22+
\PDO::MYSQL_ATTR_INIT_COMMAND => sprintf("SET time_zone = '%s'", date('P')),
2423
]);
2524
};
2625
}
2726

28-
private function getDataSource($hostname, $database)
27+
private function getDataSource(string $hostname, string $database): string
2928
{
3029
if (strncmp($hostname, '/', 1) === 0) {
3130
return sprintf('mysql:unix_socket=%s;dbname=%s;charset=utf8mb4', $hostname, $database);
@@ -43,7 +42,7 @@ private function getDataSource($hostname, $database)
4342
public function getConnection(): \PDO
4443
{
4544
if (!$this->pdo) {
46-
$this->pdo = ($this->initializer)();
45+
$this->pdo = ($this->lazyLoader)();
4746
}
4847

4948
return $this->pdo;
@@ -165,7 +164,7 @@ private function formatClause(string $field, $value, array & $parameters): strin
165164

166165
private function formatParameters(array $values, array & $parameters): string
167166
{
168-
array_push($parameters, ... array_values($parameters));
167+
array_push($parameters, ... array_values($values));
169168
return sprintf('(%s)', implode(', ', array_fill(0, \count($values), '?')));
170169
}
171170

src/Record.php

Lines changed: 45 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -10,24 +10,47 @@
1010
*/
1111
class Record implements \ArrayAccess
1212
{
13+
public const STATE_INSERT = 1;
14+
public const STATE_UPDATE = 2;
15+
public const STATE_DELETE = 3;
16+
1317
private $schema;
1418

1519
private $values;
1620

17-
private $new;
21+
private $changed;
22+
23+
private $state;
1824

1925
private $relations;
2026

2127
public function __construct(Schema $schema)
2228
{
2329
$this->schema = $schema;
2430
$this->values = array_fill_keys($schema->getFields(), null);
25-
$this->new = true;
31+
$this->state = self::STATE_INSERT;
32+
$this->changed = [];
33+
}
34+
35+
public function getPrimaryKeys(): array
36+
{
37+
return array_intersect_key($this->values, array_flip($this->schema->getPrimaryKeys()));
2638
}
2739

2840
public function isNew(): bool
2941
{
30-
return $this->new;
42+
return $this->state === self::STATE_INSERT;
43+
}
44+
45+
public function isDeleted(): bool
46+
{
47+
return $this->state === self::STATE_DELETE;
48+
}
49+
50+
public function updateState(int $state): void
51+
{
52+
$this->state = $state === self::STATE_DELETE ? self::STATE_DELETE : self::STATE_UPDATE;
53+
$this->changed = [];
3154
}
3255

3356
public function getSchema(): Schema
@@ -40,7 +63,7 @@ public function getModel(): Model
4063
return $this->schema->getModel($this);
4164
}
4265

43-
public function getRelation(string $name): array
66+
public function getReference(string $name): array
4467
{
4568
if (!isset($this->relations[$name])) {
4669
throw new \RuntimeException("Cannot access relation '$name' that has not been provided");
@@ -49,28 +72,26 @@ public function getRelation(string $name): array
4972
return $this->relations[$name];
5073
}
5174

52-
public function setRelation(string $name, array $records): void
75+
public function fillReference(string $name, array $records): void
5376
{
54-
$relation = $this->getSchema()->getRelation($name);
77+
$relation = $this->getSchema()->getReference($name);
5578

5679
foreach ($records as $record) {
5780
if ($this->isRelated($relation, $record)) {
5881
throw new \InvalidArgumentException('The provided records are not related to this record');
5982
}
6083
}
6184

62-
if (\count($records) > 1 && $relation->isSingleRelation()) {
63-
throw new \InvalidArgumentException('The relation cannot reference more than a single record');
85+
if (\count($records) > 1 && $relation->isSingleRelationship()) {
86+
throw new \InvalidArgumentException('The relationship cannot reference more than a single record');
6487
}
6588

6689
$this->relations[$name] = array_values($records);
6790
}
6891

69-
private function isRelated(Relation $relation, Record $record): bool
92+
private function isRelated(Reference $relation, Record $record): bool
7093
{
71-
$schema = $relation->getReferencedSchema();
72-
73-
if ($schema !== $record->getSchema() && \get_class($schema) !== \get_class($record->getSchema())) {
94+
if ($relation->getReferencedSchema() !== $record->getSchema()) {
7495
return false;
7596
}
7697

@@ -93,14 +114,20 @@ public function setDatabaseValues(array $row)
93114
}
94115

95116
$this->values = $row;
96-
$this->new = false;
117+
$this->state = self::STATE_UPDATE;
118+
$this->changed = [];
97119
}
98120

99121
public function getDatabaseValues(): array
100122
{
101123
return $this->values;
102124
}
103125

126+
public function getChangedFields(): array
127+
{
128+
return array_keys($this->changed);
129+
}
130+
104131
public function offsetExists($offset)
105132
{
106133
return $this->offsetGet($offset) !== null;
@@ -121,7 +148,12 @@ public function offsetSet($offset, $value)
121148
throw new \InvalidArgumentException("Invalid record field '$offset'");
122149
}
123150

151+
if ($this->state === self::STATE_UPDATE && \in_array($offset, $this->schema->getPrimaryKeys(), true)) {
152+
throw new \RuntimeException('Cannot change values of primary keys for saved records');
153+
}
154+
124155
$this->values[$offset] = $value;
156+
$this->changed[$offset] = true;
125157
}
126158

127159
public function offsetUnset($offset)

src/Relation.php renamed to src/Reference.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
* @copyright Copyright (c) 2018 Riikka Kalliomäki
99
* @license http://opensource.org/licenses/mit-license.php MIT License
1010
*/
11-
class Relation
11+
class Reference
1212
{
1313
private $schema;
1414
private $fields;
@@ -48,7 +48,7 @@ public function matchValues($value, $referencedValue): bool
4848
return (string) $value === (string) $referencedValue;
4949
}
5050

51-
public function isSingleRelation(): bool
51+
public function isSingleRelationship(): bool
5252
{
5353
return array_diff($this->referencedSchema->getPrimaryKeys(), $this->referencedFields) === [];
5454
}

src/ReferenceFiller.php

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
<?php
2+
3+
namespace Simply\Database;
4+
5+
use Simply\Database\Connection\Connection;
6+
7+
/**
8+
* ReferenceFiller.
9+
* @author Riikka Kalliomäki <[email protected]>
10+
* @copyright Copyright (c) 2018 Riikka Kalliomäki
11+
* @license http://opensource.org/licenses/mit-license.php MIT License
12+
*/
13+
class ReferenceFiller
14+
{
15+
private $connection;
16+
private $cache;
17+
18+
public function __construct(Connection $connection)
19+
{
20+
$this->connection = $connection;
21+
}
22+
23+
/**
24+
* @param Record[] $records
25+
* @param string[] $references
26+
*/
27+
public function fill(array $records, array $references): void
28+
{
29+
if (empty($records)) {
30+
return;
31+
}
32+
33+
$this->cache = [];
34+
$schema = reset($records)->getSchema();
35+
$schemaId = $this->getSchemaId($schema);
36+
37+
foreach ($records as $record) {
38+
if ($record->getSchema() !== $schema) {
39+
throw new \InvalidArgumentException('The provided list of records did not share the same schema');
40+
}
41+
42+
$recordId = $this->getRecordId($schema, $record->getDatabaseValues());
43+
$this->cache[$schemaId][$recordId] = $record;
44+
}
45+
46+
$this->fillReferences($records, $references);
47+
}
48+
49+
/**
50+
* @param Record[] $records
51+
* @param string[] $references
52+
*/
53+
private function fillReferences(array $records, array $references): void
54+
{
55+
$schema = reset($records)->getSchema();
56+
57+
foreach ($this->parseReferences($references) as $name => $childReferences) {
58+
$reference = $schema->getReference($name);
59+
$keys = $reference->getFields();
60+
$fields = $reference->getReferencedFields();
61+
$parent = $reference->getReferencedSchema();
62+
$schemaId = $this->getSchemaId($parent);
63+
64+
if (\count($fields) > 1) {
65+
throw new \RuntimeException('Filling references for composite foreign keys is not supported');
66+
}
67+
68+
$key = array_pop($keys);
69+
$field = array_pop($fields);
70+
$options = [];
71+
72+
foreach ($records as $record) {
73+
$options[] = $record[$key];
74+
}
75+
76+
$result = $this->connection->select($parent->getFields(), $parent->getTable(), [$field => $options]);
77+
$result->setFetchMode(\PDO::FETCH_ASSOC);
78+
$sorted = [];
79+
80+
foreach ($result as $row) {
81+
$record = $this->getCachedRecord($schemaId, $parent, $row);
82+
$sorted[$record[$field]][] = $record;
83+
}
84+
85+
foreach ($records as $record) {
86+
$record->fillReference($name, $sorted[$record[$key]] ?? []);
87+
}
88+
89+
if ($sorted && $childReferences) {
90+
$this->fillReferences(array_merge(... $sorted), $childReferences);
91+
}
92+
}
93+
}
94+
95+
private function parseReferences(array $references): array
96+
{
97+
$subReferences = [];
98+
99+
foreach ($references as $reference) {
100+
$parts = explode('.', $reference, 2);
101+
102+
if (!isset($subReferences[$parts[0]])) {
103+
$subReferences[$parts[0]] = [];
104+
}
105+
106+
if (isset($parts[1])) {
107+
$subReferences[$parts[0]][] = $parts[1];
108+
}
109+
}
110+
111+
return $subReferences;
112+
}
113+
114+
private function getCachedRecord(string $schemaId, Schema $schema, array $row): Record
115+
{
116+
$recordId = $this->getRecordId($schema, $row);
117+
118+
if (isset($this->cache[$schemaId][$recordId])) {
119+
return $this->cache[$schemaId][$recordId];
120+
}
121+
122+
$record = $schema->getRecord($row);
123+
$this->cache[$schemaId][$recordId] = $record;
124+
return $record;
125+
}
126+
127+
private function getSchemaId(Schema $schema): string
128+
{
129+
return spl_object_hash($schema);
130+
}
131+
132+
private function getRecordId(Schema $schema, array $row)
133+
{
134+
$values = [];
135+
136+
foreach ($schema->getPrimaryKeys() as $key) {
137+
if (!isset($row[$key])) {
138+
throw new \RuntimeException('Cannot determine cache id for record');
139+
}
140+
141+
$values[] = $row[$key];
142+
}
143+
144+
return implode('-', $values);
145+
}
146+
}

0 commit comments

Comments
 (0)