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 .= ''; foreach($aStats as $sLabel => $sValue) { $sStats .= ""; } $sStats .= '
$sLabel$sValue
'; } 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_NO) { $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'; } } } } }