2 /** @package verysimple::Phreeze */
5 * import supporting libraries
7 require_once("DataPage.php");
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
25 class DataSet
implements Iterator
// @TODO implement Countable, ArrayAccess
29 protected $_objectclass;
32 private $_current; // the current object in the set
33 private $_last; // the previous object in the set
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;
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)
47 public $CountSQL = "";
50 * Contructor initializes the object
56 * string class of object this DataSet contains
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;
67 $this->_objectclass
= $objectclass;
68 $this->_phreezer
= & $preezer;
71 $this->_cache_timeout
= is_null($cache_timeout) ?
$preezer->ValueCacheTimeout
: $cache_timeout;
75 * _getObject must be overridden and returns the type of object that
76 * this collection will contain.
80 * array to use for populating a single object
83 private function _getObject(&$row)
85 $obj = new $this->_objectclass($this->_phreezer
, $row);
90 * Next returns the next object in the collection.
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;
111 $this->_current
= null;
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
;
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()
153 $this->_no_exception
= true;
154 $this->_total
= $this->Count();
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
182 function CountIsKnown()
184 return $this->_totalcount
> - 1;
188 * Count returns the number of objects in the collection.
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
202 if (! $this->CountIsKnown()) {
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
);
211 $this->LockCache($cachekey);
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
;
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)
247 * bool asSimpleObject if true then populate the array with ToObject()
249 * array options (only relevant if asSimpleObject is true) passed through to ToObject
252 function ToObjectArray($asSimpleObject = false, $options = null)
254 $cachekey = $this->_sql
. " OBJECTARRAY" . ($asSimpleObject ?
'-AS-OBJECT-' . serialize($options) : '');
256 $arr = $this->GetDelayedCache($cachekey);
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
);
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 ();
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);
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)
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
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
324 function GetLabelArray($val_prop, $label_prop)
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
334 $this->_phreezer
->Observe("(DataSet.GetLabelArray: skipping query because cache exists) " . $this->_sql
, OBSERVE_QUERY
);
336 $this->LockCache($cachekey);
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);
354 * Release the resources held by this DataSet
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
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
381 function GetDataPage($pagenum, $pagesize, $countrecords = true)
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
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
);
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;
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;
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);
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
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
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
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
);
464 $this->UnlockCache($cachekey);
472 * @param string $cachekey
474 private function GetDelayedCache($cachekey)
476 // if no cache then don't return anything
477 if ($this->_cache_timeout
== 0) {
481 $obj = $this->_phreezer
->GetValueCache($cachekey);
483 // no cache, so try three times with a delay to prevent a cache stampede
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);
500 private function IsLocked($cachekey)
502 return $this->_phreezer
->LockFilePath
&& file_exists($this->_phreezer
->LockFilePath
. md5($cachekey) . ".lock");
510 private function LockCache($cachekey)
512 if ($this->_phreezer
->LockFilePath
) {
513 touch($this->_phreezer
->LockFilePath
. md5($cachekey) . ".lock");
522 private function UnlockCache($cachekey)
524 if ($this->_phreezer
->LockFilePath
) {
525 $lockfile = $this->_phreezer
->LockFilePath
. md5($cachekey) . ".lock";
526 if (file_exists($lockfile)) {