├── .github └── workflows │ └── ci.yml ├── LICENSE ├── README.md ├── composer.json ├── phpcs.xml.dist ├── phpstan.neon.dist ├── psalm-baseline.xml ├── psalm.xml └── src ├── DBALCompatibility.php └── Query └── AST └── Functions ├── AbstractJsonFunctionNode.php ├── AbstractJsonOperatorFunctionNode.php ├── Mariadb ├── JsonCompact.php ├── JsonDetailed.php ├── JsonEquals.php ├── JsonExists.php ├── JsonLoose.php ├── JsonNormalize.php ├── JsonQuery.php ├── JsonValue.php └── MariadbJsonFunctionNode.php ├── Mysql ├── JsonArray.php ├── JsonArrayAgg.php ├── JsonArrayAppend.php ├── JsonArrayInsert.php ├── JsonContains.php ├── JsonContainsPath.php ├── JsonDepth.php ├── JsonExtract.php ├── JsonInsert.php ├── JsonKeys.php ├── JsonLength.php ├── JsonMerge.php ├── JsonMergePatch.php ├── JsonMergePreserve.php ├── JsonObject.php ├── JsonObjectAgg.php ├── JsonOverlaps.php ├── JsonPretty.php ├── JsonQuote.php ├── JsonRemove.php ├── JsonReplace.php ├── JsonSearch.php ├── JsonSet.php ├── JsonType.php ├── JsonUnquote.php ├── JsonValid.php ├── JsonValue.php ├── MysqlAndMariadbJsonFunctionNode.php └── MysqlJsonFunctionNode.php ├── Postgresql ├── JsonExtractPath.php ├── JsonGet.php ├── JsonGetPath.php ├── JsonGetPathText.php ├── JsonGetText.php ├── JsonbContains.php ├── JsonbExists.php ├── JsonbExistsAll.php ├── JsonbExistsAny.php ├── JsonbInsert.php ├── JsonbIsContained.php ├── PostgresqlJsonFunctionNode.php └── PostgresqlJsonOperatorFunctionNode.php └── Sqlite ├── Json.php ├── JsonArray.php ├── JsonArrayLength.php ├── JsonExtract.php ├── JsonGroupArray.php ├── JsonGroupObject.php ├── JsonInsert.php ├── JsonObject.php ├── JsonPatch.php ├── JsonQuote.php ├── JsonRemove.php ├── JsonReplace.php ├── JsonSet.php ├── JsonType.php ├── JsonValid.php └── SqliteJsonFunctionNode.php /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | pull_request: 6 | 7 | jobs: 8 | phpunit: 9 | runs-on: ubuntu-latest 10 | 11 | strategy: 12 | matrix: 13 | php-version: 14 | - "8.1" 15 | - "8.2" 16 | - "8.3" 17 | dependencies: 18 | - "highest" 19 | include: 20 | - dependencies: "lowest" 21 | php-version: "8.1" 22 | 23 | steps: 24 | - uses: actions/checkout@v4 25 | 26 | - name: Add php extensions 27 | uses: shivammathur/setup-php@v2 28 | with: 29 | php-version: "${{ matrix.php-version }}" 30 | tools: pecl, cs2pr 31 | extensions: opcache, pcntl, pdo_mysql, zip 32 | 33 | - name: "Install dependencies with Composer" 34 | uses: "ramsey/composer-install@v2" 35 | with: 36 | dependency-versions: "${{ matrix.dependencies }}" 37 | 38 | - name: Run phpunit 39 | run: ./vendor/bin/phpunit 40 | 41 | phpcs: 42 | runs-on: ubuntu-latest 43 | 44 | steps: 45 | - uses: actions/checkout@v4 46 | 47 | - name: Add php extensions 48 | uses: shivammathur/setup-php@v2 49 | with: 50 | php-version: "8.1" 51 | tools: pecl, composer, cs2pr 52 | extensions: opcache, pcntl, pdo_mysql, zip 53 | 54 | - name: Install php packages 55 | run: composer install 56 | 57 | - name: Run phpcs 58 | run: ./vendor/bin/phpcs -q --report=checkstyle | tee -a /dev/stderr | cs2pr 59 | 60 | psalm: 61 | runs-on: ubuntu-latest 62 | 63 | steps: 64 | - uses: actions/checkout@v4 65 | 66 | - name: Add php extensions 67 | uses: shivammathur/setup-php@v2 68 | with: 69 | php-version: "8.1" 70 | tools: pecl, composer, cs2pr 71 | extensions: opcache, pcntl, pdo_mysql, zip 72 | 73 | - name: Install php packages 74 | run: composer install 75 | 76 | - name: Run psalm 77 | run: ./vendor/bin/psalm 78 | 79 | phpstan: 80 | runs-on: ubuntu-latest 81 | 82 | steps: 83 | - uses: actions/checkout@v4 84 | 85 | - name: Add php extensions 86 | uses: shivammathur/setup-php@v2 87 | with: 88 | php-version: "8.1" 89 | tools: pecl, composer, cs2pr 90 | extensions: opcache, pcntl, pdo_mysql, zip 91 | 92 | - name: Install php packages 93 | run: composer install 94 | 95 | - name: Run phpstan 96 | run: ./vendor/bin/phpstan 97 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2019 Scienta 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 | [![Latest Stable Version](https://poser.pugx.org/scienta/doctrine-json-functions/v/stable?format=flat)](https://packagist.org/packages/scienta/doctrine-json-functions) 2 | [![Total Downloads](https://poser.pugx.org/scienta/doctrine-json-functions/downloads?format=flat)](https://packagist.org/packages/scienta/doctrine-json-functions) 3 | [![License](https://poser.pugx.org/scienta/doctrine-json-functions/license)](https://packagist.org/packages/scienta/doctrine-json-functions) 4 | 5 | # DoctrineJsonFunctions 6 | A set of extensions to Doctrine 2+ that add support for json functions. 7 | +Functions are available for MySQL, MariaDb and PostgreSQL. 8 | 9 | | DB | Functions | 10 | |:--:|:---------:| 11 | | MySQL | `JSON_APPEND, JSON_ARRAY, JSON_ARRAYAGG, JSON_ARRAY_APPEND, JSON_ARRAY_INSERT, JSON_CONTAINS, JSON_CONTAINS_PATH, JSON_DEPTH, JSON_EXTRACT, JSON_OVERLAPS, JSON_INSERT, JSON_KEYS, JSON_LENGTH, JSON_MERGE, JSON_MERGE_PRESERVE, JSON_MERGE_PATCH, JSON_OBJECT, JSON_OBJECTAGG, JSON_PRETTY, JSON_QUOTE, JSON_REMOVE, JSON_REPLACE, JSON_SEARCH, JSON_SET, JSON_TYPE, JSON_UNQUOTE, JSON_VALID` | 12 | | PostgreSQL | `@> (JSONB_CONTAINS), ? (JSONB_EXISTS), ?& (JSONB_EXISTS_ALL), ?\| (JSONB_EXISTS_ANY), <@ (JSONB_IS_CONTAINED), JSONB_INSERT, JSON_EXTRACT_PATH, -> (JSON_GET), #> (JSON_GET_PATH), #>> (JSON_GET_PATH_TEXT), ->> (JSON_GET_TEXT)` | 13 | | MariaDb | `JSON_VALUE, JSON_EXISTS, JSON_QUERY, JSON_COMPACT, JSON_DETAILED, JSON_LOOSE, JSON_EQUALS, JSON_NORMALIZE` | 14 | | SQLite | `JSON, JSON_ARRAY, JSON_ARRAY_LENGTH, JSON_EXTRACT, JSON_GROUP_ARRAY, JSON_GROUP_OBJECT, JSON_INSERT, JSON_OBJECT, JSON_PATCH, JSON_QUOTE, JSON_REMOVE, JSON_REPLACE, JSON_SET, JSON_TYPE, JSON_VALID` | 15 | 16 | Table of Contents 17 | ----------------- 18 | 19 | - [Changelog per release](#changelog) 20 | - [Installation](#installation) 21 | - [Testing](#testing) 22 | - [Functions Registration](#functions-registration) 23 | - [Doctrine](#doctrine-orm) 24 | - [Symfony](#symfony-with-doctrine-bundle) 25 | - [Usage](#usage) 26 | - [Using Mysql 5.7+ JSON operators](#using-mysql-57-json-operators) 27 | - [Using PostgreSQL 9.3+ JSON operators](#using-postgresql-93-json-operators) 28 | - [Using SQLite JSON operators](#using-sqlite-json-operators) 29 | - [DQL Functions](#dql-functions) 30 | - [Mysql 5.7+ JSON operators](#mysql-57-json-operators) 31 | - [PostgreSQL 9.3+ JSON operators](#postgresql-93-json-operators) 32 | - [SQLite json1 extension operators](#sqlite-json1-extension-operators) 33 | - [Extendability and Database Support](#extendability-and-database-support) 34 | - [Architecture](#architecture) 35 | - [Adding new platform](#adding-a-new-platform) 36 | - [Adding new function](#adding-a-new-function) 37 | 38 | 39 | Changelog 40 | ------------ 41 | Changes per release are documented with each github release. 42 | You can find an overview here: https://github.com/ScientaNL/DoctrineJsonFunctions/releases 43 | 44 | 45 | Installation 46 | ------------ 47 | The recommended way to install DoctrineJsonFunctions is through [Composer](https://getcomposer.org/). 48 | 49 | Run the following command to install the package: 50 | 51 | composer require scienta/doctrine-json-functions 52 | 53 | Alternatively, you can download the [source code as a file](https://github.com/ScientaNL/DoctrineJsonFunctions/releases) and extract it. 54 | 55 | 56 | Testing 57 | ------------ 58 | This repository uses phpunit for testing purposes. 59 | If you just want to run the tests you can use the docker composer image to install and run phpunit. 60 | There is a docker-compose file with the correct mount but if you want to use just docker you can run this: 61 | 62 | ### php8 63 | ```bash 64 | docker run -it -v ${PWD}:/app scienta/php-composer:php8 /bin/bash -c "composer install && ./vendor/bin/phpunit" 65 | ``` 66 | 67 | 68 | Functions Registration 69 | ---------------------- 70 | 71 | ### Doctrine ORM 72 | 73 | [Doctrine documentation: "DQL User Defined Functions"](http://docs.doctrine-project.org/en/latest/cookbook/dql-user-defined-functions.html) 74 | 75 | ```php 76 | addCustomStringFunction(DqlFunctions\JsonExtract::FUNCTION_NAME, DqlFunctions\JsonExtract::class); 82 | $config->addCustomStringFunction(DqlFunctions\JsonSearch::FUNCTION_NAME, DqlFunctions\JsonSearch::class); 83 | 84 | $em = EntityManager::create($dbParams, $config); 85 | $queryBuilder = $em->createQueryBuilder(); 86 | ``` 87 | 88 | ### Symfony with Doctrine bundle 89 | 90 | [Symfony documentation: "DoctrineBundle Configuration"](https://symfony.com/doc/5.0/reference/configuration/doctrine.html#shortened-configuration-syntax) 91 | 92 | ```yaml 93 | # config/packages/doctrine.yaml 94 | doctrine: 95 | orm: 96 | dql: 97 | string_functions: 98 | JSON_EXTRACT: Scienta\DoctrineJsonFunctions\Query\AST\Functions\Mysql\JsonExtract 99 | JSON_SEARCH: Scienta\DoctrineJsonFunctions\Query\AST\Functions\Mysql\JsonSearch 100 | ``` 101 | 102 | Note that doctrine is [missing a boolean_functions entry](https://github.com/doctrine/orm/issues/6278). 103 | You can register boolean functions as `string_functions` and need to compare them with `= true` to avoid DQL parser errors. 104 | For example, to check for existence of an element in a JSONB array, use `andWhere('JSONB_EXISTS(u.roles, :role) = true)`. 105 | 106 | Usage 107 | ----- 108 | 109 | Mind the comparison when creating the expression and escape the parameters to be valid JSON. 110 | 111 | ### Using Mysql 5.7+ JSON operators 112 | ```php 113 | $q = $queryBuilder 114 | ->select('c') 115 | ->from('Customer', 'c') 116 | ->where("JSON_CONTAINS(c.attributes, :certificates, '$.certificates') = 1"); 117 | 118 | $result = $q->execute(array( 119 | 'certificates' => '"BIO"', 120 | )); 121 | ``` 122 | 123 | ### Using PostgreSQL 9.3+ JSON operators 124 | 125 | Note that you need to use the function names. This library does not add support for custom operators like `@>`. 126 | 127 | ```php 128 | $q = $queryBuilder 129 | ->select('c') 130 | ->from('Customer', 'c') 131 | ->where("JSON_GET_TEXT(c.attributes, 'gender') = :gender"); 132 | 133 | $result = $q->execute(array( 134 | 'gender' => 'male', 135 | )); 136 | ``` 137 | 138 | Boolean functions need to be registered as string functions and compared with true because [Doctrine DQL does not know about boolean functions](https://github.com/doctrine/orm/issues/6278). 139 | ```php 140 | $q = $queryBuilder 141 | ->select('c') 142 | ->from('Customer', 'c') 143 | ->where('JSONB_CONTAINS(c.roles, :role) = true'); 144 | 145 | $result = $q->execute(array( 146 | 'role' => 'ROLE_ADMIN', 147 | )); 148 | ``` 149 | 150 | ### Using SQLite JSON operators 151 | ```php 152 | $q = $queryBuilder 153 | ->select('c') 154 | ->from('Customer', 'c') 155 | ->where("JSON_EXTRACT(c.attributes, '$.gender') = :gender"); 156 | 157 | $result = $q->execute(); 158 | ``` 159 | 160 | DQL Functions 161 | ------------- 162 | 163 | The library provides this set of DQL functions. 164 | 165 | ### Mysql 5.7+ JSON operators 166 | * [JSON_ARRAY_APPEND(json_doc, path, val[, path, val] ...)](https://dev.mysql.com/doc/refman/5.7/en/json-modification-functions.html#function_json-array-append) 167 | - Appends values to the end of the indicated arrays within a JSON document and returns the result. 168 | * [JSON_ARRAYAGG(value)](https://dev.mysql.com/doc/refman/5.7/en/aggregate-functions.html#function_json-arrayagg) 169 | - Aggregates a result set as a single JSON array whose elements consist of the rows. 170 | * [JSON_ARRAY_INSERT(json_doc, path, val[, path, val] ...)](https://dev.mysql.com/doc/refman/5.7/en/json-modification-functions.html#function_json-array-insert) 171 | - Updates a JSON document, inserting into an array within the document and returning the modified document. 172 | * [JSON_ARRAY([val[, val] ...])](https://dev.mysql.com/doc/refman/5.7/en/json-creation-functions.html#function_json-array) 173 | - Evaluates a (possibly empty) list of values and returns a JSON array containing those values. 174 | * [JSON_CONTAINS_PATH(json_doc, one_or_all, path[, path] ...)](https://dev.mysql.com/doc/refman/5.7/en/json-search-functions.html#function_json-contains-path) 175 | - Returns 0 or 1 to indicate whether a JSON document contains data at a given path or paths. 176 | * [JSON_CONTAINS(json_doc, val[, path])](https://dev.mysql.com/doc/refman/5.7/en/json-search-functions.html#function_json-contains) 177 | - Returns 0 or 1 to indicate whether a specific value is contained in a target JSON document, or, if a path argument is given, at a specific path within the target document. 178 | * [JSON_DEPTH(json_doc)](https://dev.mysql.com/doc/refman/5.7/en/json-attribute-functions.html#function_json-depth) 179 | - Returns the maximum depth of a JSON document. 180 | * [JSON_EXTRACT(json_doc, path[, path] ...)](https://dev.mysql.com/doc/refman/5.7/en/json-search-functions.html#function_json-extract) 181 | - Returns data from a JSON document, selected from the parts of the document matched by the path arguments. 182 | * [JSON_OVERLAPS(json_doc1, json_doc2)](https://dev.mysql.com/doc/refman/8.0/en/json-search-functions.html#function_json-overlaps) 183 | - Compares two JSON documents. Returns true (1) if the two document have any key-value pairs or array elements in common. If both arguments are scalars, the function performs a simple equality test. 184 | * [JSON_INSERT(json_doc, path, val[, path, val] ...)](https://dev.mysql.com/doc/refman/5.7/en/json-modification-functions.html#function_json-insert) 185 | - Inserts data into a JSON document and returns the result. 186 | * [JSON_KEYS(json_doc[, path])](https://dev.mysql.com/doc/refman/5.7/en/json-search-functions.html#function_json-keys) 187 | - Returns the keys from the top-level value of a JSON object as a JSON array, or, if a path argument is given, the top-level keys from the selected path. 188 | * [JSON_LENGTH(json_doc[, path])](https://dev.mysql.com/doc/refman/5.7/en/json-attribute-functions.html#function_json-length) 189 | - Returns the length of JSON document, or, if a path argument is given, the length of the value within the document identified by the path. 190 | * [JSON_MERGE(json_doc, json_doc[, json_doc] ...)](https://dev.mysql.com/doc/refman/5.7/en/json-modification-functions.html#function_json-merge) 191 | - Merges two or more JSON documents and returns the merged result. 192 | * [JSON_MERGE_PRESERVE(json_doc, json_doc[, json_doc] ...)](https://dev.mysql.com/doc/refman/5.7/en/json-modification-functions.html#function_json-merge_preserve) 193 | - Merges two or more JSON documents and returns the merged result. Returns NULL if any argument is NULL. An error occurs if any argument is not a valid JSON document. 194 | * [JSON_MERGE_PATCH(json_doc, json_doc[, json_doc] ...)](https://dev.mysql.com/doc/refman/5.7/en/json-modification-functions.html#function_json-merge-patch) 195 | - Performs an RFC 7396 compliant merge of two or more JSON documents and returns the merged result. 196 | * [JSON_OBJECT([key, val[, key, val] ...])](https://dev.mysql.com/doc/refman/5.7/en/json-creation-functions.html#function_json-object) 197 | - Evaluates a (possibly empty) list of key/value pairs and returns a JSON object containing those pairs. 198 | * [JSON_OBJECTAGG(key, val)](https://dev.mysql.com/doc/refman/5.7/en/json-creation-functions.html#function_json-object) 199 | - Takes two column names or expressions as arguments, the first of these being used as a key and the second as a value, and returns a JSON object containing key-value pairs. 200 | * [JSON_PRETTY(json_val)](https://dev.mysql.com/doc/refman/5.7/en/json-utility-functions.html#function_json-pretty) 201 | - Provides pretty-printing of JSON values similar to that implemented in PHP and by other languages and database systems 202 | * [JSON_QUOTE(json_val)](https://dev.mysql.com/doc/refman/5.7/en/json-creation-functions.html#function_json-quote) 203 | - Quotes a string as a JSON value by wrapping it with double quote characters and escaping interior quote and other characters, then returning the result as a utf8mb4 string. 204 | * [JSON_REMOVE(json_doc, path[, path] ...)](https://dev.mysql.com/doc/refman/5.7/en/json-modification-functions.html#function_json-remove) 205 | - Removes data from a JSON document and returns the result. 206 | * [JSON_REPLACE(json_doc, path, val[, path, val] ...)](https://dev.mysql.com/doc/refman/5.7/en/json-modification-functions.html#function_json-replace) 207 | - Replaces existing values in a JSON document and returns the result. 208 | * [JSON_SEARCH(json_doc, one_or_all, search_str[, escape_char[, path] ...])](https://dev.mysql.com/doc/refman/5.7/en/json-search-functions.html#function_json-search) 209 | - Returns the path to the given string within a JSON document. 210 | * [JSON_SET(json_doc, path, val[, path, val] ...)](https://dev.mysql.com/doc/refman/5.7/en/json-modification-functions.html#function_json-set) 211 | - Inserts or updates data in a JSON document and returns the result. 212 | * [JSON_TYPE(json_val)](https://dev.mysql.com/doc/refman/5.7/en/json-attribute-functions.html#function_json-type) 213 | - Returns a utf8mb4 string indicating the type of a JSON value. 214 | * [JSON_UNQUOTE(val)](https://dev.mysql.com/doc/refman/5.7/en/json-modification-functions.html#function_json-unquote) 215 | - Unquotes JSON value and returns the result as a utf8mb4 string. 216 | * [JSON_VALID(val)](https://dev.mysql.com/doc/refman/5.7/en/json-attribute-functions.html#function_json-valid) 217 | - Returns 0 or 1 to indicate whether a value is a valid JSON document. 218 | 219 | Note that you can use MySQL Operators with MariaDb database if compatible. 220 | 221 | ### MariaDb 10.2.3 JSON operators 222 | * [JSON_VALUE(json_doc, path)](https://mariadb.com/kb/en/json_value/) 223 | - Returns the scalar specified by the path. Returns NULL if there is no match. 224 | * [JSON_EXISTS(json_doc, path)](https://mariadb.com/kb/en/json_exists/) 225 | - Determines whether a specified JSON value exists in the given data. Returns 1 if found, 0 if not, or NULL if any of the inputs were NULL. 226 | * [JSON_QUERY(json_doc, path)](https://mariadb.com/kb/en/json_query/) 227 | - Given a JSON document, returns an object or array specified by the path. Returns NULL if not given a valid JSON document, or if there is no match. 228 | 229 | ### MariaDb 10.2.4 JSON operators 230 | * [JSON_COMPACT(json_doc)](https://mariadb.com/kb/en/json_compact/) 231 | - Removes all unnecessary spaces so the json document is as short as possible. 232 | * [JSON_DETAILED(json_doc[, tab_size])](https://mariadb.com/kb/en/json_detailed/) 233 | - Represents JSON in the most understandable way emphasizing nested structures. 234 | * [JSON_LOOSE(json_doc)](https://mariadb.com/kb/en/json_loose/) 235 | - Adds spaces to a JSON document to make it look more readable. 236 | 237 | ### MariaDb 10.7.0 JSON operators 238 | * [JSON_EQUALS(json_doc, json_doc)](https://mariadb.com/kb/en/json_equals/) 239 | - Checks if there is equality between two json objects. Returns 1 if it there is, 0 if not, or NULL if any of the arguments are null. 240 | * [JSON_NORMALIZE(json_doc)](https://mariadb.com/kb/en/json_normalize/) 241 | - Recursively sorts keys and removes spaces, allowing comparison of json documents for equality. 242 | 243 | ### PostgreSQL 9.3+ JSON operators 244 | Basic support for JSON operators is implemented. This works even with `Doctrine\DBAL` v2.5. [Official documentation of JSON operators](https://www.postgresql.org/docs/9.5/functions-json.html). 245 | 246 | * **JSONB_CONTAINS(jsonb, jsonb)** 247 | - expands to `jsonb @> jsonb` 248 | * **JSONB_EXISTS(jsonb, text)** 249 | - executed as `JSONB_EXISTS(jsonb, text)`, equivalent to `jsonb ? text` 250 | * **JSONB_EXISTS_ALL(jsonb, array)** 251 | - executed as `JSONB_EXISTS_ALL(jsonb, array)`, equivalent to `jsonb ?& array` 252 | * **JSONB_EXISTS_ANY(jsonb, array)** 253 | - executed as `JSONB_EXISTS_ANY(jsonb, array)`, equivalent to `jsonb ?| array` 254 | * **JSONB_IS_CONTAINED(jsonb, jsonb)** 255 | - expands to `jsonb <@ jsonb` 256 | * **JSONB_INSERT** 257 | - executed as is 258 | * **JSON_EXTRACT_PATH** 259 | - executed as is 260 | * **JSON_GET(jsondoc, path)** 261 | - expands to `jsondoc->path` in case of numeric `path` (use with JSON arrays) 262 | - expands to `jsondoc->'path'` in case of non-numeric `path` (use with JSON objects) 263 | * **JSON_GET_TEXT(jsondoc, path)** 264 | - expands to `jsondoc->>path` in case of numeric `path` (use with JSON arrays) 265 | - expands to `jsondoc->>'path'` in case of non-numeric `path` (use with JSON objects) 266 | * **JSON_GET_PATH(jsondoc, path)** 267 | - expands to `jsondoc#>'path'` 268 | * **JSON_GET_PATH_TEXT(jsondoc, path)** 269 | - expands to `jsondoc#>>'path'` 270 | 271 | Please note that chaining of JSON operators is not supported. 272 | 273 | ### SQLite JSON1 Extension operators 274 | 275 | Support for all the scalar and aggregate functions as seen in the [JSON1 Extension documentation](https://www.sqlite.org/json1.html). 276 | 277 | #### Scalar functions 278 | 279 | * JSON(json) 280 | - Verifies that its argument is a valid JSON string and returns a minified version of that JSON string. 281 | * JSON_ARRAY([val[, val] ...]) 282 | - Accepts zero or more arguments and returns a well-formed JSON array that is composed from those arguments. 283 | * JSON_ARRAY_LENGTH(json[, path]) 284 | - Returns the number of elements in the JSON array `json`, or 0 if `json` is some kind of JSON value other than an array. 285 | * JSON_EXTRACT(json, path[, path ], ...) 286 | - Extracts and returns one or more values from the well-formed JSON. 287 | * JSON_INSERT(json[, path, value],...) 288 | - Given zero or more sets of paths and values, it inserts (without overwriting) each value at its corresponding path of the `json`. 289 | * JSON_OBJECT(label, value[, label, value], ...) 290 | - Accepts zero or more pairs of arguments and returns a well-formed JSON object that is composed from those arguments. 291 | * JSON_PATCH(target, patch) 292 | - Applies a `patch` to `target`. 293 | * JSON_QUOTE(value) 294 | - Converts the SQL `value` (a number or a string) into its corresponding JSON representation. 295 | * JSON_REMOVE(json[, path], ...) 296 | - Removes the values at each given `path`. 297 | * JSON_REPLACE(json[, path, value],...) 298 | - Given zero or more sets of paths and values, it overwrites each value at its corresponding path of the `json`. 299 | * JSON_SET(json[, path, value],...) 300 | - Given zero or more sets of paths and values, it inserts or overwrites each value at its corresponding path of the `json`. 301 | * JSON_TYPE(json[, path]) 302 | - Returns the type of the outermost element of `json` or of the value at `path`. 303 | * JSON_VALID(json) 304 | - Returns 1 if the argument `json` is well-formed JSON or 0 otherwise. 305 | 306 | #### Aggregate functions 307 | 308 | * JSON_GROUP_ARRAY(value) 309 | - Returns a JSON array comprised of all `value` in the aggregation 310 | * JSON_GROUP_OBJECT(name, value) 311 | - Returns a JSON object comprised of all `name/value` pairs in the aggregation. 312 | 313 | Extendability and Database Support 314 | ---------------------------------- 315 | 316 | ### Architecture 317 | 318 | Platform function classes naming rule is: 319 | 320 | ``` 321 | Scienta\DoctrineJsonFunctions\Query\AST\Functions\$platformName\$functionName 322 | ``` 323 | 324 | ### Adding a new platform 325 | 326 | To add support of new platform you just need to create new folder `Scienta\DoctrineJsonFunctions\Query\AST\Functions\$platformName` 327 | and implement required function there according to naming rules 328 | 329 | ### Adding a new function 330 | 331 | If you want to add new function to this library feel free to fork it and create pull request with your implementation. 332 | Please, remember to update documentation with your new functions. 333 | 334 | 335 | See also 336 | -------- 337 | 338 | [dunglas/doctrine-json-odm](https://github.com/dunglas/doctrine-json-odm): Serialize / deserialize plain old PHP objects into JSON columns. 339 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "scienta/doctrine-json-functions", 3 | "abandoned": false, 4 | "type": "library", 5 | "description": "A set of extensions to Doctrine that add support for json query functions.", 6 | "keywords": [ 7 | "doctrine", 8 | "orm", 9 | "json", 10 | "dql", 11 | "database", 12 | "mysql", 13 | "postgres", 14 | "postgresql", 15 | "mariadb", 16 | "sqlite" 17 | ], 18 | "license": "MIT", 19 | "authors": [ 20 | { 21 | "name": "Doctrine Json Functions Contributors", 22 | "homepage": "https://github.com/ScientaNL/DoctrineJsonFunctions/contributors" 23 | } 24 | ], 25 | "require": { 26 | "php": "^8.1", 27 | "ext-pdo": "*", 28 | "doctrine/dbal": "^3.2 || ^4", 29 | "doctrine/lexer": "^2.0 || ^3.0", 30 | "doctrine/orm": "^2.19 || ^3" 31 | }, 32 | "require-dev": { 33 | "doctrine/coding-standard": "^9.0 || ^10.0 || ^11.0 || ^12.0", 34 | "phpstan/phpstan": "^1.12", 35 | "phpstan/extension-installer": "^1.4", 36 | "phpstan/phpstan-doctrine": "^1.4", 37 | "phpstan/phpstan-phpunit": "^1.4", 38 | "phpunit/phpunit": "^10.1", 39 | "psalm/plugin-phpunit": "^0.18", 40 | "slevomat/coding-standard": "~8", 41 | "symfony/cache": "^5.4 || ^6.4 || ^7", 42 | "vimeo/psalm": "^5.2", 43 | "webmozart/assert": "^1.11" 44 | }, 45 | "autoload": { 46 | "psr-4": { 47 | "Scienta\\DoctrineJsonFunctions\\": "src/" 48 | } 49 | }, 50 | "autoload-dev": { 51 | "psr-4": { 52 | "Doctrine\\Tests\\": "tests/Doctrine/Tests", 53 | "Scienta\\DoctrineJsonFunctions\\Tests\\": "tests/" 54 | } 55 | }, 56 | "suggest": { 57 | "dunglas/doctrine-json-odm": "To serialize / deserialize objects as JSON documents." 58 | }, 59 | "scripts": { 60 | "codestyle": "phpcs -q --report=checkstyle", 61 | "phpstan": "phpstan analyze -v", 62 | "psalm": "psalm --config=psalm.xml --threads=1 --no-cache" 63 | }, 64 | "config": { 65 | "allow-plugins": { 66 | "composer/package-versions-deprecated": true, 67 | "dealerdirect/phpcodesniffer-composer-installer": true, 68 | "phpstan/extension-installer": true 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /phpcs.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | src 19 | tests 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | -------------------------------------------------------------------------------- /phpstan.neon.dist: -------------------------------------------------------------------------------- 1 | parameters: 2 | level: 0 3 | paths: 4 | - src 5 | - tests 6 | editorUrl: 'phpstorm://open?file=%%file%%&line=%%line%%' 7 | editorUrlTitle: '%%file%%:%%line%%' 8 | treatPhpDocTypesAsCertain: false 9 | reportUnmatchedIgnoredErrors: false 10 | -------------------------------------------------------------------------------- /psalm-baseline.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | DbalDriverCompatibility 6 | 7 | 8 | 9 | 10 | DbalPlatformCompatibility 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /psalm.xml: -------------------------------------------------------------------------------- 1 | 2 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /src/DBALCompatibility.php: -------------------------------------------------------------------------------- 1 | */ 39 | protected $parsedArguments = []; 40 | 41 | /** 42 | * @param Parser $parser 43 | * @throws \Doctrine\ORM\Query\QueryException 44 | */ 45 | public function parse(Parser $parser): void 46 | { 47 | $parser->match(TokenType::T_IDENTIFIER); 48 | $parser->match(TokenType::T_OPEN_PARENTHESIS); 49 | 50 | $argumentParsed = $this->parseArguments($parser, $this->requiredArgumentTypes); 51 | 52 | if (!empty($this->optionalArgumentTypes)) { 53 | $this->parseOptionalArguments($parser, $argumentParsed); 54 | } 55 | 56 | $parser->match(TokenType::T_CLOSE_PARENTHESIS); 57 | } 58 | 59 | /** 60 | * @param Parser $parser 61 | * @param bool $argumentParsed 62 | * @throws \Doctrine\ORM\Query\QueryException 63 | */ 64 | protected function parseOptionalArguments(Parser $parser, bool $argumentParsed): void 65 | { 66 | $continueParsing = !$parser->getLexer()->isNextToken(TokenType::T_CLOSE_PARENTHESIS); 67 | while ($continueParsing) { 68 | $argumentParsed = $this->parseArguments($parser, $this->optionalArgumentTypes, $argumentParsed); 69 | $continueParsing = $this->allowOptionalArgumentRepeat && $parser->getLexer()->isNextToken(TokenType::T_COMMA); 70 | } 71 | } 72 | 73 | /** 74 | * @param Parser $parser 75 | * @param string[] $argumentTypes 76 | * @param bool $argumentParsed 77 | * @return bool 78 | * @throws \Doctrine\ORM\Query\QueryException 79 | */ 80 | protected function parseArguments(Parser $parser, array $argumentTypes, bool $argumentParsed = false): bool 81 | { 82 | foreach ($argumentTypes as $argType) { 83 | if ($argumentParsed) { 84 | $parser->match(TokenType::T_COMMA); 85 | } else { 86 | $argumentParsed = true; 87 | } 88 | 89 | switch ($argType) { 90 | case self::STRING_PRIMARY_ARG: 91 | $this->parsedArguments[] = $parser->StringPrimary(); 92 | break; 93 | case self::STRING_ARG: 94 | $this->parsedArguments[] = $this->parseStringLiteral($parser); 95 | break; 96 | case self::ALPHA_NUMERIC: 97 | $this->parsedArguments[] = $this->parseAlphaNumericLiteral($parser); 98 | break; 99 | case self::VALUE_ARG: 100 | $this->parsedArguments[] = $parser->NewValue(); 101 | break; 102 | default: 103 | throw QueryException::semanticalError(sprintf('Unknown function argument type %s for %s()', $argType, static::FUNCTION_NAME)); 104 | } 105 | } 106 | 107 | return $argumentParsed; 108 | } 109 | 110 | /** 111 | * @param Parser $parser 112 | * @return Literal 113 | * @throws QueryException 114 | */ 115 | protected function parseStringLiteral(Parser $parser): Literal 116 | { 117 | $lexer = $parser->getLexer(); 118 | $lookaheadType = $lexer->lookahead->type; 119 | 120 | if ($lookaheadType !== TokenType::T_STRING) { 121 | $parser->syntaxError('string'); 122 | } 123 | 124 | return $this->matchStringLiteral($parser, $lexer); 125 | } 126 | 127 | /** 128 | * @param Parser $parser 129 | * @return Literal 130 | * @throws QueryException 131 | */ 132 | protected function parseAlphaNumericLiteral(Parser $parser): Literal 133 | { 134 | $lexer = $parser->getLexer(); 135 | $lookaheadType = $lexer->lookahead->type; 136 | 137 | switch ($lookaheadType) { 138 | case TokenType::T_STRING: 139 | return $this->matchStringLiteral($parser, $lexer); 140 | case TokenType::T_INTEGER: 141 | case TokenType::T_FLOAT: 142 | $parser->match( 143 | $lexer->isNextToken(TokenType::T_INTEGER) ? TokenType::T_INTEGER : TokenType::T_FLOAT 144 | ); 145 | 146 | return new Literal(Literal::NUMERIC, $lexer->token->value); 147 | default: 148 | $parser->syntaxError('numeric'); 149 | } 150 | } 151 | 152 | private function matchStringLiteral(Parser $parser, Lexer $lexer): Literal 153 | { 154 | $parser->match(TokenType::T_STRING); 155 | return new Literal(Literal::STRING, $lexer->token->value); 156 | } 157 | 158 | /** 159 | * @param SqlWalker $sqlWalker 160 | * @return string 161 | * @throws Exception 162 | * @throws \Doctrine\ORM\Query\AST\ASTException 163 | */ 164 | public function getSql(SqlWalker $sqlWalker): string 165 | { 166 | $this->validatePlatform($sqlWalker); 167 | 168 | $args = []; 169 | foreach ($this->parsedArguments as $parsedArgument) { 170 | if ($parsedArgument === null) { 171 | $args[] = 'NULL'; 172 | } else { 173 | $args[] = $parsedArgument->dispatch($sqlWalker); 174 | } 175 | } 176 | return $this->getSqlForArgs($args); 177 | } 178 | 179 | /** 180 | * @param string[] $arguments 181 | * @return string 182 | */ 183 | protected function getSqlForArgs(array $arguments): string 184 | { 185 | return sprintf('%s(%s)', $this->getSQLFunction(), implode(', ', $arguments)); 186 | } 187 | 188 | /** 189 | * @return string 190 | */ 191 | protected function getSQLFunction(): string 192 | { 193 | return static::FUNCTION_NAME; 194 | } 195 | 196 | /** 197 | * @param SqlWalker $sqlWalker 198 | * @throws Exception 199 | */ 200 | abstract protected function validatePlatform(SqlWalker $sqlWalker): void; 201 | } 202 | -------------------------------------------------------------------------------- /src/Query/AST/Functions/AbstractJsonOperatorFunctionNode.php: -------------------------------------------------------------------------------- 1 | getOperator(), $rightArg); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Query/AST/Functions/Mariadb/JsonCompact.php: -------------------------------------------------------------------------------- 1 | getConnection()->getDatabasePlatform() instanceof (DBALCompatibility::mariaDBPlatform())) { 24 | throw DBALCompatibility::notSupportedPlatformException(static::FUNCTION_NAME); 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Query/AST/Functions/Mysql/JsonArray.php: -------------------------------------------------------------------------------- 1 | match(TokenType::T_IDENTIFIER); 26 | $parser->match(TokenType::T_OPEN_PARENTHESIS); 27 | 28 | $this->parsedArguments[] = $parser->StringPrimary(); 29 | 30 | $parser->match(TokenType::T_COMMA); 31 | 32 | $this->parsedArguments[] = $this->parsePathMode($parser); 33 | 34 | $this->parseOptionalArguments($parser, true); 35 | 36 | $parser->match(TokenType::T_CLOSE_PARENTHESIS); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Query/AST/Functions/Mysql/JsonDepth.php: -------------------------------------------------------------------------------- 1 | match(TokenType::T_IDENTIFIER); 43 | $parser->match(TokenType::T_OPEN_PARENTHESIS); 44 | 45 | $this->parsedArguments[] = $parser->StringPrimary(); 46 | 47 | $parser->match(TokenType::T_COMMA); 48 | 49 | $this->parsedArguments[] = $this->parsePathMode($parser); 50 | 51 | $parser->match(TokenType::T_COMMA); 52 | 53 | $this->parsedArguments[] = $parser->StringPrimary(); 54 | 55 | $continueParsing = !$parser->getLexer()->isNextToken(TokenType::T_CLOSE_PARENTHESIS); 56 | if ($continueParsing) { 57 | $this->parseArguments($parser, [self::VALUE_ARG, self::STRING_PRIMARY_ARG], true); 58 | } 59 | 60 | $this->parseOptionalArguments($parser, true); 61 | 62 | $parser->match(TokenType::T_CLOSE_PARENTHESIS); 63 | } 64 | 65 | /** 66 | * @param Parser $parser 67 | * @return Node 68 | * @throws Exception 69 | */ 70 | protected function parsePathMode(Parser $parser) 71 | { 72 | $value = $parser->getLexer()->lookahead->value; 73 | 74 | if (strcasecmp(self::MODE_ONE, $value) === 0) { 75 | $this->mode = self::MODE_ONE; 76 | return $parser->Literal(); 77 | } 78 | 79 | if (strcasecmp(self::MODE_ALL, $value) === 0) { 80 | $this->mode = self::MODE_ALL; 81 | return $parser->Literal(); 82 | } 83 | 84 | throw DBALCompatibility::notSupportedPlatformException( 85 | "Mode '$value' is not supported by " . static::FUNCTION_NAME . "." 86 | ); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/Query/AST/Functions/Mysql/JsonSet.php: -------------------------------------------------------------------------------- 1 | */ 26 | private array $jsonArguments = []; 27 | 28 | private string | int | null $returningType = null; 29 | 30 | public function parse(Parser $parser): void 31 | { 32 | $parser->match(TokenType::T_IDENTIFIER); 33 | $parser->match(TokenType::T_OPEN_PARENTHESIS); 34 | 35 | $this->jsonArguments[] = $parser->StringPrimary(); 36 | 37 | $parser->match(TokenType::T_COMMA); 38 | 39 | $this->jsonArguments[] = $parser->StringPrimary(); 40 | 41 | if ($parser->getLexer()->isNextToken(TokenType::T_COMMA)) { 42 | $parser->match(TokenType::T_COMMA); 43 | 44 | // match complex returning types 45 | $parser->match(TokenType::T_IDENTIFIER); 46 | $this->returningType = $parser->getLexer()->token->value; 47 | 48 | if ($parser->getLexer()->isNextToken(TokenType::T_OPEN_PARENTHESIS)) { 49 | $parser->match(TokenType::T_OPEN_PARENTHESIS); 50 | $parameter = $parser->Literal(); 51 | $parameters = [$parameter->value]; 52 | 53 | if ($parser->getLexer()->isNextToken(TokenType::T_COMMA)) { 54 | while ($parser->getLexer()->isNextToken(TokenType::T_COMMA)) { 55 | $parser->match(TokenType::T_COMMA); 56 | $parameter = $parser->Literal(); 57 | $parameters[] = $parameter->value; 58 | } 59 | } 60 | 61 | $parser->match(TokenType::T_CLOSE_PARENTHESIS); 62 | $this->returningType .= '(' . implode(',', $parameters) . ')'; 63 | } 64 | } 65 | 66 | $parser->match(TokenType::T_CLOSE_PARENTHESIS); 67 | } 68 | 69 | /** 70 | * @param SqlWalker $sqlWalker 71 | * @return string 72 | * @throws Exception 73 | * @throws \Doctrine\ORM\Query\AST\ASTException 74 | */ 75 | public function getSql(SqlWalker $sqlWalker): string 76 | { 77 | $this->validatePlatform($sqlWalker); 78 | 79 | /** @var list $jsonStringArguments */ 80 | $jsonStringArguments = []; 81 | foreach ($this->jsonArguments as $jsonArgument) { 82 | if ($jsonArgument === null) { 83 | $jsonStringArguments[] = 'NULL'; 84 | } else { 85 | $jsonStringArguments[] = $jsonArgument->dispatch($sqlWalker); 86 | } 87 | } 88 | 89 | if ($this->returningType === null) { 90 | return sprintf('%s(%s)', $this->getSQLFunction(), implode(', ', $jsonStringArguments)); 91 | } 92 | 93 | return sprintf( 94 | '%s(%s RETURNING %s)', 95 | $this->getSQLFunction(), 96 | implode(', ', $jsonStringArguments), 97 | $this->returningType, 98 | ); 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/Query/AST/Functions/Mysql/MysqlAndMariadbJsonFunctionNode.php: -------------------------------------------------------------------------------- 1 | getConnection()->getDatabasePlatform() instanceof (DBALCompatibility::mysqlAndMariaDBSharedPlatform())) { 24 | throw DBALCompatibility::notSupportedPlatformException(static::FUNCTION_NAME); 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Query/AST/Functions/Mysql/MysqlJsonFunctionNode.php: -------------------------------------------------------------------------------- 1 | getConnection()->getDatabasePlatform() instanceof (DBALCompatibility::mysqlDBPlatform())) { 24 | throw DBALCompatibility::notSupportedPlatformException(static::FUNCTION_NAME); 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Query/AST/Functions/Postgresql/JsonExtractPath.php: -------------------------------------------------------------------------------- 1 | '; 14 | 15 | /** @var string[] */ 16 | protected $requiredArgumentTypes = [self::STRING_PRIMARY_ARG, self::VALUE_ARG]; 17 | } 18 | -------------------------------------------------------------------------------- /src/Query/AST/Functions/Postgresql/JsonGetPath.php: -------------------------------------------------------------------------------- 1 | '; 14 | } 15 | -------------------------------------------------------------------------------- /src/Query/AST/Functions/Postgresql/JsonGetPathText.php: -------------------------------------------------------------------------------- 1 | >'; 14 | } 15 | -------------------------------------------------------------------------------- /src/Query/AST/Functions/Postgresql/JsonGetText.php: -------------------------------------------------------------------------------- 1 | >'; 14 | 15 | /** @var string[] */ 16 | protected $requiredArgumentTypes = [self::STRING_PRIMARY_ARG, self::VALUE_ARG]; 17 | } 18 | -------------------------------------------------------------------------------- /src/Query/AST/Functions/Postgresql/JsonbContains.php: -------------------------------------------------------------------------------- 1 | '; 14 | } 15 | -------------------------------------------------------------------------------- /src/Query/AST/Functions/Postgresql/JsonbExists.php: -------------------------------------------------------------------------------- 1 | getConnection()->getDatabasePlatform() instanceof PostgreSQLPlatform) { 25 | throw DBALCompatibility::notSupportedPlatformException(static::FUNCTION_NAME); 26 | } 27 | } 28 | 29 | /** 30 | * @return string 31 | */ 32 | protected function getSQLFunction(): string 33 | { 34 | return strtolower(static::FUNCTION_NAME); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Query/AST/Functions/Postgresql/PostgresqlJsonOperatorFunctionNode.php: -------------------------------------------------------------------------------- 1 | getConnection()->getDatabasePlatform() instanceof PostgreSQLPlatform) { 27 | throw DBALCompatibility::notSupportedPlatformException(static::FUNCTION_NAME); 28 | } 29 | } 30 | 31 | /** 32 | * @return string 33 | */ 34 | public function getOperator(): string 35 | { 36 | return static::OPERATOR; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Query/AST/Functions/Sqlite/Json.php: -------------------------------------------------------------------------------- 1 | getConnection()->getDatabasePlatform() instanceof (DBALCompatibility::sqlLitePlatform())) { 24 | throw DBALCompatibility::notSupportedPlatformException(static::FUNCTION_NAME); 25 | } 26 | } 27 | } 28 | --------------------------------------------------------------------------------