Browse Source

Add negation to filters

Maurits van der Schee 8 years ago
parent
commit
74aa8d5659
3 changed files with 121 additions and 114 deletions
  1. 6
    5
      README.md
  2. 110
    106
      api.php
  3. 5
    3
      tests/tests.php

+ 6
- 5
README.md View File

@@ -151,21 +151,20 @@ Search is implemented with the "filter" parameter. You need to specify the colum
151 151
   - sw: start with (string starts with value)
152 152
   - ew: end with (string end with value)
153 153
   - eq: equal (string or number matches exactly)
154
-  - ne: not equal (string or number doen not match)
155 154
   - lt: lower than (number is lower than value)
156 155
   - le: lower or equal (number is lower than or equal to value)
157 156
   - ge: greater or equal (number is higher than or equal to value)
158 157
   - gt: greater than (number is higher than value)
159 158
   - in: in (number is in comma separated list of values)
160
-  - ni: not in (number is not in comma separated list of values)
161 159
   - is: is null (field contains "NULL" value)
162
-  - no: not null (field does not contain "NULL" value)
163
-  
160
+
161
+You can negate all filters by prepending a 'n' character, so that 'eq' becomes 'neq'.
162
+
164 163
 ```
165 164
 GET http://localhost/api.php/categories?filter=name,eq,Internet
166 165
 GET http://localhost/api.php/categories?filter=name,sw,Inter
167 166
 GET http://localhost/api.php/categories?filter=id,le,1
168
-GET http://localhost/api.php/categories?filter=id,lt,2
167
+GET http://localhost/api.php/categories?filter=id,ngt,2
169 168
 ```
170 169
 
171 170
 Output:
@@ -581,6 +580,8 @@ There is also support for spatial filters:
581 580
   - sis: spatial is simple (geometry is simple)
582 581
   - siv: spatial is valid (geometry is valid)
583 582
 
583
+You can negate these filters as well by prepending a 'n' character, so that 'sco' becomes 'nsco'.
584
+
584 585
 Example:
585 586
 
586 587
 ```

+ 110
- 106
api.php View File

@@ -168,34 +168,6 @@ class MySQL implements DatabaseInterface {
168 168
 	}
169 169
 
170 170
 	public function convertFilter($field, $comparator, $value) {
171
-		switch (strtolower($comparator)) {
172
-			// normal
173
-			case 'cs': return array('! LIKE ?',$field,'%'.$this->likeEscape($value).'%');
174
-			case 'sw': return array('! LIKE ?',$field,$this->likeEscape($value).'%');
175
-			case 'ew': return array('! LIKE ?',$field,'%'.$this->likeEscape($value));
176
-			case 'eq': return array('! = ?',$field,$value);
177
-			case 'ne': return array('! <> ?',$field,$value);
178
-			case 'lt': return array('! < ?',$field,$value);
179
-			case 'le': return array('! <= ?',$field,$value);
180
-			case 'ge': return array('! >= ?',$field,$value);
181
-			case 'gt': return array('! > ?',$field,$value);
182
-			case 'in': return array('! IN ?',$field,explode(',',$value));
183
-			case 'ni': return array('! NOT IN ?',$field,explode(',',$value));
184
-			case 'is': return array('! IS NULL',$field);
185
-			case 'no': return array('! IS NOT NULL',$field);
186
-			// spatial
187
-			case 'sco': return array('ST_Contains(!,ST_GeomFromText(?))',$field,$value);
188
-			case 'scr': return array('ST_Crosses(!,ST_GeomFromText(?))',$field,$value);
189
-			case 'sdi': return array('ST_Disjoint(!,ST_GeomFromText(?))',$field,$value);
190
-			case 'seq': return array('ST_Equals(!,ST_GeomFromText(?))',$field,$value);
191
-			case 'sin': return array('ST_Intersects(!,ST_GeomFromText(?))',$field,$value);
192
-			case 'sov': return array('ST_Overlaps(!,ST_GeomFromText(?))',$field,$value);
193
-			case 'sto': return array('ST_Touches(!,ST_GeomFromText(?))',$field,$value);
194
-			case 'swi': return array('ST_Within(!,ST_GeomFromText(?))',$field,$value);
195
-			case 'sic': return array('ST_IsClosed(!)',$field);
196
-			case 'sis': return array('ST_IsSimple(!)',$field);
197
-			case 'siv': return array('ST_IsValid(!)',$field);
198
-		}
199 171
 		return false;
200 172
 	}
201 173
 
@@ -411,34 +383,6 @@ class PostgreSQL implements DatabaseInterface {
411 383
 	}
412 384
 
413 385
 	public function convertFilter($field, $comparator, $value) {
414
-		switch (strtolower($comparator)) {
415
-			// normal
416
-			case 'cs': return array('! LIKE ?',$field,'%'.$this->likeEscape($value).'%');
417
-			case 'sw': return array('! LIKE ?',$field,$this->likeEscape($value).'%');
418
-			case 'ew': return array('! LIKE ?',$field,'%'.$this->likeEscape($value));
419
-			case 'eq': return array('! = ?',$field,$value);
420
-			case 'ne': return array('! <> ?',$field,$value);
421
-			case 'lt': return array('! < ?',$field,$value);
422
-			case 'le': return array('! <= ?',$field,$value);
423
-			case 'ge': return array('! >= ?',$field,$value);
424
-			case 'gt': return array('! > ?',$field,$value);
425
-			case 'in': return array('! IN ?',$field,explode(',',$value));
426
-			case 'ni': return array('! NOT IN ?',$field,explode(',',$value));
427
-			case 'is': return array('! IS NULL',$field);
428
-			case 'no': return array('! IS NOT NULL',$field);
429
-			// spatial
430
-			case 'sco': return array('ST_Contains(!,ST_GeomFromText(?))',$field,$value);
431
-			case 'scr': return array('ST_Crosses(!,ST_GeomFromText(?))',$field,$value);
432
-			case 'sdi': return array('ST_Disjoint(!,ST_GeomFromText(?))',$field,$value);
433
-			case 'seq': return array('ST_Equals(!,ST_GeomFromText(?))',$field,$value);
434
-			case 'sin': return array('ST_Intersects(!,ST_GeomFromText(?))',$field,$value);
435
-			case 'sov': return array('ST_Overlaps(!,ST_GeomFromText(?))',$field,$value);
436
-			case 'sto': return array('ST_Touches(!,ST_GeomFromText(?))',$field,$value);
437
-			case 'swi': return array('ST_Within(!,ST_GeomFromText(?))',$field,$value);
438
-			case 'sic': return array('ST_IsClosed(!)',$field);
439
-			case 'sis': return array('ST_IsSimple(!)',$field);
440
-			case 'siv': return array('ST_IsValid(!)',$field);
441
-		}
442 386
 		return false;
443 387
 	}
444 388
 
@@ -659,34 +603,36 @@ class SQLServer implements DatabaseInterface {
659 603
 	}
660 604
 
661 605
 	public function convertFilter($field, $comparator, $value) {
662
-		switch (strtolower($comparator)) {
663
-			// normal
664
-			case 'cs': return array('! LIKE ?',$field,'%'.$this->likeEscape($value).'%');
665
-			case 'sw': return array('! LIKE ?',$field,$this->likeEscape($value).'%');
666
-			case 'ew': return array('! LIKE ?',$field,'%'.$this->likeEscape($value));
667
-			case 'eq': return array('! = ?',$field,$value);
668
-			case 'ne': return array('! <> ?',$field,$value);
669
-			case 'lt': return array('! < ?',$field,$value);
670
-			case 'le': return array('! <= ?',$field,$value);
671
-			case 'ge': return array('! >= ?',$field,$value);
672
-			case 'gt': return array('! > ?',$field,$value);
673
-			case 'in': return array('! IN ?',$field,explode(',',$value));
674
-			case 'ni': return array('! NOT IN ?',$field,explode(',',$value));
675
-			case 'is': return array('! IS NULL',$field);
676
-			case 'no': return array('! IS NOT NULL',$field);
677
-			// spatial
678
-			case 'sco': return array('!.STContains(geometry::STGeomFromText(?,0))=1',$field,$value);
679
-			case 'scr': return array('!.STCrosses(geometry::STGeomFromText(?,0))=1',$field,$value);
680
-			case 'sdi': return array('!.STDisjoint(geometry::STGeomFromText(?,0))=1',$field,$value);
681
-			case 'seq': return array('!.STEquals(geometry::STGeomFromText(?,0))=1',$field,$value);
682
-			case 'sin': return array('!.STIntersects(geometry::STGeomFromText(?,0))=1',$field,$value);
683
-			case 'sov': return array('!.STOverlaps(geometry::STGeomFromText(?,0))=1',$field,$value);
684
-			case 'sto': return array('!.STTouches(geometry::STGeomFromText(?,0))=1',$field,$value);
685
-			case 'swi': return array('!.STWithin(geometry::STGeomFromText(?,0))=1',$field,$value);
686
-			case 'sic': return array('!.STIsClosed()=1',$field);
687
-			case 'sis': return array('!.STIsSimple()=1',$field);
688
-			case 'siv': return array('!.STIsValid()=1',$field);
689
-		}
606
+		$comparator = strtolower($comparator);
607
+		if ($comparator[0]!='n') {
608
+			switch ($comparator) {
609
+				case 'sco': return array('!.STContains(geometry::STGeomFromText(?,0))=1',$field,$value);
610
+				case 'scr': return array('!.STCrosses(geometry::STGeomFromText(?,0))=1',$field,$value);
611
+				case 'sdi': return array('!.STDisjoint(geometry::STGeomFromText(?,0))=1',$field,$value);
612
+				case 'seq': return array('!.STEquals(geometry::STGeomFromText(?,0))=1',$field,$value);
613
+				case 'sin': return array('!.STIntersects(geometry::STGeomFromText(?,0))=1',$field,$value);
614
+				case 'sov': return array('!.STOverlaps(geometry::STGeomFromText(?,0))=1',$field,$value);
615
+				case 'sto': return array('!.STTouches(geometry::STGeomFromText(?,0))=1',$field,$value);
616
+				case 'swi': return array('!.STWithin(geometry::STGeomFromText(?,0))=1',$field,$value);
617
+				case 'sic': return array('!.STIsClosed()=1',$field);
618
+				case 'sis': return array('!.STIsSimple()=1',$field);
619
+				case 'siv': return array('!.STIsValid()=1',$field);
620
+			}
621
+		} else {
622
+			switch ($comparator) {
623
+				case 'nsco': return array('!.STContains(geometry::STGeomFromText(?,0))=0',$field,$value);
624
+				case 'nscr': return array('!.STCrosses(geometry::STGeomFromText(?,0))=0',$field,$value);
625
+				case 'nsdi': return array('!.STDisjoint(geometry::STGeomFromText(?,0))=0',$field,$value);
626
+				case 'nseq': return array('!.STEquals(geometry::STGeomFromText(?,0))=0',$field,$value);
627
+				case 'nsin': return array('!.STIntersects(geometry::STGeomFromText(?,0))=0',$field,$value);
628
+				case 'nsov': return array('!.STOverlaps(geometry::STGeomFromText(?,0))=0',$field,$value);
629
+				case 'nsto': return array('!.STTouches(geometry::STGeomFromText(?,0))=0',$field,$value);
630
+				case 'nswi': return array('!.STWithin(geometry::STGeomFromText(?,0))=0',$field,$value);
631
+				case 'nsic': return array('!.STIsClosed()=0',$field);
632
+				case 'nsis': return array('!.STIsSimple()=0',$field);
633
+				case 'nsiv': return array('!.STIsValid()=0',$field);
634
+			}
635
+		} 
690 636
 		return false;
691 637
 	}
692 638
 
@@ -881,21 +827,6 @@ class SQLite implements DatabaseInterface {
881 827
 	}
882 828
 
883 829
 	public function convertFilter($field, $comparator, $value) {
884
-		switch (strtolower($comparator)) {
885
-			case 'cs': return array('! LIKE ?',$field,'%'.$this->likeEscape($value).'%');
886
-			case 'sw': return array('! LIKE ?',$field,$this->likeEscape($value).'%');
887
-			case 'ew': return array('! LIKE ?',$field,'%'.$this->likeEscape($value));
888
-			case 'eq': return array('! = ?',$field,$value);
889
-			case 'ne': return array('! <> ?',$field,$value);
890
-			case 'lt': return array('! < ?',$field,$value);
891
-			case 'le': return array('! <= ?',$field,$value);
892
-			case 'ge': return array('! >= ?',$field,$value);
893
-			case 'gt': return array('! > ?',$field,$value);
894
-			case 'in': return array('! IN ?',$field,explode(',',$value));
895
-			case 'ni': return array('! NOT IN ?',$field,explode(',',$value));
896
-			case 'is': return array('! IS NULL',$field);
897
-			case 'no': return array('! IS NOT NULL',$field);
898
-		}
899 830
 		return false;
900 831
 	}
901 832
 
@@ -981,8 +912,8 @@ class PHP_CRUD_API {
981 912
 				if ($v!==null) {
982 913
 					if (!isset($filters[$table])) $filters[$table] = array();
983 914
 					if (!isset($filters[$table]['and'])) $filters[$table]['and'] = array();
984
-					if (is_array($v)) $filters[$table]['and'][] = $this->db->convertFilter($field->name,'in',implode(',',$v));
985
-					else $filters[$table]['and'][] = $this->db->convertFilter($field->name,'eq',$v);
915
+					if (is_array($v)) $filters[$table]['and'][] = $this->convertFilter($field->name,'in',implode(',',$v));
916
+					else $filters[$table]['and'][] = $this->convertFilter($field->name,'eq',$v);
986 917
 				}
987 918
 			}
988 919
 		}
@@ -1133,6 +1064,79 @@ class PHP_CRUD_API {
1133 1064
 		return $order;
1134 1065
 	}
1135 1066
 
1067
+	protected function convertFilter($field, $comparator, $value) {
1068
+		$result = $this->db->convertFilter($field,$comparator,$value);
1069
+		if ($result) return $result;
1070
+		// default behavior 					
1071
+		$comparator = strtolower($comparator);
1072
+		if ($comparator[0]!='n') {
1073
+			if (strlen($comparator)==2) {
1074
+				switch ($comparator) {
1075
+					case 'cs': return array('! LIKE ?',$field,'%'.$this->db->likeEscape($value).'%');
1076
+					case 'sw': return array('! LIKE ?',$field,$this->db->likeEscape($value).'%');
1077
+					case 'ew': return array('! LIKE ?',$field,'%'.$this->db->likeEscape($value));
1078
+					case 'eq': return array('! = ?',$field,$value);
1079
+					case 'lt': return array('! < ?',$field,$value);
1080
+					case 'le': return array('! <= ?',$field,$value);
1081
+					case 'ge': return array('! >= ?',$field,$value);
1082
+					case 'gt': return array('! > ?',$field,$value);
1083
+					case 'in': return array('! IN ?',$field,explode(',',$value));
1084
+					case 'is': return array('! IS NULL',$field);
1085
+				}
1086
+			} else {
1087
+				switch ($comparator) {
1088
+					case 'sco': return array('ST_Contains(!,ST_GeomFromText(?))=1',$field,$value);
1089
+					case 'scr': return array('ST_Crosses(!,ST_GeomFromText(?))=1',$field,$value);
1090
+					case 'sdi': return array('ST_Disjoint(!,ST_GeomFromText(?))=1',$field,$value);
1091
+					case 'seq': return array('ST_Equals(!,ST_GeomFromText(?))=1',$field,$value);
1092
+					case 'sin': return array('ST_Intersects(!,ST_GeomFromText(?))=1',$field,$value);
1093
+					case 'sov': return array('ST_Overlaps(!,ST_GeomFromText(?))=1',$field,$value);
1094
+					case 'sto': return array('ST_Touches(!,ST_GeomFromText(?))=1',$field,$value);
1095
+					case 'swi': return array('ST_Within(!,ST_GeomFromText(?))=1',$field,$value);
1096
+					case 'sic': return array('ST_IsClosed(!)=1',$field);
1097
+					case 'sis': return array('ST_IsSimple(!)=1',$field);
1098
+					case 'siv': return array('ST_IsValid(!)=1',$field);
1099
+				}
1100
+			}
1101
+		} else {
1102
+			if (strlen($comparator)==2) {
1103
+				switch ($comparator) {
1104
+					case 'ne': return array('! <> ?',$field,$value); // deprecated
1105
+					case 'ni': return array('! NOT IN ?',$field,explode(',',$value)); // deprecated
1106
+					case 'no': return array('! IS NOT NULL',$field); // deprecated
1107
+				}
1108
+			} elseif (strlen($comparator)==3) {
1109
+				switch ($comparator) {
1110
+					case 'ncs': return array('! NOT LIKE ?',$field,'%'.$this->db->likeEscape($value).'%');
1111
+					case 'nsw': return array('! NOT LIKE ?',$field,$this->db->likeEscape($value).'%');
1112
+					case 'new': return array('! NOT LIKE ?',$field,'%'.$this->db->likeEscape($value));
1113
+					case 'neq': return array('! <> ?',$field,$value);
1114
+					case 'nlt': return array('! >= ?',$field,$value);
1115
+					case 'nle': return array('! > ?',$field,$value);
1116
+					case 'nge': return array('! < ?',$field,$value);
1117
+					case 'ngt': return array('! <= ?',$field,$value);
1118
+					case 'nin': return array('! NOT IN ?',$field,explode(',',$value));
1119
+					case 'nis': return array('! IS NOT NULL',$field);
1120
+				}
1121
+			} else {
1122
+				switch ($comparator) {
1123
+					case 'nsco': return array('ST_Contains(!,ST_GeomFromText(?))=0',$field,$value);
1124
+					case 'nscr': return array('ST_Crosses(!,ST_GeomFromText(?))=0',$field,$value);
1125
+					case 'nsdi': return array('ST_Disjoint(!,ST_GeomFromText(?))=0',$field,$value);
1126
+					case 'nseq': return array('ST_Equals(!,ST_GeomFromText(?))=0',$field,$value);
1127
+					case 'nsin': return array('ST_Intersects(!,ST_GeomFromText(?))=0',$field,$value);
1128
+					case 'nsov': return array('ST_Overlaps(!,ST_GeomFromText(?))=0',$field,$value);
1129
+					case 'nsto': return array('ST_Touches(!,ST_GeomFromText(?))=0',$field,$value);
1130
+					case 'nswi': return array('ST_Within(!,ST_GeomFromText(?))=0',$field,$value);
1131
+					case 'nsic': return array('ST_IsClosed(!)=0',$field);
1132
+					case 'nsis': return array('ST_IsSimple(!)=0',$field);
1133
+					case 'nsiv': return array('ST_IsValid(!)=0',$field);
1134
+				}
1135
+			}
1136
+		}
1137
+		return false;
1138
+	}
1139
+
1136 1140
 	public function convertFilters($filters) {
1137 1141
 		$result = array();
1138 1142
 		if ($filters) {
@@ -1142,7 +1146,7 @@ class PHP_CRUD_API {
1142 1146
 					$field = $parts[0];
1143 1147
 					$comparator = $parts[1];
1144 1148
 					$value = isset($parts[2])?$parts[2]:null;
1145
-					$filter = $this->db->convertFilter($field,$comparator,$value);
1149
+					$filter = $this->convertFilter($field,$comparator,$value);
1146 1150
 					if ($filter) $result[] = $filter;
1147 1151
 				}
1148 1152
 			}
@@ -1175,7 +1179,7 @@ class PHP_CRUD_API {
1175 1179
 		$params[] = $table;
1176 1180
 		if (!isset($filters[$table])) $filters[$table] = array();
1177 1181
 		if (!isset($filters[$table]['or'])) $filters[$table]['or'] = array();
1178
-		$filters[$table]['or'][] = $this->db->convertFilter($key[1],'eq',$key[0]);
1182
+		$filters[$table]['or'][] = $this->convertFilter($key[1],'eq',$key[0]);
1179 1183
 		$this->addWhereFromFilters($filters[$table],$sql,$params);
1180 1184
 		$object = null;
1181 1185
 		if ($result = $this->db->query($sql,$params)) {
@@ -1212,7 +1216,7 @@ class PHP_CRUD_API {
1212 1216
 		}
1213 1217
 		if (!isset($filters[$table])) $filters[$table] = array();
1214 1218
 		if (!isset($filters[$table]['or'])) $filters[$table]['or'] = array();
1215
-		$filters[$table]['or'][] = $this->db->convertFilter($key[1],'eq',$key[0]);
1219
+		$filters[$table]['or'][] = $this->convertFilter($key[1],'eq',$key[0]);
1216 1220
 		$this->addWhereFromFilters($filters[$table],$sql,$params);
1217 1221
 		$result = $this->db->query($sql,$params);
1218 1222
 		if (!$result) return null;
@@ -1225,7 +1229,7 @@ class PHP_CRUD_API {
1225 1229
 		$params = array($table);
1226 1230
 		if (!isset($filters[$table])) $filters[$table] = array();
1227 1231
 		if (!isset($filters[$table]['or'])) $filters[$table]['or'] = array();
1228
-		$filters[$table]['or'][] = $this->db->convertFilter($key[1],'eq',$key[0]);
1232
+		$filters[$table]['or'][] = $this->convertFilter($key[1],'eq',$key[0]);
1229 1233
 		$this->addWhereFromFilters($filters[$table],$sql,$params);
1230 1234
 		$result = $this->db->query($sql,$params);
1231 1235
 		if (!$result) return null;
@@ -1526,7 +1530,7 @@ class PHP_CRUD_API {
1526 1530
 					if ($values) {
1527 1531
 						if (!isset($filters[$table])) $filters[$table] = array();
1528 1532
 						if (!isset($filters[$table]['or'])) $filters[$table]['or'] = array();
1529
-						$filters[$table]['or'][] = $this->db->convertFilter($field,'in',implode(',',$values));
1533
+						$filters[$table]['or'][] = $this->convertFilter($field,'in',implode(',',$values));
1530 1534
 					}
1531 1535
 					if ($first_row) $first_row = false;
1532 1536
 					else echo ',';

+ 5
- 3
tests/tests.php View File

@@ -565,9 +565,11 @@ class PHP_CRUD_API_Test extends PHPUnit_Framework_TestCase
565 565
 
566 566
 	public function testSpatialFilterWithin()
567 567
 	{
568
-		$test = new API($this);
569
-		$test->get('/users?columns=id,username&filter=location,swi,POINT(30 20)');
570
-		$test->expect('{"users":{"columns":["id","username"],"records":[["1","user1"]]}}');
568
+		if (PHP_CRUD_API_Config::$dbengine!='SQLite') {
569
+			$test = new API($this);
570
+			$test->get('/users?columns=id,username&filter=location,swi,POINT(30 20)');
571
+			$test->expect('{"users":{"columns":["id","username"],"records":[["1","user1"]]}}');
572
+		}
571 573
 	}
572 574
 
573 575
 }

Loading…
Cancel
Save