Overview

Namespaces

  • Alchemy
    • core
      • query
      • schema
    • dialect
    • engine
    • orm
    • tests
    • util
      • promise
  • PHP

Classes

  • DataMapper
  • DDL
  • ManyToOne
  • OneToMany
  • OneToOne
  • ORMQuery
  • ORMTable
  • ORMTableRef
  • RelatedSet
  • Relationship
  • Session
  • SessionSelect
  • WorkQueue
  • Overview
  • Namespace
  • Class
  • Tree
  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: 
API documentation generated by ApiGen 2.8.0