diff --git a/.buildpath b/.buildpath index c7f9553..e97ddaf 100644 --- a/.buildpath +++ b/.buildpath @@ -6,6 +6,11 @@ + + + + + diff --git a/CHANGELOG.md b/CHANGELOG.md index 73fdafa..70e8e9d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,11 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). +## [1.1.0] - 2017-11-28 +* Add support for expressions with simple logical operators and without parenthesis. The or operator is writen with `-`, + the and operator is written with `+` ; +* Update minor composer dependency versions. + ## [1.0.0] - 2017-08-01 * Initial release. diff --git a/README.md b/README.md index f9bad4f..8b4e151 100644 --- a/README.md +++ b/README.md @@ -85,12 +85,18 @@ The expression language provides the following operators. The `!` operator is special, it can be used directly before a value string or in combination with the `=` or `in` operators. -For exemple `!5` or `!=5` to express "no equals to 5" or `!in('Paris','London')` ro express "no equals to Paris or +For exemple `!5` or `!=5` to express "not equals to 5" or `!in('Paris','London')` to express "not equals to Paris or London". -### And or operators +### AND and OR operators -The `+` and `-` operator allow to create AND and OR SQL requests. +The `+` and `-` operator allow to create AND and OR SQL requests. + +Here are sample expressions with logical operators. + +* `property=>5.4+<12` is translated to `property >= ? AND property < ?` with 2 parameters `[5.4,12]` ; +* `property=~'*ball*'-~'*tennis*'` is translated to `property like ? OR property like ?` with 2 parameters + `['%ball%','%tennis%']. ### Like operator diff --git a/composer.lock b/composer.lock index fbc566e..5d9fd85 100644 --- a/composer.lock +++ b/composer.lock @@ -4700,19 +4700,20 @@ }, { "name": "zetacomponents/base", - "version": "1.9", + "version": "1.9.1", "source": { "type": "git", "url": "https://github.com/zetacomponents/Base.git", - "reference": "f20df24e8de3e48b6b69b2503f917e457281e687" + "reference": "489e20235989ddc97fdd793af31ac803972454f1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/zetacomponents/Base/zipball/f20df24e8de3e48b6b69b2503f917e457281e687", - "reference": "f20df24e8de3e48b6b69b2503f917e457281e687", + "url": "https://api.github.com/repos/zetacomponents/Base/zipball/489e20235989ddc97fdd793af31ac803972454f1", + "reference": "489e20235989ddc97fdd793af31ac803972454f1", "shasum": "" }, "require-dev": { + "phpunit/phpunit": "~5.7", "zetacomponents/unit-test": "*" }, "type": "library", @@ -4759,7 +4760,7 @@ ], "description": "The Base package provides the basic infrastructure that all packages rely on. Therefore every component relies on this package.", "homepage": "https://github.com/zetacomponents", - "time": "2014-09-19T03:28:34+00:00" + "time": "2017-11-28T11:30:00+00:00" }, { "name": "zetacomponents/document", diff --git a/src/main/php/Gomoob/Filter/Sql/SqlFilterConverter.php b/src/main/php/Gomoob/Filter/Sql/SqlFilterConverter.php index 8038bbd..9dc9728 100644 --- a/src/main/php/Gomoob/Filter/Sql/SqlFilterConverter.php +++ b/src/main/php/Gomoob/Filter/Sql/SqlFilterConverter.php @@ -530,19 +530,31 @@ private function transformComplexFilter( // Tokenize the filter $tokens = $tokenizer->tokenize($value); - if (count($tokens) === 3 && ($tokens[1]->getTokenCode() === LogicOperatorToken::AND || - $tokens[1]->getTokenCode() === LogicOperatorToken::OR)) { - $resultFirstPart = $this->transformSimpleFilter($key, $tokens[0]->getSequence(), $context); - $resultSecondPart = $this->transformSimpleFilter($key, $tokens[2]->getSequence(), $context); + // If a logical expression is expressed + if (count($tokens) === 3 && + ($tokens[1]->getTokenCode() === LogicOperatorToken::AND || + $tokens[1]->getTokenCode() === LogicOperatorToken::OR)) { + // Transform the first part of the logical expression + $sqlFilter1 = $this->transformSimpleFilter($key, $tokens[0]->getSequence(), $context); + + // Transform the second part of the logical expression + $sqlFilter2 = $this->transformSimpleFilter($key, $tokens[2]->getSequence(), $context); + + // Creates the resulting SQL logical expression + $result[0] = $sqlFilter1->getExpression(); if ($tokens[1]->getTokenCode() === LogicOperatorToken::AND) { - $result[0] = "$resultFirstPart[0] AND $resultSecondPart[0]"; + $result[0] .= ' AND '; } elseif ($tokens[1]->getTokenCode() === LogicOperatorToken::OR) { - $result[0] = "($resultFirstPart[0] OR $resultSecondPart[0])"; + $result[0] .= ' OR '; } - $result[1] = array_merge($resultFirstPart[1], $resultSecondPart[1]); + + $result[0] .= $sqlFilter2->getExpression(); + + // Creates the SQL parameters array + $result[1] = array_merge($sqlFilter1->getParams(), $sqlFilter2->getParams()); } else { - $result = $this->transformSimpleFilter($key, $value, $context); + return $this->transformSimpleFilter($key, $value, $context); } } catch (TokenizerException $tex) { // If an exception is encountered at tokenization then we consider the value to be a simple string @@ -568,17 +580,22 @@ private function transformSimpleFilter( ) /* : array */ { $result = ['', []]; - // Creates a tokenizer to tokenize the filter value - $tokenizer = new FilterTokenizer(); + try { + // Creates a tokenizer to tokenize the filter value + $tokenizer = new FilterTokenizer(); - // Tokenize the filter - $tokens = $tokenizer->tokenize($value); + // Tokenize the filter + $tokens = $tokenizer->tokenize($value); - // Now parse the tokens - if (!empty($tokens)) { - $result = $this->parseFromFirstToken($key, $value, $tokens, false); + // Now parse the tokens + if (!empty($tokens)) { + $result = $this->parseFromFirstToken($key, $value, $tokens, false); + } + } catch (TokenizerException $tex) { + // If an exception is encountered at tokenization then we consider the value to be a simple string + $result = [$key . ' = ?', [$value]]; } - return $result; + return new SqlFilter($result[0], $result[1]); } } diff --git a/src/main/php/Gomoob/Filter/Tokenizer/FilterTokenizer.php b/src/main/php/Gomoob/Filter/Tokenizer/FilterTokenizer.php index 53dbb76..aae161e 100644 --- a/src/main/php/Gomoob/Filter/Tokenizer/FilterTokenizer.php +++ b/src/main/php/Gomoob/Filter/Tokenizer/FilterTokenizer.php @@ -60,7 +60,7 @@ public function __construct() $this->addTokenInfo('(<)', FilterToken::LESS_THAN); $this->addTokenInfo('(~)', FilterToken::LIKE); - // No operator + // Not operator $this->addTokenInfo('(!)', FilterToken::NOT); // Function operators diff --git a/src/main/php/Gomoob/Filter/Tokenizer/LogicOperatorTokenizer.php b/src/main/php/Gomoob/Filter/Tokenizer/LogicOperatorTokenizer.php index f3ef7fd..500495b 100644 --- a/src/main/php/Gomoob/Filter/Tokenizer/LogicOperatorTokenizer.php +++ b/src/main/php/Gomoob/Filter/Tokenizer/LogicOperatorTokenizer.php @@ -34,7 +34,6 @@ */ class LogicOperatorTokenizer extends AbstractTokenizer { - /** * Creates a new instance of the logic operator tokenizer. * @@ -42,7 +41,6 @@ class LogicOperatorTokenizer extends AbstractTokenizer */ public function __construct() { - // This allows to clean our matched tokens a little $this->trim = true; @@ -52,9 +50,16 @@ public function __construct() $this->addTokenInfo('(\+)', LogicOperatorToken::AND); $this->addTokenInfo('(-)', LogicOperatorToken::OR); - // Values + // "Raw" values $this->addTokenInfo('([0-9.]+)', LogicOperatorToken::NUMBER); $this->addTokenInfo('(\'[^\']+\')', LogicOperatorToken::STRING); + + // Values prefixed with Simple operators + $this->addTokenInfo('(~\'[^\']+\')', LogicOperatorToken::STRING); + + // Values prefixed with Not operator + $this->addTokenInfo('(!\'[^\']+\')', LogicOperatorToken::STRING); + $this->addTokenInfo('([^\'\+-]+)', LogicOperatorToken::STRING); } } diff --git a/src/test/php/Gomoob/Filter/Sql/SqlFilterConverterTest.php b/src/test/php/Gomoob/Filter/Sql/SqlFilterConverterTest.php index e440266..4e13024 100644 --- a/src/test/php/Gomoob/Filter/Sql/SqlFilterConverterTest.php +++ b/src/test/php/Gomoob/Filter/Sql/SqlFilterConverterTest.php @@ -79,23 +79,59 @@ public function testTransform() $this->assertCount(1, $sqlFilter->getParams()); $this->assertSame('Sample string', $sqlFilter->getParams()[0]); - // Test with a complex filter and only one property - // Sample complex filters with only one property would be - // "<10+>2" : Lower than 10 and greater than 2 - // "'Handball'-'Football'" : Equals to 'Hand ball' or 'Foot ball' - // "'*ball*'+'*tennis*'" : Like 'ball' and like 'tennis' + // Test with a key which has a bad type + try { + $this->filterConverter->transform(0.26, '>10'); + $this->fail('Must have thrown a ConverterException !'); + } catch (ConverterException $cex) { + $this->assertSame('Invalid filter key type !', $cex->getMessage()); + } + } + + + /** + * Test method for {@link SqlFilterConverter#transform(Object, String)}. + * + * @group SqlFilterConverterTest.testTransformAnd + */ + public function testTransformAnd() + { + // Test with integers $sqlFilter = $this->filterConverter->transform('property', '<10+>2'); $this->assertSame('property < ? AND property > ?', $sqlFilter->getExpression()); $this->assertCount(2, $sqlFilter->getParams()); $this->assertSame(10, $sqlFilter->getParams()[0]); $this->assertSame(2, $sqlFilter->getParams()[1]); - $sqlFilter = $this->filterConverter->transform('property', '>10-<2'); - $this->assertSame('(property > ? OR property < ?)', $sqlFilter->getExpression()); + // Test with floats + $sqlFilter = $this->filterConverter->transform('property', '<5.3+>3.4'); + $this->assertSame('property < ? AND property > ?', $sqlFilter->getExpression()); $this->assertCount(2, $sqlFilter->getParams()); - $this->assertSame(10, $sqlFilter->getParams()[0]); - $this->assertSame(2, $sqlFilter->getParams()[1]); + $this->assertSame(5.3, $sqlFilter->getParams()[0]); + $this->assertSame(3.4, $sqlFilter->getParams()[1]); + + // Test with strings + $sqlFilter = $this->filterConverter->transform('property', "Handball+Football"); + $this->assertSame('property = ? AND property = ?', $sqlFilter->getExpression()); + $this->assertCount(2, $sqlFilter->getParams()); + $this->assertSame('Handball', $sqlFilter->getParams()[0]); + $this->assertSame('Football', $sqlFilter->getParams()[1]); + // Test with strings and the like operator + $sqlFilter = $this->filterConverter->transform('property', "~'*ball*'+~'*tennis*'"); + $this->assertSame('property like ? AND property like ?', $sqlFilter->getExpression()); + $this->assertCount(2, $sqlFilter->getParams()); + $this->assertSame('%ball%', $sqlFilter->getParams()[0]); + $this->assertSame('%tennis%', $sqlFilter->getParams()[1]); + } + + /** + * Test method for {@link SqlFilterConverter#transform(Object, String)}. + * + * @group SqlFilterConverterTest.testTransformComplex + */ + public function testTransformComplex() + { // Test with a complex filter with multiple properties (currently not supported and will fail) try { $this->filterConverter->transform(0, 'price:<90-validity:>=3'); @@ -103,14 +139,6 @@ public function testTransform() } catch (ConverterException $cex) { $this->assertSame('Complex filters are currently not implemented !', $cex->getMessage()); } - - // Test with a key which has a bad type - try { - $this->filterConverter->transform(0.26, '>10'); - $this->fail('Must have thrown a ConverterException !'); - } catch (ConverterException $cex) { - $this->assertSame('Invalid filter key type !', $cex->getMessage()); - } } /** @@ -497,4 +525,40 @@ public function testTransformNotLike() ); } } + + /** + * Test method for {@link SqlFilterConverter#transform(Object, String)}. + * + * @group SqlFilterConverterTest.testTransformOr + */ + public function testTransformOr() + { + // Test with integers + $sqlFilter = $this->filterConverter->transform('property', '<10->2'); + $this->assertSame('property < ? OR property > ?', $sqlFilter->getExpression()); + $this->assertCount(2, $sqlFilter->getParams()); + $this->assertSame(10, $sqlFilter->getParams()[0]); + $this->assertSame(2, $sqlFilter->getParams()[1]); + + // Test with floats + $sqlFilter = $this->filterConverter->transform('property', '<5.3->3.4'); + $this->assertSame('property < ? OR property > ?', $sqlFilter->getExpression()); + $this->assertCount(2, $sqlFilter->getParams()); + $this->assertSame(5.3, $sqlFilter->getParams()[0]); + $this->assertSame(3.4, $sqlFilter->getParams()[1]); + + // Test with strings + $sqlFilter = $this->filterConverter->transform('property', "Handball-Football"); + $this->assertSame('property = ? OR property = ?', $sqlFilter->getExpression()); + $this->assertCount(2, $sqlFilter->getParams()); + $this->assertSame('Handball', $sqlFilter->getParams()[0]); + $this->assertSame('Football', $sqlFilter->getParams()[1]); + + // Test with strings and the like operator + $sqlFilter = $this->filterConverter->transform('property', "~'*ball*'-~'*tennis*'"); + $this->assertSame('property like ? OR property like ?', $sqlFilter->getExpression()); + $this->assertCount(2, $sqlFilter->getParams()); + $this->assertSame('%ball%', $sqlFilter->getParams()[0]); + $this->assertSame('%tennis%', $sqlFilter->getParams()[1]); + } } diff --git a/src/test/php/Gomoob/Filter/Tokenizer/LogicOperatorTokenizerTest.php b/src/test/php/Gomoob/Filter/Tokenizer/LogicOperatorTokenizerTest.php index 78eda2e..09f6063 100644 --- a/src/test/php/Gomoob/Filter/Tokenizer/LogicOperatorTokenizerTest.php +++ b/src/test/php/Gomoob/Filter/Tokenizer/LogicOperatorTokenizerTest.php @@ -37,7 +37,6 @@ */ class LogicOperatorTokenizerTest extends TestCase { - /** * An instance of the logic operator tokenizer to test. * @@ -56,12 +55,11 @@ public function setUp() /** * Test with a complex '+' and '-' operators. * - * @group LogicOperatorTokenizerTest.testTokenizeComplexAndOr + * @group LogicOperatorTokenizerTest.testTokenize */ - public function testTokenizeComplexAndOr() + public function testTokenize() { - - // Test with a simple integer '+' + // Test with 2 integers and '+' $tokens = $this->tokenizer->tokenize('>=10+<50'); $this->assertCount(3, $tokens); @@ -69,7 +67,7 @@ public function testTokenizeComplexAndOr() $this->assertSame('+', $tokens[1]->getSequence()); $this->assertSame('<50', $tokens[2]->getSequence()); - // Test with a simple float '-' + // Test with 2 floats and '-' $tokens = $this->tokenizer->tokenize('>=10.1-<50.2'); $this->assertCount(3, $tokens); @@ -77,6 +75,30 @@ public function testTokenizeComplexAndOr() $this->assertSame('-', $tokens[1]->getSequence()); $this->assertSame('<50.2', $tokens[2]->getSequence()); + // Test with 2 not quoted strings and '+' + $tokens = $this->tokenizer->tokenize("Hand+Ball"); + + $this->assertCount(3, $tokens); + $this->assertSame('Hand', $tokens[0]->getSequence()); + $this->assertSame('+', $tokens[1]->getSequence()); + $this->assertSame('Ball', $tokens[2]->getSequence()); + + // Test with 2 quoted strings and '+' + $tokens = $this->tokenizer->tokenize("'Hand'+'Ball'"); + + $this->assertCount(3, $tokens); + $this->assertSame("'Hand'", $tokens[0]->getSequence()); + $this->assertSame('+', $tokens[1]->getSequence()); + $this->assertSame("'Ball'", $tokens[2]->getSequence()); + + // Test 2 strings prefixed with the like operator and '+' + $tokens = $this->tokenizer->tokenize("~'*ball*'+~'*tennis*'"); + + $this->assertCount(3, $tokens); + $this->assertSame("~'*ball*'", $tokens[0]->getSequence()); + $this->assertSame('+', $tokens[1]->getSequence()); + $this->assertSame("~'*tennis*'", $tokens[2]->getSequence()); + // Test with complex '+' '-' $tokens = $this->tokenizer->tokenize('>=10.1+<50.2-=60.3');