├── .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 | [](https://packagist.org/packages/scienta/doctrine-json-functions)
2 | [](https://packagist.org/packages/scienta/doctrine-json-functions)
3 | [](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 |
--------------------------------------------------------------------------------