5c817dc8bbf10aa60af5b28a240902d779ba0117
[rock.divinelegy.git] / DataAccess / DataMapper / DataMapper.php
1 <?php
2
3 namespace DataAccess\DataMapper;
4
5 use Exception;
6 use Domain\Entities\IDivineEntity;
7 use DataAccess\IDatabaseFactory;
8 use DataAccess\DataMapper\IDataMapper;
9 use DataAccess\Queries\IQueryBuilder;
10 use DataAccess\DataMapper\Helpers\AbstractPopulationHelper;
11 use DataAccess\DataMapper\LazyLoadedEntities;
12 use Services\IConfigManager;
13 use ReflectionClass;
14
15 class DataMapper implements IDataMapper
16 {
17 private $_db;
18 private $_maps;
19 private $_configManager;
20
21 public function __construct($maps, IDatabaseFactory $databaseFactory, IConfigManager $configManager)
22 {
23 $this->_db = $databaseFactory->createInstance();
24 $this->_maps = include $maps;
25 $this->_configManager = $configManager;
26 }
27
28 public function map($entityName, IQueryBuilder $queryBuilder)
29 {
30 $queryString = $queryBuilder->buildQuery();
31 $statement = $this->_db->prepare(sprintf($queryString,
32 $this->_maps[$entityName]['table']
33 ));
34
35 $statement->execute();
36 $rows = $statement->fetchAll();
37
38 $entities = array();
39
40 if(count($rows) > $this->_configManager->getDirective('maxEntitiesToLoad'))
41 {
42 //TODO: Factory?
43 return new LazyLoadedEntities($rows, $entityName, $this->_maps, $this->_db, $this->_configManager->getDirective('maxEntitiesToLoad'));
44 }
45
46 foreach($rows as $row)
47 {
48 $className = $this->_maps[$entityName]['class']; //the entity to instantiate and return
49 $constructors = AbstractPopulationHelper::getConstrutorArray($this->_maps, $entityName, $row, $this->_db);
50
51 if(count($constructors) == 0)
52 {
53 $class = new $className;
54 } else {
55 $r = new ReflectionClass($className);
56 $class = $r->newInstanceArgs($constructors);
57 }
58
59 $class->setId((int)$row['id']);
60 $entities[$row['id']] = $class;
61 }
62
63 return $entities;
64 }
65
66 public function save(IDivineEntity $entity)
67 {
68 try {
69 $this->_db->beginTransaction();
70 $queries = AbstractPopulationHelper::generateUpdateSaveQuery($this->_maps, $entity, $entity->getId(), $this->_db);
71 $mergeMap = array();
72 $flattened = array();
73
74 foreach($queries as $index => $query)
75 {
76 $this_table = $query['table'];
77 $this_columns = $query['columns'];
78
79
80 for($i = $index+1; $i<count($queries); $i++)
81 {
82 if(
83 $queries[$i]['table'] == $this_table &&
84 !array_key_exists($i, $mergeMap) &&
85 !isset($query['id'])) //only merge create queries, updates are fine to run multiple times
86 {
87 //XXX: This whole biz is tricky. Basically the problem is that when creating a new simfile,
88 //the datamapper spews out a bunch of create queries. When parsing a simfile for example, there can
89 //be huge redundency - it may produce 5 queries that all create the same step artist, for example.
90 //We attempt to flatten equivalent queries. Originally I was basing it purely on the table name or something,
91 //but that is not enough. In the case of steps, it ends up mergin all the steps together, so we need to
92 //check if the arrays are equal as well, which is what this does.
93 if($this_columns === $queries[$i]['columns'])
94 {
95 //need to keep track of what we merged as future queries might reference the old ids.
96 $mergeMap[$i] = $index;
97 }
98
99 //XXX: Another thing that might happen is we have to create queries running on the same table, but with unique columns.
100 //In this case, we can take the columns of one and put it into the other. Otherwise we create two records when we really
101 //should have only one. An example of this is when a user is created, a query to add the country to users_meta is run,
102 //and then _another_ to add firstname, lastname and user_id. It should really all be done in one query.
103
104 //Make sure both queries are for the same table, and the both relate back to the main query
105 if($this_table == $queries[$i]['table'] && in_array('%MAIN_QUERY_ID%', $this_columns) && in_array('%MAIN_QUERY_ID%', $queries[$i]['columns']))
106 {
107 $this_column_names = array_keys($this_columns);
108 $other_column_names = array_keys($queries[$i]['columns']);
109 $combine = true;
110 foreach($this_column_names as $column_name)
111 {
112 if($this_columns[$column_name] != '%MAIN_QUERY_ID%' && in_array($column_name, $other_column_names))
113 {
114 $combine = false;
115 }
116 }
117
118 if($combine)
119 {
120 $this_columns = array_merge($this_columns, $queries[$i]['columns']);
121 $mergeMap[$i] = $index;
122 }
123 }
124 }
125 }
126
127 if(!array_key_exists($index, $mergeMap)) {
128 $prepared = isset($query['prepared']) ? $query['prepared'] : null;
129 $id = isset($query['id']) ? $query['id'] : null;
130
131 $flattened[$index] = array(
132 'columns' => $this_columns,
133 'table' => $this_table,
134 'prepared' => $prepared,
135 'id' => $id
136 );
137 }
138 }
139
140 $queries = array();
141
142 foreach($flattened as $index => $info)
143 {
144 if(isset($info['id']))
145 {
146 $query = $info['prepared'];
147 $query = substr($query, 0, -2);
148 $query .= sprintf(' WHERE id=%u', $info['id']);
149 } else {
150 $query = sprintf('INSERT INTO %s (%s) VALUES (%s)',
151 $info['table'],
152 implode(', ', array_keys($info['columns'])),
153 implode(', ', $info['columns']));
154 }
155
156 $queries[$index] = $query;
157 }
158
159 // if($queries['TYPE'] == AbstractPopulationHelper::QUERY_TYPE_CREATE)
160 // {
161 $idMap = [];
162 foreach($queries as $index => $query)
163 {
164 $runQuery = true;
165 //originally was preg_quote('%').'(.*?)'.preg_quote('%') but that failed with things like:
166 //...VALUES ('Voyager Full 50%', %INDEX_REF_0%
167 //it picked up ',
168 //so now only find ones with INDEX_REF and double check that MAIN QUERY isn't there.
169 if (preg_match_all('/'.preg_quote('%INDEX_REF_').'(.*?)'.preg_quote('%').'/s', $query, $matches)) {
170 foreach($matches[1] as $index_ref)
171 {
172 //if($index_ref != 'MAIN_QUERY_ID')
173 //if(strpos($query, '%MAIN_QUERY_ID%') === false)
174 //{
175 $index_id = str_replace('INDEX_REF_', '', $index_ref);
176 $query = str_replace('%INDEX_REF_' . $index_id . '%', $idMap['INDEX_REF_' . $index_id], $query);
177 //} else {
178 // $runQuery = false;
179 //}
180 }
181 }
182
183 //if we don't need the main query we can run this
184 if(strpos($query, '%MAIN_QUERY_ID%') === false)
185 {
186 $statement = $this->_db->prepare($query);
187 $statement->execute();
188 //$refIndex = $index+1; This was being used as the index for idMap below. I have nfi why I was adding 1.
189 $idMap['INDEX_REF_' . $index] = $this->_db->lastInsertId();
190
191 foreach($mergeMap as $oldIndex => $mergedIndex) {
192 if($mergedIndex == $index) {
193 $idMap['INDEX_REF_' . $oldIndex] = $idMap['INDEX_REF_' . $index];
194 }
195 }
196
197 unset($queries[$index]);
198 } else {
199 //update query so that other references are resolved.
200 $queries[$index] = $query;
201 }
202 }
203
204 //at this point we have queries left that depend on the main query id
205 foreach($queries as $query)
206 {
207 $query = str_replace('%MAIN_QUERY_ID%', end($idMap), $query);
208 $statement = $this->_db->prepare($query);
209 $statement->execute();
210 }
211 //}
212
213 if(!$entity->getId()) $entity->setId(end($idMap));
214
215 $this->_db->commit();
216
217 return $entity;
218 } catch (Exception $e) {
219 $this->_db->rollBack();
220 throw $e;
221 }
222 }
223
224 //TODO: Implement
225 public function remove(IDivineEntity $entity) {
226 ;
227 }
228 }