Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
105 / 105
100.00% covered (success)
100.00%
23 / 23
CRAP
100.00% covered (success)
100.00%
1 / 1
EntityManager
100.00% covered (success)
100.00%
105 / 105
100.00% covered (success)
100.00%
23 / 23
47
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setUseEntityHashName
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 addColumn
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 tableNameByClass
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
3
 simpleClassName
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 tableNames
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 tableName
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 tableColumns
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 primaryKey
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
6
 primaryKeyValue
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
3
 primaryKeyCondition
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
3
 primaryKeyConditionParams
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
4
 isPrimaryKeyAutoIncrement
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 safeTableName
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 allTableColumns
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 insert
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 update
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 findById
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
1
 deleteById
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 deleteByIds
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 save
100.00% covered (success)
100.00%
21 / 21
100.00% covered (success)
100.00%
1 / 1
4
 setByDataArray
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
3
 fetchDataArray
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
1<?php
2
3namespace Dynart\Micro\Entities;
4
5use Dynart\Micro\ConfigInterface;
6use Dynart\Micro\EventServiceInterface;
7use Dynart\Micro\Entities\Attribute\Column;
8
9class EntityManager {
10
11    protected array $tableColumns = [];
12    protected array $tableNames = [];
13    protected array $primaryKeys = [];
14    protected string $tableNamePrefix = '';
15    protected bool $useEntityHashName = false;
16
17    public function __construct(
18        protected ConfigInterface $config,
19        protected Database $db,
20        protected EventServiceInterface $events,
21    ) {
22        $this->tableNamePrefix = $db->configValue('table_prefix');
23    }
24
25    public function setUseEntityHashName(bool $value): void {
26        $this->useEntityHashName = $value;
27    }
28
29    public function addColumn(string $className, string $columnName, Column $column): void {
30        if (!array_key_exists($className, $this->tableNames)) {
31            $this->tableNames[$className] = $this->tableNameByClass($className);
32            $this->tableColumns[$className] = [];
33        }
34        $this->tableColumns[$className][$columnName] = $column;
35    }
36
37    public function tableNameByClass(string $className, bool $withPrefix = true): string {
38        $simpleClassName = $this->simpleClassName($className);
39        if ($this->useEntityHashName) {
40            return '#'.$simpleClassName;
41        }
42        return ($withPrefix ? $this->tableNamePrefix : '').strtolower($simpleClassName);
43    }
44
45    protected function simpleClassName(string $fullClassName): string {
46        return substr(strrchr($fullClassName, '\\'), 1);
47    }
48
49    public function tableNames(): array {
50        return $this->tableNames;
51    }
52
53    public function tableName(string $className): string {
54        if (!array_key_exists($className, $this->tableNames)) {
55            throw new EntityManagerException("Table definition doesn't exist for ".$className);
56        }
57        return $this->tableNames[$className];
58    }
59
60    public function tableColumns(string $className): array {
61        if (!array_key_exists($className, $this->tableColumns)) {
62            throw new EntityManagerException("Table definition doesn't exist for ".$className);
63        }
64        return $this->tableColumns[$className];
65    }
66
67    public function primaryKey(string $className): string|array|null {
68        if (array_key_exists($className, $this->primaryKeys)) {
69            return $this->primaryKeys[$className];
70        }
71        $primaryKey = [];
72        foreach ($this->tableColumns($className) as $columnName => $column) {
73            if ($column->primaryKey) {
74                $primaryKey[] = $columnName;
75            }
76        }
77        $result = empty($primaryKey) ? null : (count($primaryKey) > 1 ? $primaryKey : $primaryKey[0]);
78        $this->primaryKeys[$className] = $result;
79        return $result;
80    }
81
82    public function primaryKeyValue(string $className, array $data): mixed {
83        $primaryKey = $this->primaryKey($className);
84        if (is_array($primaryKey)) {
85            $result = [];
86            foreach ($primaryKey as $pk) {
87                $result[] = $data[$pk];
88            }
89            return $result;
90        } else {
91            return $data[$primaryKey];
92        }
93    }
94
95    public function primaryKeyCondition(string $className): string {
96        $primaryKey = $this->primaryKey($className);
97        if (is_array($primaryKey)) {
98            $conditions = [];
99            foreach ($primaryKey as $i => $pk) {
100                $conditions[] = $this->db->escapeName($pk).' = :pkValue'.$i;
101            }
102            return join(' and ', $conditions);
103        } else {
104            return $this->db->escapeName($primaryKey).' = :pkValue';
105        }
106    }
107
108    public function primaryKeyConditionParams(string $className, mixed $pkValue): array {
109        $result = [];
110        $primaryKey = $this->primaryKey($className);
111        if (is_array($primaryKey) && is_array($pkValue)) {
112            foreach ($pkValue as $i => $v) {
113                $result[':pkValue'.$i] = $v;
114            }
115        } else {
116            $result[':pkValue'] = $pkValue;
117        }
118        return $result;
119    }
120
121    public function isPrimaryKeyAutoIncrement(string $className): bool {
122        $pkName = $this->primaryKey($className);
123        if (is_array($pkName)) { // multi-column primary keys can't be auto incremented
124            return false;
125        }
126        $pkColumn = $this->tableColumns[$className][$pkName];
127        return $pkColumn->autoIncrement;
128    }
129
130    public function safeTableName(string $className, bool $withPrefix = true): string {
131        return $this->db->escapeName($this->tableNameByClass($className, $withPrefix));
132    }
133
134    public function allTableColumns(): array {
135        return $this->tableColumns;
136    }
137
138    public function insert(string $className, array $data): string|false {
139        $this->db->insert($this->tableName($className), $data);
140        return $this->db->lastInsertId();
141    }
142
143    public function update(string $className, array $data, string $condition = '', array $conditionParams = []): void {
144        $this->db->update($this->tableName($className), $data, $condition, $conditionParams);
145    }
146
147    public function findById(string $className, mixed $id): Entity {
148        $condition = $this->primaryKeyCondition($className);
149        $safeTableName = $this->safeTableName($className);
150        $sql = "select * from $safeTableName where $condition";
151        $params = $this->primaryKeyConditionParams($className, $id);
152        $result = $this->db->fetch($sql, $params, $className);
153        $result->setNew(false);
154        $result->takeSnapshot($this->fetchDataArray($result));
155        return $result;
156    }
157
158    public function deleteById(string $className, mixed $id): void {
159        $sql = "delete from {$this->safeTableName($className)} where {$this->primaryKeyCondition($className)} limit 1";
160        $this->db->query($sql, $this->primaryKeyConditionParams($className, $id));
161    }
162
163    public function deleteByIds(string $className, array $ids): void {
164        $safePk = $this->db->escapeName($this->primaryKey($className));
165        [$condition, $params] = $this->db->getInConditionAndParams($ids);
166        $sql = "delete from {$this->safeTableName($className)} where $safePk in ($condition)";
167        $this->db->query($sql, $params);
168    }
169
170    public function save(Entity $entity): void {
171        $this->events->emit($entity->beforeSaveEvent(), [$entity]);
172        $className = get_class($entity);
173        $tableName = $this->tableName($className);
174        $data = $this->fetchDataArray($entity);
175        if ($entity->isNew()) {
176            $this->db->insert($tableName, $data);
177            if ($this->isPrimaryKeyAutoIncrement($className)) {
178                $pkName = $this->primaryKey($className);
179                $entity->$pkName = $this->db->lastInsertId();
180                $data[$pkName] = $entity->$pkName;
181            }
182            $entity->setNew(false);
183            $entity->takeSnapshot($data);
184        } else {
185            $dirtyData = $entity->getDirtyFields($data);
186            if ($dirtyData !== []) {
187                $this->db->update(
188                    $tableName, $dirtyData,
189                    $this->primaryKeyCondition($className),
190                    $this->primaryKeyConditionParams($className, $this->primaryKeyValue($className, $data))
191                );
192                $entity->takeSnapshot($data);
193            }
194        }
195        $this->events->emit($entity->afterSaveEvent(), [$entity]);
196    }
197
198    public function setByDataArray(Entity $entity, array $data): void {
199        $className = get_class($entity);
200        $columnKeys = array_keys($this->tableColumns($className));
201        foreach ($data as $n => $v) {
202            if (!in_array($n, $columnKeys)) {
203                throw new EntityManagerException("Column '$n' doesn't exist in $className");
204            }
205            $entity->$n = $v;
206        }
207        $entity->takeSnapshot($this->fetchDataArray($entity));
208    }
209
210    public function fetchDataArray(Entity $entity): array {
211        $columnKeys = array_keys($this->tableColumns(get_class($entity)));
212        $data = [];
213        foreach ($columnKeys as $ck) {
214            $data[$ck] = $entity->$ck;
215        }
216        return $data;
217    }
218}