db = $db; $this->request = $request; } /** * @param DataTableBuildConfig $buildConfig * * @return bool */ public function canFetchData(DataTableBuildConfig $buildConfig) { if (!$this->request->isAjax()) { return false; } if ($this->request->getMethod() !== $buildConfig->getAjaxMethod()) { return false; } $tableNameDefined = $buildConfig->getTableName(); $tableNameRequested = $this->request->getParams()->getTableName(); return $tableNameDefined === $tableNameRequested; } /** * @param DataTableBuildConfig $buildConfig * * @return bool */ public function canExportData(DataTableBuildConfig $buildConfig) { $exportParams = (array)$this->request->getParams()->getExportValues(); if (empty($exportParams['format']) || empty($exportParams['result'])) { return false; } if ($this->request->isAjax()) { return false; } $tableNameDefined = $buildConfig->getTableName(); $tableNameRequested = $this->request->getParams()->getTableName(); return $tableNameDefined === $tableNameRequested; } /** * @param DataTableInterface $table * * @return DataTableDataResult */ public function fetchData(DataTableInterface $table) { $startParam = $this->request->getParams()->getStart(); $lengthParam = $this->request->getParams()->getLength(); try { $debugging = false; if ($table->getFeatures()->has(DebugFeature::class)) { /** @var DebugFeature $debugFeature */ $debugFeature = $table->getFeatures()->get(DebugFeature::class); $debugging = $debugFeature->isEnabled(); } if ($debugging === true) { $debugData = ['profiler' => ['start' => microtime(true)]]; } $baseQuery = $table->getBaseQuery(); $cols = $baseQuery->getCols(); // Set up query for total record count $recordsTotalQuery = clone $baseQuery; $recordsTotalQuery ->resetCols() ->cols([sprintf('COUNT(%s) AS num', $cols[0])]); // Apply filters and searches $this->applyFilters($table); $this->applyColumnSearch($table, $baseQuery); $this->applyGlobalSearch($table, $baseQuery); // Set up query for data + limit result set $dataQuery = clone $baseQuery; $dataQuery->offset($startParam); $dataQuery->limit($lengthParam); if ($startParam === -1 || $lengthParam === -1) { $dataQuery->offset(0); $dataQuery->limit(0); } // Apply ORDER BY + LIMIT $sortingValues = $this->prepareSortingValue($table); $this->applySorting($dataQuery, $sortingValues); $this->applyPaging($dataQuery, $startParam, $lengthParam); // Set up query for filtered record count // (= Record count with applied filters and searches) $recordsFilteredQuery = clone $baseQuery; $recordsFilteredQuery->resetCols()->cols([sprintf('COUNT(%s) AS num', $cols[0])]); // Ergebnisanzahl; mit Filter $recordsFiltered = $this->db->fetchValue( $recordsFilteredQuery->getStatement(), $recordsFilteredQuery->getBindValues() ); // Ergebnisanzahl; ohne Filter $recordsTotal = $this->db->fetchValue( $recordsTotalQuery->getStatement(), $recordsTotalQuery->getBindValues() ); // Fetch data; displayed result $data = $this->db->fetchAll( $dataQuery->getStatement(), $dataQuery->getBindValues() ); // Column-Formatter anwenden $columnFormatters = $table->getColumns()->getFormatters(); $this->applyColumnFormatters($data, $columnFormatters); // Row-Formatter anwenden // @todo In RowClassesFeature auslagern if ($table->getFeatures()->has(RowClassesFeature::class)) { /** @var RowClassesFeature $rowStyling */ $rowStyling = $table->getFeatures()->get(RowClassesFeature::class); if ($rowStyling->hasCustomFormatter()) { $rowFormatter = $rowStyling->getCustomFormatter(); foreach ($data as &$rowValues) { $rowClasses = $rowStyling->getClassesString(); foreach ($rowFormatter as $closure) { $rowClasses .= $closure($rowValues); } $rowValues['DT_RowClass'] = $rowClasses; } unset($rowValues); } } // ID-Attribut für jede Zeile setzen $tableName = $table->getConfig()->getTableName(); $this->appendRowIdAttribute($data, $tableName); // Result-Objekt bauen $result = new DataTableDataResult( (int)$this->request->getParams()->getDraw(), (int)$recordsTotal, (int)$recordsFiltered, (array)$data ); } catch (Exception $exception) { $result = new DataTableDataResult(); $result->setErrorMessage(sprintf( 'Unhandled exception: (%s) %s', get_class($exception), $exception->getMessage() )); } if ($debugging === true) { $debugData['profiler']['finish'] = microtime(true); $debugData['profiler']['duration_real'] = $debugData['profiler']['finish'] - $debugData['profiler']['start']; $debugData['profiler']['duration'] = sprintf('%.6f', $debugData['profiler']['duration_real']) . ' seconds'; if (isset($dataQuery)) { $debugData['query']['statement'] = $dataQuery->getStatement(); $debugData['query']['bindings'] = var_export($dataQuery->getBindValues(), true); } $result->setDebugInfo($debugData); } return $result; } /** * @param DataTableInterface $table * * @return string Path to export file */ public function exportData(DataTableInterface $table) { $startParam = (int)$this->request->getParams()->getStart(); $lengthParam = (int)$this->request->getParams()->getLength(); $exportParams = (array)$this->request->getParams()->getExportValues(); $exportFormat = !empty($exportParams['format']) ? $exportParams['format'] : 'csv'; if ($exportFormat !== 'csv') { throw new InvalidArgumentException(sprintf( 'Invalid export format "%s". Only "csv" is valid.', $exportFormat )); } $exportResult = !empty($exportParams['result']) ? $exportParams['result'] : 'page'; if (!in_array($exportResult, ['all', 'page'], true)) { throw new InvalidArgumentException(sprintf( 'Invalid export result parameter value "%s". Valid values: %s', $exportResult, implode(', ', ['all', 'page']) )); } // Alle Ergebnisse exportieren if ($exportResult === 'all') { $startParam = -1; $lengthParam = -1; } $fileName = uniqid('export-' . $table->getConfig()->getTableName(), false) . '.csv'; $filePath = sys_get_temp_dir() . '/' . $fileName; $csv = @fopen($filePath, 'x+b'); if ($csv === false) { throw new InvalidResourceException(sprintf('Failed to open resource for file path "%s".', $filePath)); } $writer = new CsvWriter($csv, new CsvConfig()); $titles = []; $dbCols = []; foreach ($table->getColumns() as $column) { /** @var Column $column */ if ($column->isExportable() && !empty($column->getDbColumn())) { $titles[] = $column->getTitle(); $dbCols[] = $column->getDbColumn(); } } $writer->writeLine($titles); $dataQuery = clone $table->getBaseQuery(); $dataQuery->resetCols()->cols($dbCols); // Apply filters and searches $this->applyFilters($table); $this->applyColumnSearch($table, $dataQuery); $this->applyGlobalSearch($table, $dataQuery); // Apply ORDER BY $sortingValues = $this->prepareSortingValue($table); $this->applySorting($dataQuery, $sortingValues); $itemsPerStep = 2500; $currentOffset = 0; $hasResults = true; if ($exportResult === 'page') { $itemsPerStep = $lengthParam; $currentOffset = $startParam; } do { $dataQuery->offset($currentOffset); $dataQuery->limit($itemsPerStep); $data = $this->db->yieldAll( $dataQuery->getStatement(), $dataQuery->getBindValues() ); if (!$data->valid()) { $hasResults = false; continue; } $writer->writeLines($data); $currentOffset += $itemsPerStep; // Nach einer Iteration aufhören, wenn nur eine Seite exportiert werden soll if ($exportResult === 'page') { $hasResults = false; } } while ($hasResults); fclose($csv); return $filePath; } /** * @param DataTableInterface $table * * @return void */ private function applyFilters(DataTableInterface $table) { /** @var FilterInterface $filter */ foreach ($table->getFilters() as $filter) { $filter->applyFilter($table, $this->request); } } /** * Suche über das allgemeine Suchfeld verarbeiten (oben rechts) * * @param DataTableInterface $table * @param SelectQuery $query * * @return void */ private function applyGlobalSearch(DataTableInterface $table, SelectQuery $query) { $searchValue = $this->getSearchParam(); if (empty($searchValue)) { return; } $searchParts = explode(' ', $searchValue); $searchParts = array_filter($searchParts, 'trim'); // Custom Search @todo Momentan ohne Funktion; Es gibt keine Möglichkeit zum Setzen der Einstellung // Beispiel-Setter: //$this->setCustomSearch(function (SelectQuery $query) { // return $query // ->cols(['artikel.id']) // ->from('artikel') // ->where('artikel.name_de LIKE :query') // ->orWhere('artikel.name_en LIKE :query'); //}); //$customSearchClosure = $table->getCustomSearch(); //if ($customSearchClosure !== null) { // $matchColumn = $query->getCols()[0]; // $customSearchQuery = $customSearchClosure($this->db->select()); // // $query->joinSubSelect('inner', $customSearchQuery, 'matches', 'matches.id = ' . $matchColumn); // $query->bindValue('query', '%' . $searchValue . '%'); // // return; //} // Normale Suche $searchableDbColumns = $table->getColumns()->getSearchableDbColumns(); foreach ($searchParts as $searchWord) { $query->where(static function (SelectQuery $select) use ($searchableDbColumns, $searchWord) { foreach ($searchableDbColumns as $searchDbColumn) { $select->orWhere(sprintf('%s LIKE ?', $searchDbColumn), '%' . $searchWord . '%'); } }); } } /** * @todo In ColumnFilterFeature auslagern * * @param DataTableInterface $table * @param SelectQuery $query * * @return void */ private function applyColumnSearch(DataTableInterface $table, SelectQuery $query) { //$columnFilter = $table->getFeatures()->get(ColumnFilterFeature::class); $params = (array)$this->request->getParams()->getColumnsValues(); foreach ($params as $index => $param) { $searchValue = $param['search']['value']; if (empty($searchValue)) { continue; } // Spaltensuche wurde ausgefüllt $column = $table->getColumns()->getByName($param['name']); if ($column === null) { continue; } // Zahlenbereich-Suche if (strpos($searchValue, 'number_range:') === 0) { $searchPattern = str_replace([':null|', '|null'], '|', $searchValue); if ($searchPattern === 'number_range:|') { continue; // Leere Suche } $searchPattern = str_replace('number_range:', '', $searchPattern); $searchParts = explode('|', $searchPattern); if (count($searchParts) !== 2) { continue; } $valueFrom = str_replace(',', '.', $searchParts[0]); $valueTo = str_replace(',', '.', $searchParts[1]); if (is_numeric($valueFrom)) { $query->where($column->getDbColumn() . ' >= ?', (float)$valueFrom); } if (is_numeric($valueTo)) { $query->where($column->getDbColumn() . ' <= ?', (float)$valueTo); } continue; } // Normale Textsuche $query->where($column->getDbColumn() . ' LIKE ?', '%' . $searchValue . '%'); } } /** * @see https://datatables.net/manual/server-side#Sent-parameters Parameters 'start' and 'length' * * @param SelectQuery $dataQuery * @param int $startValue * @param int $lengthValue * * @return void */ private function applyPaging(SelectQuery $dataQuery, $startValue, $lengthValue) { $dataQuery->offset($startValue); $dataQuery->limit($lengthValue); if ($startValue === -1 || $lengthValue === -1) { $dataQuery->offset(0); $dataQuery->limit(0); } } /** * @param SelectQuery $query * @param array $sortingValues * * @return void */ private function applySorting(SelectQuery $query, array $sortingValues) { if (!empty($sortingValues)) { $query->resetOrderBy(); foreach ($sortingValues as $sortColumn => $sortDirection) { $query->orderBy([sprintf('%s %s', $sortColumn, strtoupper($sortDirection))]); } } } /** * Column-Formatter anwenden * * @param array $data * @param array $formatters * * @return void */ private function applyColumnFormatters(array &$data, array $formatters = []) { if (empty($formatters)) { return; } foreach ($formatters as $colName => $formatter) { if (!is_callable($formatter)) { continue; } foreach ($data as &$rowData) { $cellData = $rowData[$colName]; $newValue = $this->callColumnFormatter($formatter, $cellData, $rowData); $rowData[$colName] = $newValue; } unset($rowData); } } /** * @param Closure $callback * @param string $value * @param array $rowValues * * @return string */ private function callColumnFormatter(Closure $callback, $value, $rowValues) { return $callback($value, $rowValues); } /** * @param DataTableInterface $table * * @return array */ private function prepareSortingValue(DataTableInterface $table) { $orderValue = $this->getOrderParam(); $sorting = []; foreach ($orderValue as $orderItem) { $columnIndex = (int)$orderItem['column']; $column = $table->getColumns()->getByIndex($columnIndex); if ($column === null) { break; } $columnName = $column->getDbColumn(); $sortDirection = in_array(strtolower($orderItem['dir']), ['asc', 'desc'], true) ? strtolower($orderItem['dir']) : null; if ($columnName !== null && $sortDirection !== null) { $sorting[$columnName] = strtoupper($sortDirection); } } return $sorting; } /** * ID-Attribut für jede Zeile setzen * * @param array $data * @param string $tableName * * @return void */ private function appendRowIdAttribute(&$data, $tableName) { // Row-ID hinzufügen foreach ($data as &$rowValues) { foreach ($rowValues as $key => &$value) { if ($key === 'id') { $rowValues['DT_RowId'] = sprintf('%s_row_%s', $tableName, $value); } } unset($value); } unset($rowValues); } /** * @return string */ private function getSearchParam() { return $this->request->getParams()->getSearchValues()['value']; } /** * @return array */ private function getOrderParam() { return (array)$this->request->getParams()->getOrderValues(); } }