1: <?php
2:
3: namespace Alchemy\orm;
4: use Alchemy\core\schema\Table;
5: use Alchemy\core\schema\Column;
6: use Alchemy\core\query\Insert;
7: use Alchemy\engine\IEngine;
8: use Alchemy\engine\ResultSet;
9: use Alchemy\util\Promise;
10: use Exception;
11:
12:
13: /**
14: * Acts as a controller between domain objects, the SQL expression language, and
15: * the RDBMS Engine layer. Manages queuing of work and transactions
16: */
17: class Session {
18: protected $queue;
19: protected $engine;
20: protected $records = array();
21: protected $updated = array();
22:
23:
24: /**
25: * Object constructor
26: *
27: * @param IEngine $engine
28: */
29: public function __construct(IEngine $engine) {
30: $this->engine = $engine;
31: $this->queue = new WorkQueue();
32: }
33:
34:
35: /**
36: * Add a new domain object to the session
37: *
38: * @param DataMapper $obj
39: * @return Promise Resolved when INSERT is sent to the database
40: */
41: public function add(DataMapper $obj) {
42: $cls = get_class($obj);
43:
44: // Get the primary key for this object. This is most likely
45: // transient, until the DB replaces it with an AutoInc value
46: $tempID = $this->getPrimaryKey($cls);
47:
48: // Configure the object to use this session as it's data source
49: $obj->setSession($this, $tempID);
50: $obj->save(false);
51:
52: // Queue insert query
53: $self = $this;
54: return $obj->onDependanciesPersisted()->then(function() use (&$self, &$obj) {
55: $self->queueInsert($obj);
56: });
57: }
58:
59:
60: public function queueInsert(DataMapper $obj) {
61: $cls = get_class($obj);
62: $sid = $obj->getSessionID();
63:
64: // Queue an INSERT query to be run later
65: $inserting = $this->queue->insert($obj, $this->records[$cls][$sid]);
66:
67: // When the insert is run, update our record of the primary key
68: $self = $this;
69: $inserting->then(function(ResultSet $r) use ($self, $obj, $sid) {
70: $self->updatePrimaryKey($obj, $sid, $r->lastInsertID());
71: });
72:
73: // Kill the updated records for this object since this is the initial inset
74: unset($this->updated[$cls][$sid]);
75:
76: // Return the promise for the user to do fun things with
77: return $inserting;
78: }
79:
80:
81: /**
82: * Commit changed to the database. Will automatically call
83: * {@link Session::flush()} to send queries to the database
84: * then commit the transaction. You should almost always
85: * call this instead of {@link Session::flush()}
86: */
87: public function commit() {
88: $this->flush();
89: $this->engine->commitTransaction();
90: }
91:
92:
93: /**
94: * Returns a DDL object for this session
95: *
96: * @return DDL
97: */
98: public function ddl() {
99: return new DDL($this);
100: }
101:
102:
103: /**
104: * Return the Engine associated with this session
105: *
106: * @return IEngine
107: */
108: public function engine() {
109: return $this->engine;
110: }
111:
112:
113: /**
114: * Execute a query, wrap the results in the given class
115: * and return a set.
116: *
117: * @param string $cls Class Name of a DataMapper subclass
118: * @param Query $query Query to execute
119: * @return array Set of Objects
120: */
121: public function execute($cls, $query) {
122: $rows = $this->engine->query($query);
123: return $this->wrap($cls, $rows);
124: }
125:
126:
127: /**
128: * Start a new transaction (if one isn't already open), and flushes
129: * all pending queries to the RDBMS in the order they were created.
130: * Leaves the transaction open for you to commit / rollback.
131: */
132: public function flush() {
133: $this->engine->beginTransaction();
134: $this->queue->flush($this->engine);
135: }
136:
137:
138: /**
139: * Get the primary key for the given class and record. If one
140: * doesn't exist yet, it generates a transient key to be used
141: * until the database allocates the object a real key.
142: *
143: * @param string $cls Class Name of a DataMapper subclass
144: * @param array $record Data Record
145: * @return array
146: */
147: protected function getPrimaryKey($cls, $record = array()) {
148: $pk = array();
149: foreach ($cls::schema()->getPrimaryKey()->listColumns() as $column) {
150: $name = $column->getName();
151: if (isset($record[$name])) {
152: $pk[] = $record[$name];
153: }
154: }
155:
156: // Generate a single string from the (possibly composite) primary key
157: $pk = implode("-", $pk);
158:
159: // PK probably hasn't been allocated by the DB yet. Create a temporary
160: // one to identify it's record in the session
161: if (empty($pk)) {
162: $pk = "TRANSIENT-KEY-" . rand();
163: }
164:
165: return $pk;
166: }
167:
168:
169: /**
170: * Return a reference to the given field
171: *
172: * @param string $cls Class Name
173: * @param mixed $id Record ID
174: * @param string $prop Column Name
175: * @return mixed
176: */
177: public function &getProperty($cls, $id, $prop) {
178: return $this->records[$cls][$id][$prop];
179: }
180:
181:
182: /**
183: * Get an object from the session store without running a query.
184: *
185: * @param string $cls DataMapper type
186: * @param string $sid SessionID
187: * @return DataMapper
188: */
189: public function object($cls, $sid) {
190: if (is_array($sid)) {
191: $sid = $this->getPrimaryKey($cls, $sid);
192: }
193:
194: if (isset($this->records[$cls][$sid])) {
195: return $cls::from_session($this, $sid);
196: }
197: }
198:
199:
200: /**
201: * Return a SessionSelect for the given class
202: *
203: * @return SessionSelect
204: */
205: public function objects($cls) {
206: return new SessionSelect($this, $cls);
207: }
208:
209:
210: /**
211: * Delete the given object form the database
212: *
213: * @return Promise resolved when DELETE statement is run
214: */
215: public function remove(DataMapper &$obj) {
216: $cls = get_class($obj);
217:
218: $keys = array();
219: foreach ($cls::schema()->getPrimaryKey()->listColumns() as $column) {
220: $name = $column->getName();
221: $keys[$name] = $obj->$name;
222: }
223:
224: // Queue an INSERT query to be run later
225: $deleting = $this->queue->delete($cls, $keys);
226:
227: // Delete records
228: $id = $obj->getSessionID();
229: unset($this->records[$cls][$id]);
230: unset($this->updated[$cls][$id]);
231:
232: // Return the promise for the user to do fun things with
233: return $deleting;
234: }
235:
236:
237: /**
238: * Queue an UPDATE query to be run later to update values
239: * set with {@see Session::setProperty()}
240: *
241: * @param string $cls Class Name
242: * @param mixed $id Record ID
243: * @return Promise Resolved when the query is run
244: */
245: public function save($cls, $id) {
246: // If nothing is in $this->updated, theres nothing to do here
247: if (empty($this->updated[$cls][$id])) {
248: return;
249: }
250:
251: // Filter the UPDATE by primary key
252: $pk = array();
253: foreach ($cls::schema()->getPrimaryKey()->listColumns() as $column) {
254: $name = $column->getName();
255: $value = $this->getProperty($cls, $id, $name);
256:
257: // Abort if primary key is partial
258: if (is_null($value)) {
259: throw new Exception("Can not send UPDATE for DataMapper[{$cls}] when primary key is null");
260: }
261:
262: $pk[$name] = $value;
263: }
264:
265: // Schedule the UPDATE
266: $updating = $this->queue->update($cls, $pk, $this->updated[$cls][$id]);
267:
268: // Remove these rows form $this->updated so we won't try to update
269: // them again later
270: unset($this->updated[$cls][$id]);
271:
272: return $updating;
273: }
274:
275:
276: /**
277: * Update a property value
278: *
279: * @param string $cls Class Name
280: * @param mixed $id Record ID
281: * @param string $prop Column Name
282: * @param mixed $value Property Value
283: */
284: public function setProperty($cls, $id, $prop, $value) {
285: // Don't update the value if it's already equivalent
286: if (isset($this->records[$cls][$id][$prop]) && $this->records[$cls][$id][$prop] === $value) {
287: return;
288: }
289:
290: $this->records[$cls][$id][$prop] = $value;
291: $this->updated[$cls][$id][$prop] = $value;
292: }
293:
294:
295: /**
296: * Accepts an object and moves it from one primary key to a new primary
297: * key. This is only ever needed when migrating from a transient key
298: * to a real db-allocated key after an INSERT.
299: *
300: * @param DataMapper $obj
301: * @param mixed $oldID
302: * @param mixed $newID
303: */
304: public function updatePrimaryKey(DataMapper $obj, $oldID, $newID) {
305: $cls = get_class($obj);
306:
307: // Update the auto increment column in our rocred to match the new value
308: foreach ($cls::schema()->getPrimaryKey()->listColumns() as $column) {
309: $name = $column->getName();
310: if (!isset($this->records[$cls][$oldID][$name])) {
311: $this->records[$cls][$oldID][$name] = $newID;
312: }
313: }
314:
315: // Copy the record to it's new address
316: $storeID = $this->getPrimaryKey($cls, $this->records[$cls][$oldID]);
317: $this->records[$cls][$storeID] = $this->records[$cls][$oldID];
318:
319: // Tell the object it's new address
320: $obj->setSession($this, $storeID, true);
321:
322: // Remove the transient record
323: unset($this->records[$cls][$oldID]);
324: unset($this->updated[$cls][$oldID]);
325: }
326:
327:
328: /**
329: * Wrap a set of rows in the given DataMapper class
330: *
331: * @param string $cls Class Name
332: * @param array $rows Two-dimensional array of records
333: */
334: protected function wrap($cls, $rows) {
335: $objects = array();
336: $table = $cls::schema();
337: $rows = $rows ?: array();
338:
339: foreach ($rows as $row) {
340: $sid = $this->getPrimaryKey($cls, $row);
341:
342: $record = array();
343: foreach ($row as $column => $value) {
344: $record[$column] = $table->getColumn($column)->decode($value);
345: }
346:
347: $this->records[$cls][$sid] = $record;
348: $objects[] = $this->object($cls, $sid);
349: }
350:
351: return $objects;
352: }
353: }
354: