├── demo ├── demo.gif ├── server.php └── index.php ├── src ├── Query │ ├── Visitors │ │ ├── LikeVisitor.php │ │ ├── LogicOperatorVisitor.php │ │ ├── EqVisitor.php │ │ ├── GtVisitor.php │ │ ├── LtVisitor.php │ │ ├── GteVisitor.php │ │ ├── LteVisitor.php │ │ ├── NotEqVisitor.php │ │ ├── BaseVisitor.php │ │ ├── CtVisitor.php │ │ ├── EwVisitor.php │ │ ├── SwVisitor.php │ │ ├── GroupVisitor.php │ │ ├── OrVisitor.php │ │ ├── NullVisitor.php │ │ ├── NotNullVisitor.php │ │ ├── AndVisitor.php │ │ ├── InVisitor.php │ │ ├── NotInVisitor.php │ │ ├── KeyVisitor.php │ │ └── BaseOperatorVisitor.php │ ├── Functions │ │ ├── SumFunction.php │ │ ├── ConcatFunction.php │ │ ├── DateFormatFunction.php │ │ └── BaseFunction.php │ └── Builder.php ├── Exceptions │ └── FilterSyntaxException.php └── Filter.php ├── .gitignore ├── phpstan.neon ├── tests ├── Foo.php └── FilterTest.php ├── composer.json ├── phpunit.xml.dist ├── .github └── workflows │ ├── style.yml │ ├── quality.yml │ └── test.yml ├── LICENSE └── README.md /demo/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/railken/lara-eye/HEAD/demo/demo.gif -------------------------------------------------------------------------------- /src/Query/Visitors/LikeVisitor.php: -------------------------------------------------------------------------------- 1 | '; 22 | } 23 | -------------------------------------------------------------------------------- /src/Query/Visitors/LtVisitor.php: -------------------------------------------------------------------------------- 1 | ='; 22 | } 23 | -------------------------------------------------------------------------------- /src/Query/Visitors/LteVisitor.php: -------------------------------------------------------------------------------- 1 | =8.1", 7 | "railken/bag": "^2.0", 8 | "illuminate/database": "9.* || 10.*", 9 | "railken/search-query": "3.*" 10 | }, 11 | "require-dev": { 12 | "orchestra/testbench": "*", 13 | "friendsofphp/php-cs-fixer": "^3.52" 14 | }, 15 | "autoload": { 16 | "psr-4" : { 17 | "Railken\\LaraEye\\" : "src/" 18 | } 19 | }, 20 | "autoload-dev": { 21 | "psr-4": { 22 | "Railken\\LaraEye\\Tests\\": "tests/" 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Query/Functions/BaseFunction.php: -------------------------------------------------------------------------------- 1 | node; 27 | } 28 | 29 | /** 30 | * @return string 31 | */ 32 | public function getName() 33 | { 34 | return $this->name; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Query/Visitors/BaseVisitor.php: -------------------------------------------------------------------------------- 1 | builder = $builder; 22 | } 23 | 24 | /** 25 | * Get builder. 26 | * 27 | * @return \Railken\LaraEye\Query\Builder 28 | */ 29 | public function getBuilder() 30 | { 31 | return $this->builder; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Query/Visitors/CtVisitor.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | ./tests 6 | 7 | 8 | 9 | 10 | ./src 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/Query/Visitors/EwVisitor.php: -------------------------------------------------------------------------------- 1 | getChildren() as $child) { 22 | $this->getBuilder()->build($q, $child, $context); 23 | } 24 | }; 25 | 26 | $context === Nodes\OrNode::class && $query->orWhere($callback); 27 | $context === Nodes\AndNode::class && $query->where($callback); 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Query/Visitors/OrVisitor.php: -------------------------------------------------------------------------------- 1 | getChildren() as $child) { 22 | $this->getBuilder()->build($q, $child, Nodes\OrNode::class); 23 | } 24 | }; 25 | 26 | $context === Nodes\OrNode::class && $query->orWhere($callback); 27 | $context === Nodes\AndNode::class && $query->where($callback); 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /demo/server.php: -------------------------------------------------------------------------------- 1 | load(); 19 | parent::setUp(); 20 | } 21 | } 22 | 23 | $t = new FilterTest(); 24 | $t->setUp(); 25 | 26 | use Railken\LaraEye\Filter; 27 | use Railken\LaraEye\Tests\Foo; 28 | 29 | $filter = new Filter("foo", ['*']); 30 | 31 | $query = \Illuminate\Support\Facades\DB::table('foo'); 32 | 33 | try { 34 | $result = $filter->build($query, $_GET['q']); 35 | } catch (\Exception $e) { 36 | http_response_code(400); 37 | echo json_encode((object) ['message' => $e->getMessage()]); 38 | die(); 39 | } 40 | 41 | echo json_encode((object) ['query' => ['sql' => $query->toSql(), 'params' => $query->getBindings()]]); 42 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 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 | -------------------------------------------------------------------------------- /.github/workflows/quality.yml: -------------------------------------------------------------------------------- 1 | name: Quality 2 | on: 3 | pull_request: 4 | paths: 5 | - '**.php' 6 | push: 7 | paths: 8 | - '**.php' 9 | jobs: 10 | quality: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout 14 | uses: actions/checkout@v1 15 | - name: Setup PHP, with composer and extensions 16 | uses: shivammathur/setup-php@master #https://github.com/shivammathur/setup-php 17 | with: 18 | php-version: 8.3 19 | extension-csv: mbstring, dom, fileinfo, mysql, zip 20 | coverage: xdebug #optional 21 | - name: Get composer cache directory 22 | id: composer-cache 23 | run: echo "::set-output name=dir::$(composer config cache-files-dir)" 24 | - name: Install Composer dependencies 25 | run: | 26 | composer install --no-progress --no-suggest --prefer-dist --optimize-autoloader 27 | composer require --dev phpstan/phpstan 28 | - name: Test Quality 29 | run: | 30 | export PATH="$HOME/.composer/vendor/bin:$PATH" 31 | ./vendor/bin/phpstan analyse --level=0 src 32 | ./vendor/bin/phpstan analyse --level=0 tests 33 | -------------------------------------------------------------------------------- /src/Query/Visitors/NullVisitor.php: -------------------------------------------------------------------------------- 1 | node) { 26 | $bindings = []; 27 | $sql = []; 28 | 29 | $child0 = $node->getChildByIndex(0); 30 | 31 | if ($child0 instanceof Nodes\KeyNode) { 32 | $context === Nodes\OrNode::class && $query->orWhereNull($this->parseKey($child0->getValue())); 33 | $context === Nodes\AndNode::class && $query->whereNull($this->parseKey($child0->getValue())); 34 | } 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Query/Visitors/NotNullVisitor.php: -------------------------------------------------------------------------------- 1 | node) { 26 | $bindings = []; 27 | $sql = []; 28 | 29 | $child0 = $node->getChildByIndex(0); 30 | $child1 = $node->getChildByIndex(1); 31 | 32 | if ($child0 instanceof Nodes\KeyNode) { 33 | $context === Nodes\OrNode::class && $query->orWhereNotNull($this->parseKey($child0->getValue())); 34 | $context === Nodes\AndNode::class && $query->whereNotNull($this->parseKey($child0->getValue())); 35 | } 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Query/Visitors/AndVisitor.php: -------------------------------------------------------------------------------- 1 | getChildren() as $child) { 22 | if ($child instanceof Nodes\KeyNode || $child instanceof Nodes\ValueNode) { 23 | throw new \Railken\SQ\Exceptions\QuerySyntaxException('Wrong node detected in a logic comparison. Kes Nodes and Value Nodes cannot be used'); 24 | } 25 | 26 | $this->getBuilder()->build($q, $child, Nodes\AndNode::class); 27 | } 28 | }; 29 | 30 | $context === Nodes\OrNode::class && $query->orWhere($callback); 31 | $context === Nodes\AndNode::class && $query->where($callback); 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /demo/index.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 13 | 14 | 15 |
16 |
17 |

Filter

18 | 19 |

{{ error }} 

20 |
{{ result }}
21 |
22 |
23 | 24 | 56 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Laravel Eye 2 | 3 | [![Actions Status](https://github.com/railken/lara-eye/workflows/Test/badge.svg)](https://github.com/railken/lara-eye/actions) 4 | 5 | Filter your ```Illuminate\DataBase\Query\Builder``` using a structured query language. 6 | This can be pretty usefull when you're building an API and you don't want to waste hours of your time creating predefined filters that may change at any time. 7 | 8 | ## Requirements 9 | 10 | PHP 8.1 or later. 11 | 12 | ## Usage 13 | 14 | ```php 15 | 16 | use Railken\LaraEye\Filter; 17 | use Railken\SQ\Exceptions\QuerySyntaxException; 18 | use App\Foo; 19 | 20 | 21 | // Instance of Illuminate\DataBase\Query\Builder 22 | $query = (new Foo())->newQuery()->getQuery(); 23 | 24 | $str_filter = "x > 5 or y < z"; 25 | 26 | $filter = new Filter("foo", ['id', 'x', 'y', 'z', 'created_at', 'updated_at']); 27 | 28 | try { 29 | $filter->build($query, $str_filter); 30 | } catch (QuerySyntaxException $e) { 31 | // handle syntax error 32 | } 33 | 34 | 35 | ``` 36 | 37 | Syntax [here](https://github.com/railken/search-query) 38 | 39 | ## Composer 40 | 41 | You can install it via [Composer](https://getcomposer.org/) by typing the following command: 42 | 43 | ```bash 44 | composer require railken/lara-eye 45 | ``` 46 | 47 | ## Demo 48 | 49 | ![demo](https://raw.githubusercontent.com/railken/lara-eye/master/demo/demo.gif) 50 | 51 | ## License 52 | 53 | Open-source software licensed under the [MIT license](https://opensource.org/licenses/MIT). 54 | -------------------------------------------------------------------------------- /src/Query/Visitors/InVisitor.php: -------------------------------------------------------------------------------- 1 | node) { 33 | $column = null; 34 | $values = null; 35 | 36 | if ($node->getChildByIndex(0) instanceof Nodes\KeyNode) { 37 | $column = $this->parseKey($node->getChildByIndex(0)->getValue()); 38 | } 39 | 40 | if ($node->getChildByIndex(1) instanceof Nodes\GroupNode) { 41 | $values = array_map(function ($node) { 42 | return $this->parseValue($node->getValue()); 43 | }, $node->getChildByIndex(1)->getChildren()); 44 | } 45 | 46 | if ($column && $values) { 47 | $context === Nodes\OrNode::class && $query->orWhereIn($column, $values); 48 | $context === Nodes\AndNode::class && $query->whereIn($column, $values); 49 | } 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Query/Visitors/NotInVisitor.php: -------------------------------------------------------------------------------- 1 | node) { 33 | $column = null; 34 | $values = null; 35 | 36 | if ($node->getChildByIndex(0) instanceof Nodes\KeyNode) { 37 | $column = $this->parseKey($node->getChildByIndex(0)->getValue()); 38 | } 39 | 40 | if ($node->getChildByIndex(1) instanceof Nodes\GroupNode) { 41 | $values = array_map(function ($node) { 42 | return $this->parseValue($node->getValue()); 43 | }, $node->getChildByIndex(1)->getChildren()); 44 | } 45 | 46 | if ($column && $values) { 47 | $context === Nodes\OrNode::class && $query->orWhereNotIn($column, $values); 48 | $context === Nodes\AndNode::class && $query->whereNotIn($column, $values); 49 | } 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Query/Builder.php: -------------------------------------------------------------------------------- 1 | visitors = $visitors; 45 | 46 | return $this; 47 | } 48 | 49 | /** 50 | * Set functions. 51 | * 52 | * @param array $functions 53 | * 54 | * @return $this 55 | */ 56 | public function setFunctions($functions) 57 | { 58 | $this->functions = new Collection($functions); 59 | 60 | return $this; 61 | } 62 | 63 | /** 64 | * @return Collection 65 | */ 66 | public function getFunctions() 67 | { 68 | return $this->functions; 69 | } 70 | 71 | /** 72 | * Build the query. 73 | * 74 | * @param mixed $query 75 | * @param \Railken\SQ\Contracts\NodeContract $node 76 | * @param string $context 77 | */ 78 | public function build($query, NodeContract $node, $context = Nodes\AndNode::class) 79 | { 80 | foreach ($this->visitors as $visitor) { 81 | $visitor->visit($query, $node, $context); 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | on: 3 | pull_request: 4 | paths: 5 | - '**.php' 6 | - '**.yml' 7 | push: 8 | paths: 9 | - '**.php' 10 | - '**.yml' 11 | jobs: 12 | laravel: 13 | name: Laravel ${{ matrix.laravel }} (PHP ${{ matrix.php }}) 14 | runs-on: ubuntu-latest 15 | env: 16 | DB_DATABASE: laravel 17 | DB_USERNAME: root 18 | DB_PASSWORD: password 19 | BROADCAST_DRIVER: log 20 | services: 21 | mysql: 22 | image: mysql:5.7 23 | env: 24 | MYSQL_ALLOW_EMPTY_PASSWORD: false 25 | MYSQL_ROOT_PASSWORD: password 26 | MYSQL_DATABASE: laravel 27 | ports: 28 | - 3306 29 | options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3 30 | redis: 31 | image: redis 32 | ports: 33 | - 6379/tcp 34 | options: --health-cmd="redis-cli ping" --health-interval=10s --health-timeout=5s --health-retries=3 35 | strategy: 36 | fail-fast: false 37 | matrix: 38 | php: ['8.2', '8.3'] 39 | laravel: ['9.*', '10.*'] 40 | steps: 41 | - name: Checkout 42 | uses: actions/checkout@v1 43 | - name: Setup PHP, with composer and extensions 44 | uses: shivammathur/setup-php@master #https://github.com/shivammathur/setup-php 45 | with: 46 | php-version: ${{ matrix.php }} 47 | extension-csv: mbstring, dom, fileinfo, mysql, zip 48 | coverage: xdebug #optional 49 | - name: Get composer cache directory 50 | id: composer-cache 51 | run: echo "::set-output name=dir::$(composer config cache-files-dir)" 52 | - name: Install Composer dependencies 53 | run: | 54 | composer require --dev "laravel/framework:${{ matrix.laravel }}" --no-update 55 | composer update --no-progress --no-suggest --prefer-dist --optimize-autoloader 56 | - name: Prepare the application 57 | run: | 58 | php -r "file_exists('.env') || copy('.env.example', '.env');" 59 | - name: Test 60 | run: ./vendor/bin/phpunit --coverage-text --coverage-clover=build/logs/clover.xml 61 | env: 62 | DB_PORT: ${{ job.services.mysql.ports['3306'] }} 63 | -------------------------------------------------------------------------------- /src/Query/Visitors/KeyVisitor.php: -------------------------------------------------------------------------------- 1 | base_table = $base_table; 29 | 30 | return $this; 31 | } 32 | 33 | /** 34 | * @return string 35 | */ 36 | public function getBaseTable() 37 | { 38 | return $this->base_table; 39 | } 40 | 41 | /** 42 | * @param mixed $keys 43 | * 44 | * @return $this 45 | */ 46 | public function setKeys($keys) 47 | { 48 | $this->keys = $keys; 49 | 50 | return $this; 51 | } 52 | 53 | /** 54 | * @return array 55 | */ 56 | public function getKeys() 57 | { 58 | return $this->keys; 59 | } 60 | 61 | /** 62 | * Visit the node and update the query. 63 | * 64 | * @param mixed $query 65 | * @param \Railken\SQ\Contracts\NodeContract $node 66 | * @param string $context 67 | */ 68 | public function visit($query, NodeContract $node, string $context) 69 | { 70 | if ($node instanceof Nodes\KeyNode) { 71 | $key = $node->getValue(); 72 | 73 | $keys = explode('.', $key); 74 | 75 | 76 | if (count($this->getKeys()) === 0) { 77 | throw new QuerySyntaxException(sprintf("No range of keys defined")); 78 | } 79 | 80 | if (count($keys) === 1) { 81 | $keys = [$this->getBaseTable(), $keys[0]]; 82 | } 83 | 84 | $key = implode('.', $keys); 85 | 86 | $node->setValue($key); 87 | 88 | if ($this->getKeys()[0] === '*') { 89 | return; 90 | } 91 | 92 | if (!in_array($key, $this->getKeys())) { 93 | throw new QuerySyntaxException(sprintf("Invalid key %s", $key)); 94 | } 95 | } 96 | 97 | foreach ($node->getChildren() as $child) { 98 | $this->visit($query, $child, $context); 99 | } 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/Query/Visitors/BaseOperatorVisitor.php: -------------------------------------------------------------------------------- 1 | node) { 35 | $child0 = $node->getChildByIndex(0); 36 | $child1 = $node->getChildByIndex(1); 37 | 38 | if ($context === Nodes\OrNode::class) { 39 | $query->orWhere($this->parseNode($query, $child0), $this->operator, $this->parseNode($query, $child1)); 40 | } 41 | 42 | if ($context === Nodes\AndNode::class) { 43 | $query->where($this->parseNode($query, $child0), $this->operator, $this->parseNode($query, $child1)); 44 | } 45 | } 46 | } 47 | 48 | /** 49 | * Parse the node. 50 | * 51 | * @param mixed $query 52 | * @param \Railken\SQ\Contracts\NodeContract $node 53 | * 54 | * @return mixed 55 | */ 56 | public function parseNode($query, $node) 57 | { 58 | if ($node instanceof Nodes\KeyNode) { 59 | return $this->parseKey($node->getValue()); 60 | } 61 | 62 | if ($node instanceof Nodes\ValueNode) { 63 | return $this->parseValue($node->getValue()); 64 | } 65 | 66 | if ($node instanceof Nodes\FunctionNode) { 67 | // .. ? 68 | 69 | $f = $this->getBuilder()->getFunctions()->first(function ($item, $key) use ($node) { 70 | $class = $item->getNode(); 71 | 72 | return $node instanceof $class; 73 | }); 74 | 75 | if (!$f) { 76 | throw new \Railken\SQ\Exceptions\QuerySyntaxException(sprintf("Function %s not allowed", $node->getName())); 77 | } 78 | 79 | $childs = new Collection(); 80 | 81 | foreach ($node->getChildren() as $child) { 82 | $childs[] = $this->parseNode($query, $child); 83 | } 84 | 85 | $childs = $childs->map(function ($v) use ($query) { 86 | if ($v instanceof \Illuminate\Database\Query\Expression) { 87 | $qb = $query instanceof \Illuminate\Database\Query\Builder ? $query : $query->getQuery(); 88 | return $v->getValue($qb->grammar); 89 | } 90 | 91 | $query->addBinding($v, 'where'); 92 | 93 | return '?'; 94 | }); 95 | 96 | return DB::raw($f->getName().'('.$childs->implode(',').')'); 97 | } 98 | } 99 | 100 | /** 101 | * Parse key. 102 | * 103 | * @param string $key 104 | * 105 | * @return string 106 | */ 107 | public function parseKey($key) 108 | { 109 | $keys = explode('.', $key); 110 | 111 | $keys = [implode(".", array_slice($keys, 0, -1)), $keys[count($keys) - 1]]; 112 | 113 | $key = (new Collection($keys))->map(function ($part) { 114 | return '`'.$part.'`'; 115 | })->implode('.'); 116 | 117 | return DB::raw($key); 118 | } 119 | 120 | /** 121 | * Parse value. 122 | * 123 | * @param string $value 124 | * 125 | * @return string 126 | */ 127 | public function parseValue($value) 128 | { 129 | return $value; 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /src/Filter.php: -------------------------------------------------------------------------------- 1 | table = $table; 31 | $this->keys = array_map(function ($key) use ($table) { 32 | if ($key === '*') { 33 | return $key; 34 | } 35 | 36 | $keys = explode('.', $key); 37 | 38 | if (count($keys) === 1) { 39 | return implode('.', [$table, $key]); 40 | } 41 | 42 | return $key; 43 | }, $keys); 44 | } 45 | 46 | /** 47 | * Filter query with where. 48 | * 49 | * @return Query\Builder 50 | */ 51 | public function getBuilder() 52 | { 53 | $builder = new Query\Builder(); 54 | $builder->setVisitors([ 55 | (new Visitors\KeyVisitor($builder))->setKeys($this->keys)->setBaseTable($this->table), 56 | new Visitors\GroupVisitor($builder), 57 | new Visitors\EqVisitor($builder), 58 | new Visitors\NotEqVisitor($builder), 59 | new Visitors\GtVisitor($builder), 60 | new Visitors\GteVisitor($builder), 61 | new Visitors\LtVisitor($builder), 62 | new Visitors\LteVisitor($builder), 63 | new Visitors\CtVisitor($builder), 64 | new Visitors\SwVisitor($builder), 65 | new Visitors\EwVisitor($builder), 66 | new Visitors\AndVisitor($builder), 67 | new Visitors\OrVisitor($builder), 68 | new Visitors\NotInVisitor($builder), 69 | new Visitors\InVisitor($builder), 70 | new Visitors\NullVisitor($builder), 71 | new Visitors\NotNullVisitor($builder), 72 | ]); 73 | $builder->setFunctions([ 74 | new Functions\ConcatFunction(), 75 | new Functions\SumFunction(), 76 | new Functions\DateFormatFunction(), 77 | ]); 78 | 79 | return $builder; 80 | } 81 | 82 | /** 83 | * Convert the string query into an object (e.g.). 84 | * 85 | * @return QueryParser 86 | */ 87 | public function getParser() 88 | { 89 | $parser = new QueryParser(); 90 | $parser->addResolvers([ 91 | new Resolvers\ValueResolver(), 92 | new Resolvers\KeyResolver(), 93 | new Resolvers\GroupingResolver(), 94 | new Resolvers\SumFunctionResolver(), 95 | new Resolvers\DateFormatFunctionResolver(), 96 | new Resolvers\ConcatFunctionResolver(), 97 | new Resolvers\NotEqResolver(), 98 | new Resolvers\EqResolver(), 99 | new Resolvers\LteResolver(), 100 | new Resolvers\LtResolver(), 101 | new Resolvers\GteResolver(), 102 | new Resolvers\GtResolver(), 103 | new Resolvers\CtResolver(), 104 | new Resolvers\SwResolver(), 105 | new Resolvers\EwResolver(), 106 | new Resolvers\NotInResolver(), 107 | new Resolvers\InResolver(), 108 | new Resolvers\NotNullResolver(), 109 | new Resolvers\NullResolver(), 110 | new Resolvers\AndResolver(), 111 | new Resolvers\OrResolver(), 112 | ]); 113 | 114 | return $parser; 115 | } 116 | 117 | /** 118 | * Filter query with where. 119 | * 120 | * @param mixed $query 121 | * @param string $filter 122 | */ 123 | public function build($query, $filter) 124 | { 125 | $parser = $this->getParser(); 126 | $builder = $this->getBuilder(); 127 | 128 | try { 129 | $builder->build($query, $parser->parse($filter)); 130 | } catch (\Railken\SQ\Exceptions\QuerySyntaxException $e) { 131 | throw new Exceptions\FilterSyntaxException($filter, $e->getMessage()); 132 | } 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /tests/FilterTest.php: -------------------------------------------------------------------------------- 1 | increments('id'); 23 | $table->string('x')->nullable(); 24 | $table->string('y')->nullable(); 25 | $table->string('z')->nullable(); 26 | $table->string('d')->nullable(); 27 | $table->timestamps(); 28 | }); 29 | } 30 | 31 | /** 32 | * Retrieve a new instance of query. 33 | * 34 | * @param string $str_filter 35 | * @param array $keys 36 | * 37 | * @return \Illuminate\Database\Query\Builder 38 | */ 39 | public function newQuery($str_filter, $keys) 40 | { 41 | $filter = new Filter('foo', $keys); 42 | $query = (new Foo())->newQuery()->getQuery(); 43 | $filter->build($query, $str_filter); 44 | 45 | return $query; 46 | } 47 | 48 | public function testFilterUndefindKey() 49 | { 50 | $this->expectException(FilterSyntaxException::class); 51 | $this->newQuery('d eq 1', ['x']); 52 | } 53 | 54 | public function assertQuery(string $sql, string $filter, $keys = ['id', 'x', 'y', 'z', 'created_at']) 55 | { 56 | $query = $this->newQuery($filter, $keys); 57 | $this->assertEquals($sql, $query->toSql()); 58 | $query->get(); 59 | } 60 | 61 | public function testFilterAndWrong() 62 | { 63 | $this->expectException(FilterSyntaxException::class); 64 | $this->newQuery('x and 1', ['*']); 65 | } 66 | 67 | public function testFilterConcatFunction() 68 | { 69 | $this->assertQuery('select * from `foo` where `foo`.`x` = CONCAT(`foo`.`x`,?)', 'x eq concat(x,2)'); 70 | $this->assertQuery('select * from `foo` where `foo`.`x` = CONCAT(`foo`.`x`,CONCAT(`foo`.`y`,?))', 'x eq concat(x,concat(y,3))'); 71 | } 72 | 73 | public function testFilterDateFormatFunction() 74 | { 75 | $this->assertQuery('select * from `foo` where `foo`.`x` = DATE_FORMAT(`foo`.`x`,?)', 'x eq date_format(x,"%d")'); 76 | } 77 | 78 | /*public function testFilterSumFunction() 79 | { 80 | $this->assertQuery('select * from `foo` where `foo`.`x` = SUM(`foo`.`x`)', 'x eq sum(x)'); 81 | }*/ 82 | 83 | public function testFilterAllKeysValid() 84 | { 85 | $this->assertQuery('select * from `foo` where `foo`.`d` = `foo`.`x`', 'd eq x', ['*']); 86 | } 87 | 88 | public function testFilterEqColumns() 89 | { 90 | $this->assertQuery('select * from `foo` where `foo`.`x` = `foo`.`x`', 'x eq x'); 91 | $this->assertQuery('select * from `foo` where `foo`.`x` = `foo`.`x`', 'x = x'); 92 | } 93 | 94 | public function testFilterEq() 95 | { 96 | $this->assertQuery('select * from `foo` where `foo`.`x` = ?', 'x eq 1'); 97 | $this->assertQuery('select * from `foo` where `foo`.`x` = ?', 'x = 1'); 98 | } 99 | 100 | public function testFilterGt() 101 | { 102 | $this->assertQuery('select * from `foo` where `foo`.`x` > ?', 'x gt 1'); 103 | $this->assertQuery('select * from `foo` where `foo`.`x` > ?', 'x > 1'); 104 | } 105 | 106 | public function testFilterGte() 107 | { 108 | $this->assertQuery('select * from `foo` where `foo`.`x` >= ?', 'x gte 1'); 109 | $this->assertQuery('select * from `foo` where `foo`.`x` >= ?', 'x >= 1'); 110 | } 111 | 112 | public function testFilterLt() 113 | { 114 | $this->assertQuery('select * from `foo` where `foo`.`x` < ?', 'x lt 1'); 115 | $this->assertQuery('select * from `foo` where `foo`.`x` < ?', 'x < 1'); 116 | } 117 | 118 | public function testFilterLte() 119 | { 120 | $this->assertQuery('select * from `foo` where `foo`.`x` <= ?', 'x lte 1'); 121 | $this->assertQuery('select * from `foo` where `foo`.`x` <= ?', 'x <= 1'); 122 | } 123 | 124 | public function testFilterCt() 125 | { 126 | $this->assertQuery('select * from `foo` where `foo`.`x` like ?', 'x ct 1'); 127 | $this->assertQuery('select * from `foo` where `foo`.`x` like ?', 'x *= 1'); 128 | } 129 | 130 | public function testFilterSw() 131 | { 132 | $this->assertQuery('select * from `foo` where `foo`.`x` like ?', 'x sw 1'); 133 | $this->assertQuery('select * from `foo` where `foo`.`x` like ?', 'x ^= 1'); 134 | } 135 | 136 | public function testFilterEw() 137 | { 138 | $this->assertQuery('select * from `foo` where `foo`.`x` like ?', 'x ew 1'); 139 | $this->assertQuery('select * from `foo` where `foo`.`x` like ?', 'x $= 1'); 140 | } 141 | 142 | public function testFilterIn() 143 | { 144 | $this->assertQuery('select * from `foo` where `foo`.`x` in (?)', 'x in (1)'); 145 | $this->assertQuery('select * from `foo` where `foo`.`x` in (?)', 'x =[] (1)'); 146 | } 147 | 148 | public function testFilterNotIn() 149 | { 150 | $this->assertQuery('select * from `foo` where `foo`.`x` not in (?)', 'x not in (1)'); 151 | $this->assertQuery('select * from `foo` where `foo`.`x` not in (?)', 'x !=[] (1)'); 152 | } 153 | 154 | public function testFilterAnd() 155 | { 156 | $this->assertQuery('select * from `foo` where (`foo`.`x` = ? and `foo`.`x` = ?)', 'x = 1 and x = 2'); 157 | $this->assertQuery('select * from `foo` where (`foo`.`x` = ? and `foo`.`x` = ?)', 'x = 1 && x = 2'); 158 | } 159 | 160 | public function testFilterOr() 161 | { 162 | $this->assertQuery('select * from `foo` where (`foo`.`x` = ? or `foo`.`x` = ?)', 'x = 1 or x = 2'); 163 | $this->assertQuery('select * from `foo` where (`foo`.`x` = ? or `foo`.`x` = ?)', 'x = 1 || x = 2'); 164 | } 165 | 166 | public function testFilterNull() 167 | { 168 | $this->assertQuery('select * from `foo` where `foo`.`x` is null', 'x is null'); 169 | } 170 | 171 | public function testFilterNotNull() 172 | { 173 | $this->assertQuery('select * from `foo` where `foo`.`x` is not null', 'x is not null'); 174 | } 175 | 176 | public function testGrouping() 177 | { 178 | $this->assertQuery('select * from `foo` where (`foo`.`x` = ? or (`foo`.`x` = ? and `foo`.`x` = ?))', 'x = 1 or (x = 2 and x = 3)'); 179 | $this->assertQuery('select * from `foo` where (`foo`.`x` = ? and (`foo`.`x` = ? or `foo`.`x` = ?))', 'x = 1 and (x = 2 or x = 3)'); 180 | $this->assertQuery('select * from `foo` where (`foo`.`x` = ? and (`foo`.`x` = ?))', 'x = 1 and (x = 2)'); 181 | } 182 | } 183 | --------------------------------------------------------------------------------