![]() |
Xataface HTML Reports Module 0.1
HTML Reports Module for Xataface
|
00001 <?php 00002 /* 00003 * Xataface HTML Reports Module 00004 * Copyright (C) 2011 Steve Hannah <steve@weblite.ca> 00005 * 00006 * This library is free software; you can redistribute it and/or 00007 * modify it under the terms of the GNU Library General Public 00008 * License as published by the Free Software Foundation; either 00009 * version 2 of the License, or (at your option) any later version. 00010 * 00011 * This library is distributed in the hope that it will be useful, 00012 * but WITHOUT ANY WARRANTY; without even the implied warranty of 00013 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 00014 * Library General Public License for more details. 00015 * 00016 * You should have received a copy of the GNU Library General Public 00017 * License along with this library; if not, write to the 00018 * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, 00019 * Boston, MA 02110-1301, USA. 00020 * 00021 */ 00022 require_once dirname(__FILE__).DIRECTORY_SEPARATOR.'simple_html_dom.php'; 00023 00028 class XfHtmlReportBuilder { 00029 00030 const COMPILE_ERROR = 501; 00031 public static $SUMMARY_FUNCTIONS = array('sum','min','max','count'); 00032 00033 private $templateContent = null; 00034 private $records = null; 00035 private $output = ''; 00036 00037 private $context = null; 00038 00043 protected $strict = false; 00044 00045 00051 public static function getFields($el){ 00052 $out = array(); 00053 if ( preg_match_all('/\{\$([^\}]+)\}/', $el->outertext, $matches, PREG_SET_ORDER) ){ 00054 foreach ($matches as $match){ 00055 $out[] = trim($match[1]); 00056 } 00057 } 00058 $out = array_unique($out); 00059 return $out; 00060 } 00061 00067 public static function getSummaryFields($el){ 00068 $out = array(); 00069 if ( preg_match_all('/\{@([a-zA-Z]+\([^\}]+\))\}/', $el->outertext, $matches, PREG_SET_ORDER) ){ 00070 foreach ($matches as $match){ 00071 $out[] = trim($match[1]); 00072 } 00073 } 00074 $out = array_unique($out); 00075 return $out; 00076 00077 } 00078 00079 00090 public static function printGroupData(&$groupData, $groupFields, $summaryFields, $groupHeaders, $groupFooters, $template, $asTable){ 00091 $header = $groupHeaders[$groupData['level']]; 00092 $footer = $groupFooters[$groupData['level']]; 00093 $gfields = $groupFields[$groupData['level']]; 00094 $sfields = $summaryFields[$groupData['level']]; 00095 if ( !@$groupData['summaryRecord'] ){ 00096 throw new Exception("No summary record provided.", 2000); 00097 } 00098 $headerText = ''; 00099 if ( $header ){ 00100 $headerText = self::fillReportSingle($groupData['summaryRecord'], $header->innertext); 00101 } 00102 $bodyText = ''; 00103 if ( isset($groupData['sections']) ){ 00104 foreach ($groupData['sections'] as $key=>$section){ 00105 $bodyText .= self::printGroupData($groupData['sections'][$key], $groupFields, $summaryFields, $groupHeaders, $groupFooters, $template, $asTable); 00106 00107 } 00108 } else if ( isset($groupData['records']) ){ 00109 $bodyText = self::fillReportMultiple($groupData['records'], $template, false /* no headers */, $asTable); 00110 } 00111 $footerText = ''; 00112 if ( $footer ){ 00113 $footerText = self::fillReportSingle($groupData['summaryRecord'], $footer->innertext); 00114 } 00115 return $headerText.$bodyText.$footerText; 00116 } 00117 00126 public static function getGroupKey(Dataface_Record $record, $fields){ 00127 $out = array(); 00128 foreach ($fields as $f){ 00129 $out[] = urlencode($f).'='.urlencode($record->val($f)); 00130 } 00131 return implode('&', $out); 00132 } 00133 00134 00145 public static function fillReportTable($records, $template, $headersAndFooters=true){ 00146 00147 return self::fillReportMultiple($records, $template, $headersAndFooters, true); 00148 } 00149 00150 00159 public static function fillReportMultiple($records, $template, $headersAndFooters=true, $asTable=false){ 00160 if ( !is_array($records) ){ 00161 throw new Exception("Records must be an array.", 2000); 00162 } 00163 if ( is_string($template) ){ 00164 $el = str_get_html($template); 00165 } else { 00166 $el = $template; 00167 } 00168 $groupFields = array(); 00169 $summaryFields = array(); 00170 00171 $sectionHeaderEls = $el->find('div.xf-htmlreports-section-header'); 00172 $sectionFooterEls = $el->find('div.xf-htmlreports-section-footer'); 00173 $numLevels = max(count($sectionHeaderEls), count($sectionFooterEls)); 00174 00175 if ( $numLevels > 0 ){ 00176 if ( !$headersAndFooters ){ 00177 // Caller doesn't want headers and footers. We clear them out 00178 // then process the remainder of the template. 00179 foreach ($sectionHeaderEls as $myel){ 00180 $myel->outertext = ''; 00181 } 00182 foreach ($sectionFooterEls as $myel){ 00183 $myel->outertext = ''; 00184 } 00185 return self::fillReportMultiple($records, $el->outertext, false, $asTable); 00186 } 00187 $groupFooters = array(); 00188 $groupHeaders = array(); 00189 00190 00191 for ($i=0; $i<$numLevels; $i++){ 00192 $groupFields[$i] = $summaryFields[$i] = array(); 00193 $groupHeaders[$i] = $groupFooters[$i] = null; 00194 } 00195 00196 for ($i=0; $i<$numLevels; $i++){ 00197 00198 $j = $numLevels-$i-1; 00199 00200 if ( isset($sectionHeaderEls[$i]) ){ 00201 $groupFields[$i] = array_merge($groupFields[$i], self::getFields($sectionHeaderEls[$i])); 00202 $summaryFields[$i] = array_merge($summaryFields[$i], self::getSummaryFields($sectionHeaderEls[$i])); 00203 $groupHeaders[$i] = $sectionHeaderEls[$i]; 00204 } 00205 00206 00207 if ( isset($sectionFooterEls[$j]) ){ 00208 $groupFields[$i] = array_merge($groupFields[$j], self::getFields($sectionFooterEls[$j])); 00209 $summaryFields[$i] = array_merge($summaryFields[$j], self::getSummaryFields($sectionFooterEls[$j])); 00210 $groupFooters[$i] = $sectionFooterEls[$j]; 00211 } 00212 00213 } 00214 00215 for ( $i=0; $i<$numLevels; $i++){ 00216 $groupFields[$i] = array_unique($groupFields[$i]); 00217 $summaryFields[$i] = array_unique($summaryFields[$i]); 00218 00219 } 00220 00221 00222 // Now that we have our groupings and summaries we can proceed to 00223 // group the records and create summary rows. 00224 00225 00226 foreach ($records as $record){ 00227 $node =& $tree; 00228 for ( $i=0; $i<$numLevels; $i++){ 00229 $groupKey = self::getGroupKey($record, $groupFields[$i]); 00230 if ( !isset($node[$groupKey]) ){ 00231 $node[$groupKey] = array( 00232 'summaryRecord'=> null, 00233 'level'=>$i 00234 ); 00235 if ( $i<$numLevels-1 ){ 00236 $node[$groupKey]['sections'] = array(); 00237 } else { 00238 $node[$groupKey]['records'] = array(); 00239 } 00240 } 00241 00242 $temp =& $node[$groupKey]; 00243 unset($node); 00244 if ( isset($temp['sections']) ){ 00245 $node =& $temp['sections']; 00246 } else { 00247 $node =& $temp['records']; 00248 } 00249 unset($temp); 00250 00251 } 00252 $node[] = $record; 00253 unset($node); 00254 } 00255 $out = ''; 00256 foreach (array_keys($tree) as $groupKey){ 00257 $groupData =& $tree[$groupKey]; 00258 self::compileGroupData(&$groupData, $groupFields, $summaryFields); 00259 $out .= self::printGroupData(&$groupData, $groupFields, $summaryFields, $groupHeaders, $groupFooters, $template, $asTable); 00260 00261 00262 } 00263 00264 } else { 00265 00266 $out = ''; 00267 00268 00269 00270 if ( !is_array($records) ){ 00271 throw new Exception("No array of records provided.", 2000); 00272 } 00273 00274 00275 if ( $asTable ){ 00276 $fields = self::getFields($el); 00277 $rowtemplate = '<tr>'; 00278 foreach ($fields as $field){ 00279 $fielddef = $records[0]->table()->getField($field); 00280 if ( PEAR::isError($fielddef) ) continue; 00281 $rowtemplate .= '<td class="xf-htmlreports-field xf-htmlreports-field-'.str_replace('.','_', $field).'">{$'.$field.'}</td>'; 00282 } 00283 $rowtemplate .= '</tr>'; 00284 $template = $rowtemplate; 00285 00286 $cols = array(); 00287 foreach ($fields as $fieldname){ 00288 $field = $records[0]->table()->getField($fieldname); 00289 if ( PEAR::isError($field) ) continue; 00290 $label = $field['widget']['label']; 00291 if ( @$field['column'] and @$field['column']['label'] ) $label = $field['column']['label']; 00292 $cols[] = $label; 00293 00294 } 00295 00296 $colTemplate = '<tr><th>'.implode('</th><th>', array_map('htmlspecialchars', $cols)).'</th></tr>'; 00297 $out = '<table><thead>'.$colTemplate.'</thead><tbody>'; 00298 } 00299 00300 foreach ($records as $rec){ 00301 $out .= self::fillReportSingle($rec, $template); 00302 } 00303 if ( $asTable ){ 00304 $out .= '</tbody></table>'; 00305 } 00306 } 00307 return $out; 00308 00309 } 00310 00311 00312 public static function isDelegateField(Dataface_Table $table, $fieldname){ 00313 if ( strpos($fieldname, '.') !== false ){ 00314 list($rel, $fld) = explode('.', $fieldname); 00315 $relObj = $table->getRelationship($rel); 00316 if ( PEAR::isError($relObj) ) throw new Exception($relObj->getMessage(), $relObj->getCode()); 00317 $rtable = $relObj->getTable($fld); 00318 if ( PEAR::isError($rtable) ) throw new Exception($rtable->getMessage(), $rtable->getCode()); 00319 return self::isDelegateField($rtable, $fld); 00320 } else { 00321 $flds =& $table->delegateFields(true); 00322 return isset($flds[$fieldname]); 00323 } 00324 } 00325 00332 public static function extractRecordsFromGroupData(&$groupData){ 00333 00334 $out = array(); 00335 if ( isset($groupData['records']) ){ 00336 return $groupData['records']; 00337 } else if ( isset($groupData['sections']) ){ 00338 00339 foreach ($groupdData['sections'] as $key=>$section){ 00340 $secRecs = self::extractRecordsFromGroupData($groupData['sections'][$key]); 00341 foreach ($secRecs as $rec){ 00342 $out[] = $rec; 00343 } 00344 } 00345 } 00346 return $out; 00347 00348 } 00349 00350 00366 public static function compileGroupData(&$groupData, $groupFields, $summaryFields){ 00367 $gfields = $groupFields[$groupData['level']]; 00368 $sfields = $summaryFields[$groupData['level']]; 00369 00370 $records = self::extractRecordsFromGroupData(&$groupData); 00371 if ( !$records ) return; 00372 $summaryRecord = new Dataface_Record($records[0]->table()->tablename, array()); 00373 foreach ($gfields as $gf ){ 00374 $summaryRecord->setValue($gf, $records[0]->val($gf)); 00375 } 00376 00377 00378 foreach ($sfields as $opt){ 00379 $summaryRecord->pouch['summaries'][$opt] = self::evaluateAggregateExpression($records, $opt); 00380 00381 } 00382 $groupData['summaryRecord'] = $summaryRecord; 00383 if ( isset($groupData['sections']) ){ 00384 foreach (array_keys($groupData['sections']) as $secid){ 00385 $secGroupData =& $groupData['sections'][$secid]; 00386 self::compileGroupData($secGroupData, $groupFields, $summaryFields); 00387 unset($secGroupData); 00388 } 00389 } 00390 00391 00392 00393 } 00394 00404 public static function evaluateAggregateExpression(array $records, $expression){ 00405 00406 $parsed = self::parseExpression($expression); 00407 switch ($parsed['opt']){ 00408 00409 case 'sum': 00410 $total = 0; 00411 if ( !$records ) return $total; 00412 if ( $records[0]->table()->isInt($parsed['field']) ){ 00413 foreach ($records as $rec){ 00414 $total += intval($rec->val($parsed['field'])); 00415 } 00416 } else { 00417 foreach ($records as $rec){ 00418 $total += doubleval($rec->val($parsed['field'])); 00419 } 00420 } 00421 return $total; 00422 00423 case 'count': 00424 return count($records); 00425 00426 case 'max': 00427 $max = 0; 00428 if ( !$records ) return $max; 00429 if ( $records[0]->table()->isInt($parsed['field']) ){ 00430 foreach ($records as $rec){ 00431 $max = max($max, intval($rec->val($parsed['field']))); 00432 } 00433 return $max; 00434 } else if ( $records[0]->table()->isFloat($parsed['field']) ){ 00435 foreach ($records as $rec){ 00436 $max = max($max, doubleval($rec->val($parsed['field']))); 00437 } 00438 return $max; 00439 00440 } else if ( $records[0]->table()->isDate($parsed['field'])){ 00441 foreach ($records as $rec){ 00442 $max = max($max, strtotime($record->strval($parsed['field']))); 00443 } 00444 return date('Y-m-d H:i:s', $max); 00445 } else { 00446 $max = null; 00447 foreach ($records as $rec){ 00448 if ( !isset($max) ){ 00449 $max = $rec->val($parsed['field']); 00450 } else if ( ($currtest = $rec->val($parsed['field'])) > $max ){ 00451 $max = $currtest; 00452 } 00453 } 00454 return $max; 00455 } 00456 00457 00458 case 'min': 00459 $min = null; 00460 if ( !$records ) return $min; 00461 if ( $records[0]->table()->isInt($parsed['field']) ){ 00462 foreach ($records as $rec){ 00463 if ( !isset($min) ){ 00464 $min = intval($rec->val($parsed['field'])); 00465 } else { 00466 $min = min($min, intval($rec->val($parsed['field']))); 00467 } 00468 } 00469 return $min; 00470 } else if ( $records[0]->table()->isFloat($parsed['field']) ){ 00471 foreach ($records as $rec){ 00472 if ( !isset($min) ){ 00473 $min = doubleval($rec->val($parsed['field'])); 00474 } else { 00475 $min = min($min, doubleval($rec->val($parsed['field']))); 00476 } 00477 } 00478 return $min; 00479 00480 } else if ( $records[0]->table()->isDate($parsed['field'])){ 00481 foreach ($records as $rec){ 00482 if ( !isset($min) ){ 00483 $min = strtotime($record->strval($parsed['field'])); 00484 } else { 00485 00486 $min = min($min, strtotime($record->strval($parsed['field']))); 00487 } 00488 } 00489 return date('Y-m-d H:i:s', $min); 00490 } else { 00491 $min = null; 00492 foreach ($records as $rec){ 00493 if ( !isset($min) ){ 00494 $min = $rec->val($parsed['field']); 00495 } else if ( ($currtest = $rec->val($parsed['field'])) > $min ){ 00496 $min = $currtest; 00497 } 00498 } 00499 return $min; 00500 } 00501 default: 00502 throw new Exception("Unrecognized aggregate operator: ".$parsed['opt'], self::COMPILE_ERROR); 00503 00504 00505 } 00506 } 00507 00508 00520 public static function parseExpression($expression){ 00521 $expression = trim($expression); 00522 if ( preg_match('/^([a-zA-Z]+)\(([^\)]+)\)$/', $expression, $matches)){ 00523 return array( 00524 'opt'=>trim(strtolower($matches[1])), 00525 'field'=>trim($matches[2]) 00526 ); 00527 } else { 00528 throw new Exception("Failed to parse expression '".$expression."'. It does not match the required pattern."); 00529 00530 } 00531 } 00532 00541 public static function fillReportSingle(Dataface_Record $record, $template, $strict = false){ 00542 if ( !$record ) throw new Exception("Null record provided.", 2000); 00543 if ( is_string($template) ){ 00544 $el = str_get_html($template); 00545 } else { 00546 $el = $template; 00547 } 00548 $builder = new XfHtmlReportBuilder(); 00549 if ( $strict ){ 00550 $builder->strict = true; 00551 } 00552 return $builder->fillReport($record, $record, $el, ''); 00553 } 00554 00555 00565 public static function validateTemplate(Dataface_Table $table, $template){ 00566 $rec = new Dataface_Record($table->tablename, array()); 00567 self::fillReportSingle($rec, $template, true); 00568 return true; 00569 } 00570 00571 00580 public function fillReport($root, $record, $el, $basePath=''){ 00581 $oldContext = $this->context; 00582 $this->context = new stdClass; 00583 $this->context->root = $root; 00584 $this->context->record = $record; 00585 $this->context->el = $el; 00586 $this->context->basePath = $basePath; 00587 $this->context->rrec = null; 00588 00589 //$dom = str_get_html($template); 00590 $rrec = null; 00591 if ( is_a($record, 'Dataface_RelatedRecord') ){ 00592 $rrec = $record; 00593 $record = $rrec->toRecord(); 00594 $this->context->rrec = $rrec; 00595 $this->context->record = $record; 00596 } 00597 $uls = $el->find('ul[relationship]'); 00598 foreach ($uls as $ul){ 00599 $rel = $ul->relationship; 00600 00601 if ( $this->strict ){ 00602 00603 $relationship = $root->table()->getRelationship($rel); 00604 if ( PEAR::isError($relationship) ){ 00605 throw new Exception(sprintf( 00606 'Relationship "%s" does not exist in table "%s" in the ul tag: %s', 00607 $rel, 00608 $root->table()->tablename, 00609 str_replace($ul->innertext,'',$ul->outertext) 00610 ), 00611 self::COMPILE_ERROR 00612 ); 00613 } 00614 } 00615 00616 $relatedRecords = $root->getRelatedRecordObjects($rel); 00617 $lis = $ul->find('li'); 00618 00619 $newlis = array(); 00620 foreach ($relatedRecords as $rrec){ 00621 foreach ($lis as $li){ 00622 $newlis[] = $this->fillReport($root, $rrec, $li, $rel.'.' ); 00623 } 00624 } 00625 00626 $ul->innertext = implode("\n", $newlis); 00627 00628 } 00629 00630 $ols = $el->find('ol[relationship]'); 00631 foreach ($ols as $ol){ 00632 $rel = $ol->relationship; 00633 00634 00635 if ( $this->strict ){ 00636 00637 $relationship = $root->table()->getRelationship($rel); 00638 if ( PEAR::isError($relationship) ){ 00639 throw new Exception(sprintf( 00640 'Relationship "%s" does not exist in table "%s" in the ol tag: %s', 00641 $rel, 00642 $root->table()->tablename, 00643 str_replace($ol->innertext,'',$ol->outertext) 00644 ), 00645 self::COMPILE_ERROR 00646 ); 00647 } 00648 } 00649 00650 $relatedRecords = $root->getRelatedRecordObjects($rel); 00651 $lis = $ol->find('li'); 00652 00653 $newlis = array(); 00654 foreach ($relatedRecords as $rrec){ 00655 foreach ($lis as $li){ 00656 $newlis[] = $this->fillReport($root, $rrec, $li, $rel.'.' ); 00657 } 00658 } 00659 00660 $ol->innertext = implode("\n", $newlis); 00661 00662 } 00663 00664 $tables = $el->find('table[relationship]'); 00665 foreach ($tables as $table){ 00666 $rel = $table->relationship; 00667 00668 if ( $this->strict ){ 00669 00670 $relationship = $root->table()->getRelationship($rel); 00671 if ( PEAR::isError($relationship) ){ 00672 throw new Exception(sprintf( 00673 'Relationship "%s" does not exist in table "%s" in the table tag: %s', 00674 $rel, 00675 $root->table()->tablename, 00676 str_replace($table->innertext,'',$table->outertext) 00677 ), 00678 self::COMPILE_ERROR 00679 ); 00680 } 00681 } 00682 00683 $tbody = $table->find('tbody'); 00684 $relatedRecords = $root->getRelatedRecordObjects($rel); 00685 $trs = $tbody[0]->find('tr'); 00686 00687 $newtrs = array(); 00688 foreach ($relatedRecords as $rrec){ 00689 foreach ($trs as $tr){ 00690 $newtrs[] = $this->fillReport($root, $rrec, $tr, $rel.'.' ); 00691 } 00692 } 00693 00694 $tbody[0]->innertext = implode("\n", $newtrs); 00695 00696 } 00697 00698 00699 $content = $el->outertext; 00700 $content = preg_replace_callback('#\{\$([a-zA-Z0-9_\.]+)\}#', array($this, '_replace_fields'), $content); 00701 00702 $content = preg_replace_callback('#\{@([a-zA-Z]+\([a-zA-Z0-9_\.]+\))\}#', array($this, '_replace_summary_fields'), $content); 00703 00704 00705 $this->context = $oldContext; 00706 return $content; 00707 00708 00709 00710 } 00711 00712 00718 function _replace_fields($matches){ 00719 //print_r($matches); 00720 if ( $this->strict ){ 00721 00722 if ( !$this->context->root->table()->hasField($matches[1]) ){ 00723 throw new Exception(sprintf( 00724 'Field "%s" does not exist in table "%s" so the macro "%s" cannot be resolved.', 00725 $matches[1], 00726 $this->context->root->table()->tablename, 00727 $matches[0] 00728 ), 00729 self::COMPILE_ERROR 00730 ); 00731 } 00732 } 00733 $parts = explode('.', $matches[1]); 00734 if ( count($parts)>1 and $this->context->rrec ){ 00735 $rel = $parts[0]; 00736 $fld = $parts[1]; 00737 00738 if ( isset($this->context->rrec) and $this->context->rrec->_relationshipName == $rel ){ 00739 return $this->context->rrec->htmlValue($fld); 00740 } else { 00741 //echo "Related record: ".$this->context->rrec->_relationshipName; 00742 } 00743 } else { 00744 //echo "Getting field ".$parts[0]."."; 00745 if ( !$this->context->root ){ 00746 throw new Exception("NO root record to replace fields from.", 2000); 00747 } 00748 return $this->context->root->htmlValue($matches[1]); 00749 } 00750 00751 return $matches[0]; 00752 } 00753 00754 00758 function _replace_summary_fields($matches){ 00759 //print_r($matches); 00760 00761 //echo "Getting field ".$parts[0]."."; 00762 if ( !$this->context->root ){ 00763 throw new Exception("NO root record to replace fields from.", 2000); 00764 } 00765 if ( isset($this->context->root->pouch['summaries'][$matches[1]]) ){ 00766 $val = $this->context->root->pouch['summaries'][$matches[1]]; 00767 $expr = self::parseExpression($matches[1]); 00768 $fld = $expr['field']; 00769 00770 if ( $this->strict ){ 00771 00772 if ( !$this->context->root->table()->hasField($fld) ){ 00773 throw new Exception(sprintf( 00774 'Field "%s" does not exist in table "%s" so the summary macro "%s" cannot be resolved.', 00775 $fld, 00776 $this->context->root->table()->tablename, 00777 $matches[0] 00778 ), 00779 self::COMPILE_ERROR 00780 ); 00781 } 00782 00783 if ( !in_array($expr['opt'] , self::$SUMMARY_FUNCTIONS) ){ 00784 throw new Exception(sprintf( 00785 'Summary function "%s" is not supported. Currently only %s are supported. (Found in macro "%s").', 00786 $expr['opt'], 00787 '"'.implode('", "', self::$SUMMARY_FUNCTIONS).'"', 00788 $matches[0] 00789 ), 00790 self::COMPILE_ERROR 00791 ); 00792 00793 } 00794 } 00795 if ( self::isDelegateField($this->context->root->table(), $fld) ) return $val; 00796 00797 $old = $this->context->root->val($fld); 00798 $this->context->root->setValue($fld, $val); 00799 $out = $this->context->root->htmlValue($fld); 00800 $this->context->root->setValue($fld, $old); 00801 return $out; 00802 } 00803 //return $this->context->root->htmlValue($parts[0]); 00804 00805 00806 return $matches[0]; 00807 } 00808 00809 }