123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536 |
- <?php
- require_once('xlsxwriter.class.php');
- class ExcelExporter
- {
- protected $sToken;
- protected $aStatistics;
- protected $sState;
- protected $fStartTime;
- protected $oSearch;
- protected $aObjectsIDs;
- protected $aTableHeaders;
- protected $aAuthorizedClasses;
- protected $iChunkSize = 1000;
- protected $iPosition;
- protected $sOutputFilePath;
- protected $bAdvancedMode;
-
- public function __construct($sToken = null)
- {
- $this->aStatistics = array(
- 'objects_count' => 0,
- 'total_duration' => 0,
- 'data_retrieval_duration' => 0,
- 'excel_build_duration' => 0,
- 'excel_write_duration' => 0,
- 'peak_memory_usage' => 0,
- );
- $this->fStartTime = microtime(true);
- $this->oSearch = null;
-
- $this->sState = 'new';
- $this->aObjectsIDs = array();
- $this->iPosition = 0;
- $this->aAuthorizedClasses = null;
- $this->aTableHeaders = null;
- $this->sOutputFilePath = null;
- $this->bAdvancedMode = false;
- $this->CheckDataDir();
- if ($sToken == null)
- {
- $this->sToken = $this->GetNewToken();
- }
- else
- {
- $this->sToken = $sToken;
- $this->ReloadState();
- }
- }
-
- public function __destruct()
- {
- if (($this->sState != 'done') && ($this->sState != 'error') && ($this->sToken != null))
- {
- // Operation in progress, save the state
- $this->SaveState();
- }
- else
- {
- // Operation completed, cleanup the temp files
- @unlink($this->GetStateFile());
- @unlink($this->GetDataFile());
- }
- self::CleanupOldFiles();
- }
-
- public function SetChunkSize($iChunkSize)
- {
- $this->iChunkSize = $iChunkSize;
- }
-
- public function SetOutputFilePath($sDestFilePath)
- {
- $this->sOutputFilePath = $sDestFilePath;
- }
-
- public function SetAdvancedMode($bAdvanced)
- {
- $this->bAdvancedMode = $bAdvanced;
- }
-
- public function SaveState()
- {
- $aState = array(
- 'state' => $this->sState,
- 'statistics' => $this->aStatistics,
- 'filter' => $this->oSearch->serialize(),
- 'position' => $this->iPosition,
- 'chunk_size' => $this->iChunkSize,
- 'object_ids' => $this->aObjectsIDs,
- 'output_file_path' => $this->sOutputFilePath,
- 'advanced_mode' => $this->bAdvancedMode,
- );
-
- file_put_contents($this->GetStateFile(), json_encode($aState));
-
- return $this->sToken;
- }
-
- public function ReloadState()
- {
- if ($this->sToken == null)
- {
- throw new Exception('ExcelExporter not initialized with a token, cannot reload state');
- }
-
- if (!file_exists($this->GetStateFile()))
- {
- throw new Exception("ExcelExporter: missing status file '".$this->GetStateFile()."', cannot reload state.");
- }
- $sJson = file_get_contents($this->GetStateFile());
- $aState = json_decode($sJson, true);
- if ($aState === null)
- {
- throw new Exception("ExcelExporter:corrupted status file '".$this->GetStateFile()."', not a JSON, cannot reload state.");
- }
-
- $this->sState = $aState['state'];
- $this->aStatistics = $aState['statistics'];
- $this->oSearch = DBObjectSearch::unserialize($aState['filter']);
- $this->iPosition = $aState['position'];
- $this->iChunkSize = $aState['chunk_size'];
- $this->aObjectsIDs = $aState['object_ids'];
- $this->sOutputFilePath = $aState['output_file_path'];
- $this->bAdvancedMode = $aState['advanced_mode'];
- }
-
- public function SetObjectList($oSearch)
- {
- $this->oSearch = $oSearch;
- }
-
- public function Run()
- {
- $sCode = 'error';
- $iPercentage = 100;
- $sMessage = Dict::Format('ExcelExporter:ErrorUnexpected_State', $this->sState);
- $fTime = microtime(true);
-
- try
- {
- switch($this->sState)
- {
- case 'new':
- $oIDSet = new DBObjectSet($this->oSearch);
- $oIDSet->OptimizeColumnLoad(array('id'));
- $this->aObjectsIDs = array();
- while($oObj = $oIDSet->Fetch())
- {
- $this->aObjectsIDs[] = $oObj->GetKey();
- }
- $sCode = 'retrieving-data';
- $iPercentage = 5;
- $sMessage = Dict::S('ExcelExporter:RetrievingData');
- $this->iPosition = 0;
- $this->aStatistics['objects_count'] = count($this->aObjectsIDs);
- $this->aStatistics['data_retrieval_duration'] += microtime(true) - $fTime;
-
- // The first line of the file is the "headers" specifying the label and the type of each column
- $this->GetFieldsList($oIDSet, $this->bAdvancedMode);
- $sRow = json_encode($this->aTableHeaders);
- $hFile = @fopen($this->GetDataFile(), 'ab');
- if ($hFile === false)
- {
- throw new Exception('ExcelExporter: Failed to open temporary data file: "'.$this->GetDataFile().'" for writing.');
- }
- fwrite($hFile, $sRow."\n");
- fclose($hFile);
-
- // Next state
- $this->sState = 'retrieving-data';
- break;
-
- case 'retrieving-data':
- $oCurrentSearch = clone $this->oSearch;
- $aIDs = array_slice($this->aObjectsIDs, $this->iPosition, $this->iChunkSize);
-
- $oCurrentSearch->AddCondition('id', $aIDs, 'IN');
- $hFile = @fopen($this->GetDataFile(), 'ab');
- if ($hFile === false)
- {
- throw new Exception('ExcelExporter: Failed to open temporary data file: "'.$this->GetDataFile().'" for writing.');
- }
- $oSet = new DBObjectSet($oCurrentSearch);
- $this->GetFieldsList($oSet, $this->bAdvancedMode);
- while($aObjects = $oSet->FetchAssoc())
- {
- $aRow = array();
- foreach($this->aAuthorizedClasses as $sAlias => $sClassName)
- {
- $oObj = $aObjects[$sAlias];
- if ($this->bAdvancedMode)
- {
- $aRow[] = $oObj->GetKey();
- }
- foreach($this->aFieldsList[$sAlias] as $sAttCodeEx => $oAttDef)
- {
- $value = $oObj->Get($sAttCodeEx);
- if ($value instanceOf ormCaseLog)
- {
- // Extract the case log as text and remove the "===" which make Excel think that the cell contains a formula the next time you edit it!
- $sExcelVal = trim(preg_replace('/========== ([^=]+) ============/', '********** $1 ************', $value->GetText()));
- }
- else
- {
- $sExcelVal = $oAttDef->GetEditValue($value, $oObj);
- }
- $aRow[] = $sExcelVal;
- }
- }
- $sRow = json_encode($aRow);
- fwrite($hFile, $sRow."\n");
- }
- fclose($hFile);
-
- if (($this->iPosition + $this->iChunkSize) > count($this->aObjectsIDs))
- {
- // Next state
- $this->sState = 'building-excel';
- $sCode = 'building-excel';
- $iPercentage = 80;
- $sMessage = Dict::S('ExcelExporter:BuildingExcelFile');
- }
- else
- {
- $sCode = 'retrieving-data';
- $this->iPosition += $this->iChunkSize;
- $iPercentage = 5 + round(75 * ($this->iPosition / count($this->aObjectsIDs)));
- $sMessage = Dict::S('ExcelExporter:RetrievingData');
- }
- break;
-
- case 'building-excel':
- $hFile = @fopen($this->GetDataFile(), 'rb');
- if ($hFile === false)
- {
- throw new Exception('ExcelExporter: Failed to open temporary data file: "'.$this->GetDataFile().'" for reading.');
- }
- $sHeaders = fgets($hFile);
- $aHeaders = json_decode($sHeaders, true);
-
- $aData = array();
- while($sLine = fgets($hFile))
- {
- $aRow = json_decode($sLine);
- $aData[] = $aRow;
- }
- fclose($hFile);
- @unlink($this->GetDataFile());
-
- $fStartExcel = microtime(true);
- $writer = new XLSXWriter();
- $writer->setAuthor(UserRights::GetUserFriendlyName());
- $writer->writeSheet($aData,'Sheet1', $aHeaders);
- $fExcelTime = microtime(true) - $fStartExcel;
- $this->aStatistics['excel_build_duration'] = $fExcelTime;
-
- $fTime = microtime(true);
- $writer->writeToFile($this->GetExcelFilePath());
- $fExcelSaveTime = microtime(true) - $fTime;
- $this->aStatistics['excel_write_duration'] = $fExcelSaveTime;
-
- // Next state
- $this->sState = 'done';
- $sCode = 'done';
- $iPercentage = 100;
- $sMessage = Dict::S('ExcelExporter:Done');
- break;
-
- case 'done':
- $this->sState = 'done';
- $sCode = 'done';
- $iPercentage = 100;
- $sMessage = Dict::S('ExcelExporter:Done');
- break;
- }
- }
- catch(Exception $e)
- {
- $sCode = 'error';
- $sMessage = $e->getMessage();
- }
-
- $this->aStatistics['total_duration'] += microtime(true) - $fTime;
- $peak_memory = memory_get_peak_usage(true);
- if ($peak_memory > $this->aStatistics['peak_memory_usage'])
- {
- $this->aStatistics['peak_memory_usage'] = $peak_memory;
- }
-
- return array(
- 'code' => $sCode,
- 'message' => $sMessage,
- 'percentage' => $iPercentage,
- );
- }
-
- public function GetExcelFilePath()
- {
- if ($this->sOutputFilePath == null)
- {
- return APPROOT.'data/bulk_export/'.$this->sToken.'.xlsx';
- }
- else
- {
- return $this->sOutputFilePath;
- }
- }
-
- public static function GetExcelFileFromToken($sToken)
- {
- return @file_get_contents(APPROOT.'data/bulk_export/'.$sToken.'.xlsx');
- }
-
- public static function CleanupFromToken($sToken)
- {
- @unlink(APPROOT.'data/bulk_export/'.$sToken.'.status');
- @unlink(APPROOT.'data/bulk_export/'.$sToken.'.data');
- @unlink(APPROOT.'data/bulk_export/'.$sToken.'.xlsx');
- }
-
- public function Cleanup()
- {
- self::CleanupFromToken($this->sToken);
- }
-
- /**
- * Delete all files in the data/bulk_export directory which are older than 1 day
- * unless a different delay is configured.
- */
- public static function CleanupOldFiles()
- {
- $aFiles = glob(APPROOT.'data/bulk_export/*.*');
- $iDelay = MetaModel::GetConfig()->Get('xlsx_exporter_cleanup_old_files_delay');
-
- if($iDelay > 0)
- {
- foreach($aFiles as $sFile)
- {
- $iModificationTime = filemtime($sFile);
-
- if($iModificationTime < (time() - $iDelay))
- {
- // Temporary files older than one day are deleted
- //echo "Supposed to delete: '".$sFile." (Unix Modification Time: $iModificationTime)'\n";
- @unlink($sFile);
- }
- }
- }
- }
-
- public function DisplayStatistics(Page $oPage)
- {
- $aStats = array(
- 'Number of objects exported' => $this->aStatistics['objects_count'],
- 'Total export duration' => sprintf('%.3f s', $this->aStatistics['total_duration']),
- 'Data retrieval duration' => sprintf('%.3f s', $this->aStatistics['data_retrieval_duration']),
- 'Excel build duration' => sprintf('%.3f s', $this->aStatistics['excel_build_duration']),
- 'Excel write duration' => sprintf('%.3f s', $this->aStatistics['excel_write_duration']),
- 'Peak memory usage' => self::HumanDisplay($this->aStatistics['peak_memory_usage']),
- );
-
- if ($oPage instanceof CLIPage)
- {
- $oPage->add($this->GetStatistics('text'));
- }
- else
- {
- $oPage->add($this->GetStatistics('html'));
- }
- }
-
- public function GetStatistics($sFormat = 'html')
- {
- $sStats = '';
- $aStats = array(
- 'Number of objects exported' => $this->aStatistics['objects_count'],
- 'Total export duration' => sprintf('%.3f s', $this->aStatistics['total_duration']),
- 'Data retrieval duration' => sprintf('%.3f s', $this->aStatistics['data_retrieval_duration']),
- 'Excel build duration' => sprintf('%.3f s', $this->aStatistics['excel_build_duration']),
- 'Excel write duration' => sprintf('%.3f s', $this->aStatistics['excel_write_duration']),
- 'Peak memory usage' => self::HumanDisplay($this->aStatistics['peak_memory_usage']),
- );
-
- if ($sFormat == 'text')
- {
- foreach($aStats as $sLabel => $sValue)
- {
- $sStats .= "+------------------------------+----------+\n";
- $sStats .= sprintf("|%-30s|%10s|\n", $sLabel, $sValue);
- }
- $sStats .= "+------------------------------+----------+";
- }
- else
- {
- $sStats .= '<table><tbody>';
- foreach($aStats as $sLabel => $sValue)
- {
- $sStats .= "<tr><td>$sLabel</td><td>$sValue</td></tr>";
- }
- $sStats .= '</tbody></table>';
-
- }
- return $sStats;
- }
-
- public static function HumanDisplay($iSize)
- {
- $aUnits = array('B','KB','MB','GB','TB','PB');
- return @round($iSize/pow(1024,($i=floor(log($iSize,1024)))),2).' '.$aUnits[$i];
- }
-
- protected function CheckDataDir()
- {
- if(!is_dir(APPROOT."data/bulk_export"))
- {
- @mkdir(APPROOT."data/bulk_export", 0777, true /* recursive */);
- clearstatcache();
- }
- if (!is_writable(APPROOT."data/bulk_export"))
- {
- throw new Exception('Data directory "'.APPROOT.'data/bulk_export" could not be written.');
- }
- }
-
- protected function GetStateFile($sToken = null)
- {
- if ($sToken == null)
- {
- $sToken = $this->sToken;
- }
- return APPROOT."data/bulk_export/$sToken.status";
- }
-
- protected function GetDataFile()
- {
- return APPROOT.'data/bulk_export/'.$this->sToken.'.data';
- }
-
- protected function GetNewToken()
- {
- $iNum = rand();
- do
- {
- $iNum++;
- $sToken = sprintf("%08x", $iNum);
- $sFileName = $this->GetStateFile($sToken);
- $hFile = @fopen($sFileName, 'x');
- }
- while($hFile === false);
-
- fclose($hFile);
- return $sToken;
- }
-
- protected function GetFieldsList($oSet, $bFieldsAdvanced = false, $bLocalize = true, $aFields = null)
- {
- $this->aFieldsList = array();
-
- $oAppContext = new ApplicationContext();
- $aClasses = $oSet->GetFilter()->GetSelectedClasses();
- $this->aAuthorizedClasses = array();
- foreach($aClasses as $sAlias => $sClassName)
- {
- if (UserRights::IsActionAllowed($sClassName, UR_ACTION_READ, $oSet) && (UR_ALLOWED_YES || UR_ALLOWED_DEPENDS))
- {
- $this->aAuthorizedClasses[$sAlias] = $sClassName;
- }
- }
- $aAttribs = array();
- $this->aTableHeaders = array();
- foreach($this->aAuthorizedClasses as $sAlias => $sClassName)
- {
- $aList[$sAlias] = array();
-
- foreach(MetaModel::ListAttributeDefs($sClassName) as $sAttCode => $oAttDef)
- {
- if (is_null($aFields) || (count($aFields) == 0))
- {
- // Standard list of attributes (no link sets)
- if ($oAttDef->IsScalar() && ($oAttDef->IsWritable() || $oAttDef->IsExternalField()))
- {
- $sAttCodeEx = $oAttDef->IsExternalField() ? $oAttDef->GetKeyAttCode().'->'.$oAttDef->GetExtAttCode() : $sAttCode;
-
- if ($oAttDef->IsExternalKey(EXTKEY_ABSOLUTE))
- {
- if ($bFieldsAdvanced)
- {
- $aList[$sAlias][$sAttCodeEx] = $oAttDef;
-
- if ($oAttDef->IsExternalKey(EXTKEY_RELATIVE))
- {
- $sRemoteClass = $oAttDef->GetTargetClass();
- foreach(MetaModel::GetReconcKeys($sRemoteClass) as $sRemoteAttCode)
- {
- $this->aFieldsList[$sAlias][$sAttCode.'->'.$sRemoteAttCode] = MetaModel::GetAttributeDef($sRemoteClass, $sRemoteAttCode);
- }
- }
- }
- }
- else
- {
- // Any other attribute
- $this->aFieldsList[$sAlias][$sAttCodeEx] = $oAttDef;
- }
- }
- }
- else
- {
- // User defined list of attributes
- if (in_array($sAttCode, $aFields) || in_array($sAlias.'.'.$sAttCode, $aFields))
- {
- $this->aFieldsList[$sAlias][$sAttCode] = $oAttDef;
- }
- }
- }
- if ($bFieldsAdvanced)
- {
- $this->aTableHeaders['id'] = '0';
- }
- foreach($this->aFieldsList[$sAlias] as $sAttCodeEx => $oAttDef)
- {
- $sLabel = $bLocalize ? MetaModel::GetLabel($sClassName, $sAttCodeEx, isset($aParams['showMandatoryFields'])) : $sAttCodeEx;
- if($oAttDef instanceof AttributeDateTime)
- {
- $this->aTableHeaders[$sLabel] = 'datetime';
- }
- else
- {
- $this->aTableHeaders[$sLabel] = 'string';
- }
- }
- }
- }
- }
|