*/ class SingleDeliveryResultsExporter implements ResultsExporterInterface { use OntologyAwareTrait; use ServiceLocatorAwareTrait; use FilesystemAwareTrait; public const RESULT_FORMAT = 'CSV'; /** * @var \core_kernel_classes_Resource */ private $delivery; /** * @var ResultsService */ private $resultsService; /** * Metadata columns to be exported. * * @var array */ private $columnsToExport = []; /** * @var tao_models_classes_table_Column[] */ private $builtColumns = []; /** * Which submitted variables are we exporting? * * Possible values: * - lastSubmitted (default) * - firstSubmitted * * @var string */ private $variableToExport = ResultsService::VARIABLES_FILTER_LAST_SUBMITTED; /** * @var array */ private $storageOptions = []; /** * @var ColumnsProvider */ private $columnsProvider; /** * @var array */ private $filters = []; const CHUNK_SIZE = 100; /** * @param string|\core_kernel_classes_Resource $delivery * @param ResultsService $resultsService * @param ColumnsProvider $columnsProvider * @throws \common_exception_NotFound */ public function __construct($delivery, ResultsService $resultsService, ColumnsProvider $columnsProvider) { $this->delivery = $this->getResource($delivery); if (!$this->delivery->exists()) { throw new \common_exception_NotFound('Results Exporter: delivery "' . $this->delivery->getUri() . '" does not exist.'); } $this->resultsService = $resultsService; $this->columnsProvider = $columnsProvider; } /** * @return string */ public function getResultFormat() { return static::RESULT_FORMAT; } /** * @inheritdoc */ public function getResourceToExport() { return $this->delivery; } /** * @inheritdoc */ public function setColumnsToExport($columnsToExport) { if (is_string($columnsToExport)) { $columnsToExport = $this->decodeColumns($columnsToExport); } $this->columnsToExport = (array) $columnsToExport; return $this; } /** * @inheritdoc */ public function getColumnsToExport() { if (empty($this->builtColumns)) { if (!empty($this->columnsToExport)) { $columns = $this->columnsToExport; } else { $variables = array_merge($this->columnsProvider->getGradeColumns(), $this->columnsProvider->getResponseColumns()); usort($variables, function ($a, $b) { return strcmp($a["label"], $b["label"]); }); $columns = array_merge( $this->columnsProvider->getTestTakerColumns(), $this->columnsProvider->getDeliveryColumns(), $variables ); } // Needed by the filter to filter by start and end date // filtering will be done as a post-processing $columns = array_merge($columns, $this->columnsProvider->getDeliveryExecutionColumns()); // build column objects $this->builtColumns = $this->buildColumns($columns); } return $this->builtColumns; } /** * @inheritdoc */ public function setVariableToExport($variableToExport) { $allowedFilters = [ ResultsService::VARIABLES_FILTER_ALL, ResultsService::VARIABLES_FILTER_FIRST_SUBMITTED, ResultsService::VARIABLES_FILTER_LAST_SUBMITTED, ]; if (!in_array($variableToExport, $allowedFilters)) { throw new \InvalidArgumentException('Results Exporter: wrong submitted variable "' . $variableToExport . '"'); } $this->variableToExport = $variableToExport; return $this; } public function setFiltersToExport($filters) { $this->filters = $filters; return $this; } public function getFiltersToExport() { return $this->filters; } /** * @inheritdoc */ public function getVariableToExport() { return $this->variableToExport; } /** * @inheritdoc */ public function setStorageOptions(array $storageOptions) { $this->storageOptions = $storageOptions; return $this; } /** * @return array */ public function getData() { $results = $this->resultsService->getResultsByDelivery( $this->getResourceToExport(), $this->storageOptions, $this->getFiltersToExport() ); $cells = $this->resultsService->getCellsByResults( $results, $this->getColumnsToExport(), $this->getVariableToExport(), $this->getFiltersToExport(), 0, PHP_INT_MAX ); if ($cells === null) { $cells = []; } // flattening data: only 'cell' is what we need return array_map(function ($row) { return $row['cell']; }, $cells); } private function sortByStartDate(&$data) { usort($data, function ($a, $b) { $bDate = $b[ColumnsProvider::LABEL_START_DELIVERY_EXECUTION]; $aDate = $a[ColumnsProvider::LABEL_START_DELIVERY_EXECUTION]; $startB = $bDate ? strtotime($bDate) : 0; $startA = $aDate ? strtotime($aDate) : 0; return $startB - $startA; }); $data = array_reverse($data); } /** * @param array $results * @param int $offset * @param null $limit * @return array * @throws \common_Exception * @throws \common_exception_Error */ private function getCells($results, $offset = 0, $limit = null) { $cells = $this->resultsService->getCellsByResults( $results, $this->getColumnsToExport(), $this->getVariableToExport(), $this->getFiltersToExport(), $offset, $limit ); if ($cells === null) { return null; } // flattening data: only 'cell' is what we need return array_map(function ($row) { return $row['cell']; }, $cells); } /** * @inheritdoc */ public function export($destination = null) { $columnNames = $this->resultsService->getColumnNames($this->getColumnsToExport()); $data = $this->resultsService->getResultsByDelivery( $this->getResourceToExport(), $this->storageOptions, $this->getFiltersToExport() ); $offset = 0; $result = []; // getCells() consumes much memory inside it, so let's collect cells iteratively do { $cells = $this->getCells($data, $offset, self::CHUNK_SIZE); $offset += self::CHUNK_SIZE; if ($cells === null) { break; } foreach ($cells as $row) { $rowResult = []; foreach ($row as $rowKey => $rowVal) { $rowResult[$columnNames[$rowKey]] = $rowVal[0]; } $result[] = $rowResult; } } while ($cells !== null); $this->sortByStartDate($result); //If there are no executions yet, the file is exported but contains only the header if (empty($result)) { $result = [array_fill_keys($columnNames, '')]; } $exporter = $this->getExporter($result); unset($columnNames, $data, $result); return is_null($destination) ? $this->saveStringToStorage($this->getExportData($exporter), $this->getFileName()) : $this->saveToLocal($exporter, $destination); } /** * @param array $result * @return CsvExporter */ protected function getExporter(array $result) { return new CsvExporter($result); } /** * @param CsvExporter $exporter * @return string * @throws \common_exception_InvalidArgumentType */ protected function getExportData($exporter) { return $exporter->export(true, false); } /** * @param CsvExporter $exporter * @param string $destination * @return string * @throws \common_exception_InvalidArgumentType */ private function saveToLocal($exporter, $destination) { $fullPath = realpath($destination) . DIRECTORY_SEPARATOR . $this->getFileName(); file_put_contents($fullPath, $this->getExportData($exporter)); return $fullPath; } /** * @return string */ private function getFileName() { return 'results_export_' . strtolower(\tao_helpers_Display::textCleaner($this->delivery->getLabel(), '*')) . '_' . \tao_helpers_Uri::getUniqueId($this->delivery->getUri()) . '_' . date('YmdHis') . rand(10, 99) //more unique name . '.' . strtolower($this->getResultFormat()); } /** * Decode the JSON encoded columns. * * @param string $columnsJson * @return array */ private function decodeColumns($columnsJson) { return ($columnsData = json_decode($columnsJson, true)) !== null && json_last_error() === JSON_ERROR_NONE ? $columnsData : []; } /** * Build the column objects from the provided array of decoded column values. For example: * * [ * type = "oat\taoOutcomeUi\model\table\ContextTypePropertyColumn" * label = "Test Taker" * prop = "http://www.w3.org/2000/01/rdf-schema#label" * contextType = "test_taker" * ] * [ * type = "oat\taoOutcomeUi\model\table\GradeColumn" * label = "Planets and moons-SCORE" * contextId = "http://taoplatform.loc/tao.rdf#i1499248290562399" * contextLabel = "Planets and moons" * variableIdentifier = "SCORE" * ] * * @param array $columnsData * @return tao_models_classes_table_Column[] */ private function buildColumns($columnsData) { $columns = []; $dataProvider = new VariableDataProvider(); foreach ($columnsData as $column) { if (!isset($column['type']) || !is_subclass_of($column['type'], tao_models_classes_table_Column::class)) { throw new \RuntimeException('Column type not specified or wrong type provided'); } $column = tao_models_classes_table_Column::buildColumnFromArray($column); if (!is_null($column)) { if ($column instanceof VariableColumn) { $column->setDataProvider($dataProvider); } if ($column instanceof ContextTypePropertyColumn && $column->getProperty()->getUri() == RDFS_LABEL) { $column->label = $column->isTestTakerType() ? __('Test Taker') : __('Delivery'); } $columns[] = $column; } } return $columns; } /** * @see FilesystemAwareTrait::getFileSystemService() */ protected function getFileSystemService() { return $this->getServiceLocator() ->get(FileSystemService::SERVICE_ID); } }