├── .editorconfig ├── .gitignore ├── .travis.yml ├── DevExtreme ├── AggregateHelper.php ├── DataSourceLoader.php ├── DbSet.php ├── FilterHelper.php ├── LoadHelper.php └── Utils.php ├── LICENSE ├── README.md ├── SECURITY.md ├── example ├── css │ └── index.css ├── index.html ├── js │ └── index.js ├── package.json └── php │ ├── DataController.php │ └── service.php ├── phpunit.xml └── tests ├── AggregateHelperTest.php ├── ConfigHelper.php ├── DataSourceLoaderTest.php ├── DbSetAPITest.php ├── FilterHelperTest.php ├── TestBase.php ├── UtilsTest.php ├── bootstrap.php ├── config.travis.json └── test_products.sql /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | insert_final_newline = true 6 | trim_trailing_whitespace = true 7 | indent_style = space 8 | indent_size = 4 9 | 10 | [*.{json,yml}] 11 | indent_size = 2 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | example/node_modules 2 | example/package-lock.json 3 | example/php/.vscode 4 | DevExtreme/.vscode 5 | tests/config.json 6 | .vscode 7 | 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: php 3 | 4 | php: 5 | - 5.6 6 | - 7.1 7 | 8 | env: 9 | - TEST_CONF="config.travis.json" 10 | 11 | services: 12 | - mysql 13 | 14 | before_script: 15 | - mysql -e "CREATE DATABASE IF NOT EXISTS test;" -uroot 16 | - mysql test < tests/test_products.sql 17 | - | 18 | echo "TRAVIS_PHP_VERSION:" $TRAVIS_PHP_VERSION; 19 | php -v; 20 | php -r 'echo "PHP: ".PHP_VERSION."\n";'; 21 | if [ $(echo "$TRAVIS_PHP_VERSION >= 7.1" | bc -l) -eq 1 ]; then 22 | echo using PHPUnit 5.7.23 23 | curl -sSfL -o ~/.phpenv/versions/$TRAVIS_PHP_VERSION/bin/phpunit https://phar.phpunit.de/phpunit-5.7.23.phar; 24 | fi 25 | 26 | script: phpunit 27 | -------------------------------------------------------------------------------- /DevExtreme/AggregateHelper.php: -------------------------------------------------------------------------------- 1 | $item) { 25 | $currentSummaries = $item["summary"]; 26 | if ($index == 0) { 27 | foreach ($currentSummaries as $summaryItem) { 28 | $result[] = $summaryItem; 29 | } 30 | continue; 31 | } 32 | foreach ($groupInfo["summaryTypes"] as $si => $stItem) { 33 | if ($stItem == self::MIN_OP) { 34 | if ($result[$si] > $currentSummaries[$si]) { 35 | $result[$si] = $currentSummaries[$si]; 36 | } 37 | continue; 38 | } 39 | if ($stItem == self::MAX_OP) { 40 | if ($result[$si] < $currentSummaries[$si]) { 41 | $result[$si] = $currentSummaries[$si]; 42 | } 43 | continue; 44 | } 45 | $result[$si] += $currentSummaries[$si]; 46 | } 47 | } 48 | foreach ($groupInfo["summaryTypes"] as $si => $stItem) { 49 | if ($stItem == self::AVG_OP) { 50 | $result[$si] /= $itemsCount; 51 | } 52 | } 53 | $dataItem["summary"] = $result; 54 | } 55 | } 56 | private static function _GetNewDataItem($row, $groupInfo) { 57 | $dataItem = array(); 58 | $dataFieldCount = count($groupInfo["dataFieldNames"]); 59 | for ($index = 0; $index < $dataFieldCount; $index++) { 60 | $dataItem[$groupInfo["dataFieldNames"][$index]] = $row[$groupInfo["groupCount"] + $index]; 61 | } 62 | return $dataItem; 63 | } 64 | private static function _GetNewGroupItem($row, $groupInfo) { 65 | $groupIndexOffset = $groupInfo["lastGroupExpanded"] ? 1 : 2; 66 | $groupItem = array(); 67 | $groupItem["key"] = $row[$groupInfo["groupIndex"]]; 68 | $groupItem["items"] = $groupInfo["groupIndex"] < $groupInfo["groupCount"] - $groupIndexOffset ? array() : 69 | ($groupInfo["lastGroupExpanded"] ? array() : NULL); 70 | if ($groupInfo["groupIndex"] == $groupInfo["groupCount"] - $groupIndexOffset) { 71 | if (isset($groupInfo["summaryTypes"])) { 72 | $summaries = array(); 73 | $endIndex = $groupInfo["groupIndex"] + count($groupInfo["summaryTypes"]) + 1; 74 | for ($index = $groupInfo["groupCount"]; $index <= $endIndex; $index++) { 75 | $summaries[] = $row[$index]; 76 | } 77 | $groupItem["summary"] = $summaries; 78 | } 79 | if (!$groupInfo["lastGroupExpanded"]) { 80 | $groupItem["count"] = $row[$groupInfo["groupIndex"] + 1]; 81 | } 82 | else { 83 | $groupItem["items"][] = self::_GetNewDataItem($row, $groupInfo); 84 | } 85 | } 86 | return $groupItem; 87 | } 88 | private static function _GroupData($row, &$resultItems, $groupInfo) { 89 | $itemsCount = count($resultItems); 90 | if (!isset($row) && !$itemsCount) { 91 | return; 92 | } 93 | $currentItem = NULL; 94 | $groupIndexOffset = $groupInfo["lastGroupExpanded"] ? 1 : 2; 95 | if ($itemsCount) { 96 | $currentItem = &$resultItems[$itemsCount - 1]; 97 | if (!$groupInfo["lastGroupExpanded"]) { 98 | if ($currentItem["key"] != $row[$groupInfo["groupIndex"]] || !isset($row)) { 99 | if ($groupInfo["groupIndex"] == 0 && $groupInfo["groupCount"] > 2) { 100 | self::_RecalculateGroupCountAndSummary($currentItem, $groupInfo); 101 | } 102 | unset($currentItem); 103 | if (!isset($row)) { 104 | return; 105 | } 106 | } 107 | } 108 | else { 109 | if ($currentItem["key"] != $row[$groupInfo["groupIndex"]]) { 110 | unset($currentItem); 111 | } 112 | else { 113 | if ($groupInfo["groupIndex"] == $groupInfo["groupCount"] - $groupIndexOffset) { 114 | $currentItem["items"][] = self::_GetNewDataItem($row, $groupInfo); 115 | } 116 | } 117 | } 118 | } 119 | if (!isset($currentItem)) { 120 | $currentItem = self::_GetNewGroupItem($row, $groupInfo); 121 | $resultItems[] = &$currentItem; 122 | } 123 | if ($groupInfo["groupIndex"] < $groupInfo["groupCount"] - $groupIndexOffset) { 124 | $groupInfo["groupIndex"]++; 125 | self::_GroupData($row, $currentItem["items"], $groupInfo); 126 | } 127 | } 128 | public static function GetGroupedDataFromQuery($queryResult, $groupSettings) { 129 | $result = array(); 130 | $row = NULL; 131 | $groupSummaryTypes = NULL; 132 | $dataFieldNames = NULL; 133 | $startSummaryFieldIndex = NULL; 134 | $endSummaryFieldIndex = NULL; 135 | if ($groupSettings["lastGroupExpanded"]) { 136 | $queryFields = $queryResult->fetch_fields(); 137 | $dataFieldNames = array(); 138 | for ($i = $groupSettings["groupCount"]; $i < count($queryFields); $i++) { 139 | $dataFieldNames[] = $queryFields[$i]->name; 140 | } 141 | } 142 | if (isset($groupSettings["summaryTypes"])) { 143 | $groupSummaryTypes = $groupSettings["summaryTypes"]; 144 | $startSummaryFieldIndex = $groupSettings["groupCount"] - 1; 145 | $endSummaryFieldIndex = $startSummaryFieldIndex + count($groupSummaryTypes); 146 | } 147 | $groupInfo = array( 148 | "groupCount" => $groupSettings["groupCount"], 149 | "groupIndex" => 0, 150 | "summaryTypes" => $groupSummaryTypes, 151 | "lastGroupExpanded" => $groupSettings["lastGroupExpanded"], 152 | "dataFieldNames" => $dataFieldNames 153 | ); 154 | while ($row = $queryResult->fetch_array(MYSQLI_NUM)) { 155 | if (isset($startSummaryFieldIndex)) { 156 | for ($i = $startSummaryFieldIndex; $i <= $endSummaryFieldIndex; $i++) { 157 | $row[$i] = Utils::StringToNumber($row[$i]); 158 | } 159 | } 160 | self::_GroupData($row, $result, $groupInfo); 161 | } 162 | if (!$groupSettings["lastGroupExpanded"]) { 163 | self::_GroupData($row, $result, $groupInfo); 164 | } 165 | else { 166 | if (isset($groupSettings["skip"]) && $groupSettings["skip"] >= 0 && 167 | isset($groupSettings["take"]) && $groupSettings["take"] >= 0) { 168 | $result = array_slice($result, $groupSettings["skip"], $groupSettings["take"]); 169 | } 170 | } 171 | return $result; 172 | } 173 | public static function IsLastGroupExpanded($items) { 174 | $result = true; 175 | $itemsCount = count($items); 176 | if ($itemsCount > 0) { 177 | $lastItem = $items[$itemsCount - 1]; 178 | if (gettype($lastItem) === "object") { 179 | $result = isset($lastItem->isExpanded) ? $lastItem->isExpanded === true : true; 180 | } 181 | else { 182 | $result = true; 183 | } 184 | } 185 | return $result; 186 | } 187 | public static function GetFieldSetBySelectors($items) { 188 | $group = ""; 189 | $sort = ""; 190 | $select = ""; 191 | foreach ($items as $item) { 192 | $groupField = NULL; 193 | $sortField = NULL; 194 | $selectField = NULL; 195 | $desc = false; 196 | if (is_string($item) && strlen($item = trim($item))) { 197 | $selectField = $groupField = $sortField = Utils::QuoteStringValue($item); 198 | } 199 | else if (gettype($item) === "object" && isset($item->selector)) { 200 | $quoteSelector = Utils::QuoteStringValue($item->selector); 201 | $desc = isset($item->desc) ? $item->desc : false; 202 | if (isset($item->groupInterval)) { 203 | if (is_int($item->groupInterval)) { 204 | $groupField = Utils::QuoteStringValue(sprintf("%s%s_%d", self::GENERATED_FIELD_PREFIX, $item->selector, $item->groupInterval)); 205 | $selectField = sprintf("(%s - (%s %% %d)) %s %s", 206 | $quoteSelector, 207 | $quoteSelector, 208 | $item->groupInterval, 209 | self::AS_OP, 210 | $groupField); 211 | } 212 | else { 213 | $groupField = Utils::QuoteStringValue(sprintf("%s%s_%s", self::GENERATED_FIELD_PREFIX, $item->selector, $item->groupInterval)); 214 | $selectField = sprintf("%s(%s) %s %s", 215 | strtoupper($item->groupInterval), 216 | $quoteSelector, 217 | self::AS_OP, 218 | $groupField); 219 | } 220 | $sortField = $groupField; 221 | } 222 | else { 223 | $selectField = $groupField = $sortField = $quoteSelector; 224 | } 225 | } 226 | if (isset($selectField)) { 227 | $select .= (strlen($select) > 0 ? ", ".$selectField : $selectField); 228 | } 229 | if (isset($groupField)) { 230 | $group .= (strlen($group) > 0 ? ", ".$groupField : $groupField); 231 | } 232 | if (isset($sortField)) { 233 | $sort .= (strlen($sort) > 0 ? ", ".$sortField : $sortField). 234 | ($desc ? " DESC" : ""); 235 | } 236 | } 237 | return array( 238 | "group" => $group, 239 | "sort" => $sort, 240 | "select" => $select 241 | ); 242 | } 243 | private static function _IsSummaryTypeValid($summaryType) { 244 | return in_array($summaryType, array(self::MIN_OP, self::MAX_OP, self::AVG_OP, self::COUNT_OP, self::SUM_OP)); 245 | } 246 | public static function GetSummaryInfo($expression) { 247 | $result = array(); 248 | $fields = ""; 249 | $summaryTypes = array(); 250 | foreach ($expression as $index => $item) { 251 | if (gettype($item) === "object" && isset($item->summaryType)) { 252 | $summaryType = strtoupper(trim($item->summaryType)); 253 | if (!self::_IsSummaryTypeValid($summaryType)) { 254 | continue; 255 | } 256 | $summaryTypes[] = $summaryType; 257 | $fields .= sprintf("%s(%s) %s %sf%d", 258 | strlen($fields) > 0 ? ", ".$summaryTypes[$index] : $summaryTypes[$index], 259 | (isset($item->selector) && is_string($item->selector)) ? Utils::QuoteStringValue($item->selector) : "1", 260 | self::AS_OP, 261 | self::GENERATED_FIELD_PREFIX, 262 | $index); 263 | } 264 | } 265 | $result["fields"] = $fields; 266 | $result["summaryTypes"] = $summaryTypes; 267 | return $result; 268 | } 269 | } 270 | -------------------------------------------------------------------------------- /DevExtreme/DataSourceLoader.php: -------------------------------------------------------------------------------- 1 | Select(Utils::GetItemValueOrDefault($params, "select")) 8 | ->Filter(Utils::GetItemValueOrDefault($params, "filter")); 9 | $totalSummary = $dbSet->GetTotalSummary(Utils::GetItemValueOrDefault($params, "totalSummary"), 10 | Utils::GetItemValueOrDefault($params, "filter")); 11 | if ($dbSet->GetLastError() !== NULL) { 12 | return $result; 13 | } 14 | $totalCount = (isset($params["requireTotalCount"]) && $params["requireTotalCount"] === true) 15 | ? $dbSet->GetCount() : NULL; 16 | if ($dbSet->GetLastError() !== NULL) { 17 | return $result; 18 | } 19 | $dbSet->Sort(Utils::GetItemValueOrDefault($params, "sort")); 20 | $groupCount = NULL; 21 | $skip = Utils::GetItemValueOrDefault($params, "skip"); 22 | $take = Utils::GetItemValueOrDefault($params, "take"); 23 | if (isset($params["group"])) { 24 | $groupExpression = $params["group"]; 25 | $groupSummary = Utils::GetItemValueOrDefault($params, "groupSummary"); 26 | $dbSet->Group($groupExpression, $groupSummary, $skip, $take); 27 | if (isset($params["requireGroupCount"]) && $params["requireGroupCount"] === true) { 28 | $groupCount = $dbSet->GetGroupCount(); 29 | } 30 | } 31 | else { 32 | $dbSet->SkipTake($skip, $take); 33 | } 34 | $result = array(); 35 | $result["data"] = $dbSet->AsArray(); 36 | if ($dbSet->GetLastError() !== NULL) { 37 | return $result; 38 | } 39 | if (isset($totalCount)) { 40 | $result["totalCount"] = $totalCount; 41 | } 42 | if (isset($totalSummary)) { 43 | $result["summary"] = $totalSummary; 44 | } 45 | if (isset($groupCount)) { 46 | $result["groupCount"] = $groupCount; 47 | } 48 | } 49 | else { 50 | throw new \Exception("Invalid params"); 51 | } 52 | return $result; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /DevExtreme/DbSet.php: -------------------------------------------------------------------------------- 1 | mySQL = $mySQL; 29 | $this->dbTableName = $table; 30 | $this->resultQuery = sprintf("%s %s %s %s", 31 | self::$SELECT_OP, 32 | self::$ALL_FIELDS, 33 | self::$FROM_OP, 34 | $this->dbTableName); 35 | } 36 | public function GetLastError() { 37 | return $this->lastError; 38 | } 39 | private function _WrapQuery() { 40 | $this->tableNameIndex++; 41 | $this->lastWrappedTableName = "{$this->dbTableName}_{$this->tableNameIndex}"; 42 | $this->resultQuery = sprintf("%s %s %s (%s) %s %s", 43 | self::$SELECT_OP, 44 | self::$ALL_FIELDS, 45 | self::$FROM_OP, 46 | $this->resultQuery, 47 | AggregateHelper::AS_OP, 48 | $this->lastWrappedTableName); 49 | } 50 | private function _PrepareQueryForLastOperator($operator) { 51 | $operator = trim($operator); 52 | $lastOperatorPos = strrpos($this->resultQuery, " ".$operator." "); 53 | if ($lastOperatorPos !== false) { 54 | $lastBracketPos = strrpos($this->resultQuery, ")"); 55 | if (($lastBracketPos !== false && $lastOperatorPos > $lastBracketPos) || ($lastBracketPos === false)) { 56 | $this->_WrapQuery(); 57 | } 58 | } 59 | } 60 | public function Select($expression) { 61 | Utils::EscapeExpressionValues($this->mySQL, $expression); 62 | $this->_SelectImpl($expression); 63 | return $this; 64 | } 65 | private function _SelectImpl($expression, $needQuotes = true) { 66 | if (isset($expression)) { 67 | $fields = ""; 68 | if (is_string($expression)) { 69 | $expression = explode(",", $expression); 70 | } 71 | if (is_array($expression)) { 72 | foreach ($expression as $field) { 73 | $fields .= (strlen($fields) ? ", " : "").($needQuotes ? Utils::QuoteStringValue(trim($field)) : trim($field)); 74 | } 75 | } 76 | if (strlen($fields)) { 77 | $allFieldOperatorPos = strpos($this->resultQuery, self::$ALL_FIELDS); 78 | if ($allFieldOperatorPos == 7) { 79 | $this->resultQuery = substr_replace($this->resultQuery, $fields, 7, strlen(self::$ALL_FIELDS)); 80 | } 81 | else { 82 | $this->_WrapQuery(); 83 | $this->_SelectImpl($expression); 84 | } 85 | } 86 | } 87 | } 88 | public function Filter($expression) { 89 | Utils::EscapeExpressionValues($this->mySQL, $expression); 90 | if (isset($expression) && is_array($expression)) { 91 | $result = FilterHelper::GetSqlExprByArray($expression); 92 | if (strlen($result)) { 93 | $this->_PrepareQueryForLastOperator(self::$WHERE_OP); 94 | $this->resultQuery .= sprintf(" %s %s", 95 | self::$WHERE_OP, 96 | $result); 97 | } 98 | } 99 | return $this; 100 | } 101 | public function Sort($expression) { 102 | Utils::EscapeExpressionValues($this->mySQL, $expression); 103 | if (isset($expression)) { 104 | $result = ""; 105 | if (is_string($expression)) { 106 | $result = trim($expression); 107 | } 108 | if (is_array($expression)) { 109 | $fieldSet = AggregateHelper::GetFieldSetBySelectors($expression); 110 | $result = $fieldSet["sort"]; 111 | } 112 | if (strlen($result)) { 113 | $this->_PrepareQueryForLastOperator(self::$ORDER_OP); 114 | $this->resultQuery .= sprintf(" %s %s", 115 | self::$ORDER_OP, 116 | $result); 117 | } 118 | } 119 | return $this; 120 | } 121 | public function SkipTake($skip, $take) { 122 | $skip = (!isset($skip) || !is_int($skip) ? 0 : $skip); 123 | $take = (!isset($take) || !is_int($take) ? self::$MAX_ROW_INDEX : $take); 124 | if ($skip != 0 || $take != 0) { 125 | $this->_PrepareQueryForLastOperator(self::$LIMIT_OP); 126 | $this->resultQuery .= sprintf(" %s %0.0f, %0.0f", 127 | self::$LIMIT_OP, 128 | $skip, 129 | $take); 130 | } 131 | return $this; 132 | } 133 | private function _CreateGroupCountQuery($firstGroupField, $skip = NULL, $take = NULL) { 134 | $groupCount = $this->groupSettings["groupCount"]; 135 | $lastGroupExpanded = $this->groupSettings["lastGroupExpanded"]; 136 | if (!$lastGroupExpanded) { 137 | if ($groupCount === 2) { 138 | $this->groupSettings["groupItemCountQuery"] = sprintf("%s COUNT(1) %s (%s) AS %s_%d", 139 | self::$SELECT_OP, 140 | self::$FROM_OP, 141 | $this->resultQuery, 142 | $this->dbTableName, 143 | $this->tableNameIndex + 1); 144 | if (isset($skip) || isset($take)) { 145 | $this->SkipTake($skip, $take); 146 | } 147 | } 148 | } 149 | else { 150 | $groupQuery = sprintf("%s COUNT(1) %s %s %s %s", 151 | self::$SELECT_OP, 152 | self::$FROM_OP, 153 | $this->dbTableName, 154 | self::$GROUP_OP, 155 | $firstGroupField); 156 | $this->groupSettings["groupItemCountQuery"] = sprintf("%s COUNT(1) %s (%s) AS %s_%d", 157 | self::$SELECT_OP, 158 | self::$FROM_OP, 159 | $groupQuery, 160 | $this->dbTableName, 161 | $this->tableNameIndex + 1); 162 | if (isset($skip) || isset($take)) { 163 | $this->groupSettings["skip"] = isset($skip) ? Utils::StringToNumber($skip) : 0; 164 | $this->groupSettings["take"] = isset($take) ? Utils::StringToNumber($take) : 0; 165 | } 166 | } 167 | } 168 | public function Group($expression, $groupSummary = NULL, $skip = NULL, $take = NULL) { 169 | Utils::EscapeExpressionValues($this->mySQL, $expression); 170 | Utils::EscapeExpressionValues($this->mySQL, $groupSummary); 171 | $this->groupSettings = NULL; 172 | if (isset($expression)) { 173 | $groupFields = ""; 174 | $sortFields = ""; 175 | $selectFields = ""; 176 | $lastGroupExpanded = true; 177 | $groupCount = 0; 178 | if (is_string($expression)) { 179 | $selectFields = $sortFields = $groupFields = trim($expression); 180 | $groupCount = count(explode(",", $expression)); 181 | } 182 | if (is_array($expression)) { 183 | $groupCount = count($expression); 184 | $fieldSet = AggregateHelper::GetFieldSetBySelectors($expression); 185 | $groupFields = $fieldSet["group"]; 186 | $selectFields = $fieldSet["select"]; 187 | $sortFields = $fieldSet["sort"]; 188 | $lastGroupExpanded = AggregateHelper::IsLastGroupExpanded($expression); 189 | } 190 | if ($groupCount > 0) { 191 | if (!$lastGroupExpanded) { 192 | $groupSummaryData = isset($groupSummary) && is_array($groupSummary) ? AggregateHelper::GetSummaryInfo($groupSummary) : NULL; 193 | $selectExpression = sprintf("%s, %s(1)%s", 194 | strlen($selectFields) ? $selectFields : $groupFields, 195 | AggregateHelper::COUNT_OP, 196 | (isset($groupSummaryData) && isset($groupSummaryData["fields"]) && strlen($groupSummaryData["fields"]) ? 197 | ", ".$groupSummaryData["fields"] : "")); 198 | $groupCount++; 199 | $this->_WrapQuery(); 200 | $this->_SelectImpl($selectExpression, false); 201 | $this->resultQuery .= sprintf(" %s %s", 202 | self::$GROUP_OP, 203 | $groupFields); 204 | $this->Sort($sortFields); 205 | } 206 | else { 207 | $this->_WrapQuery(); 208 | $selectExpression = "{$selectFields}, {$this->lastWrappedTableName}.*"; 209 | $this->_SelectImpl($selectExpression, false); 210 | $this->resultQuery .= sprintf(" %s %s", 211 | self::$ORDER_OP, 212 | $sortFields); 213 | } 214 | $this->groupSettings = array(); 215 | $this->groupSettings["groupCount"] = $groupCount; 216 | $this->groupSettings["lastGroupExpanded"] = $lastGroupExpanded; 217 | $this->groupSettings["summaryTypes"] = !$lastGroupExpanded ? $groupSummaryData["summaryTypes"] : NULL; 218 | $firstGroupField = explode(",", $groupFields)[0]; 219 | $this->_CreateGroupCountQuery($firstGroupField, $skip, $take); 220 | } 221 | } 222 | return $this; 223 | } 224 | public function GetTotalSummary($expression, $filterExpression = NULL) { 225 | Utils::EscapeExpressionValues($this->mySQL, $expression); 226 | Utils::EscapeExpressionValues($this->mySQL, $filterExpression); 227 | $result = NULL; 228 | if (isset($expression) && is_array($expression)) { 229 | $summaryInfo = AggregateHelper::GetSummaryInfo($expression); 230 | $fields = $summaryInfo["fields"]; 231 | if (strlen($fields) > 0) { 232 | $filter = ""; 233 | if (isset($filterExpression)) { 234 | if (is_string($filterExpression)) { 235 | $filter = trim($filterExpression); 236 | } 237 | if (is_array($filterExpression)) { 238 | $filter = FilterHelper::GetSqlExprByArray($filterExpression); 239 | } 240 | } 241 | $totalSummaryQuery = sprintf("%s %s %s %s %s", 242 | self::$SELECT_OP, 243 | $fields, 244 | self::$FROM_OP, 245 | $this->dbTableName, 246 | strlen($filter) > 0 ? self::$WHERE_OP." ".$filter : $filter); 247 | $this->lastError = NULL; 248 | $queryResult = $this->mySQL->query($totalSummaryQuery); 249 | if (!$queryResult) { 250 | $this->lastError = $this->mySQL->error; 251 | } 252 | else if ($queryResult->num_rows > 0) { 253 | $result = $queryResult->fetch_array(MYSQLI_NUM); 254 | foreach ($result as $i => $item) { 255 | $result[$i] = Utils::StringToNumber($item); 256 | } 257 | } 258 | if ($queryResult !== false) { 259 | $queryResult->close(); 260 | } 261 | } 262 | } 263 | return $result; 264 | } 265 | public function GetGroupCount() { 266 | $result = 0; 267 | if ($this->mySQL && isset($this->groupSettings) && isset($this->groupSettings["groupItemCountQuery"])) { 268 | $this->lastError = NULL; 269 | $queryResult = $this->mySQL->query($this->groupSettings["groupItemCountQuery"]); 270 | if (!$queryResult) { 271 | $this->lastError = $this->mySQL->error; 272 | } 273 | else if ($queryResult->num_rows > 0) { 274 | $row = $queryResult->fetch_array(MYSQLI_NUM); 275 | $result = Utils::StringToNumber($row[0]); 276 | } 277 | if ($queryResult !== false) { 278 | $queryResult->close(); 279 | } 280 | } 281 | return $result; 282 | } 283 | public function GetCount() { 284 | $result = 0; 285 | if ($this->mySQL) { 286 | $countQuery = sprintf("%s %s(1) %s (%s) %s %s_%d", 287 | self::$SELECT_OP, 288 | AggregateHelper::COUNT_OP, 289 | self::$FROM_OP, 290 | $this->resultQuery, 291 | AggregateHelper::AS_OP, 292 | $this->dbTableName, 293 | $this->tableNameIndex + 1); 294 | $this->lastError = NULL; 295 | $queryResult = $this->mySQL->query($countQuery); 296 | if (!$queryResult) { 297 | $this->lastError = $this->mySQL->error; 298 | } 299 | else if ($queryResult->num_rows > 0) { 300 | $row = $queryResult->fetch_array(MYSQLI_NUM); 301 | $result = Utils::StringToNumber($row[0]); 302 | } 303 | if ($queryResult !== false) { 304 | $queryResult->close(); 305 | } 306 | } 307 | return $result; 308 | } 309 | public function AsArray() { 310 | $result = NULL; 311 | if ($this->mySQL) { 312 | $this->lastError = NULL; 313 | $queryResult = $this->mySQL->query($this->resultQuery); 314 | if (!$queryResult) { 315 | $this->lastError = $this->mySQL->error; 316 | } 317 | else { 318 | if (isset($this->groupSettings)) { 319 | $result = AggregateHelper::GetGroupedDataFromQuery($queryResult, $this->groupSettings); 320 | } 321 | else { 322 | $result = $queryResult->fetch_all(MYSQLI_ASSOC); 323 | } 324 | $queryResult->close(); 325 | } 326 | } 327 | return $result; 328 | } 329 | public function Insert($values) { 330 | Utils::EscapeExpressionValues($this->mySQL, $values); 331 | $result = NULL; 332 | if (isset($values) && is_array($values)) { 333 | $fields = ""; 334 | $fieldValues = ""; 335 | foreach ($values as $prop => $value) { 336 | $fields .= (strlen($fields) ? ", " : "").Utils::QuoteStringValue($prop); 337 | $fieldValues .= (strlen($fieldValues) ? ", " : "").Utils::QuoteStringValue($value, false); 338 | } 339 | if (strlen($fields) > 0) { 340 | $queryString = sprintf("%s %s (%s) %s(%s)", 341 | self::$INSERT_OP, 342 | $this->dbTableName, 343 | $fields, 344 | self::$VALUES_OP, 345 | $fieldValues); 346 | $this->lastError = NULL; 347 | if ($this->mySQL->query($queryString) == true) { 348 | $result = $this->mySQL->affected_rows; 349 | } 350 | else { 351 | $this->lastError = $this->mySQL->error; 352 | } 353 | } 354 | } 355 | return $result; 356 | } 357 | public function Update($key, $values) { 358 | Utils::EscapeExpressionValues($this->mySQL, $key); 359 | Utils::EscapeExpressionValues($this->mySQL, $values); 360 | $result = NULL; 361 | if (isset($key) && is_array($key) && isset($values) && is_array($values)) { 362 | $fields = ""; 363 | foreach ($values as $prop => $value) { 364 | $templ = strlen($fields) == 0 ? "%s = %s" : ", %s = %s"; 365 | $fields .= sprintf($templ, 366 | Utils::QuoteStringValue($prop), 367 | Utils::QuoteStringValue($value, false)); 368 | } 369 | if (strlen($fields) > 0) { 370 | $queryString = sprintf("%s %s %s %s %s %s", 371 | self::$UPDATE_OP, 372 | $this->dbTableName, 373 | self::$SET_OP, 374 | $fields, 375 | self::$WHERE_OP, 376 | FilterHelper::GetSqlExprByKey($key)); 377 | $this->lastError = NULL; 378 | if ($this->mySQL->query($queryString) == true) { 379 | $result = $this->mySQL->affected_rows; 380 | } 381 | else { 382 | $this->lastError = $this->mySQL->error; 383 | } 384 | } 385 | } 386 | return $result; 387 | } 388 | public function Delete($key) { 389 | Utils::EscapeExpressionValues($this->mySQL, $key); 390 | $result = NULL; 391 | if (isset($key) && is_array($key)) { 392 | $queryString = sprintf("%s %s %s %s %s", 393 | self::$DELETE_OP, 394 | self::$FROM_OP, 395 | $this->dbTableName, 396 | self::$WHERE_OP, 397 | FilterHelper::GetSqlExprByKey($key)); 398 | $this->lastError = NULL; 399 | if ($this->mySQL->query($queryString) == true) { 400 | $result = $this->mySQL->affected_rows; 401 | } 402 | else { 403 | $this->lastError = $this->mySQL->error; 404 | } 405 | } 406 | return $result; 407 | } 408 | } 409 | -------------------------------------------------------------------------------- /DevExtreme/FilterHelper.php: -------------------------------------------------------------------------------- 1 | ": { 62 | $clause = self::$IS_OP." ".self::$NOT_OP; 63 | break; 64 | } 65 | } 66 | } 67 | else { 68 | switch ($clause) { 69 | case "=": 70 | case "<>": 71 | case ">": 72 | case ">=": 73 | case "<": 74 | case "<=": { 75 | $pattern = "%s %s %s"; 76 | $val = Utils::QuoteStringValue($val, false); 77 | break; 78 | } 79 | case "startswith": { 80 | $pattern = "%s %s '%s%%'"; 81 | $clause = self::$LIKE_OP; 82 | $val = addcslashes($val, "%_"); 83 | break; 84 | } 85 | case "endswith": { 86 | $pattern = "%s %s '%%%s'"; 87 | $val = addcslashes($val, "%_"); 88 | $clause = self::$LIKE_OP; 89 | break; 90 | } 91 | case "contains": { 92 | $pattern = "%s %s '%%%s%%'"; 93 | $val = addcslashes($val, "%_"); 94 | $clause = self::$LIKE_OP; 95 | break; 96 | } 97 | case "notcontains": { 98 | $pattern = "%s %s '%%%s%%'"; 99 | $val = addcslashes($val, "%_"); 100 | $clause = sprintf("%s %s", self::$NOT_OP, self::$LIKE_OP); 101 | break; 102 | } 103 | default: { 104 | $clause = ""; 105 | } 106 | } 107 | } 108 | $result = sprintf($pattern, $fieldName, $clause, $val); 109 | } 110 | return $result; 111 | } 112 | public static function GetSqlExprByArray($expression) { 113 | $result = "("; 114 | $prevItemWasArray = false; 115 | foreach ($expression as $index => $item) { 116 | if (is_string($item)) { 117 | $prevItemWasArray = false; 118 | if ($index == 0) { 119 | if ($item == "!") { 120 | $result .= sprintf("%s ", self::$NOT_OP); 121 | continue; 122 | } 123 | $result .= (isset($expression) && is_array($expression)) ? self::_GetSimpleSqlExpr($expression) : ""; 124 | break; 125 | } 126 | $strItem = strtoupper(trim($item)); 127 | if ($strItem == self::$AND_OP || $strItem == self::$OR_OP) { 128 | $result .= sprintf(" %s ", $strItem); 129 | } 130 | continue; 131 | } 132 | if (is_array($item)) { 133 | if ($prevItemWasArray) { 134 | $result .= sprintf(" %s ", self::$AND_OP); 135 | } 136 | $result .= self::GetSqlExprByArray($item); 137 | $prevItemWasArray = true; 138 | } 139 | } 140 | $result .= ")"; 141 | return $result; 142 | } 143 | public static function GetSqlExprByKey($key) { 144 | $result = ""; 145 | foreach ($key as $prop => $value) { 146 | $templ = strlen($result) == 0 ? 147 | "%s = %s" : 148 | " ".self::$AND_OP." %s = %s"; 149 | $result .= sprintf($templ, 150 | Utils::QuoteStringValue($prop), 151 | Utils::QuoteStringValue($value, false)); 152 | } 153 | return $result; 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /DevExtreme/LoadHelper.php: -------------------------------------------------------------------------------- 1 | ", ",", "{", 9 | "}", "?", ":", ";", "\r", "\n" 10 | ); 11 | public static function StringToNumber($str) { 12 | $currentLocale = localeconv(); 13 | $decimalPoint = $currentLocale["decimal_point"]; 14 | $result = strpos($str, $decimalPoint) === false ? intval($str) : floatval($str); 15 | return $result; 16 | } 17 | public static function EscapeExpressionValues($mySql, &$expression = NULL) { 18 | if (isset($expression)) { 19 | if (is_string($expression)) { 20 | $expression = $mySql->real_escape_string($expression); 21 | } 22 | else if (is_array($expression)) { 23 | foreach ($expression as &$arr_value) { 24 | self::EscapeExpressionValues($mySql, $arr_value); 25 | } 26 | unset($arr_value); 27 | } 28 | else if (gettype($expression) === "object") { 29 | foreach ($expression as $prop => $value) { 30 | self::EscapeExpressionValues($mySql, $expression->$prop); 31 | } 32 | } 33 | } 34 | } 35 | public static function QuoteStringValue($value, $isFieldName = true) { 36 | if (!$isFieldName) { 37 | $value = self::_ConvertDateTimeToMySQLValue($value); 38 | } else { 39 | $value = str_replace(self::$FORBIDDEN_CHARACTERS, "", $value); 40 | } 41 | $resultPattern = $isFieldName ? "`%s`" : (is_bool($value) || is_null($value) ? "%s" : "'%s'"); 42 | $stringValue = is_bool($value) ? ($value ? "1" : "0") : (is_null($value) ? self::$NULL_VAL : strval($value)); 43 | $result = sprintf($resultPattern, $stringValue); 44 | return $result; 45 | } 46 | public static function GetItemValueOrDefault($params, $key, $defaultValue = NULL) { 47 | return isset($params[$key]) ? $params[$key] : $defaultValue; 48 | } 49 | private static function _ConvertDatePartToISOValue($date) { 50 | $dateParts = explode("/", $date); 51 | return sprintf("%s-%s-%s", $dateParts[2], $dateParts[0], $dateParts[1]); 52 | } 53 | private static function _ConvertDateTimeToMySQLValue($strValue) { 54 | $result = $strValue; 55 | if (preg_match("/^\d{1,2}\/\d{1,2}\/\d{4}$/", $strValue) === 1) { 56 | $result = self::_ConvertDatePartToISOValue($strValue); 57 | } 58 | else if (preg_match("/^\d{1,2}\/\d{1,2}\/\d{4} \d{2}:\d{2}:\d{2}\.\d{3}$/", $strValue) === 1) { 59 | $spacePos = strpos($strValue, " "); 60 | $datePart = substr($strValue, 0, $spacePos); 61 | $timePart = substr($strValue, $spacePos + 1); 62 | $result = sprintf("%s %s", self::_ConvertDatePartToISOValue($datePart), $timePart); 63 | } 64 | return $result; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) Developer Express Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/DevExpress/DevExtreme-PHP-Data.svg?branch=master)](https://travis-ci.org/DevExpress/DevExtreme-PHP-Data) 2 | # DevExtreme PHP Data 3 | --- 4 | 5 | This library is intended for loading data from a MySQL (__[mysqlnd](http://php.net/manual/en/book.mysqlnd.php) is required__) database, based on expressions passed from a client by a DevExtreme data source. The library will allow you to implement a data service that supports 6 | the protocol described in the following help topic: [Custom Data Sources](https://js.devexpress.com/Documentation/Guide/Data_Binding/Specify_a_Data_Source/Custom_Data_Sources/). 7 | 8 | Moreover, the library will allow you to perform all operations such as, [filtering](https://js.devexpress.com/Documentation/Guide/Data_Layer/Data_Layer/#Data_Layer_Data_Layer_Reading_Data_Filtering), 9 | [sorting](https://js.devexpress.com/Documentation/Guide/Data_Layer/Data_Layer/#Data_Layer_Data_Layer_Reading_Data_Sorting), [paging](https://js.devexpress.com/Documentation/Guide/Data_Layer/Data_Layer/#Data_Layer_Data_Layer_Reading_Data_Paging) and [grouping](https://js.devexpress.com/Documentation/Guide/Data_Layer/Data_Layer/#Data_Layer_Data_Layer_Reading_Data_Grouping) on the server side. 10 | 11 | __NOTE__ 12 | This library is for demonstrative purposes only. It illustrates how to use PHP to process client requests on the server. If the current functionality does not meet your needs, feel free to fork the repository and modify the source code according to your requirements. 13 | 14 | ## Getting Started 15 | --- 16 | 17 | If you wish to load data from a MySQL data table based on parameters received from the client side, perform the following steps. 18 | 19 | * Copy the __DevExtreme__ folder to your project. 20 | * Include the __LoadHelper.php__ file to your PHP script where you wish to call the __DevExtreme PHP Data__ library API. 21 | * Register the library autoloader by executing the following code: 22 | 23 | ```PHP 24 | spl_autoload_register(array("DevExtreme\LoadHelper", "LoadModule")); 25 | ``` 26 | 27 | Since all classes in the library are wrapped in the DevExtreme namespace, use the [use](http://php.net/manual/en/language.namespaces.importing.php) 28 | operator to get rid of long names in your code. Only two classes are required: ```DbSet``` and ```DataSourceLoader```. Execute the following code to be ready to use these classes: 29 | 30 | ```PHP 31 | use DevExtreme\DbSet; 32 | use DevExtreme\DataSourceLoader; 33 | ``` 34 | 35 | To load data, perform the following steps. 36 | 37 | * Create the [mysqli](http://php.net/manual/en/book.mysqli.php) instance passing your MySQL server user credentials to its constructor. 38 | * Create the ```DbSet``` instance passing the ```mysqli``` instance as a first parameter and a table name from which you wish to load data as a second parameter 39 | to the ```DbSet``` constructor. 40 | * Get parameters received from the client side and pass them with the ```DbSet``` instance to the ```Load``` method of the ```DataSourceLoader``` class. This 41 | is a static method and you will not need to create an instance of the class. The following code snippet shows how to do this: 42 | 43 | ```PHP 44 | $mySQL = new mysqli("serverName", "userName", "password", "databaseName"); 45 | $dbSet = new DbSet($mySQL, "tableName"); 46 | $result = DataSourceLoader::Load($dbSet, $params); 47 | ``` 48 | Note that parameters (```$params```) must be passed as an associative [array](http://php.net/manual/en/language.types.array.php). 49 | 50 | That's it. The ```$result``` variable will contain the required results. It will represent an array of items. If you wish to send it to the client to be 51 | dislplayed in a DevExtreme widget, convert it to the [JSON](https://en.wikipedia.org/wiki/JSON) format. You can use the [json_encode](http://php.net/manual/en/function.json-encode.php) function 52 | for this task. 53 | 54 | The __example__ folder of this repository contains an example that shows how to use the library. To test it: 55 | * copy this folder to your web server folder; 56 | * execute the following command in the __example__ folder; 57 | ``` 58 | npm install 59 | ``` 60 | * Specify user credentials, database name and table name in the _php/DataController.php_ file (see the ```DataController``` constructor) and open the _index.html_ file in a web browser. 61 | 62 | 63 | 64 | ## Diving deeper 65 | --- 66 | 67 | The ```DbSet``` class has the following public methods that you can use without using the ```DataSourceLoader``` class. 68 | * ```Delete``` - deletes an item from a data table. 69 | It accepts a single parameter that represents a key. The key must be passed as an associative array. For example, if your key is ID and its value is 1, it should be passed to the method in the following manner: 70 | 71 | ```PHP 72 | $dbSet->Delete(array("ID" => 1)); 73 | ``` 74 | The method returns a number of the affected rows. 75 | 76 | 77 | * ```GetLastError``` - returns a string representation of the error that was thrown by the last executed SQL query. 78 | The method does not accept parameters. 79 | 80 | 81 | * ```Insert``` - insert a new row to your data table. 82 | It accepts a single parameter that represents an associative array of items. 83 | The method returns a number of the affected rows. 84 | 85 | 86 | * ```Update``` - updates a certain item in your data table that is identified by a key value. 87 | It accepts two parameters. The first parameter represents a key value. It should be specified in the same manner as a key value for the ```Delete``` method. 88 | The second parameter represents an associative array of values that corresponds to data fields you wish to update. 89 | The method returns a number of the affected rows. 90 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | ## Security 2 | 3 | Please refer to [DevExpress Security Policy](https://github.com/DevExpress/Shared/security/policy) 4 | -------------------------------------------------------------------------------- /example/css/index.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | height: 100%; 3 | width: 100%; 4 | margin: 0; 5 | padding: 0; 6 | } -------------------------------------------------------------------------------- /example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | DevExtreme PHP Data Demo 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 |
21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /example/js/index.js: -------------------------------------------------------------------------------- 1 | $(function() { 2 | var url = "http://localhost/php/service.php"; 3 | var db = DevExpress.data.AspNet.createStore({ 4 | key: "ID", 5 | loadUrl: url, 6 | insertUrl: url, 7 | updateUrl: url, 8 | deleteUrl: url 9 | }); 10 | $("#gridContainer").dxDataGrid({ 11 | dataSource: { 12 | store: db 13 | }, 14 | height: "100%", 15 | columns: [{ 16 | dataField: "ID", 17 | dataType: "number", 18 | allowEditing: false 19 | }, { 20 | dataField: "Name" 21 | }, { 22 | dataField: "Category" 23 | }, { 24 | dataField: "CustomerName" 25 | }, { 26 | dataField: "BDate", 27 | dataType: "date" 28 | }], 29 | groupPanel: { 30 | visible: true 31 | }, 32 | sorting: { 33 | mode: "multiple" 34 | }, 35 | searchPanel: { 36 | visible: true 37 | }, 38 | scrolling: { 39 | mode: "virtual" 40 | }, 41 | filterRow: { 42 | visible: true 43 | }, 44 | editing: { 45 | mode: "batch", 46 | allowAdding: true, 47 | allowUpdating: true, 48 | allowDeleting: true 49 | }, 50 | grouping: { 51 | autoExpandAll: false 52 | }, 53 | pager: { 54 | showPageSizeSelector: true, 55 | showInfo: true 56 | }, 57 | summary: { 58 | totalItems: [{ 59 | column: "ID", 60 | summaryType: "sum" 61 | }, 62 | { 63 | column: "ID", 64 | summaryType: "avg" 65 | }, 66 | { 67 | column: "ID", 68 | summaryType: "min" 69 | }, { 70 | column: "ID", 71 | summaryType: "max" 72 | }], 73 | groupItems: [{ 74 | summaryType: "count" 75 | },{ 76 | column: "ID", 77 | summaryType: "min" 78 | }, { 79 | column: "ID", 80 | summaryType: "max"}] 81 | }, 82 | remoteOperations: { 83 | filtering: true, 84 | grouping: true, 85 | groupPaging: true, 86 | paging: true, 87 | sorting: true, 88 | summary: true 89 | }, 90 | headerFilter: { 91 | visible: true 92 | } 93 | }); 94 | }); -------------------------------------------------------------------------------- /example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "devextreme-php-data", 3 | "license": "MIT", 4 | "dependencies": { 5 | "devextreme": "21.2_next", 6 | "devextreme-aspnet-data": "^2.8.6", 7 | "jquery": "^3.5.1" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /example/php/DataController.php: -------------------------------------------------------------------------------- 1 | dbSet = new DbSet($mySQL, "tableName"); 14 | } 15 | public function FillDbIfEmpty() { 16 | if ($this->dbSet->GetCount() == 0) { 17 | $curDateString = "2013-1-1"; 18 | for ($i = 1; $i <= 10000; $i++) { 19 | $curDT = new DateTime($curDateString); 20 | $curDT->add(new DateInterval("P".strval(rand(1, 1500))."D")); 21 | $item = array( 22 | "Name" => "Name_".strval(rand(1, 100)), 23 | "Category" => "Category_".strval(rand(1, 30)), 24 | "CustomerName" => "Customer_".strval(rand(1, 50)), 25 | "BDate" => $curDT->format("Y-m-d") 26 | ); 27 | $this->dbSet->Insert($item); 28 | } 29 | } 30 | } 31 | public function Get($params) { 32 | $result = DataSourceLoader::Load($this->dbSet, $params); 33 | if (!isset($result)) { 34 | $result = $this->dbSet->GetLastError(); 35 | } 36 | return $result; 37 | } 38 | public function Post($values) { 39 | $result = $this->dbSet->Insert($values); 40 | if (!isset($result)) { 41 | $result = $this->dbSet->GetLastError(); 42 | } 43 | return $result; 44 | } 45 | public function Put($key, $values) { 46 | $result = NULL; 47 | if (isset($key) && isset($values) && is_array($values)) { 48 | if (!is_array($key)) { 49 | $keyVal = $key; 50 | $key = array(); 51 | $key["ID"] = $keyVal; 52 | } 53 | $result = $this->dbSet->Update($key, $values); 54 | if (!isset($result)) { 55 | $result = $this->dbSet->GetLastError(); 56 | } 57 | } 58 | else { 59 | throw new Exeption("Invalid params"); 60 | } 61 | return $result; 62 | } 63 | public function Delete($key) { 64 | $result = NULL; 65 | if (isset($key)) { 66 | if (!is_array($key)) { 67 | $keyVal = $key; 68 | $key = array(); 69 | $key["ID"] = $keyVal; 70 | } 71 | $result = $this->dbSet->Delete($key); 72 | if (!isset($result)) { 73 | $result = $this->dbSet->GetLastError(); 74 | } 75 | } 76 | else { 77 | throw new Exeption("Invalid params"); 78 | } 79 | return $result; 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /example/php/service.php: -------------------------------------------------------------------------------- 1 | $value) { 9 | $result[$key] = json_decode($params[$key], $assoc); 10 | if ($result[$key] === NULL) { 11 | $result[$key] = $params[$key]; 12 | } 13 | } 14 | } 15 | else { 16 | $result = $params; 17 | } 18 | return $result; 19 | } 20 | function GetParamsFromInput() { 21 | $result = NULL; 22 | $content = file_get_contents("php://input"); 23 | if ($content !== false) { 24 | $params = array(); 25 | parse_str($content, $params); 26 | $result = GetParseParams($params, true); 27 | } 28 | return $result; 29 | } 30 | $response = NULL; 31 | $controller = new DataController(); 32 | $controller->FillDbIfEmpty(); 33 | switch($_SERVER["REQUEST_METHOD"]) { 34 | case "GET": { 35 | $params = GetParseParams($_GET); 36 | $response = $controller->Get($params); 37 | break; 38 | } 39 | case "POST": { 40 | $params = GetParamsFromInput(); 41 | $response = $controller->Post($params["values"]); 42 | break; 43 | } 44 | case "PUT": { 45 | $params = GetParamsFromInput(); 46 | $response = $controller->Put($params["key"], $params["values"]); 47 | break; 48 | } 49 | case "DELETE": { 50 | $params = GetParamsFromInput(); 51 | $response = $controller->Delete($params["key"]); 52 | break; 53 | } 54 | } 55 | unset($controller); 56 | if (isset($response) && !is_string($response)) { 57 | header("Content-type: application/json"); 58 | echo json_encode($response); 59 | } 60 | else { 61 | header("HTTP/1.1 500 Internal Server Error"); 62 | header("Content-Type: application/json"); 63 | echo json_encode(array("message" => $response, "code" => 500)); 64 | } 65 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | tests/UtilsTest.php 5 | tests/FilterHelperTest.php 6 | tests/AggregateHelperTest.php 7 | 8 | 9 | tests/DbSetAPITest.php 10 | tests/DataSourceLoaderTest.php 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /tests/AggregateHelperTest.php: -------------------------------------------------------------------------------- 1 | "field1", 20 | "isExpanded" => false, 21 | ), 22 | (object)array( 23 | "selector" => "field2" 24 | ) 25 | ), 26 | true 27 | ), 28 | array( 29 | array( 30 | (object)array( 31 | "selector" => "field1", 32 | ), 33 | (object)array( 34 | "selector" => "field2", 35 | "isExpanded" => false 36 | ) 37 | ), 38 | false 39 | ), 40 | array( 41 | array( 42 | "field1", 43 | (object)array( 44 | "selector" => "field2", 45 | "isExpanded" => false 46 | ) 47 | ), 48 | false 49 | ) 50 | ); 51 | } 52 | public function testGetFieldSetBySelectors() { 53 | $params = array( 54 | "field1", 55 | (object)array( 56 | "selector" => "field2" 57 | ), 58 | (object)array( 59 | "selector" => "field3", 60 | "desc" => true 61 | ), 62 | (object)array( 63 | "selector" => "field4", 64 | "groupInterval" => 10 65 | ), 66 | (object)array( 67 | "selector" => "field5", 68 | "groupInterval" => 10, 69 | "desc" => true 70 | ), 71 | (object)array( 72 | "selector" => "field6", 73 | "groupInterval" => "year", 74 | ), 75 | (object)array( 76 | "selector" => "field6", 77 | "groupInterval" => "month", 78 | "desc" => true 79 | ) 80 | ); 81 | $groupFields = "`field1`, `field2`, `field3`, `dx_field4_10`, `dx_field5_10`, `dx_field6_year`, `dx_field6_month`"; 82 | $sortFields = "`field1`, `field2`, `field3` DESC, `dx_field4_10`, `dx_field5_10` DESC, `dx_field6_year`, `dx_field6_month` DESC"; 83 | $selectFields = "`field1`, `field2`, `field3`, (`field4` - (`field4` % 10)) AS `dx_field4_10`, (`field5` - (`field5` % 10)) AS `dx_field5_10`, YEAR(`field6`) AS `dx_field6_year`, MONTH(`field6`) AS `dx_field6_month`"; 84 | 85 | $fieldSet = AggregateHelper::GetFieldSetBySelectors($params); 86 | 87 | $this->assertEquals($groupFields, $fieldSet["group"]); 88 | $this->assertEquals($sortFields, $fieldSet["sort"]); 89 | $this->assertEquals($selectFields, $fieldSet["select"]); 90 | } 91 | public function testGetGroupedDataFromQuery_lastGroupExpanded() { 92 | $groupSettings = array( 93 | "groupCount" => 2, 94 | "lastGroupExpanded" => true 95 | ); 96 | $expectedResult = array( 97 | array( 98 | "key" => "2013", 99 | "items" => array( 100 | array( 101 | "key" => "Beverages", 102 | "count" => 10 103 | ), 104 | array( 105 | "key" => "Condiments", 106 | "count" => 9 107 | ), 108 | array( 109 | "key" => "Dairy Products", 110 | "count" => 4 111 | ), 112 | array( 113 | "key" => "Seafood", 114 | "count" => 8 115 | ) 116 | ) 117 | ) 118 | ); 119 | $query = "SELECT YEAR(`BDate`) AS `dx_BDate_year`, `Category`, test_products_1.* FROM (SELECT * FROM test_products) AS test_products_1 ORDER BY `dx_BDate_year`, `Category`"; 120 | $queryResult = AggregateHelperTest::$mySQL->query($query); 121 | $result = AggregateHelper::GetGroupedDataFromQuery($queryResult, $groupSettings); 122 | $queryResult->close(); 123 | 124 | $this->assertEquals(count($expectedResult), count($result)); 125 | 126 | $checked = true; 127 | for ($i = 0; $i < count($expectedResult); $i++) { 128 | if ($expectedResult[$i]["key"] != $result[$i]["key"]) { 129 | $checked = false; 130 | break; 131 | } 132 | for ($j = 0; $j < count($expectedResult[$i]["items"]); $j++) { 133 | $expectedGroup = $expectedResult[$i]["items"][$j]; 134 | $group = $result[$i]["items"][$j]; 135 | if ($expectedGroup["key"] != $group["key"]) { 136 | $checked = false; 137 | break; 138 | } 139 | if ($expectedGroup["count"] != count($group["items"])) { 140 | $checked = false; 141 | break; 142 | } 143 | } 144 | if (!$checked) { 145 | break; 146 | } 147 | } 148 | 149 | $this->assertTrue($checked); 150 | } 151 | public function testGetGroupedDataFromQuery_summaryType() { 152 | $groupSettings = array( 153 | "groupCount" => 3, 154 | "summaryTypes" => array("SUM"), 155 | "lastGroupExpanded" => false 156 | ); 157 | $expectedResult = array( 158 | array( 159 | "key" => "2013", 160 | "items" => array( 161 | array( 162 | "key" => "Beverages", 163 | "count" => 10, 164 | "summary" => array(141) 165 | ), 166 | array( 167 | "key" => "Condiments", 168 | "count" => 9, 169 | "summary" => array(138) 170 | ), 171 | array( 172 | "key" => "Dairy Products", 173 | "count" => 4, 174 | "summary" => array(65) 175 | ), 176 | array( 177 | "key" => "Seafood", 178 | "count" => 8, 179 | "summary" => array(152) 180 | ) 181 | ) 182 | ) 183 | ); 184 | $query = "SELECT YEAR(`BDate`) AS `dx_BDate_year`, `Category`, COUNT(1), SUM(`ID`) AS dx_f0 FROM (SELECT * FROM test_products) AS test_products_1 GROUP BY `dx_BDate_year`, `Category` ORDER BY `dx_BDate_year`, `Category`"; 185 | $queryResult = AggregateHelperTest::$mySQL->query($query); 186 | $result = AggregateHelper::GetGroupedDataFromQuery($queryResult, $groupSettings); 187 | $queryResult->close(); 188 | 189 | $this->assertEquals(count($expectedResult), count($result)); 190 | 191 | $checked = true; 192 | for ($i = 0; $i < count($expectedResult); $i++) { 193 | if ($expectedResult[$i]["key"] != $result[$i]["key"]) { 194 | $checked = false; 195 | break; 196 | } 197 | for ($j = 0; $j < count($expectedResult[$i]["items"]); $j++) { 198 | $expectedGroup = $expectedResult[$i]["items"][$j]; 199 | $group = $result[$i]["items"][$j]; 200 | if ($expectedGroup["key"] != $group["key"]) { 201 | $checked = false; 202 | break; 203 | } 204 | if ($expectedGroup["count"] != $group["count"]) { 205 | $checked = false; 206 | break; 207 | } 208 | if (count($expectedGroup["summary"]) != count($group["summary"])) { 209 | $checked = false; 210 | break; 211 | } 212 | if ($expectedGroup["summary"][0] != $group["summary"][0]) { 213 | $checked = false; 214 | break; 215 | } 216 | } 217 | if (!$checked) { 218 | break; 219 | } 220 | } 221 | 222 | $this->assertTrue($checked); 223 | } 224 | /** 225 | * @dataProvider providerGroupSelectors 226 | */ 227 | public function testIsLastGroupExpanded($selectors, $expectedResult) { 228 | $result = AggregateHelper::IsLastGroupExpanded($selectors); 229 | 230 | $this->assertTrue($expectedResult === $result); 231 | } 232 | public function testGetSummaryInfo() { 233 | $selectors = array( 234 | (object)array( 235 | "selector" => "field1", 236 | "summaryType" => "max" 237 | ), 238 | (object)array( 239 | "selector" => "field2", 240 | "summaryType" => "sum" 241 | ) 242 | ); 243 | $expectedResult = array( 244 | "fields" => "MAX(`field1`) AS dx_f0, SUM(`field2`) AS dx_f1", 245 | "summaryTypes" => array("MAX", "SUM") 246 | ); 247 | 248 | $result = AggregateHelper::GetSummaryInfo($selectors); 249 | 250 | $this->assertEquals($expectedResult["fields"], $result["fields"]); 251 | $this->assertEquals(count($expectedResult["summaryTypes"]), count($result["summaryTypes"])); 252 | $this->assertEquals($expectedResult["summaryTypes"][0], $result["summaryTypes"][0]); 253 | $this->assertEquals($expectedResult["summaryTypes"][1], $result["summaryTypes"][1]); 254 | } 255 | } 256 | -------------------------------------------------------------------------------- /tests/ConfigHelper.php: -------------------------------------------------------------------------------- 1 | "Name", 13 | "desc" => false 14 | ) 15 | ), 16 | "", 17 | false, 18 | "Name" 19 | ), 20 | array( 21 | array( 22 | (object)array( 23 | "selector" => "Name", 24 | "desc" => true 25 | ) 26 | ), 27 | "Z", 28 | true, 29 | "Name" 30 | ) 31 | ); 32 | } 33 | public function providerFilter() { 34 | return array( 35 | array( 36 | array("ID", "=", 10), 37 | array(10) 38 | ), 39 | array( 40 | array( 41 | array("ID", ">", 1), 42 | "and", 43 | array("ID", "<=", 3) 44 | ), 45 | array(2, 3) 46 | ), 47 | array( 48 | array("ID", ">=", 29), 49 | array(29, 30, 31) 50 | ), 51 | array( 52 | array("ID", "<", 2), 53 | array(1) 54 | ), 55 | array( 56 | array( 57 | array("!", array("ID", "=", 2)), 58 | "and", 59 | array("ID", "<=", 3) 60 | ), 61 | array(1, 3) 62 | ), 63 | array( 64 | array("Name", "startswith", "Cha"), 65 | array(1, 2) 66 | ), 67 | array( 68 | array("Name", "endswith", "ku"), 69 | array(9) 70 | ), 71 | array( 72 | array("Name", "contains", "onb"), 73 | array(13) 74 | ), 75 | array( 76 | array( 77 | array("Name", "notcontains", "A"), 78 | "and", 79 | array("Name", "notcontains", "a") 80 | ), 81 | array(9, 13, 14, 15, 21, 23, 26) 82 | ), 83 | array( 84 | array( 85 | array("CustomerName", "<>", null), 86 | "and", 87 | array("ID", ">", 27) 88 | ), 89 | array(28, 29, 30) 90 | ) 91 | ); 92 | } 93 | public function providerGroup() { 94 | return array( 95 | array(array("Category"), "", false, "key", 4, array(10, 9, 4, 8)), 96 | array( 97 | array( 98 | (object)array( 99 | "selector" => "Category", 100 | "desc" => false 101 | ) 102 | ), 103 | "", 104 | false, 105 | "key", 106 | 4, 107 | array(10, 9, 4, 8) 108 | ), 109 | array( 110 | array( 111 | (object)array( 112 | "selector" => "Category", 113 | "desc" => true, 114 | "isExpanded" => false 115 | ) 116 | ), 117 | "Z", 118 | true, 119 | "key", 120 | 4, 121 | array(8, 4, 9, 10) 122 | ), 123 | array( 124 | array( 125 | (object)array( 126 | "selector" => "BDate", 127 | "groupInterval" => "year", 128 | "desc" => true, 129 | "isExpanded" => false 130 | ) 131 | ), 132 | "9999", 133 | true, 134 | "key", 135 | 1, 136 | array(31) 137 | ) 138 | ); 139 | } 140 | public function providerGroupPaging() { 141 | $groupExpression1 = array( 142 | (object)array( 143 | "selector" => "Category", 144 | "desc" => false, 145 | "isExpanded" => false 146 | ) 147 | ); 148 | $groupExpression2 = array( 149 | (object)array( 150 | "selector" => "Category", 151 | "desc" => false, 152 | "isExpanded" => true 153 | ) 154 | ); 155 | $params1 = array( 156 | "requireGroupCount" => true, 157 | "group" => $groupExpression1, 158 | "skip" => 1, 159 | "take" => 2 160 | ); 161 | $params2 = array( 162 | "requireGroupCount" => true, 163 | "group" => $groupExpression2, 164 | "skip" => 1, 165 | "take" => 2 166 | ); 167 | $resultGroupItems = array("Condiments", "Dairy Products"); 168 | return array( 169 | array($params1, $resultGroupItems), 170 | array($params2, $resultGroupItems) 171 | ); 172 | } 173 | public function providerTotalSummary() { 174 | $summaryExpression1 = array( 175 | (object)array( 176 | "summaryType" => "count" 177 | ) 178 | ); 179 | $summaryExpression2 = array( 180 | (object)array( 181 | "selector" => "ID", 182 | "summaryType" => "min" 183 | ) 184 | ); 185 | $summaryExpression3 = array( 186 | (object)array( 187 | "selector" => "ID", 188 | "summaryType" => "max" 189 | ) 190 | ); 191 | $summaryExpression4 = array( 192 | (object)array( 193 | "selector" => "ID", 194 | "summaryType" => "sum" 195 | ) 196 | ); 197 | $summaryExpression5 = array( 198 | (object)array( 199 | "selector" => "ID", 200 | "summaryType" => "avg" 201 | ) 202 | ); 203 | return array( 204 | array($summaryExpression1, 31), 205 | array($summaryExpression2, 1), 206 | array($summaryExpression3, 31), 207 | array($summaryExpression4, 496), 208 | array($summaryExpression5, 16) 209 | ); 210 | } 211 | public function testLoaderSelect() { 212 | $columns = array("BDate", "Category", "CustomerName"); 213 | $params = array( 214 | "select" => $columns 215 | ); 216 | $data = DataSourceLoader::Load($this->dbSet, $params); 217 | $result = isset($data) && is_array($data) && isset($data["data"]) && count($data["data"]) > 0 ? 218 | array_keys($data["data"][0]) : 219 | array(); 220 | $this->assertEquals($columns, $result); 221 | } 222 | public function testLoaderTotalCount() { 223 | $params = array( 224 | "requireTotalCount" => true 225 | ); 226 | $data = DataSourceLoader::Load($this->dbSet, $params); 227 | $result = isset($data) && is_array($data) && 228 | isset($data["data"]) && isset($data["totalCount"]) && 229 | count($data["data"]) == $data["totalCount"] && $data["totalCount"] == 31; 230 | $this->assertTrue($result); 231 | } 232 | /** 233 | * @dataProvider providerSort 234 | */ 235 | public function testLoaderSort($sortExpression, $currentValue, $desc, $field) { 236 | $sorted = true; 237 | $params = array( 238 | "sort" => $sortExpression 239 | ); 240 | $data = DataSourceLoader::Load($this->dbSet, $params); 241 | $result = isset($data) && isset($data["data"]) && is_array($data["data"]) ? $data["data"] : NULL; 242 | $dataItemsCount = isset($result) ? count($result) : 0; 243 | for ($i = 0; $i < $dataItemsCount; $i++) { 244 | $compareResult = strcmp($currentValue, $result[$i][$field]); 245 | if ((!$desc && $compareResult > 0) || ($desc && $compareResult < 0)) { 246 | $sorted = false; 247 | break; 248 | } 249 | $currentValue = $result[$i][$field]; 250 | } 251 | $this->assertTrue($sorted && $dataItemsCount == 31); 252 | } 253 | public function testLoaderSkipTake() { 254 | $params = array( 255 | "skip" => 5, 256 | "take" => 10 257 | ); 258 | $ids = array(6, 7, 8, 9, 10, 11, 12, 13, 14, 15); 259 | $data = DataSourceLoader::Load($this->dbSet, $params); 260 | $result = isset($data) && isset($data["data"]) && is_array($data["data"]) ? $data["data"] : NULL; 261 | $itemsCount = isset($result) ? count($result) : 0; 262 | $paginated = true; 263 | if ($itemsCount != count($ids)) { 264 | $paginated = false; 265 | } 266 | else { 267 | for ($i = 0; $i < $itemsCount; $i++) { 268 | if ($result[$i]["ID"] != $ids[$i]) { 269 | $paginated = false; 270 | break; 271 | } 272 | } 273 | } 274 | $this->assertTrue($paginated); 275 | } 276 | /** 277 | * @dataProvider providerFilter 278 | */ 279 | public function testLoaderFilter($expression, $ids) { 280 | $params = array( 281 | "filter" => $expression 282 | ); 283 | $data = DataSourceLoader::Load($this->dbSet, $params); 284 | $result = isset($data) && isset($data["data"]) && is_array($data["data"]) ? $data["data"] : NULL; 285 | $itemsCount = isset($result) ? count($result) : 0; 286 | $filtered = true; 287 | if ($itemsCount != count($ids)) { 288 | $filtered = false; 289 | } 290 | else { 291 | for ($i = 0; $i < $itemsCount; $i++) { 292 | if ($result[$i]["ID"] != $ids[$i]) { 293 | $filtered = false; 294 | break; 295 | } 296 | } 297 | } 298 | $this->assertTrue($filtered); 299 | } 300 | /** 301 | * @dataProvider providerGroup 302 | */ 303 | public function testLoaderGroup($groupExpression, $currentValue, $desc, $field, $groupCount, $itemsInGroups) { 304 | $grouped = true; 305 | $params = array( 306 | "group" => $groupExpression 307 | ); 308 | $data = DataSourceLoader::Load($this->dbSet, $params); 309 | $result = isset($data) && isset($data["data"]) && is_array($data["data"]) ? $data["data"] : NULL; 310 | $dataItemsCount = isset($result) ? count($result) : 0; 311 | for ($i = 0; $i < $dataItemsCount; $i++) { 312 | $compareResult = strcmp($currentValue, strval($result[$i][$field])); 313 | $count = isset($groupExpression[0]->isExpanded) && $groupExpression[0]->isExpanded === false ? $result[$i]["count"] : count($result[$i]["items"]); 314 | if ((!$desc && $compareResult > 0) || ($desc && $compareResult < 0) || ($count != $itemsInGroups[$i])) { 315 | $grouped = false; 316 | break; 317 | } 318 | $currentValue = strval($result[$i][$field]); 319 | } 320 | $this->assertTrue($grouped && $dataItemsCount == $groupCount); 321 | } 322 | /** 323 | * @dataProvider providerGroupPaging 324 | */ 325 | public function testLoaderGroupPaging($params, $resultGroupItems) { 326 | $data = DataSourceLoader::Load($this->dbSet, $params); 327 | $isPaginated = false; 328 | $groupCount = 0; 329 | if (isset($data) && isset($data["data"]) && isset($data["groupCount"]) && count($resultGroupItems) === count($data["data"])) { 330 | $groupItems = $data["data"]; 331 | $isPaginated = true; 332 | foreach ($groupItems as $index => $groupItem) { 333 | if (strcmp($groupItem["key"], $resultGroupItems[$index]) !== 0) { 334 | $isPaginated = false; 335 | break; 336 | } 337 | } 338 | $groupCount = $data["groupCount"]; 339 | } 340 | $this->assertTrue($isPaginated && $groupCount === 4); 341 | } 342 | /** 343 | * @dataProvider providerTotalSummary 344 | */ 345 | public function testLoaderTotalSummary($summaryExpression, $value) { 346 | $params = array( 347 | "totalSummary" => $summaryExpression 348 | ); 349 | $data = DataSourceLoader::Load($this->dbSet, $params); 350 | $result = isset($data) && is_array($data) && isset($data["summary"]) ? $data["summary"][0] : 0; 351 | $this->assertEquals($value, $result); 352 | } 353 | } 354 | -------------------------------------------------------------------------------- /tests/DbSetAPITest.php: -------------------------------------------------------------------------------- 1 | ", 21)) 23 | ); 24 | $filterExpression3 = array( 25 | array("Category", "=", "Dairy Products"), 26 | array("BDate.year", "=", "2013"), 27 | array("Name", "=", "Sir Rodney's Scones"), 28 | array("CustomerName", "=", "Fuller Andrew"), 29 | array("ID", "=", 21) 30 | ); 31 | $filterExpression4 = array( 32 | array("Category", "=", "Dairy Products"), 33 | array("BDate.month", "=", "6"), 34 | array("Name", "=", "Sir Rodney's Scones"), 35 | array("CustomerName", "=", "Fuller Andrew"), 36 | array("ID", "=", 21) 37 | ); 38 | $filterExpression5 = array( 39 | array("Category", "=", "Dairy Products"), 40 | array("BDate.day", "=", "19"), 41 | array("Name", "=", "Sir Rodney's Scones"), 42 | array("CustomerName", "=", "Fuller Andrew"), 43 | array("ID", "=", 21) 44 | ); 45 | $filterExpression6 = array( 46 | array("Category", "=", "Dairy Products"), 47 | array("BDate.dayOfWeek", "=", "3"), 48 | array("Name", "=", "Sir Rodney's Scones"), 49 | array("CustomerName", "=", "Fuller Andrew"), 50 | array("ID", "=", 21) 51 | ); 52 | $filterExpression7 = array( 53 | array("Category", "=", "Dairy Products"), 54 | array("CustomerName", "=", null) 55 | ); 56 | $values1 = array(21, "Sir Rodney's Scones", "Dairy Products", "Fuller Andrew", "2013-06-19"); 57 | $values2 = array(31, "Camembert Pierrot", "Dairy Products", "", "2013-11-17"); 58 | return array( 59 | array($filterExpression1, $values1), 60 | array($filterExpression2, $values1), 61 | array($filterExpression3, $values1), 62 | array($filterExpression4, $values1), 63 | array($filterExpression5, $values1), 64 | array($filterExpression6, $values1), 65 | array($filterExpression7, $values2) 66 | ); 67 | } 68 | public function providerSort() { 69 | $field = "Name"; 70 | $sortExpression1 = array($field); 71 | $sortExpression2 = array( 72 | (object)array( 73 | "selector" => $field, 74 | "desc" => false 75 | ) 76 | ); 77 | $sortExpression3 = array( 78 | (object)array( 79 | "selector" => $field, 80 | "desc" => true 81 | ) 82 | ); 83 | return array( 84 | array($sortExpression1, "", false, $field), 85 | array($sortExpression2, "", false, $field), 86 | array($sortExpression3, "Z", true, $field) 87 | ); 88 | } 89 | public function providerGroup() { 90 | $field = "Category"; 91 | $groupCount = 4; 92 | $groupField = "key"; 93 | $groupExpression1 = array($field); 94 | $groupExpression2 = array( 95 | (object)array( 96 | "selector" => $field, 97 | "desc" => false 98 | ) 99 | ); 100 | $groupExpression3 = array( 101 | (object)array( 102 | "selector" => $field, 103 | "desc" => true 104 | ) 105 | ); 106 | return array( 107 | array($groupExpression1, "", false, $groupField, $groupCount), 108 | array($groupExpression2, "", false, $groupField, $groupCount), 109 | array($groupExpression3, "Seafood", true, $groupField, $groupCount) 110 | ); 111 | } 112 | private function GroupSummariesEqual($data, $standard) { 113 | $dataCount = count($data); 114 | $standardCount = count($standard); 115 | $result = $dataCount === $standardCount; 116 | if ($result) { 117 | for ($i = 0; $i < $dataCount; $i++) { 118 | $dataSummary = $data[$i]["summary"]; 119 | $standardSummary = $standard[$i]["summary"]; 120 | if (is_array($dataSummary) && 121 | (count($dataSummary) == count($standard[$i]["summary"])) && 122 | (count(array_diff($dataSummary, $standardSummary)) === 0)) { 123 | if (isset($standard[$i]["items"])) { 124 | if (isset($data[$i]["items"])) { 125 | $result = $this->GroupSummariesEqual($data[$i]["items"], $standard[$i]["items"]); 126 | } 127 | } 128 | if ($result) { 129 | continue; 130 | } 131 | } 132 | $result = false; 133 | break; 134 | } 135 | } 136 | return $result; 137 | } 138 | public function providerGroupSummary() { 139 | $group = array( 140 | (object)array( 141 | "selector" => "Category", 142 | "desc" => false, 143 | "isExpanded" => false 144 | ), 145 | (object)array( 146 | "selector" => "CustomerName", 147 | "desc" => true, 148 | "isExpanded" => false 149 | ) 150 | ); 151 | $groupSummary = array( 152 | (object)array( 153 | "selector" => "ID", 154 | "summaryType" => "min" 155 | ), 156 | (object)array( 157 | "selector" => "ID", 158 | "summaryType" => "max" 159 | ), 160 | (object)array( 161 | "selector" => "ID", 162 | "summaryType" => "sum" 163 | ), 164 | (object)array( 165 | "summaryType" => "count" 166 | ) 167 | ); 168 | $result = array( 169 | array( 170 | "summary" => array(3, 29, 141, 10), 171 | "items" => array( 172 | array("summary" => array(5, 29, 56, 3)), 173 | array("summary" => array(18, 18, 18, 1)), 174 | array("summary" => array(3, 16, 23, 3)), 175 | array("summary" => array(6, 23, 44, 3)) 176 | ) 177 | ), 178 | array( 179 | "summary" => array(1, 28, 138, 9), 180 | "items" => array( 181 | array("summary" => array(1, 28, 41, 3)), 182 | array("summary" => array(26, 26, 26, 1)), 183 | array("summary" => array(8, 17, 34, 3)), 184 | array("summary" => array(13, 24, 37, 2)) 185 | ) 186 | ), 187 | array( 188 | "summary" => array(2, 31, 65, 4), 189 | "items" => array( 190 | array("summary" => array(2, 2, 2, 1)), 191 | array("summary" => array(21, 21, 21, 1)), 192 | array("summary" => array(11, 11, 11, 1)), 193 | array("summary" => array(31, 31, 31, 1)), 194 | ) 195 | ), 196 | array( 197 | "summary" => array(7, 30, 152, 8), 198 | "items" => array( 199 | array("summary" => array(10, 20, 30, 2)), 200 | array("summary" => array(27, 30, 57, 2)), 201 | array("summary" => array(7, 25, 46, 3)), 202 | array("summary" => array(19, 19, 19, 1)) 203 | ) 204 | ) 205 | ); 206 | return array( 207 | array($group, $groupSummary, $result) 208 | ); 209 | } 210 | public function providerGetTotalSummary() { 211 | $summaryExpression1 = array( 212 | (object)array( 213 | "summaryType" => "count" 214 | ) 215 | ); 216 | $summaryExpression2 = array( 217 | (object)array( 218 | "selector" => "ID", 219 | "summaryType" => "min" 220 | ) 221 | ); 222 | $summaryExpression3 = array( 223 | (object)array( 224 | "selector" => "ID", 225 | "summaryType" => "max" 226 | ) 227 | ); 228 | $summaryExpression4 = array( 229 | (object)array( 230 | "selector" => "ID", 231 | "summaryType" => "sum" 232 | ) 233 | ); 234 | $summaryExpression5 = array( 235 | (object)array( 236 | "selector" => "ID", 237 | "summaryType" => "avg" 238 | ) 239 | ); 240 | return array( 241 | array($summaryExpression1, 31), 242 | array($summaryExpression2, 1), 243 | array($summaryExpression3, 31), 244 | array($summaryExpression4, 496), 245 | array($summaryExpression5, 16) 246 | ); 247 | } 248 | public function providerEscapeExpressionValues() { 249 | $filterExpression1 = array("Name", "=", "N'o\"r\d-Ost Mat%123_jes)hering#"); 250 | $filterExpression2 = array("Name", "contains", "%123_jes)"); 251 | return array( 252 | array($filterExpression1, 30), 253 | array($filterExpression2, 30) 254 | ); 255 | } 256 | public function testGetCount() { 257 | $this->assertEquals(31, $this->dbSet->GetCount()); 258 | } 259 | public function testSelect() { 260 | $columns = array("BDate", "Category", "CustomerName"); 261 | $this->dbSet->Select($columns); 262 | $data = $this->dbSet->AsArray(); 263 | $result = count($data) > 0 ? array_keys($data[0]) : array(); 264 | $this->assertEquals($columns, $result); 265 | } 266 | /** 267 | * @dataProvider providerFilterAnd 268 | */ 269 | public function testFilterAnd($filterExpression, $values) { 270 | $this->dbSet->Filter($filterExpression); 271 | $data = $this->dbSet->AsArray(); 272 | $result = count($data) > 0 ? array_values($data[0]) : array(); 273 | $this->assertEquals($values, $result); 274 | } 275 | public function testFilterOr() { 276 | $filterExpression = array( 277 | array("ID", "=", 10), 278 | "or", 279 | array("ID", "=" , 20) 280 | ); 281 | $values = array(10, 20); 282 | $this->dbSet->Filter($filterExpression); 283 | $data = $this->dbSet->AsArray(); 284 | $result = array(); 285 | $dataItemsCount = count($data); 286 | for ($i = 0; $i < $dataItemsCount; $i++) { 287 | $result[$i] = $data[$i]["ID"]; 288 | } 289 | $this->assertEquals($values, $result); 290 | } 291 | public function testFilterNotNull() { 292 | $filterExpression = array( 293 | array("CustomerName", "<>", null), 294 | array("ID", ">", 29) 295 | ); 296 | $this->dbSet->Filter($filterExpression); 297 | $data = $this->dbSet->AsArray(); 298 | $this->assertTrue($data !== null && count($data) == 1 && $data[0]["ID"] == 30); 299 | } 300 | /** 301 | * @dataProvider providerSort 302 | */ 303 | public function testSort($sortExpression, $currentValue, $desc, $field) { 304 | $sorted = true; 305 | $this->dbSet->Sort($sortExpression); 306 | $data = $this->dbSet->AsArray(); 307 | $dataItemsCount = count($data); 308 | for ($i = 0; $i < $dataItemsCount; $i++) { 309 | $compareResult = strcmp($currentValue, $data[$i][$field]); 310 | if ((!$desc && $compareResult > 0) || ($desc && $compareResult < 0)) { 311 | $sorted = false; 312 | break; 313 | } 314 | $currentValue = $data[$i][$field]; 315 | } 316 | $this->assertTrue($sorted && $dataItemsCount > 0); 317 | } 318 | public function testSkipTake() { 319 | $this->dbSet->SkipTake(10, 5); 320 | $data = $this->dbSet->AsArray(); 321 | $itemsCount = count($data); 322 | $firstIndex = $itemsCount > 0 ? $data[0]["ID"] : 0; 323 | $lastIndex = $itemsCount == 5 ? $data[4]["ID"] : 0; 324 | $this->assertTrue($itemsCount == 5 && $firstIndex == 11 && $lastIndex == 15); 325 | } 326 | /** 327 | * @dataProvider providerGroup 328 | */ 329 | public function testGroup($groupExpression, $currentValue, $desc, $field, $groupCount) { 330 | $grouped = true; 331 | $this->dbSet->Group($groupExpression); 332 | $data = $this->dbSet->AsArray(); 333 | $dataItemsCount = count($data); 334 | for ($i = 0; $i < $dataItemsCount; $i++) { 335 | $compareResult = strcmp($currentValue, $data[$i][$field]); 336 | if ((!$desc && $compareResult > 0) || ($desc && $compareResult < 0)) { 337 | $grouped = false; 338 | break; 339 | } 340 | $currentValue = $data[$i][$field]; 341 | } 342 | $this->assertTrue($grouped && $dataItemsCount == $groupCount); 343 | } 344 | /** 345 | * @dataProvider providerGroupSummary 346 | */ 347 | public function testGroupSummary($group, $groupSummary, $standard) { 348 | $this->dbSet->Group($group, $groupSummary); 349 | $data = $this->dbSet->AsArray(); 350 | $result = $this->GroupSummariesEqual($data, $standard); 351 | $this->assertTrue($result); 352 | } 353 | /** 354 | * @dataProvider providerGetTotalSummary 355 | */ 356 | public function testGetTotalSummary($summaryExpression, $value) { 357 | $data = $this->dbSet->GetTotalSummary($summaryExpression); 358 | $result = count($data) > 0 ? $data[0] : 0; 359 | $this->assertEquals($value, $result); 360 | } 361 | public function testGetGroupCount() { 362 | $groupExpression = array( 363 | (object)array( 364 | "selector" => "Category", 365 | "desc" => false, 366 | "isExpanded" => false 367 | ) 368 | ); 369 | $this->dbSet->Group($groupExpression); 370 | $groupCount = $this->dbSet->GetGroupCount(); 371 | $this->assertEquals($groupCount, 4); 372 | } 373 | /** 374 | * @dataProvider providerEscapeExpressionValues 375 | */ 376 | public function testEscapeExpressionValues($filterExpression, $value) { 377 | $data = $this->dbSet->Select("ID")->Filter($filterExpression)->AsArray(); 378 | $result = false; 379 | if (count($data) == 1) { 380 | $itemData = $data[0]; 381 | $result = isset($itemData["ID"]) && $itemData["ID"] == $value; 382 | } 383 | $this->assertTrue($result); 384 | } 385 | } 386 | -------------------------------------------------------------------------------- /tests/FilterHelperTest.php: -------------------------------------------------------------------------------- 1 | 1), 71 | "`field1` = '1'" 72 | ), 73 | array( 74 | array( 75 | "field1" => 1, 76 | "field2" => 2 77 | ), 78 | "`field1` = '1' AND `field2` = '2'" 79 | ) 80 | ); 81 | } 82 | /** 83 | * @dataProvider providerFilterExpression 84 | */ 85 | public function testGetSqlExprByArray($expression, $expectedResult) { 86 | $result = FilterHelper::GetSqlExprByArray($expression); 87 | 88 | $this->assertEquals($expectedResult, $result); 89 | } 90 | /** 91 | * @dataProvider providerKey 92 | */ 93 | public function testGetSqlExprByKey($key, $expectedResult) { 94 | $result = FilterHelper::GetSqlExprByKey($key); 95 | 96 | $this->assertEquals($expectedResult, $result); 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /tests/TestBase.php: -------------------------------------------------------------------------------- 1 | close(); 19 | } 20 | protected function setUp() { 21 | $this->dbSet = new DbSet(self::$mySQL, self::$tableName); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /tests/UtilsTest.php: -------------------------------------------------------------------------------- 1 | x,y{z}1?2:3;4\r5\n", 15 | true, 16 | "`abcdefghijklmnopqrstuvwxyz12345`" 17 | ) 18 | ); 19 | } 20 | public function providerItemValue() { 21 | return array( 22 | array( 23 | array("field" => 1), 24 | "field", 25 | NULL, 26 | 1 27 | ), 28 | array( 29 | array("field" => 1), 30 | "field1", 31 | "test", 32 | "test" 33 | ) 34 | ); 35 | } 36 | public function testEscapeExpressionValues() { 37 | $result = "tes't"; 38 | Utils::EscapeExpressionValues(UtilsTest::$mySQL, $result); 39 | 40 | $this->assertEquals("tes\'t", $result); 41 | } 42 | /** 43 | * @dataProvider providerValue 44 | */ 45 | public function testQuoteStringValue($value, $isFieldName, $expectedResult) { 46 | $result = Utils::QuoteStringValue($value, $isFieldName); 47 | 48 | $this->assertEquals($expectedResult, $result); 49 | } 50 | /** 51 | * @dataProvider providerItemValue 52 | */ 53 | public function testGetItemValueOrDefault($params, $key, $defaultValue, $expectedResult) { 54 | $result = Utils::GetItemValueOrDefault($params, $key, $defaultValue); 55 | 56 | $this->assertEquals($expectedResult, $result); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /tests/bootstrap.php: -------------------------------------------------------------------------------- 1 |