Highway to PSR2
[openemr.git] / portal / patient / fwk / libs / verysimple / Phreeze / DataSet.php
blob1969d3fed70354502764a7a35b354923763eddda
1 <?php
2 /** @package verysimple::Phreeze */
4 /**
5 * import supporting libraries
6 */
7 require_once("DataPage.php");
9 /**
10 * DataSet stores zero or more Loadable objects
11 * The DataSet is the object that is returned by every Phreezer Query operation.
12 * The DataSet contains various methods to enumerate through , or retrieve all
13 * results all at once.
15 * The DataSet executes queries lazily, only when the first result is retrieved.
16 * Using GetDataPage will allow retreival of sub-sets of large amounts of data without
17 * querying the entire database
19 * @package verysimple::Phreeze
20 * @author VerySimple Inc. <noreply@verysimple.com>
21 * @copyright 1997-2007 VerySimple Inc.
22 * @license http://www.gnu.org/licenses/lgpl.html LGPL
23 * @version 1.2
25 class DataSet implements Iterator // @TODO implement Countable, ArrayAccess
27 protected $_phreezer;
28 protected $_rs;
29 protected $_objectclass;
30 protected $_counter;
31 private $_sql;
32 private $_current; // the current object in the set
33 private $_last; // the previous object in the set
34 private $_totalcount;
35 private $_no_exception; // used during iteration to suppress exception on the final Next call
36 private $_cache_timeout; // length of time to cache query results
37 public $UnableToCache = true;
39 /**
40 * A custom SQL query may be provided to count the results of the query.
41 * This query should return one column "counter" which is the number of rows
42 * and must take into account all criteria parameters.
43 * If no value is provided, the counter query will be generated (which is likely less efficient)
45 * @var string
47 public $CountSQL = "";
49 /**
50 * Contructor initializes the object
52 * @access public
53 * @param
54 * Phreezer
55 * @param
56 * string class of object this DataSet contains
57 * @param string $sql
58 * code
59 * @param
60 * int cache timeout (in seconds). Default is Phreezer->ValueCacheTimeout. Set to 0 for no cache
62 function __construct(&$preezer, $objectclass, $sql, $cache_timeout = null)
64 $this->_counter = - 1;
65 $this->_totalcount = - 1;
66 $this->_eof = false;
67 $this->_objectclass = $objectclass;
68 $this->_phreezer = & $preezer;
69 $this->_rs = null;
70 $this->_sql = $sql;
71 $this->_cache_timeout = is_null($cache_timeout) ? $preezer->ValueCacheTimeout : $cache_timeout;
74 /**
75 * _getObject must be overridden and returns the type of object that
76 * this collection will contain.
78 * @access private
79 * @param array $row
80 * array to use for populating a single object
81 * @return Preezable
83 private function _getObject(&$row)
85 $obj = new $this->_objectclass($this->_phreezer, $row);
86 return $obj;
89 /**
90 * Next returns the next object in the collection.
92 * @access public
93 * @return Preezable
95 function Next()
97 if ($this->UnableToCache) {
98 require_once("verysimple/Util/ExceptionFormatter.php");
99 $info = ExceptionFormatter::FormatTrace(debug_backtrace());
100 $this->_phreezer->Observe("(DataSet.Next: unable to cache query with cursor) " . $info . " " . $this->_sql, OBSERVE_DEBUG);
102 // use this line to discover where an uncachable query is coming from
103 throw new Exception("WTF");
105 // stop this warning from repeating on every next call for this dataset
106 $this->UnableToCache = false;
109 $this->_verifyRs();
111 $this->_current = null;
112 $this->_counter ++;
114 if ($this->_eof) {
115 if (! $this->_no_exception) {
116 throw new Exception("EOF: This is a forward-only dataset.");
120 if ($row = $this->_phreezer->DataAdapter->Fetch($this->_rs)) {
121 $this->_current = $this->_getObject($row);
122 $this->_last = $this->_current;
123 } else {
124 $this->_eof = true;
127 return $this->_current;
131 * Executes the sql statement and fills the resultset if necessary
133 private function _verifyRs()
135 if ($this->_rs == null) {
136 $this->_phreezer->IncludeModel($this->_objectclass);
137 $this->_rs = $this->_phreezer->DataAdapter->Select($this->_sql);
142 * If a reporter query does not return data (insert/update/delete) then
143 * calling Execute will execute the sql without expecting return data
145 public function Execute()
147 return $this->_phreezer->DataAdapter->Execute($this->_sql);
149 public function rewind()
151 $this->_rs = null;
152 $this->_counter = 0;
153 $this->_no_exception = true;
154 $this->_total = $this->Count();
155 $this->_verifyRs();
156 $this->Next(); // we have to get the party started for php iteration
158 public function current()
160 // php iteration calls next then gets the current record. The DataSet
161 // Next return the current object. so, we have to fudge a little on the
162 // laster iteration to make it work properly
163 return ($this->key() == $this->Count()) ? $this->_last : $this->_current;
165 public function key()
167 return $this->_counter;
169 public function valid()
171 return $this->key() <= $this->Count();
175 * Returns true if the total number of records is known.
176 * Because calling "Count"
177 * directly may fire a database query, this method can be used to tell if
178 * the number of records is known without actually firing any queries
180 * @return boolean
182 function CountIsKnown()
184 return $this->_totalcount > - 1;
188 * Count returns the number of objects in the collection.
189 * If the
190 * count is not available, a count statement will be executed to determine the total
191 * number of rows available
193 * Note: if you get an "Unknown Column" error during a query, it may be due to tables being
194 * joined in the wrong order. To fix this, simply include references in your FieldMap to
195 * the foreign tables in the same order that you wish them to be included in the query
197 * @access public
198 * @return int
200 function Count()
202 if (! $this->CountIsKnown()) {
203 // check the cache
204 $cachekey = $this->_sql . " COUNT";
205 $this->_totalcount = $this->GetDelayedCache($cachekey);
207 // if no cache, go to the db
208 if ($this->_totalcount != null) {
209 $this->_phreezer->Observe("DataSet.Count: skipping count query because cache exists", OBSERVE_DEBUG);
210 } else {
211 $this->LockCache($cachekey);
213 $sql = "";
215 // if a custom counter sql query was provided, use that because it should be more efficient
216 if ($this->CountSQL) {
217 $this->_phreezer->Observe("DataSet.Count: using CountSQL to obtain total number of records", OBSERVE_DEBUG);
218 $sql = $this->CountSQL;
219 } else {
220 $this->_phreezer->Observe("(DataSet.Count: CountSQL was not provided so a counter query will be generated. Implement GetCustomCountQuery in the reporter class to improve performance.)", OBSERVE_WARN);
221 $sql = "select count(1) as counter from (" . $this->_sql . ") tmptable" . rand(1000, 9999);
224 $rs = $this->_phreezer->DataAdapter->Select($sql);
225 $row = $this->_phreezer->DataAdapter->Fetch($rs);
226 $this->_phreezer->DataAdapter->Release($rs);
227 $this->_totalcount = $row ["counter"];
229 $this->_phreezer->SetValueCache($cachekey, $this->_totalcount, $this->_cache_timeout);
231 $this->UnlockCache($cachekey);
235 return $this->_totalcount;
239 * Returns the entire collection as an array of objects.
240 * if the asSimpleObject is false
241 * then the stateful Phreezable objects will be returned. If asSimpleObject is true
242 * then the objects returned will be whatever is returned by ToObject() on each
243 * Phreezable object (the default is a stdClass with all public properties)
245 * @access public
246 * @param
247 * bool asSimpleObject if true then populate the array with ToObject()
248 * @param
249 * array options (only relevant if asSimpleObject is true) passed through to ToObject
250 * @return array
252 function ToObjectArray($asSimpleObject = false, $options = null)
254 $cachekey = $this->_sql . " OBJECTARRAY" . ($asSimpleObject ? '-AS-OBJECT-' . serialize($options) : '');
256 $arr = $this->GetDelayedCache($cachekey);
258 if ($arr != null) {
259 // we have a cache value, so we will repopulate from that
260 $this->_phreezer->Observe("(DataSet.ToObjectArray: skipping query because cache exists) " . $this->_sql, OBSERVE_DEBUG);
261 if (! $asSimpleObject) {
262 foreach ($arr as $obj) {
263 $obj->Refresh($this->_phreezer);
266 } else {
267 // there is nothing in the cache so we have to reload it
269 $this->LockCache($cachekey);
271 $this->UnableToCache = false;
273 // use a fixed count array if the count is known for performance
274 $arr = $this->CountIsKnown() ? $this->GetEmptyArray($this->Count()) : array ();
276 $i = 0;
277 while ($object = $this->Next()) {
278 $arr [$i ++] = $asSimpleObject ? $object->ToObject($options) : $object;
281 $this->_phreezer->SetValueCache($cachekey, $arr, $this->_cache_timeout);
283 $this->UnlockCache($cachekey);
286 return $arr;
291 * @deprecated Use GetLabelArray instead
293 function ToLabelArray($val_prop, $label_prop)
295 return $this->GetLabelArray($val_prop, $label_prop);
299 * Returns an empty array structure, determining which is appropriate
300 * based on the system capabilities and whether a count is known.
301 * If the count parameter is provided then the returned array may be
302 * a fixed-size array (depending on php version)
304 * @param
305 * int count (if known)
306 * @return Array or SplFixedArray
308 private function GetEmptyArray($count = 0)
310 return ($count && class_exists('SplFixedArray')) ? new SplFixedArray($count) : array ();
314 * Returns the entire collection as an associative array that can be easily used
315 * for Smarty dropdowns
317 * @access public
318 * @param string $val_prop
319 * the object property to be used for the dropdown value
320 * @param string $label_prop
321 * the object property to be used for the dropdown label
322 * @return array
324 function GetLabelArray($val_prop, $label_prop)
326 // check the cache
327 // $cachekey = md5($this->_sql . " VAL=".$val_prop." LABEL=" . $label_prop);
328 $cachekey = $this->_sql . " VAL=" . $val_prop . " LABEL=" . $label_prop;
330 $arr = $this->GetDelayedCache($cachekey);
332 // if no cache, go to the db
333 if ($arr != null) {
334 $this->_phreezer->Observe("(DataSet.GetLabelArray: skipping query because cache exists) " . $this->_sql, OBSERVE_QUERY);
335 } else {
336 $this->LockCache($cachekey);
338 $arr = array ();
339 $this->UnableToCache = false;
341 while ($object = $this->Next()) {
342 $arr [$object->$val_prop] = $object->$label_prop;
345 $this->_phreezer->SetValueCache($cachekey, $arr, $this->_cache_timeout);
347 $this->UnlockCache($cachekey);
350 return $arr;
354 * Release the resources held by this DataSet
356 * @access public
358 function Clear()
360 $this->_phreezer->DataAdapter->Release($this->_rs);
364 * Returns a DataPage object suitable for binding to the smarty PageView plugin.
365 * If $countrecords is true then the total number of records will be eagerly fetched
366 * using a count query. This is necessary in order to calculate the total number of
367 * results and total number of pages. If you do not care about pagination and simply
368 * want to limit the results, then this can be set to false to supress the count
369 * query. However, the pagination settings will not be correct and the total number
370 * of rows will be -1
372 * @access public
373 * @param int $pagenum
374 * which page of the results to view
375 * @param int $pagesize
376 * the size of the page (or zero to disable paging).
377 * @param bool $countrecords
378 * will eagerly fetch the total number of records with a count query
379 * @return DataPage
381 function GetDataPage($pagenum, $pagesize, $countrecords = true)
383 // check the cache
384 // $cachekey = md5($this->_sql . " PAGE=".$pagenum." SIZE=" . $pagesize);
385 $cachekey = $this->_sql . " PAGE=" . $pagenum . " SIZE=" . $pagesize . " COUNT=" . ($countrecords ? '1' : '0');
387 $page = $this->GetDelayedCache($cachekey);
389 // if no cache, go to the db
390 if ($page != null) {
391 $this->_phreezer->Observe("(DataSet.GetDataPage: skipping query because cache exists) " . $this->_sql, OBSERVE_QUERY);
393 foreach ($page->Rows as $obj) {
394 $obj->Refresh($this->_phreezer);
396 } else {
397 $this->LockCache($cachekey);
399 $this->UnableToCache = false;
401 $page = new DataPage();
402 $page->ObjectName = $this->_objectclass;
403 $page->ObjectInstance = new $this->_objectclass($this->_phreezer);
404 $page->PageSize = $pagesize;
405 $page->CurrentPage = $pagenum;
407 if ($countrecords) {
408 $page->TotalResults = $this->Count();
410 // first check if we have less than or exactly the same number of
411 // results as the pagesize. if so, don't bother doing the math.
412 // we know we just have one page
413 if ($page->TotalPages > 0 && $page->TotalPages <= $page->PageSize) {
414 $page->TotalPages = 1;
415 } else if ($pagesize == 0) {
416 // we don't want paging to occur in this case
417 $page->TotalPages = 1;
418 } else {
419 // we have more than one page. we always need to round up
420 // here because 5.1 pages means we are spilling out into
421 // a 6th page. (this will also handle zero results properly)
422 $page->TotalPages = ceil($page->TotalResults / $pagesize);
424 } else {
425 $page->TotalResults = $pagesize; // this will get adjusted after we run the query
426 $page->TotalPages = 1;
429 // now enumerate through the rows in the page that we want.
430 // decrement the requested pagenum here so that we will be
431 // using a zero-based array - which saves us from having to
432 // decrement on every iteration
433 $pagenum --;
435 $start = $pagesize * $pagenum;
437 // @TODO the limit statement should come from the DataAdapter
438 // ~~~ more efficient method where we limit the data queried ~~~
439 // since we are doing paging, we want to get only the records that we
440 // want from the database, so we wrap the original query with a
441 // limit query.
442 // $sql = "select * from (" . $this->_sql . ") page limit $start,$pagesize";
443 $sql = $this->_sql . ($pagesize == 0 ? "" : " limit $start,$pagesize");
444 $this->_rs = $this->_phreezer->DataAdapter->Select($sql);
446 // if we know the number of rows we have, then use SplFixedArray for performance
447 $page->Rows = ($page->TotalPages > $page->CurrentPage) ? $this->GetEmptyArray($pagesize) : array ();
449 // transfer all of the results into the page object
450 $i = 0;
451 while ($obj = $this->Next()) {
452 $page->Rows [$i ++] = $obj;
455 if (! $countrecords) {
456 // we don't know the total count so just set it to the total number of rows in this page
457 $page->TotalResults = $i;
460 $this->_phreezer->SetValueCache($cachekey, $page, $this->_cache_timeout);
462 $this->Clear();
464 $this->UnlockCache($cachekey);
467 return $page;
472 * @param string $cachekey
474 private function GetDelayedCache($cachekey)
476 // if no cache then don't return anything
477 if ($this->_cache_timeout == 0) {
478 return null;
481 $obj = $this->_phreezer->GetValueCache($cachekey);
483 // no cache, so try three times with a delay to prevent a cache stampede
484 $counter = 1;
485 while ($counter < 4 && $obj == null && $this->IsLocked($cachekey)) {
486 $this->_phreezer->Observe("(DataSet.GetDelayedCache: flood prevention. delayed attempt " . $counter . " of 3...) " . $cachekey, OBSERVE_DEBUG);
487 usleep(50000); // 5/100th of a second
488 $obj = $this->_phreezer->GetValueCache($cachekey);
489 $counter ++;
492 return $obj;
497 * @param
498 * $cachekey
500 private function IsLocked($cachekey)
502 return $this->_phreezer->LockFilePath && file_exists($this->_phreezer->LockFilePath . md5($cachekey) . ".lock");
507 * @param
508 * $cachekey
510 private function LockCache($cachekey)
512 if ($this->_phreezer->LockFilePath) {
513 touch($this->_phreezer->LockFilePath . md5($cachekey) . ".lock");
519 * @param
520 * $cachekey
522 private function UnlockCache($cachekey)
524 if ($this->_phreezer->LockFilePath) {
525 $lockfile = $this->_phreezer->LockFilePath . md5($cachekey) . ".lock";
526 if (file_exists($lockfile)) {
527 @unlink($lockfile);