diff options
author | Thomas Steur <thomas.steur@gmail.com> | 2015-02-26 05:23:28 +0300 |
---|---|---|
committer | Thomas Steur <thomas.steur@gmail.com> | 2015-03-05 05:31:18 +0300 |
commit | 287aad82841f0f0e85406192b2f1f865bc0be67d (patch) | |
tree | d9c035a484c3f2727d7fa1cdf14ccd213c308508 /core | |
parent | a1cb3695319b321f92bb0a4fd31892a9bc1bdf38 (diff) |
Faster flattening for many reports
Diffstat (limited to 'core')
-rw-r--r-- | core/API/DataTableManipulator.php | 8 | ||||
-rw-r--r-- | core/API/DataTableManipulator/Flattener.php | 41 | ||||
-rw-r--r-- | core/API/DataTableManipulator/ReportTotalsCalculator.php | 27 | ||||
-rw-r--r-- | core/API/DataTablePostProcessor.php | 10 | ||||
-rw-r--r-- | core/Archive.php | 33 | ||||
-rw-r--r-- | core/DataTable.php | 69 | ||||
-rw-r--r-- | core/DataTable/Filter/Sort.php | 101 | ||||
-rw-r--r-- | core/DataTable/Filter/Truncate.php | 2 | ||||
-rw-r--r-- | core/DataTable/Map.php | 33 | ||||
-rw-r--r-- | core/DataTable/Row.php | 44 | ||||
-rw-r--r-- | core/Plugin/API.php | 2 | ||||
-rw-r--r-- | core/Plugin/Metric.php | 45 | ||||
-rw-r--r-- | core/Plugin/Report.php | 16 | ||||
-rw-r--r-- | core/UrlHelper.php | 5 |
14 files changed, 304 insertions, 132 deletions
diff --git a/core/API/DataTableManipulator.php b/core/API/DataTableManipulator.php index 23036c8e82..862f2db087 100644 --- a/core/API/DataTableManipulator.php +++ b/core/API/DataTableManipulator.php @@ -114,14 +114,6 @@ abstract class DataTableManipulator return null; } - if ($row->getMetadata('idsubdatatable_in_db')) { - $manager = DataTable\Manager::getInstance(); - $table = $manager->getTable($idSubTable); - if ($table) { - return $table; - } - } - $request['idSubtable'] = $idSubTable; if ($dataTable) { $period = $dataTable->getMetadata(DataTableFactory::TABLE_METADATA_PERIOD_INDEX); diff --git a/core/API/DataTableManipulator/Flattener.php b/core/API/DataTableManipulator/Flattener.php index cfee16e6ba..a976b7a62e 100644 --- a/core/API/DataTableManipulator/Flattener.php +++ b/core/API/DataTableManipulator/Flattener.php @@ -38,17 +38,16 @@ class Flattener extends DataTableManipulator * Separator for building recursive labels (or paths) * @var string */ - public $recursiveLabelSeparator = ' - '; + public $recursiveLabelSeparator = ''; /** * @param DataTable $dataTable + * @param string $recursiveLabelSeparator * @return DataTable|DataTable\Map */ - public function flatten($dataTable) + public function flatten($dataTable, $recursiveLabelSeparator) { - if ($this->apiModule == 'Actions' || $this->apiMethod == 'getWebsites') { - $this->recursiveLabelSeparator = '/'; - } + $this->recursiveLabelSeparator = $recursiveLabelSeparator; return $this->manipulate($dataTable); } @@ -72,9 +71,10 @@ class Flattener extends DataTableManipulator } $newDataTable = $dataTable->getEmptyClone($keepFilters); - foreach ($dataTable->getRows() as $row) { - $this->flattenRow($row, $newDataTable); + foreach ($dataTable->getRows() as $rowId => $row) { + $this->flattenRow($row, $rowId, $newDataTable); } + return $newDataTable; } @@ -84,15 +84,21 @@ class Flattener extends DataTableManipulator * @param string $labelPrefix * @param bool $parentLogo */ - private function flattenRow(Row $row, DataTable $dataTable, + private function flattenRow(Row $row, $rowId, DataTable $dataTable, $labelPrefix = '', $parentLogo = false) { $label = $row->getColumn('label'); if ($label !== false) { $label = trim($label); - if (substr($label, 0, 1) == '/' && $this->recursiveLabelSeparator == '/') { - $label = substr($label, 1); + + if ($this->recursiveLabelSeparator == '/') { + if (substr($label, 0, 1) == '/') { + $label = substr($label, 1); + } elseif ($rowId === DataTable::ID_SUMMARY_ROW && $labelPrefix && $label != DataTable::LABEL_SUMMARY_ROW) { + $label = ' - ' . $label; + } } + $label = $labelPrefix . $label; $row->setColumn('label', $label); } @@ -103,7 +109,16 @@ class Flattener extends DataTableManipulator $row->setMetadata('logo', $logo); } - $subTable = $this->loadSubtable($dataTable, $row); + /** @var DataTable $subTable */ + $subTable = $row->getSubtable(); + + if ($subTable) { + $subTable->applyQueuedFilters(); + $row->deleteMetadata('idsubdatatable_in_db'); + } else { + $subTable = $this->loadSubtable($dataTable, $row); + } + $row->removeSubtable(); if ($subTable === null) { @@ -117,8 +132,8 @@ class Flattener extends DataTableManipulator $dataTable->addRow($row); } $prefix = $label . $this->recursiveLabelSeparator; - foreach ($subTable->getRows() as $row) { - $this->flattenRow($row, $dataTable, $prefix, $logo); + foreach ($subTable->getRows() as $rowId => $row) { + $this->flattenRow($row, $rowId, $dataTable, $prefix, $logo); } } } diff --git a/core/API/DataTableManipulator/ReportTotalsCalculator.php b/core/API/DataTableManipulator/ReportTotalsCalculator.php index b6b82effad..4703c36c7d 100644 --- a/core/API/DataTableManipulator/ReportTotalsCalculator.php +++ b/core/API/DataTableManipulator/ReportTotalsCalculator.php @@ -66,14 +66,19 @@ class ReportTotalsCalculator extends DataTableManipulator $firstLevelTable = $this->makeSureToWorkOnFirstLevelDataTable($dataTable); $metricsToCalculate = Metrics::getMetricIdsToProcessReportTotal(); + $realMetricNames = array(); foreach ($metricsToCalculate as $metricId) { - $realMetricName = $this->hasDataTableMetric($firstLevelTable, $metricId); - if (empty($realMetricName)) { - continue; + $metricName = Metrics::getReadableColumnName($metricId); + $realMetricName = $this->hasDataTableMetric($firstLevelTable, $metricId, $metricName); + if (!empty($realMetricName)) { + $realMetricNames[$metricName] = $realMetricName; } + } - foreach ($firstLevelTable->getRows() as $row) { - $totalValues = $this->sumColumnValueToTotal($row, $metricId, $realMetricName, $totalValues); + foreach ($firstLevelTable->getRows() as $row) { + $columns = $row->getColumns(); + foreach ($realMetricNames as $metricName => $realMetricName) { + $totalValues = $this->sumColumnValueToTotal($columns, $metricName, $realMetricName, $totalValues); } } @@ -82,7 +87,7 @@ class ReportTotalsCalculator extends DataTableManipulator return $dataTable; } - private function hasDataTableMetric(DataTable $dataTable, $metricId) + private function hasDataTableMetric(DataTable $dataTable, $metricId, $readableColumnName) { $firstRow = $dataTable->getFirstRow(); @@ -90,7 +95,6 @@ class ReportTotalsCalculator extends DataTableManipulator return false; } - $readableColumnName = Metrics::getReadableColumnName($metricId); $columnAlternatives = array( $metricId, $readableColumnName, @@ -151,17 +155,18 @@ class ReportTotalsCalculator extends DataTableManipulator return $table; } - private function sumColumnValueToTotal(Row $row, $metricId, $realMetricId, $totalValues) + private function sumColumnValueToTotal($columns, $metricName, $realMetricId, $totalValues) { - $value = $row->getColumn($realMetricId); + $value = false; + if (array_key_exists($realMetricId, $columns)) { + $value = $columns[$realMetricId]; + } if (false === $value) { return $totalValues; } - $metricName = Metrics::getReadableColumnName($metricId); - if (array_key_exists($metricName, $totalValues)) { $totalValues[$metricName] += $value; } else { diff --git a/core/API/DataTablePostProcessor.php b/core/API/DataTablePostProcessor.php index 999b3fe339..b13fff1439 100644 --- a/core/API/DataTablePostProcessor.php +++ b/core/API/DataTablePostProcessor.php @@ -85,8 +85,8 @@ class DataTablePostProcessor // this is non-trivial since it will require, eg, to make sure processed metrics aren't added // after pivotBy is handled. $dataTable = $this->applyPivotByFilter($dataTable); - $dataTable = $this->applyFlattener($dataTable); $dataTable = $this->applyTotalsCalculator($dataTable); + $dataTable = $this->applyFlattener($dataTable); $dataTable = $this->applyGenericFilters($dataTable); @@ -145,7 +145,13 @@ class DataTablePostProcessor if (Common::getRequestVar('include_aggregate_rows', '0', 'string', $this->request) == '1') { $flattener->includeAggregateRows(); } - $dataTable = $flattener->flatten($dataTable); + + $recursiveLabelSeparator = ' - '; + if ($this->report) { + $recursiveLabelSeparator = $this->report->getRecursiveLabelSeparator(); + } + + $dataTable = $flattener->flatten($dataTable, $recursiveLabelSeparator); } return $dataTable; } diff --git a/core/Archive.php b/core/Archive.php index 1931b6a343..c7eb6e3106 100644 --- a/core/Archive.php +++ b/core/Archive.php @@ -452,6 +452,7 @@ class Archive * @return DataTable|DataTable\Map See {@link getDataTable()} and * {@link getDataTableExpanded()} for more * information + * @deprecated Since Piwik 2.12.0 Use Archive::createDataTableFromArchive() instead */ public static function getDataTableFromArchive($name, $idSite, $period, $date, $segment, $expanded, $idSubtable = null, $depth = null) @@ -474,6 +475,38 @@ class Archive return $dataTable; } + /** + * Helper function that creates an Archive instance and queries for report data using + * query parameter data. API methods can use this method to reduce code redundancy. + * + * @param string $recordName The name of the report to return. + * @param int|string|array $idSite @see {@link build()} + * @param string $period @see {@link build()} + * @param string $date @see {@link build()} + * @param string $segment @see {@link build()} + * @param bool $expanded If true, loads all subtables. See {@link getDataTableExpanded()} + * @param bool $flat If true, loads all subtables and disabled all recursive filters. + * @param int|null $idSubtable See {@link getDataTableExpanded()} + * @param int|null $depth See {@link getDataTableExpanded()} + * @return DataTable|DataTable\Map + */ + public static function createDataTableFromArchive($recordName, $idSite, $period, $date, $segment, $expanded = false, $flat = false, $idSubtable = null, $depth = null) + { + if ($flat && !$idSubtable) { + $expanded = true; + } + + $dataTable = self::getDataTableFromArchive($recordName, $idSite, $period, $date, $segment, $expanded, $idSubtable, $depth); + + $dataTable->filter('ReplaceColumnNames'); + + if ($flat) { + $dataTable->disableRecursiveFilters(); + } + + return $dataTable; + } + private function appendIdSubtable($recordName, $id) { return $recordName . "_" . $id; diff --git a/core/DataTable.php b/core/DataTable.php index b060a87245..0b0d9845f0 100644 --- a/core/DataTable.php +++ b/core/DataTable.php @@ -342,6 +342,17 @@ class DataTable implements DataTableInterface, \IteratorAggregate, \ArrayAccess } /** + * @ignore + * does not update the summary row! + */ + public function setRows($rows) + { + unset($this->rows); + $this->rows = $rows; + $this->indexNotUpToDate = true; + } + + /** * Sorts the DataTable rows using the supplied callback function. * * @param string $functionCallback A comparison callback compatible with {@link usort}. @@ -350,11 +361,11 @@ class DataTable implements DataTableInterface, \IteratorAggregate, \ArrayAccess */ public function sort($functionCallback, $columnSortedBy) { - $this->indexNotUpToDate = true; - $this->tableSortedBy = $columnSortedBy; + $this->setTableSortedBy($columnSortedBy); + usort($this->rows, $functionCallback); - if ($this->enableRecursiveSort === true) { + if ($this->isSortRecursiveEnabled()) { foreach ($this->getRows() as $row) { $subTable = $row->getSubtable(); @@ -388,6 +399,23 @@ class DataTable implements DataTableInterface, \IteratorAggregate, \ArrayAccess } /** + * @ignore + */ + public function isSortRecursiveEnabled() + { + return $this->enableRecursiveSort === true; + } + + /** + * @ignore + */ + public function setTableSortedBy($column) + { + $this->indexNotUpToDate = true; + $this->tableSortedBy = $column; + } + + /** * Enables recursive filtering. If this method is called then the {@link filter()} method * will apply filters to every subtable in addition to this instance. */ @@ -397,6 +425,14 @@ class DataTable implements DataTableInterface, \IteratorAggregate, \ArrayAccess } /** + * @ignore + */ + public function disableRecursiveFilters() + { + $this->enableRecursiveFilters = false; + } + + /** * Applies a filter to this datatable. * * If {@link enableRecursiveFilters()} was called, the filter will be applied @@ -434,6 +470,25 @@ class DataTable implements DataTableInterface, \IteratorAggregate, \ArrayAccess } /** + * Applies a filter to all subtables but not to this datatable. + * + * @param string|Closure $className Class name, eg. `"Sort"` or "Piwik\DataTable\Filters\Sort"`. If no + * namespace is supplied, `Piwik\DataTable\BaseFilter` is assumed. This parameter + * can also be a closure that takes a DataTable as its first parameter. + * @param array $parameters Array of extra parameters to pass to the filter. + */ + public function filterSubtables($className, $parameters = array()) + { + foreach ($this->getRows() as $row) { + $subtable = $row->getSubtable(); + if ($subtable) { + $subtable->filter($className, $parameters); + $subtable->filterSubtables($className, $parameters); + } + } + } + + /** * Adds a filter and a list of parameters to the list of queued filters. These filters will be * executed when {@link applyQueuedFilters()} is called. * @@ -730,6 +785,14 @@ class DataTable implements DataTableInterface, \IteratorAggregate, \ArrayAccess } /** + * @ignore + */ + public function getRowsWithoutSummaryRow() + { + return $this->rows; + } + + /** * Returns an array containing all column values for the requested column. * * @param string $name The column name. diff --git a/core/DataTable/Filter/Sort.php b/core/DataTable/Filter/Sort.php index 42441c8e30..632da35dc8 100644 --- a/core/DataTable/Filter/Sort.php +++ b/core/DataTable/Filter/Sort.php @@ -21,7 +21,8 @@ use Piwik\Metrics; * * @api */ -class Sort extends BaseFilter +class +Sort extends BaseFilter { protected $columnToSort; protected $order; @@ -71,34 +72,21 @@ class Sort extends BaseFilter * @param Row $b * @return int */ - public function numberSort($a, $b) + public function numberSort($rowA, $rowB) { - $valA = $this->getColumnValue($a); - $valB = $this->getColumnValue($b); + if (isset($rowA[0]) && isset($rowB[0])) { + if ($rowA[0] != $rowB[0] || !isset($rowA[1])) { + return $this->sign * ($rowA[0] < $rowB[0] ? -1 : 1); + } else { + return -1 * $this->sign * strnatcasecmp($rowA[1], $rowB[1]); + } + } elseif (!isset($rowB[0])) { + return -1; + } elseif (!isset($rowA[0])) { + return 1; + } - return !isset($valA) - && !isset($valB) - ? 0 - : ( - !isset($valA) - ? 1 - : ( - !isset($valB) - ? -1 - : (($valA != $valB - || !isset($a->c[Row::COLUMNS]['label'])) - ? ($this->sign * ( - $valA - < $valB - ? -1 - : 1) - ) - : -1 * $this->sign * strnatcasecmp( - $a->c[Row::COLUMNS]['label'], - $b->c[Row::COLUMNS]['label']) - ) - ) - ); + return 0; } /** @@ -108,10 +96,10 @@ class Sort extends BaseFilter * @param mixed $b * @return int */ - function naturalSort($a, $b) + function naturalSort($rowA, $rowB) { - $valA = $this->getColumnValue($a); - $valB = $this->getColumnValue($b); + $valA = $rowA[0]; + $valB = $rowB[0]; return !isset($valA) && !isset($valB) @@ -135,10 +123,10 @@ class Sort extends BaseFilter * @param mixed $b * @return int */ - function sortString($a, $b) + function sortString($rowA, $rowB) { - $valA = $this->getColumnValue($a); - $valB = $this->getColumnValue($b); + $valA = $rowA[0]; + $valB = $rowB[0]; return !isset($valA) && !isset($valB) @@ -243,6 +231,51 @@ class Sort extends BaseFilter } } - $table->sort(array($this, $methodToUse), $this->columnToSort); + $this->sort($table, $methodToUse); } + + /** + * Sorts the DataTable rows using the supplied callback function. + * + * @param string $functionCallback A comparison callback compatible with {@link usort}. + * @param string $columnSortedBy The column name `$functionCallback` sorts by. This is stored + * so we can determine how the DataTable was sorted in the future. + */ + private function sort(DataTable $table, $functionCallback) + { + $table->setTableSortedBy($this->columnToSort); + + $rows = $table->getRowsWithoutSummaryRow(); + + // get column value and label only once for performance tweak + $values = array(); + foreach ($rows as $key => $row) { + $values[$key] = array($this->getColumnValue($row), $row->getColumn('label')); + } + + uasort($values, array($this, $functionCallback)); + + $sortedRows = array(); + foreach ($values as $key => $value) { + $sortedRows[$key] = $rows[$key]; + } + + $table->setRows(array_values($sortedRows)); + + unset($rows); + unset($sortedRows); + + if ($table->isSortRecursiveEnabled()) { + foreach ($table->getRows() as $row) { + + $subTable = $row->getSubtable(); + if ($subTable) { + $subTable->enableRecursiveSort(); + $this->sort($subTable, $functionCallback); + } + } + } + + } + } diff --git a/core/DataTable/Filter/Truncate.php b/core/DataTable/Filter/Truncate.php index 632eb2755e..a8fa0bd08f 100644 --- a/core/DataTable/Filter/Truncate.php +++ b/core/DataTable/Filter/Truncate.php @@ -96,7 +96,7 @@ class Truncate extends BaseFilter return; } - $rows = $table->getRows(); + $rows = array_values($table->getRows()); $count = $table->getRowsCount(); $newRow = new Row(array(Row::COLUMNS => array('label' => DataTable::LABEL_SUMMARY_ROW))); diff --git a/core/DataTable/Map.php b/core/DataTable/Map.php index 8795eac134..8787b06404 100644 --- a/core/DataTable/Map.php +++ b/core/DataTable/Map.php @@ -110,6 +110,19 @@ class Map implements DataTableInterface } /** + * Apply a filter to all subtables contained by this instance. + * + * @param string|Closure $className Name of filter class or a Closure. + * @param array $parameters Parameters to pass to the filter. + */ + public function filterSubtables($className, $parameters = array()) + { + foreach ($this->getDataTables() as $table) { + $table->filterSubtables($className, $parameters); + } + } + + /** * Returns the array of DataTables contained by this class. * * @return DataTable[]|Map[] @@ -185,6 +198,26 @@ class Map implements DataTableInterface } /** + * @ignore + */ + public function disableRecursiveFilters() + { + foreach ($this->getDataTables() as $table) { + $table->disableRecursiveFilters(); + } + } + + /** + * @ignore + */ + public function enableRecursiveFilters() + { + foreach ($this->getDataTables() as $table) { + $table->enableRecursiveFilters(); + } + } + + /** * Renames the given column in each contained {@link DataTable}. * * See {@link DataTable::renameColumn()}. diff --git a/core/DataTable/Row.php b/core/DataTable/Row.php index 5f21f3fdec..71dde68031 100644 --- a/core/DataTable/Row.php +++ b/core/DataTable/Row.php @@ -209,37 +209,9 @@ class Row implements \ArrayAccess, \IteratorAggregate return false; } - if ($this->isColumnValueCallable($this->c[self::COLUMNS][$name])) { - $value = $this->resolveCallableColumn($name); - - if (!isset($value)) { - return false; - } - - return $value; - } - return $this->c[self::COLUMNS][$name]; } - private function isColumnValueCallable($name) - { - if (! is_callable($name)) { - return false; - } - - if (is_object($name) && ($name instanceof \Closure)) { - return true; - } - - return is_array($name) && isset($name[0]) && is_object($name[0]); - } - - private function resolveCallableColumn($columnName) - { - return call_user_func($this->c[self::COLUMNS][$columnName], $this); - } - /** * Returns the array of all metadata, or one requested metadata value. * @@ -287,16 +259,7 @@ class Row implements \ArrayAccess, \IteratorAggregate */ public function getColumns() { - $values = array(); - foreach ($this->c[self::COLUMNS] as $columnName => $val) { - if ($this->isColumnValueCallable($val)) { - $values[$columnName] = $this->resolveCallableColumn($columnName); - } else { - $values[$columnName] = $val; - } - } - - return $values; + return $this->c[self::COLUMNS]; } /** @@ -517,11 +480,6 @@ class Row implements \ArrayAccess, \IteratorAggregate continue; } - if ($this->isColumnValueCallable($columnToSumValue)) { - $this->setColumn($columnToSumName, $columnToSumValue); - continue; - } - $thisColumnValue = $this->getColumn($columnToSumName); $operation = 'sum'; diff --git a/core/Plugin/API.php b/core/Plugin/API.php index cc16cfb64c..4e5095a3aa 100644 --- a/core/Plugin/API.php +++ b/core/Plugin/API.php @@ -41,5 +41,5 @@ use Piwik\Singleton; */ abstract class API extends Singleton { - + } diff --git a/core/Plugin/Metric.php b/core/Plugin/Metric.php index e475eb837f..6d69350d92 100644 --- a/core/Plugin/Metric.php +++ b/core/Plugin/Metric.php @@ -108,26 +108,41 @@ abstract class Metric */ public static function getMetric($row, $columnName, $mappingNameToId = null) { - if (empty($mappingNameToId)) { - $mappingNameToId = Metrics::getMappingFromNameToId(); - } - if ($row instanceof Row) { $value = $row->getColumn($columnName); - if ($value === false - && isset($mappingNameToId[$columnName]) - ) { - $value = $row->getColumn($mappingNameToId[$columnName]); + + if ($value === false) { + + if (empty($mappingNameToId)) { + $mappingNameToId = Metrics::getMappingFromNameToId(); + } + + if (isset($mappingNameToId[$columnName])) { + return $row->getColumn($mappingNameToId[$columnName]); + } } + + return $value; + } else { - $value = @$row[$columnName]; - if ($value === null - && isset($mappingNameToId[$columnName]) - ) { - $columnName = $mappingNameToId[$columnName]; - $value = @$row[$columnName]; + $value = null; + if (array_key_exists($columnName, $row)) { + $value = $row[$columnName]; + } else { + + if (empty($mappingNameToId)) { + $mappingNameToId = Metrics::getMappingFromNameToId(); + } + + if (isset($mappingNameToId[$columnName])) { + $columnName = $mappingNameToId[$columnName]; + + if (array_key_exists($columnName, $row)) { + return $row[$columnName]; + } + } } - return $value; + } return $value; diff --git a/core/Plugin/Report.php b/core/Plugin/Report.php index 62ed594f8c..b9c407e10e 100644 --- a/core/Plugin/Report.php +++ b/core/Plugin/Report.php @@ -182,6 +182,13 @@ class Report protected $order = 1; /** + * Separator for building recursive labels (or paths) + * @var string + * @api + */ + protected $recursiveLabelSeparator = ' - '; + + /** * @var array * @ignore */ @@ -357,6 +364,15 @@ class Report } /** + * @ignore + * @see $recursiveLabelSeparator + */ + public function getRecursiveLabelSeparator() + { + return $this->recursiveLabelSeparator; + } + + /** * Returns an array of supported metrics and their corresponding translations. Eg `array('nb_visits' => 'Visits')`. * By default the given {@link $metrics} are used and their corresponding translations are looked up automatically. * If a metric is not translated, you should add the default metric translation for this metric using diff --git a/core/UrlHelper.php b/core/UrlHelper.php index a0bc340bbd..78682acbfc 100644 --- a/core/UrlHelper.php +++ b/core/UrlHelper.php @@ -231,7 +231,10 @@ class UrlHelper $parsedUrl = parse_url($url); $result = ''; if (isset($parsedUrl['path'])) { - $result .= substr($parsedUrl['path'], 1); + if (substr($parsedUrl['path'], 0, 1) == '/') { + $parsedUrl['path'] = substr($parsedUrl['path'], 1); + } + $result .= $parsedUrl['path']; } if (isset($parsedUrl['query'])) { $result .= '?' . $parsedUrl['query']; |