RelationshipFiller::__construct()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 2
nc 1
nop 1
dl 0
loc 4
ccs 3
cts 3
cp 1
crap 1
rs 10
c 0
b 0
f 0
1
<?php
2
3
namespace Simply\Database;
4
5
use Simply\Database\Connection\Connection;
6
use Simply\Database\Exception\InvalidRelationshipException;
7
8
/**
9
 * Facilitates filling relationships for multiple records at the same time.
10
 * @author Riikka Kalliomäki <[email protected]>
11
 * @copyright Copyright (c) 2018 Riikka Kalliomäki
12
 * @license http://opensource.org/licenses/mit-license.php MIT License
13
 */
14
class RelationshipFiller
15
{
16
    /** @var Connection Connection used to load data for the records from the database */
17
    private $connection;
18
19
    /** @var Record[] Records that are cached during the filling operation */
20
    private $cache;
21
22
    /**
23
     * RelationshipFiller constructor.
24
     * @param Connection $connection Connection used to load data for the records from the database
25
     */
26 6
    public function __construct(Connection $connection)
27
    {
28 6
        $this->connection = $connection;
29 6
        $this->cache = [];
30 6
    }
31
32
    /**
33
     * Fills the given relationships for all records of a single type.
34
     * @param Record[] $records List of records that must all belong to the same schema
35
     * @param string[] $relationships List of relationships to fill with deeper levels separated by periods
36
     */
37 6
    public function fill(array $records, array $relationships): void
38
    {
39 6
        if (empty($records)) {
40 1
            return;
41
        }
42
43 5
        $this->cache = [];
44 5
        $schema = reset($records)->getSchema();
45
46 5
        foreach ($records as $record) {
47 5
            if ($record->getSchema() !== $schema) {
48 1
                throw new \InvalidArgumentException('The provided list of records did not share the same schema');
49
            }
50
51 5
            foreach ($record->getAllReferencedRecords() as $mappedRecord) {
52 5
                $this->cacheRecord($this->getSchemaId($mappedRecord->getSchema()), $mappedRecord);
53
            }
54
        }
55
56 3
        $this->fillRelationships($records, $relationships);
57 2
    }
58
59
    /**
60
     * Fills the relationships for the records.
61
     * @param Record[] $records List of records to fill
62
     * @param string[] $relationships The relationships to fill for the records
63
     */
64 3
    private function fillRelationships(array $records, array $relationships): void
65
    {
66 3
        $schema = reset($records)->getSchema();
67
68 3
        foreach ($this->parseChildRelationships($relationships) as $name => $childRelationships) {
69 3
            $relationship = $schema->getRelationship($name);
70 3
            $keys = $relationship->getFields();
71 3
            $fields = $relationship->getReferencedFields();
72 3
            $parent = $relationship->getReferencedSchema();
73 3
            $schemaId = $this->getSchemaId($parent);
74
75 3
            if (\count($fields) > 1) {
76 1
                throw new InvalidRelationshipException('Composite foreign keys are not supported by batch fill');
77
            }
78
79 2
            $isPrimaryReference = $fields === $parent->getPrimaryKey();
80 2
            $key = array_pop($keys);
81 2
            $field = array_pop($fields);
82 2
            $fillRecords = [];
83 2
            $options = [];
84 2
            $filled = [];
85
86 2
            foreach ($records as $record) {
87 2
                $value = $record[$key];
88
89 2
                if ($value === null) {
90 1
                    $record->setReferencedRecords($name, []);
91 2
                } elseif ($record->hasReferencedRecords($name)) {
92 1
                    $filled[$value] = $record->getReferencedRecords($name);
93 2
                } elseif ($isPrimaryReference && isset($this->cache[$schemaId][$value])) {
94 2
                    $filled[$value] = [$this->cache[$schemaId][$value]];
95 2
                    $fillRecords[] = $record;
96
                } else {
97 1
                    $options[$value] = true;
98 2
                    $fillRecords[] = $record;
99
                }
100
            }
101
102 2
            $loaded = empty($filled) ? [] : array_merge(... array_values($filled));
103 2
            $options = array_keys(array_diff_key($options, $filled));
104
105 2
            if ($options !== []) {
106 1
                $result = $this->connection->select($parent->getFields(), $parent->getTable(), [$field => $options]);
107 1
                $result->setFetchMode(\PDO::FETCH_ASSOC);
108
109 1
                foreach ($result as $row) {
110 1
                    $loaded[] = $this->getCachedRecord($schemaId, $parent, $row);
111
                }
112
            }
113
114 2
            $relationship->fillRelationship($fillRecords, $loaded);
115
116 2
            if ($loaded && $childRelationships) {
117 2
                $this->fillRelationships($loaded, $childRelationships);
118
            }
119
        }
120 2
    }
121
122
    /**
123
     * Parses the list of relationships into associative array of relationships and their children.
124
     * @param string[] $relationships List of relationships to parse
125
     * @return array[] Associative array of top level relationships and their children
126
     */
127 3
    private function parseChildRelationships(array $relationships): array
128
    {
129 3
        $childRelationships = [];
130
131 3
        foreach ($relationships as $relationship) {
132 3
            $parts = explode('.', $relationship, 2);
133
134 3
            if (!isset($childRelationships[$parts[0]])) {
135 3
                $childRelationships[$parts[0]] = [];
136
            }
137
138 3
            if (isset($parts[1])) {
139 3
                $childRelationships[$parts[0]][] = $parts[1];
140
            }
141
        }
142
143 3
        return $childRelationships;
144
    }
145
146
    /**
147
     * Caches the given record for the schema with given id.
148
     * @param int $schemaId Cache id of of the schema
149
     * @param Record $record The record to cache
150
     */
151 5
    private function cacheRecord(int $schemaId, Record $record): void
152
    {
153 5
        $recordId = implode('-', $record->getPrimaryKey());
154
155 5
        if (isset($this->cache[$schemaId][$recordId]) && $this->cache[$schemaId][$recordId] !== $record) {
156 1
            throw new \RuntimeException('Duplicated record detected when filling relationships for records');
157
        }
158
159 5
        $this->cache[$schemaId][$recordId] = $record;
160 5
    }
161
162
    /**
163
     * Returns a new or cached record for the given schema and schema id based on the database row.
164
     * @param int $schemaId Cache id of of the schema
165
     * @param Schema $schema The schema for the record
166
     * @param array $row The database row
167
     * @return Record A new or cached record based on the database row
168
     */
169 1
    private function getCachedRecord(int $schemaId, Schema $schema, array $row): Record
170
    {
171 1
        $primaryKey = [];
172
173 1
        foreach ($schema->getPrimaryKey() as $key) {
174 1
            $primaryKey[] = $row[$key];
175
        }
176
177 1
        $recordId = implode('-', $primaryKey);
178
179 1
        if (isset($this->cache[$schemaId][$recordId])) {
180 1
            return $this->cache[$schemaId][$recordId];
181
        }
182
183 1
        $record = $schema->createRecordFromValues($row);
184 1
        $this->cache[$schemaId][$recordId] = $record;
185 1
        return $record;
186
    }
187
188
    /**
189
     * Returns cache id for the given schema.
190
     * @param Schema $schema The schema to use
191
     * @return int The cache id for the given schema
192
     */
193 5
    private function getSchemaId(Schema $schema): int
194
    {
195 5
        return spl_object_id($schema);
196
    }
197
}
198