├── .editorconfig ├── .github └── workflows │ ├── phpstan.yml │ └── test.yml ├── .gitignore ├── .phpstan-baseline.neon ├── LICENSE ├── README.md ├── composer.json ├── infection.json.dist ├── phpstan-baseline.neon ├── phpstan-use-baseline.neon ├── phpstan.dist.neon ├── phpunit.xml.dist ├── src ├── AbstractStaticQuery.php ├── PDOAggregate.php ├── PDOInterface.php ├── PDOStatementInterface.php ├── Processor │ ├── CallbackProcessor.php │ ├── IfBlock.php │ ├── PregCallbackReplacer.php │ └── SimpleSingleLine.php ├── ProcessorInterface.php ├── Query.php ├── QueryBuilder.php ├── Replacer │ ├── ForBlock.php │ └── Placeholder.php ├── ReplacerInterface.php ├── StaticQueryExecuteTrait.php ├── Type │ └── PgIdentifier.php └── TypeInterface.php ├── tests ├── DummyPDO.php ├── DummyPDOStatement.php ├── Processor │ ├── CallbackProcessorTest.php │ └── IfBlockTest.php ├── QueryTest.php ├── Replacer │ ├── ForBlockTest.php │ ├── PlaceholderTest.php │ └── Sample │ │ └── DummyType.php ├── SQLite │ └── QueryTest.php ├── Type │ └── PgIdentifierTest.php ├── bootstrap.php └── fuji36_01.jpg └── tools ├── .infection ├── .gitignore └── composer.json ├── .phpstan ├── composer.json └── setup ├── infection └── phpstan /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: http://EditorConfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | end_of_line = lf 7 | insert_final_newline = true 8 | 9 | [*.php] 10 | indent_size = 4 11 | indent_style = space 12 | 13 | [*.md] 14 | trim_trailing_whitespace = false 15 | indent_style = space 16 | 17 | [{composer.lock,composer.json}] 18 | indent_size = 4 19 | indent_style = space 20 | 21 | [{*.neon,.*.neon}] 22 | indent_size = 2 23 | indent_style = tab 24 | -------------------------------------------------------------------------------- /.github/workflows/phpstan.yml: -------------------------------------------------------------------------------- 1 | name: PHPStan 2 | on: 3 | workflow_dispatch: 4 | pull_request: 5 | branches: 6 | - master 7 | paths-ignore: 8 | - '**.md' 9 | push: 10 | branches: 11 | - master 12 | paths-ignore: 13 | - '**.md' 14 | jobs: 15 | run: 16 | name: Run 17 | runs-on: ubuntu-20.04 18 | strategy: 19 | fail-fast: false 20 | env: 21 | key: cache-v1 22 | steps: 23 | - name: Checkout 24 | uses: actions/checkout@v3 25 | - name: Setup PHP with tools 26 | uses: shivammathur/setup-php@v2 27 | with: 28 | php-version: '8.1' 29 | extensions: mbstring, intl, opcache, xdebug, xml 30 | tools: composer, cs2pr 31 | - name: Get Composer cache directory 32 | id: composer-cache-dir 33 | run: | 34 | echo "::set-output name=dir::$(composer config cache-files-dir)" 35 | - name: Restore composer cache 36 | id: composer-cache 37 | uses: actions/cache@v3 38 | with: 39 | path: ${{ steps.composer-cache-dir.outputs.dir }} 40 | key: ${{ runner.os }}-composer-${{ hashFiles('composer.lock') }} 41 | restore-keys: | 42 | ${{ runner.os }}-composer- 43 | - name: Remove composer.lock 44 | run: rm -f composer.lock 45 | - name: Setup Composer 46 | run: composer install 47 | - name: Setup PHPStan 48 | run: tools/.phpstan/setup 49 | - name: Run PHPStan analysis 50 | run: tools/phpstan analyse -c phpstan-use-baseline.neon 51 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | on: 3 | workflow_dispatch: 4 | pull_request: 5 | branches: 6 | - master 7 | paths-ignore: 8 | - '**.md' 9 | push: 10 | branches: 11 | - master 12 | paths-ignore: 13 | - '**.md' 14 | jobs: 15 | run: 16 | name: Run 17 | runs-on: ${{ matrix.operating-system }} 18 | strategy: 19 | fail-fast: false 20 | matrix: 21 | operating-system: [ubuntu-20.04] 22 | php-versions: ['5.4', '5.5', '5.6', '7.0', '7.1', '7.2', '7.3', '7.4', '8.0', '8.1'] 23 | env: 24 | key: cache-v1 25 | steps: 26 | - name: Checkout 27 | uses: actions/checkout@v3 28 | - name: Setup PHP with tools 29 | uses: shivammathur/setup-php@v2 30 | with: 31 | php-version: ${{ matrix.php-versions }} 32 | extensions: mbstring, intl, opcache, xdebug, xml 33 | tools: composer, cs2pr 34 | - name: Get Composer cache directory 35 | id: composer-cache-dir 36 | run: | 37 | echo "::set-output name=dir::$(composer config cache-files-dir)" 38 | - name: Restore composer cache 39 | id: composer-cache 40 | uses: actions/cache@v3 41 | with: 42 | path: ${{ steps.composer-cache-dir.outputs.dir }} 43 | key: ${{ runner.os }}-composer-${{ hashFiles('composer.lock') }} 44 | restore-keys: | 45 | ${{ runner.os }}-composer- 46 | - name: Remove composer.lock 47 | run: rm -f composer.lock 48 | - name: Setup Composer 49 | run: composer install 50 | - name: Run PHPUnit tests 51 | run: vendor/bin/phpunit 52 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /build/logs 2 | /build/phpdox 3 | /build/phploc 4 | /build/report 5 | /composer.lock 6 | /phpunit.xml 7 | /vendor/* 8 | /.phpunit.result.cache 9 | /.idea 10 | /*.iml 11 | /tests/db.sq3 12 | /tests/SQLite/db.sq3 13 | /_infection/* 14 | /tools/*/composer.lock 15 | /tools/*/vendor 16 | -------------------------------------------------------------------------------- /.phpstan-baseline.neon: -------------------------------------------------------------------------------- 1 | parameters: 2 | ignoreErrors: 3 | - 4 | message: "#^Generic type Teto\\\\SQL\\\\PDOAggregate\\\\> in PHPDoc tag @param for parameter \\$pdo does not specify all template types of interface Teto\\\\SQL\\\\PDOAggregate\\: S, T$#" 5 | count: 2 6 | path: src/AbstractStaticQuery.php 7 | 8 | - 9 | message: "#^Type PDO\\|Teto\\\\SQL\\\\PDOInterface\\ in generic type Teto\\\\SQL\\\\PDOAggregate\\\\> in PHPDoc tag @param for parameter \\$pdo is not subtype of template type S of PDOStatement\\|Teto\\\\SQL\\\\PDOStatementInterface of interface Teto\\\\SQL\\\\PDOAggregate\\.$#" 10 | count: 2 11 | path: src/AbstractStaticQuery.php 12 | 13 | - 14 | message: "#^Method Teto\\\\SQL\\\\Processor\\\\CallbackProcessor\\:\\:processQuery\\(\\) should return string but returns mixed\\.$#" 15 | count: 1 16 | path: src/Processor/CallbackProcessor.php 17 | 18 | - 19 | message: "#^Property Teto\\\\SQL\\\\Processor\\\\CallbackProcessor\\:\\:\\$callback with generic interface Teto\\\\SQL\\\\PDOInterface does not specify its types\\: T$#" 20 | count: 1 21 | path: src/Processor/CallbackProcessor.php 22 | 23 | - 24 | message: "#^Parameter \\#2 \\$callback of function preg_replace_callback expects callable\\(array\\\\)\\: string, Closure\\(array\\)\\: int\\|string given\\.$#" 25 | count: 1 26 | path: src/Processor/PregCallbackReplacer.php 27 | 28 | - 29 | message: "#^Parameter \\#2 \\$matches of method Teto\\\\SQL\\\\ReplacerInterface\\:\\:replaceQuery\\(\\) expects array\\, array\\ given\\.$#" 30 | count: 1 31 | path: src/Processor/PregCallbackReplacer.php 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Mozilla Public License Version 2.0 2 | ================================== 3 | 4 | 1. Definitions 5 | -------------- 6 | 7 | 1.1. "Contributor" 8 | means each individual or legal entity that creates, contributes to 9 | the creation of, or owns Covered Software. 10 | 11 | 1.2. "Contributor Version" 12 | means the combination of the Contributions of others (if any) used 13 | by a Contributor and that particular Contributor's Contribution. 14 | 15 | 1.3. "Contribution" 16 | means Covered Software of a particular Contributor. 17 | 18 | 1.4. "Covered Software" 19 | means Source Code Form to which the initial Contributor has attached 20 | the notice in Exhibit A, the Executable Form of such Source Code 21 | Form, and Modifications of such Source Code Form, in each case 22 | including portions thereof. 23 | 24 | 1.5. "Incompatible With Secondary Licenses" 25 | means 26 | 27 | (a) that the initial Contributor has attached the notice described 28 | in Exhibit B to the Covered Software; or 29 | 30 | (b) that the Covered Software was made available under the terms of 31 | version 1.1 or earlier of the License, but not also under the 32 | terms of a Secondary License. 33 | 34 | 1.6. "Executable Form" 35 | means any form of the work other than Source Code Form. 36 | 37 | 1.7. "Larger Work" 38 | means a work that combines Covered Software with other material, in 39 | a separate file or files, that is not Covered Software. 40 | 41 | 1.8. "License" 42 | means this document. 43 | 44 | 1.9. "Licensable" 45 | means having the right to grant, to the maximum extent possible, 46 | whether at the time of the initial grant or subsequently, any and 47 | all of the rights conveyed by this License. 48 | 49 | 1.10. "Modifications" 50 | means any of the following: 51 | 52 | (a) any file in Source Code Form that results from an addition to, 53 | deletion from, or modification of the contents of Covered 54 | Software; or 55 | 56 | (b) any new file in Source Code Form that contains any Covered 57 | Software. 58 | 59 | 1.11. "Patent Claims" of a Contributor 60 | means any patent claim(s), including without limitation, method, 61 | process, and apparatus claims, in any patent Licensable by such 62 | Contributor that would be infringed, but for the grant of the 63 | License, by the making, using, selling, offering for sale, having 64 | made, import, or transfer of either its Contributions or its 65 | Contributor Version. 66 | 67 | 1.12. "Secondary License" 68 | means either the GNU General Public License, Version 2.0, the GNU 69 | Lesser General Public License, Version 2.1, the GNU Affero General 70 | Public License, Version 3.0, or any later versions of those 71 | licenses. 72 | 73 | 1.13. "Source Code Form" 74 | means the form of the work preferred for making modifications. 75 | 76 | 1.14. "You" (or "Your") 77 | means an individual or a legal entity exercising rights under this 78 | License. For legal entities, "You" includes any entity that 79 | controls, is controlled by, or is under common control with You. For 80 | purposes of this definition, "control" means (a) the power, direct 81 | or indirect, to cause the direction or management of such entity, 82 | whether by contract or otherwise, or (b) ownership of more than 83 | fifty percent (50%) of the outstanding shares or beneficial 84 | ownership of such entity. 85 | 86 | 2. License Grants and Conditions 87 | -------------------------------- 88 | 89 | 2.1. Grants 90 | 91 | Each Contributor hereby grants You a world-wide, royalty-free, 92 | non-exclusive license: 93 | 94 | (a) under intellectual property rights (other than patent or trademark) 95 | Licensable by such Contributor to use, reproduce, make available, 96 | modify, display, perform, distribute, and otherwise exploit its 97 | Contributions, either on an unmodified basis, with Modifications, or 98 | as part of a Larger Work; and 99 | 100 | (b) under Patent Claims of such Contributor to make, use, sell, offer 101 | for sale, have made, import, and otherwise transfer either its 102 | Contributions or its Contributor Version. 103 | 104 | 2.2. Effective Date 105 | 106 | The licenses granted in Section 2.1 with respect to any Contribution 107 | become effective for each Contribution on the date the Contributor first 108 | distributes such Contribution. 109 | 110 | 2.3. Limitations on Grant Scope 111 | 112 | The licenses granted in this Section 2 are the only rights granted under 113 | this License. No additional rights or licenses will be implied from the 114 | distribution or licensing of Covered Software under this License. 115 | Notwithstanding Section 2.1(b) above, no patent license is granted by a 116 | Contributor: 117 | 118 | (a) for any code that a Contributor has removed from Covered Software; 119 | or 120 | 121 | (b) for infringements caused by: (i) Your and any other third party's 122 | modifications of Covered Software, or (ii) the combination of its 123 | Contributions with other software (except as part of its Contributor 124 | Version); or 125 | 126 | (c) under Patent Claims infringed by Covered Software in the absence of 127 | its Contributions. 128 | 129 | This License does not grant any rights in the trademarks, service marks, 130 | or logos of any Contributor (except as may be necessary to comply with 131 | the notice requirements in Section 3.4). 132 | 133 | 2.4. Subsequent Licenses 134 | 135 | No Contributor makes additional grants as a result of Your choice to 136 | distribute the Covered Software under a subsequent version of this 137 | License (see Section 10.2) or under the terms of a Secondary License (if 138 | permitted under the terms of Section 3.3). 139 | 140 | 2.5. Representation 141 | 142 | Each Contributor represents that the Contributor believes its 143 | Contributions are its original creation(s) or it has sufficient rights 144 | to grant the rights to its Contributions conveyed by this License. 145 | 146 | 2.6. Fair Use 147 | 148 | This License is not intended to limit any rights You have under 149 | applicable copyright doctrines of fair use, fair dealing, or other 150 | equivalents. 151 | 152 | 2.7. Conditions 153 | 154 | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted 155 | in Section 2.1. 156 | 157 | 3. Responsibilities 158 | ------------------- 159 | 160 | 3.1. Distribution of Source Form 161 | 162 | All distribution of Covered Software in Source Code Form, including any 163 | Modifications that You create or to which You contribute, must be under 164 | the terms of this License. You must inform recipients that the Source 165 | Code Form of the Covered Software is governed by the terms of this 166 | License, and how they can obtain a copy of this License. You may not 167 | attempt to alter or restrict the recipients' rights in the Source Code 168 | Form. 169 | 170 | 3.2. Distribution of Executable Form 171 | 172 | If You distribute Covered Software in Executable Form then: 173 | 174 | (a) such Covered Software must also be made available in Source Code 175 | Form, as described in Section 3.1, and You must inform recipients of 176 | the Executable Form how they can obtain a copy of such Source Code 177 | Form by reasonable means in a timely manner, at a charge no more 178 | than the cost of distribution to the recipient; and 179 | 180 | (b) You may distribute such Executable Form under the terms of this 181 | License, or sublicense it under different terms, provided that the 182 | license for the Executable Form does not attempt to limit or alter 183 | the recipients' rights in the Source Code Form under this License. 184 | 185 | 3.3. Distribution of a Larger Work 186 | 187 | You may create and distribute a Larger Work under terms of Your choice, 188 | provided that You also comply with the requirements of this License for 189 | the Covered Software. If the Larger Work is a combination of Covered 190 | Software with a work governed by one or more Secondary Licenses, and the 191 | Covered Software is not Incompatible With Secondary Licenses, this 192 | License permits You to additionally distribute such Covered Software 193 | under the terms of such Secondary License(s), so that the recipient of 194 | the Larger Work may, at their option, further distribute the Covered 195 | Software under the terms of either this License or such Secondary 196 | License(s). 197 | 198 | 3.4. Notices 199 | 200 | You may not remove or alter the substance of any license notices 201 | (including copyright notices, patent notices, disclaimers of warranty, 202 | or limitations of liability) contained within the Source Code Form of 203 | the Covered Software, except that You may alter any license notices to 204 | the extent required to remedy known factual inaccuracies. 205 | 206 | 3.5. Application of Additional Terms 207 | 208 | You may choose to offer, and to charge a fee for, warranty, support, 209 | indemnity or liability obligations to one or more recipients of Covered 210 | Software. However, You may do so only on Your own behalf, and not on 211 | behalf of any Contributor. You must make it absolutely clear that any 212 | such warranty, support, indemnity, or liability obligation is offered by 213 | You alone, and You hereby agree to indemnify every Contributor for any 214 | liability incurred by such Contributor as a result of warranty, support, 215 | indemnity or liability terms You offer. You may include additional 216 | disclaimers of warranty and limitations of liability specific to any 217 | jurisdiction. 218 | 219 | 4. Inability to Comply Due to Statute or Regulation 220 | --------------------------------------------------- 221 | 222 | If it is impossible for You to comply with any of the terms of this 223 | License with respect to some or all of the Covered Software due to 224 | statute, judicial order, or regulation then You must: (a) comply with 225 | the terms of this License to the maximum extent possible; and (b) 226 | describe the limitations and the code they affect. Such description must 227 | be placed in a text file included with all distributions of the Covered 228 | Software under this License. Except to the extent prohibited by statute 229 | or regulation, such description must be sufficiently detailed for a 230 | recipient of ordinary skill to be able to understand it. 231 | 232 | 5. Termination 233 | -------------- 234 | 235 | 5.1. The rights granted under this License will terminate automatically 236 | if You fail to comply with any of its terms. However, if You become 237 | compliant, then the rights granted under this License from a particular 238 | Contributor are reinstated (a) provisionally, unless and until such 239 | Contributor explicitly and finally terminates Your grants, and (b) on an 240 | ongoing basis, if such Contributor fails to notify You of the 241 | non-compliance by some reasonable means prior to 60 days after You have 242 | come back into compliance. Moreover, Your grants from a particular 243 | Contributor are reinstated on an ongoing basis if such Contributor 244 | notifies You of the non-compliance by some reasonable means, this is the 245 | first time You have received notice of non-compliance with this License 246 | from such Contributor, and You become compliant prior to 30 days after 247 | Your receipt of the notice. 248 | 249 | 5.2. If You initiate litigation against any entity by asserting a patent 250 | infringement claim (excluding declaratory judgment actions, 251 | counter-claims, and cross-claims) alleging that a Contributor Version 252 | directly or indirectly infringes any patent, then the rights granted to 253 | You by any and all Contributors for the Covered Software under Section 254 | 2.1 of this License shall terminate. 255 | 256 | 5.3. In the event of termination under Sections 5.1 or 5.2 above, all 257 | end user license agreements (excluding distributors and resellers) which 258 | have been validly granted by You or Your distributors under this License 259 | prior to termination shall survive termination. 260 | 261 | ************************************************************************ 262 | * * 263 | * 6. Disclaimer of Warranty * 264 | * ------------------------- * 265 | * * 266 | * Covered Software is provided under this License on an "as is" * 267 | * basis, without warranty of any kind, either expressed, implied, or * 268 | * statutory, including, without limitation, warranties that the * 269 | * Covered Software is free of defects, merchantable, fit for a * 270 | * particular purpose or non-infringing. The entire risk as to the * 271 | * quality and performance of the Covered Software is with You. * 272 | * Should any Covered Software prove defective in any respect, You * 273 | * (not any Contributor) assume the cost of any necessary servicing, * 274 | * repair, or correction. This disclaimer of warranty constitutes an * 275 | * essential part of this License. No use of any Covered Software is * 276 | * authorized under this License except under this disclaimer. * 277 | * * 278 | ************************************************************************ 279 | 280 | ************************************************************************ 281 | * * 282 | * 7. Limitation of Liability * 283 | * -------------------------- * 284 | * * 285 | * Under no circumstances and under no legal theory, whether tort * 286 | * (including negligence), contract, or otherwise, shall any * 287 | * Contributor, or anyone who distributes Covered Software as * 288 | * permitted above, be liable to You for any direct, indirect, * 289 | * special, incidental, or consequential damages of any character * 290 | * including, without limitation, damages for lost profits, loss of * 291 | * goodwill, work stoppage, computer failure or malfunction, or any * 292 | * and all other commercial damages or losses, even if such party * 293 | * shall have been informed of the possibility of such damages. This * 294 | * limitation of liability shall not apply to liability for death or * 295 | * personal injury resulting from such party's negligence to the * 296 | * extent applicable law prohibits such limitation. Some * 297 | * jurisdictions do not allow the exclusion or limitation of * 298 | * incidental or consequential damages, so this exclusion and * 299 | * limitation may not apply to You. * 300 | * * 301 | ************************************************************************ 302 | 303 | 8. Litigation 304 | ------------- 305 | 306 | Any litigation relating to this License may be brought only in the 307 | courts of a jurisdiction where the defendant maintains its principal 308 | place of business and such litigation shall be governed by laws of that 309 | jurisdiction, without reference to its conflict-of-law provisions. 310 | Nothing in this Section shall prevent a party's ability to bring 311 | cross-claims or counter-claims. 312 | 313 | 9. Miscellaneous 314 | ---------------- 315 | 316 | This License represents the complete agreement concerning the subject 317 | matter hereof. If any provision of this License is held to be 318 | unenforceable, such provision shall be reformed only to the extent 319 | necessary to make it enforceable. Any law or regulation which provides 320 | that the language of a contract shall be construed against the drafter 321 | shall not be used to construe this License against a Contributor. 322 | 323 | 10. Versions of the License 324 | --------------------------- 325 | 326 | 10.1. New Versions 327 | 328 | Mozilla Foundation is the license steward. Except as provided in Section 329 | 10.3, no one other than the license steward has the right to modify or 330 | publish new versions of this License. Each version will be given a 331 | distinguishing version number. 332 | 333 | 10.2. Effect of New Versions 334 | 335 | You may distribute the Covered Software under the terms of the version 336 | of the License under which You originally received the Covered Software, 337 | or under the terms of any subsequent version published by the license 338 | steward. 339 | 340 | 10.3. Modified Versions 341 | 342 | If you create software not governed by this License, and you want to 343 | create a new license for such software, you may create and use a 344 | modified version of this License if you rename the license and remove 345 | any references to the name of the license steward (except to note that 346 | such modified license differs from this License). 347 | 348 | 10.4. Distributing Source Code Form that is Incompatible With Secondary 349 | Licenses 350 | 351 | If You choose to distribute Source Code Form that is Incompatible With 352 | Secondary Licenses under the terms of this version of the License, the 353 | notice described in Exhibit B of this License must be attached. 354 | 355 | Exhibit A - Source Code Form License Notice 356 | ------------------------------------------- 357 | 358 | This Source Code Form is subject to the terms of the Mozilla Public 359 | License, v. 2.0. If a copy of the MPL was not distributed with this 360 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 361 | 362 | If it is not possible or desirable to put the notice in a particular 363 | file, then You may include the notice in a location (such as a LICENSE 364 | file in a relevant directory) where a recipient would be likely to look 365 | for such a notice. 366 | 367 | You may add additional accurate notices of copyright ownership. 368 | 369 | Exhibit B - "Incompatible With Secondary Licenses" Notice 370 | --------------------------------------------------------- 371 | 372 | This Source Code Form is "Incompatible With Secondary Licenses", as 373 | defined by the Mozilla Public License, v. 2.0. 374 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | TetoSQL 2 | ======= 3 | 4 | [![Test](https://github.com/BaguettePHP/TetoSQL/actions/workflows/test.yml/badge.svg)](https://github.com/BaguettePHP/TetoSQL/actions/workflows/test.yml) 5 | [![lang:PHP 8.1](https://img.shields.io/badge/lang-PHP%208.1-brightgreen.svg)](https://php.net/manual/migration82.php) 6 | [![lang:PHP 5.4](https://img.shields.io/badge/lang-PHP%205.4-green.svg)](https://php.net/downloads.php) 7 | 8 | [PHP Data Objects](http://php.net/manual/book.pdo.php)(PDO) wrapper and SQL Template for PHP 9 | 10 | Features 11 | -------- 12 | 13 | - PDO Wrapper 14 | - [BLOB support](http://php.net/manual/pdo.lobs.php) 15 | - Query Template 16 | - Type safe 17 | - Sequence of values 18 | 19 | Manual 20 | ------ 21 | 22 | Japanese: [憂鬱なSQLのためのアレ、またはPDOと仲良くして枕を高くしてねむる](http://qiita.com/tadsan/items/e615a779baa6eabdab47) 23 | 24 | Syntax 25 | ------ 26 | 27 | ### type 28 | 29 | * `@int` - Integer value (`-9223372036854775808 <= n <=9223372036854775807`) 30 | * `@int[]` - Sequence of integers 31 | * `@string` - String 32 | * `@string[]` - Sequence of strings 33 | * `@lob` - [Large OBject](http://php.net/manual/pdo.lobs.php) 34 | * `@ascdesc` - `"ASC"` or `"DESC"` or `"asc"` or `"desc"` 35 | 36 | ### Example 37 | 38 | ``` php 39 | true]); 46 | $data = Query::execute( 47 | $conn, 48 | <<<'SQL' 49 | SELECT * FROM `users` 50 | WHERE `status` IN (:statuses@int[]) 51 | LIMIT :offset@int, :limit@int 52 | SQL, 53 | [ 54 | ':statuses' => [1, 3], 55 | ':offset' => 60, 56 | ':limit' => 30, 57 | ] 58 | )->fetch(\PDO::FETCH_ASSOC); 59 | ``` 60 | 61 | Copyright 62 | --------- 63 | 64 | **TetoSQL** is licensed under [Mozilla Public License Version 2.0](https://www.mozilla.org/en-US/MPL/2.0/). 65 | 66 | Simple and secure SQL templating 67 | Copyright (c) 2019 USAMI Kenta 68 | 69 | ### PxvSql 70 | 71 | **TetoSQL** is forked (*and detuned*) from private library of [pixiv Inc.](http://www.pixiv.co.jp/) that is called `PxvSql`. 72 | 73 | ### PHP Manual 74 | 75 | [`PDOInterface.php`](http://php.net/manual/en/class.pdo.php) and [`PDOStatementInterface.php`](http://php.net/manual/en/class.pdostatement.php) is based on [PHP Manual (en)](http://php.net/manual/en/index.php). 76 | 77 | > Copyright © 1997 - 2016 by the PHP Documentation Group. This material may be distributed only subject to the terms and conditions set forth in the Creative Commons Attribution 3.0 License or later. A copy of the [Creative Commons Attribution 3.0 license](http://php.net/manual/en/copyright.php) is distributed with this manual. The latest version is presently available at [» http://creativecommons.org/licenses/by/3.0/](http://creativecommons.org/licenses/by/3.0/). 78 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "zonuexe/tetosql", 3 | "description": "Simple and secure SQL templating", 4 | "license": "MPL-2.0", 5 | "authors": [ 6 | { 7 | "name": "pixiv Inc." 8 | }, 9 | { 10 | "name": "USAMI Kenta", 11 | "email": "tadsan@zonu.me" 12 | } 13 | ], 14 | "keywords": [ 15 | "sql" 16 | ], 17 | "require": { 18 | "php": ">=5.4" 19 | }, 20 | "require-dev": { 21 | "php-coveralls/php-coveralls": "^2.1 || ^1.1", 22 | "phpunit/phpunit": "^8.5 || ^7.5 || ^4.8", 23 | "yoast/phpunit-polyfills": "^1.0" 24 | }, 25 | "autoload": { 26 | "psr-4": { 27 | "Teto\\SQL\\": "src/" 28 | } 29 | }, 30 | "autoload-dev": { 31 | "psr-4": { 32 | "Teto\\SQL\\": "tests/" 33 | } 34 | }, 35 | "suggest": { 36 | "ext-mysql": "MySQL support" 37 | }, 38 | "scripts": { 39 | "test": "phpunit" 40 | }, 41 | "config": { 42 | "sort-packages": true 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /infection.json.dist: -------------------------------------------------------------------------------- 1 | { 2 | "timeout": 10, 3 | "source": { 4 | "directories": [ 5 | "src" 6 | ] 7 | }, 8 | "logs": { 9 | "text": "_infection/infection.log", 10 | "summary": "_infection/summary.log", 11 | "perMutator": "_infection/per-mutator.md", 12 | "badge": { 13 | "branch": "master" 14 | } 15 | }, 16 | "mutators": { 17 | "@default": true 18 | }, 19 | "testFramework":"phpunit", 20 | "bootstrap":"./tests/bootstrap.php" 21 | } 22 | -------------------------------------------------------------------------------- /phpstan-baseline.neon: -------------------------------------------------------------------------------- 1 | parameters: 2 | ignoreErrors: 3 | - 4 | message: "#^Generic type Teto\\\\SQL\\\\PDOAggregate\\\\> in PHPDoc tag @param for parameter \\$pdo does not specify all template types of interface Teto\\\\SQL\\\\PDOAggregate\\: S, T$#" 5 | count: 2 6 | path: src/AbstractStaticQuery.php 7 | 8 | - 9 | message: "#^Type PDO\\|Teto\\\\SQL\\\\PDOInterface\\ in generic type Teto\\\\SQL\\\\PDOAggregate\\\\> in PHPDoc tag @param for parameter \\$pdo is not subtype of template type S of PDOStatement\\|Teto\\\\SQL\\\\PDOStatementInterface of interface Teto\\\\SQL\\\\PDOAggregate\\.$#" 10 | count: 2 11 | path: src/AbstractStaticQuery.php 12 | 13 | - 14 | message: "#^Method Teto\\\\SQL\\\\Processor\\\\CallbackProcessor\\:\\:processQuery\\(\\) should return string but returns mixed\\.$#" 15 | count: 1 16 | path: src/Processor/CallbackProcessor.php 17 | 18 | - 19 | message: "#^Property Teto\\\\SQL\\\\Processor\\\\CallbackProcessor\\:\\:\\$callback with generic interface Teto\\\\SQL\\\\PDOInterface does not specify its types\\: T$#" 20 | count: 1 21 | path: src/Processor/CallbackProcessor.php 22 | 23 | - 24 | message: "#^Parameter \\#2 \\$callback of function preg_replace_callback expects callable\\(array\\\\)\\: string, Closure\\(array\\)\\: int\\|string given\\.$#" 25 | count: 1 26 | path: src/Processor/PregCallbackReplacer.php 27 | 28 | - 29 | message: "#^Parameter \\#2 \\$matches of method Teto\\\\SQL\\\\ReplacerInterface\\:\\:replaceQuery\\(\\) expects array\\, array\\ given\\.$#" 30 | count: 1 31 | path: src/Processor/PregCallbackReplacer.php 32 | -------------------------------------------------------------------------------- /phpstan-use-baseline.neon: -------------------------------------------------------------------------------- 1 | includes: 2 | - phpstan.dist.neon 3 | - .phpstan-baseline.neon 4 | -------------------------------------------------------------------------------- /phpstan.dist.neon: -------------------------------------------------------------------------------- 1 | parameters: 2 | level: max 3 | paths: 4 | - src/ 5 | - tests/ 6 | bootstrapFiles: 7 | - vendor/autoload.php 8 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 13 | 14 | 15 | ./tests/ 16 | 17 | 18 | 19 | 20 | 21 | src/ 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /src/AbstractStaticQuery.php: -------------------------------------------------------------------------------- 1 | 22 | * @param \PDO|PDOInterface $pdo 23 | * @phpstan-param T $pdo 24 | * @phpstan-param non-empty-string $sql 25 | * @phpstan-param array $params 26 | * @return \PDOStatement 27 | * @phpstan-return ($pdo is \PDO ? \PDOStatement : S) 28 | */ 29 | public static function build($pdo, $sql, array $params) 30 | { 31 | return static::getQueryBuilder()->build($pdo, $sql, $params); 32 | } 33 | 34 | /** 35 | * @return QueryBuilder 36 | */ 37 | protected static function getQueryBuilder() 38 | { 39 | throw new LogicException('Must be implemented ' . __FUNCTION__ . ' method in ' . __CLASS__ . '.'); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/PDOAggregate.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | interface PDOAggregate 14 | { 15 | /** 16 | * @return \PDO|PDOInterface 17 | * @phpstan-return T 18 | */ 19 | public function getPDO(); 20 | } 21 | -------------------------------------------------------------------------------- /src/PDOInterface.php: -------------------------------------------------------------------------------- 1 | Returns an array of error information about the last operation performed by this database handle. 53 | */ 54 | public function errorInfo(); 55 | 56 | /** 57 | * Execute an SQL statement and return the number of affected rows 58 | * 59 | * @link https://www.php.net/manual/pdo.exec.php 60 | * @param string $statement The SQL statement to prepare and execute. 61 | * @return int Returns the number of rows that were modified or deleted by the SQL statement you issued. If no rows were affected, returns 0. 62 | */ 63 | public function exec($statement); 64 | 65 | /** 66 | * Retrieve a database connection attribute 67 | * 68 | * @link https://www.php.net/manual/pdo.getattribute.php 69 | * @param int $attribute One of the PDO::ATTR_* constants. 70 | * @return mixed 71 | */ 72 | public function getAttribute($attribute); 73 | 74 | /** 75 | * Return an array of available PDO drivers 76 | * 77 | * @link https://www.php.net/manual/pdo.getavailabledrivers.php 78 | * @return string[] 79 | */ 80 | public static function getAvailableDrivers(); 81 | 82 | /** 83 | * Checks if inside a transaction 84 | * 85 | * @link https://www.php.net/manual/pdo.intransaction.php 86 | * @return bool Returns TRUE if a transaction is currently active, and FALSE if not. 87 | */ 88 | public function inTransaction(); 89 | 90 | /** 91 | * Returns the ID of the last inserted row or sequence value 92 | * 93 | * @link https://www.php.net/manual/pdo.lastinsertid.php 94 | * @param string $name 95 | * @return string 96 | */ 97 | public function lastInsertId($name = null); 98 | 99 | /** 100 | * Prepares a statement for execution and returns a statement object 101 | * 102 | * @link https://www.php.net/manual/pdo.prepare.php 103 | * @param string $statement This must be a valid SQL statement template for the target database server. 104 | * @param array $driver_options This array holds one or more key=>value pairs to set attribute values for the PDOStatement object that this method returns. 105 | * @return \PDOStatement|PDOStatementInterface|false 106 | * @phpstan-return T|false 107 | */ 108 | public function prepare($statement, $driver_options = []); 109 | 110 | /** 111 | * Executes an SQL statement, returning a result set as a PDOStatement object 112 | * 113 | * @link http://php.net/manual/pdo.query.php 114 | * @param string $statement The SQL statement to prepare and execute. 115 | * @return \PDOStatement|PDOStatementInterface|false Returns a PDOStatement object, or FALSE on failure. 116 | * @phpstan-return T|false 117 | */ 118 | public function query($statement); 119 | 120 | /** 121 | * Quotes a string for use in a query 122 | * 123 | * @link https://www.php.net/manual/pdo.quote.php 124 | * @param string $string The string to be quoted. 125 | * @param int $parameter_type Provides a data type hint for drivers that have alternate quoting styles. 126 | * @return string Returns a quoted string that is theoretically safe to pass into an SQL statement. Returns FALSE if the driver does not support quoting in this way. 127 | */ 128 | public function quote($string, $parameter_type = \PDO::PARAM_STR); 129 | 130 | /** 131 | * Rolls back a transaction 132 | * 133 | * @link https://www.php.net/manual/pdo.rollback.php 134 | * @return bool Returns TRUE on success or FALSE on failure. 135 | */ 136 | public function rollBack(); 137 | 138 | /** 139 | * Set an attribute 140 | * 141 | * @link https://www.php.net/manual/pdo.setattribute.php 142 | * @param int $attribute One of the PDO::ATTR_* constants. 143 | * @param mixed $value 144 | * @return bool Returns TRUE on success or FALSE on failure. 145 | */ 146 | public function setAttribute($attribute, $value); 147 | } 148 | -------------------------------------------------------------------------------- /src/PDOStatementInterface.php: -------------------------------------------------------------------------------- 1 | $input_parameters 103 | * @return bool TRUE on success or FALSE on failure. 104 | */ 105 | public function execute($input_parameters); 106 | 107 | /** 108 | * Fetches the next row from a result set 109 | * 110 | * @link https://www.php.net/manual/en/pdostatement.fetch.php 111 | * @param int $fetch_style Controls how the next row will be returned to the caller. This value must be one of the PDO::FETCH_* constants, defaulting to value of PDO::ATTR_DEFAULT_FETCH_MODE (which defaults to PDO::FETCH_BOTH). 112 | * @param int $cursor_orientation For a PDOStatement object representing a scrollable cursor, this value determines which row will be returned to the caller. This value must be one of the PDO::FETCH_ORI_* constants, defaulting to PDO::FETCH_ORI_NEXT. 113 | * @param int $cursor_offset For a PDOStatement object representing a scrollable cursor for which the cursor_orientation parameter is set to PDO::FETCH_ORI_ABS, this value specifies the absolute number of the row in the result set that shall be fetched. 114 | * @return mixed|false The return value of this function on success depends on the fetch type. In all cases, FALSE is returned on failure. 115 | */ 116 | public function fetch($fetch_style = \PDO::ATTR_DEFAULT_FETCH_MODE, $cursor_orientation = \PDO::FETCH_ORI_NEXT, $cursor_offset = 0); 117 | 118 | /** 119 | * Returns an array containing all of the result set rows 120 | * 121 | * @link https://www.php.net/manual/en/pdostatement.fetchall.php 122 | * @param int $fetch_style Controls the contents of the returned array as documented in PDOStatement::fetch(). Defaults to value of PDO::ATTR_DEFAULT_FETCH_MODE (which defaults to PDO::FETCH_BOTH) 123 | * @param mixed $fetch_argument This argument has a different meaning depending on the value of the fetch_style parameter: 124 | * @param array $ctor_args 125 | * @phpstan-param list $ctor_args 126 | * @return array An array containing all of the remaining rows in the result set. The array represents each row as either an array of column values or an object with properties corresponding to each column name. An empty array is returned if there are zero results to fetch, or FALSE on failure. 127 | * @phpstan-return array 128 | */ 129 | public function fetchAll($fetch_style, $fetch_argument = null, $ctor_args = array()); 130 | 131 | /** 132 | * Returns a single column from the next row of a result set 133 | * 134 | * @link https://www.php.net/manual/pdostatement.fetchcolumn.php 135 | * @param int $column_number 0-indexed number of the column you wish to retrieve from the row. If no value is supplied, PDOStatement::fetchColumn() fetches the first column. 136 | * @return mixed 137 | */ 138 | public function fetchColumn($column_number = 0); 139 | 140 | /** 141 | * Fetches the next row and returns it as an object 142 | * 143 | * @link https://www.php.net/manual/pdostatement.fetchobject.php 144 | * @param string $class_name Name of the created class. 145 | * @param array $ctor_args Elements of this array are passed to the constructor. 146 | * @param list $ctor_args 147 | * @return object|false An instance of the required class with property names that correspond to the column names or FALSE on failure. 148 | */ 149 | public function fetchObject($class_name = 'stdClass', $ctor_args = array()); 150 | 151 | /** 152 | * Retrieve a statement attribute 153 | * 154 | * @link https://www.php.net/manual/pdostatement.getattribute.php 155 | * @param int $attribute Gets an attribute of the statement. 156 | * @return mixed Returns the attribute value. 157 | */ 158 | public function getAttribute($attribute); 159 | 160 | /** 161 | * Returns metadata for a column in a result set 162 | * 163 | * @link https://www.php.net/manual/pdostatement.getcolumnmeta.php 164 | * @param int $column The 0-indexed column in the result set. 165 | * @return array|false An associative array containing the following values representing the metadata for a single column: 166 | * @phpstan-return array{native_type:non-empty-string, flags:list, name:string, table:string, len:int, precision:int, pdo_type:int}|false 167 | */ 168 | public function getColumnMeta($column); 169 | 170 | /** 171 | * Advances to the next rowset in a multi-rowset statement handle 172 | * 173 | * @link https://www.php.net/manual/pdostatement.nextrowset.php 174 | * @return bool TRUE on success or FALSE on failure. 175 | */ 176 | public function nextRowset(); 177 | 178 | /** 179 | * Returns the number of rows affected by the last SQL statement 180 | * 181 | * @link https://www.php.net/manual/pdostatement.rowcount.php 182 | * @return int The number of rows. 183 | */ 184 | public function rowCount(); 185 | 186 | /** 187 | * Set a statement attribute 188 | * 189 | * @link https://www.php.net/manual/en/pdostatement.setattribute.php 190 | * @param int $attribute 191 | * @param mixed $value 192 | * @return bool TRUE on success or FALSE on failure. 193 | */ 194 | public function setAttribute($attribute, $value); 195 | 196 | /** 197 | * Set the default fetch mode for this statement 198 | * 199 | * public bool PDOStatement::setFetchMode ( int $mode ) 200 | * public bool PDOStatement::setFetchMode ( int $PDO::FETCH_COLUMN , int $colno ) 201 | * public bool PDOStatement::setFetchMode ( int $PDO::FETCH_CLASS , string $classname , array $ctorargs ) 202 | * public bool PDOStatement::setFetchMode ( int $PDO::FETCH_INTO , object $object ) 203 | * 204 | * @link https://www.php.net/manual/pdostatement.setfetchmode.php 205 | * @param int $mode PDO::FETCH_COLUMN|PDO::FETCH_CLASS|PDO::FETCH_INTO 206 | * @param int|string|object $colno_or_classname_or_object 207 | * @param ?array $ctorargs 208 | * @return bool TRUE on success or FALSE on failure. 209 | */ 210 | public function setFetchMode($mode, $colno_or_classname_or_object, array $ctorargs = null); 211 | } 212 | -------------------------------------------------------------------------------- /src/Processor/CallbackProcessor.php: -------------------------------------------------------------------------------- 1 | , array): string 18 | */ 19 | protected $callback; 20 | 21 | public function __construct(Closure $callback) 22 | { 23 | $this->callback = $callback; 24 | } 25 | 26 | public function processQuery($pdo, $sql, array $params, array &$bind_values) 27 | { 28 | return call_user_func_array($this->callback, [$pdo, $sql, $params, &$bind_values]); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Processor/IfBlock.php: -------------------------------------------------------------------------------- 1 | :[a-zA-Z0-9_]+\s) # first line 19 | (?[\s\S]*?) # block, includes %else 20 | \s%endif\b # block termination 21 | /mx'; 22 | const ELSE_SPLITTER = '/(?:^|\s)%else(?:$|\s)/m'; 23 | 24 | public function processQuery($pdo, $sql, array $params, array &$bind_values) 25 | { 26 | $built_sql = preg_replace_callback(self::IF_PATTERN, function (array $matches) use ($params) { 27 | $cond = rtrim($matches['cond']); 28 | if (!isset($params[$cond])) { 29 | throw new DomainException(sprintf('Must be assigned parameter %s.', $cond)); 30 | } 31 | 32 | $block = $matches['block']; 33 | if (strpos($block, '%if') !== false) { 34 | throw new DomainException('Nested %if is not supported.'); 35 | } 36 | 37 | $blocks = preg_split(self::ELSE_SPLITTER, $block) ?: [$block, ' ']; 38 | if (count($blocks) > 2) { 39 | throw new DomainException('Multiple else is not allowed for %if.'); 40 | } 41 | 42 | if ($params[$cond]) { 43 | return $blocks[0]; 44 | } 45 | 46 | return isset($blocks[1]) ? $blocks[1] : ''; 47 | }, $sql); 48 | 49 | assert($built_sql !== null); 50 | 51 | return $built_sql; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/Processor/PregCallbackReplacer.php: -------------------------------------------------------------------------------- 1 | */ 19 | protected $replacers; 20 | 21 | /** 22 | * @phpstan-var non-empty-string 23 | */ 24 | private $regexp; 25 | 26 | /** 27 | * @param non-empty-list $replacers 28 | */ 29 | public function __construct(array $replacers) 30 | { 31 | $replacer_map = []; 32 | foreach ($replacers as $replacer) { 33 | $replacer_map[$replacer->getKey()] = $replacer; 34 | } 35 | $this->replacers = $replacer_map; 36 | $this->regexp = $this->compileRegExp($replacer_map); 37 | } 38 | 39 | public function processQuery($pdo, $sql, array $params, array &$bind_values) 40 | { 41 | $built_sql = \preg_replace_callback($this->regexp, function (array $matches) use ( 42 | $pdo, $params, &$bind_values 43 | ) { 44 | foreach ($this->replacers as $key => $replacer) { 45 | if ($matches[$key] !== '') { 46 | /** @phpstan-param non-empty-array $matches */ 47 | return $replacer->replaceQuery($pdo, $matches, $params, $bind_values); 48 | } 49 | } 50 | 51 | throw new LogicException('Did not match any replacer.'); 52 | }, $sql); 53 | 54 | assert($built_sql !== null); 55 | 56 | return $built_sql; 57 | } 58 | 59 | /** 60 | * Compile regular-expression pattern 61 | * 62 | * @phpstan-param non-empty-array $processors 63 | * @phpstan-return non-empty-string 64 | */ 65 | public static function compileRegExp(array $processors) 66 | { 67 | $patterns = []; 68 | foreach ($processors as $key => $processor) { 69 | $patterns[] = "(?<{$key}>{$processor->getPattern()})"; 70 | } 71 | 72 | return "/" . \implode('|', $patterns) . "/mx"; 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/Processor/SimpleSingleLine.php: -------------------------------------------------------------------------------- 1 | 12 | * @param \PDO|PDOInterface $pdo 13 | * @phpstan-param T $pdo 14 | * @param string $sql 15 | * @phpstan-param array $params 16 | * @phpstan-param array $bind_values 17 | * @return string Return processed query string 18 | */ 19 | public function processQuery($pdo, $sql, array $params, array &$bind_values); 20 | } 21 | -------------------------------------------------------------------------------- /src/Query.php: -------------------------------------------------------------------------------- 1 | 15 | */ 16 | private $processors; 17 | 18 | /** 19 | * @phpstan-param list $processors 20 | */ 21 | public function __construct(array $processors) 22 | { 23 | $this->processors = $processors; 24 | } 25 | 26 | /** 27 | * Build SQL query 28 | * 29 | * @template S of \PDOStatement|PDOStatementInterface 30 | * @template T of \PDO|PDOInterface 31 | * @param \PDO|PDOInterface $pdo 32 | * @phpstan-param T $pdo 33 | * @phpstan-param non-empty-string $sql 34 | * @phpstan-param array $params 35 | * @return \PDOStatement 36 | * @phpstan-return ($pdo is \PDO ? \PDOStatement : S) 37 | */ 38 | public function build($pdo, $sql, array $params) 39 | { 40 | $bind_values = []; 41 | $built_sql = $sql; 42 | foreach ($this->processors as $processor) { 43 | $built_sql = $processor->processQuery($pdo, $built_sql, $params, $bind_values); 44 | } 45 | 46 | $stmt = $pdo->prepare($built_sql); 47 | assert($stmt !== false); 48 | 49 | foreach ($bind_values as $key => $param) { 50 | list($type, $value) = $param; 51 | $stmt->bindParam($key, $value, $type); 52 | } 53 | 54 | return $stmt; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/Replacer/ForBlock.php: -------------------------------------------------------------------------------- 1 | [^\]]*)\])?\s+ # separator, default [,] 20 | (?:[a-zA-Z0-9_]+\s) # # variable name to iterate 21 | (?\s[\s\S]*?)\s # block part, including %else 22 | (?:^|\s*)%endfor(?:\s|$) # end of block 23 | '; 24 | 25 | /** @phpstan-var list */ 26 | private $processors; 27 | 28 | /** 29 | * @phpstan-param list $processors 30 | */ 31 | public function __construct(array $processors) 32 | { 33 | $this->processors = $processors; 34 | } 35 | 36 | public function getKey() 37 | { 38 | return 'for'; 39 | } 40 | 41 | public function getPattern() 42 | { 43 | return self::FOR_PATTERN; 44 | } 45 | 46 | public function replaceQuery($pdo, array $matches, array $params, array &$bind_values) 47 | { 48 | $glue = $matches['forGlue']; 49 | if ($glue === '') { 50 | $glue = ','; 51 | } 52 | $name = \rtrim($matches['forName']); 53 | if (!isset($params[$name])) { 54 | throw new DomainException(sprintf('Must be assigned parameter %s.', $name)); 55 | } 56 | if (!\is_array($params[$name])) { 57 | throw new DomainException(sprintf('Parameter %s must be an array.', $name)); 58 | } 59 | 60 | /** @phpstan-var array> $array */ 61 | $array = $params[$name]; 62 | 63 | $block = $matches['forBlock']; 64 | if (\strpos($block, '%for') !== false) { 65 | throw new DomainException('Nested %for is not supported.'); 66 | } 67 | 68 | $replaced = []; 69 | foreach ($array as $row) { 70 | $new = $block; 71 | foreach ($this->processors as $processor) { 72 | $new = $processor->processQuery($pdo, $new, $row, $bind_values); 73 | } 74 | $replaced[] = \ltrim($new); 75 | } 76 | 77 | return \implode($glue, $replaced); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/Replacer/Placeholder.php: -------------------------------------------------------------------------------- 1 | [a-zA-Z0-9_]+)(?(?:@[a-zA-Z_\[\]]+)?)'; 19 | 20 | /** @var string */ 21 | protected $var_prefix; 22 | 23 | /** @phpstan-var array */ 24 | protected $additional_types; 25 | 26 | /** 27 | * @param string $var_prefix An array key prefix of variables 28 | * @phpstan-param array $additional_types 29 | */ 30 | public function __construct($var_prefix = ':', array $additional_types = []) 31 | { 32 | assert(\is_string($var_prefix)); 33 | $this->var_prefix = $var_prefix; 34 | $this->additional_types = $additional_types; 35 | } 36 | 37 | public function replaceQuery($pdo, array $matches, array $params, array &$bind_values) 38 | { 39 | $key = $this->var_prefix . $matches['placeholderKey']; 40 | $type = $matches['placeholderType']; 41 | 42 | if (!isset($params[$key])) { 43 | throw new \OutOfRangeException(\sprintf('param "%s" expected but not assigned', $key)); 44 | } 45 | 46 | return $this->replaceHolder($pdo, $key, $type, $params[$key], $bind_values); 47 | } 48 | 49 | public function getKey() 50 | { 51 | return 'placeholder'; 52 | } 53 | 54 | public function getPattern() 55 | { 56 | return self::PATTERN; 57 | } 58 | 59 | /** 60 | * @param \PDO|\Teto\SQL\PDOInterface $pdo 61 | * @template S of \PDOStatement|\Teto\SQL\PDOStatementInterface 62 | * @phpstan-param \PDO|\Teto\SQL\PDOInterface $pdo 63 | * @param string $key 64 | * @param string $type 65 | * @param mixed $value 66 | * @param array $bind_values 67 | * @return string|int 68 | */ 69 | public function replaceHolder($pdo, $key, $type, $value, &$bind_values) 70 | { 71 | if (isset($this->additional_types[$type])) { 72 | return $this->additional_types[$type]->escapeValue($pdo, $key, $type, $value, $bind_values); 73 | } 74 | 75 | if ($type === '@ascdesc') { 76 | if (!\in_array($value, ['ASC', 'DESC', 'asc', 'desc'], true)) { 77 | throw new \DomainException(\sprintf('param "%s" must be "ASC", "DESC", "asc" or "desc"', $key)); 78 | } 79 | 80 | return $value; 81 | } 82 | 83 | if ($type === '@int') { 84 | if (\is_int($value)) { 85 | return $value; 86 | } 87 | 88 | if (!\is_numeric($value)) { 89 | throw new \DomainException(\sprintf('param "%s" must be numeric', $key)); 90 | } 91 | 92 | if ($value < self::INT64_MIN || self::INT64_MAX < $value) { 93 | throw new \DomainException(\sprintf('param "%s" is integer out of range.', $key)); 94 | } 95 | 96 | /** @var numeric-string $value */ 97 | if ($value !== '0' && !\preg_match('/\A-?[1-9][0-9]*\z/', $value)) { 98 | throw new \DomainException(\sprintf('param "%s" is unexpected integer notation.', $key)); 99 | } 100 | 101 | return (int)$value; 102 | } 103 | 104 | if ($type === '@int[]') { 105 | if (!\is_array($value)) { 106 | throw new \DomainException(\sprintf('param "%s" must be int array', $key)); 107 | } 108 | if (\count($value) === 0) { 109 | throw new \DomainException(\sprintf('param "%s" must be not empty int array', $key)); 110 | } 111 | 112 | foreach ($value as $i => $item) { 113 | $s = (string)$item; 114 | if ($s < self::INT64_MIN || self::INT64_MAX < $s) { 115 | throw new \DomainException(\sprintf('param "%s[%d]" is integer out of range.', $key, $i)); 116 | } 117 | } 118 | 119 | $valuesString = \implode(',', $value); 120 | if (\strpos(',' . $valuesString . ',', ',,') !== false) { 121 | throw new \LogicException('Validation Error.'); 122 | } 123 | 124 | if ($valuesString !== '0' && !\preg_match('/\A(?:-?[1-9][0-9]*|0)(?:,(?:-?[1-9][0-9]*|0))*\z/', $valuesString)) { 125 | throw new \DomainException(\sprintf('param "%s" must be int array', $key)); 126 | } 127 | 128 | return $valuesString; 129 | } 130 | 131 | if ($type === '@string') { 132 | if (!\is_string($value) && !\is_numeric($value)) { 133 | throw new \DomainException(\sprintf('param "%s" must be string or numeric', $key)); 134 | } 135 | 136 | return $pdo->quote((string)$value, \PDO::PARAM_STR); 137 | } 138 | 139 | if ($type === '@string[]') { 140 | if (!\is_array($value)) { 141 | throw new \DomainException(\sprintf('param "%s" must be string array', $key)); 142 | } 143 | if (\count($value) == 0) { 144 | throw new \DomainException(\sprintf('param "%s" must be not empty string array', $key)); 145 | } 146 | foreach ($value as $i => $item) { 147 | if (!\is_string($item) && !\is_numeric($item)) { 148 | throw new \DomainException(\sprintf('element of param "%s" must be string or numeric', $key)); 149 | } 150 | $value[$i] = $pdo->quote((string)$item, \PDO::PARAM_STR); 151 | } 152 | return \implode(',', $value); 153 | } 154 | 155 | if ($type === '@lob') { 156 | if (!\is_resource($value)) { 157 | throw new \DomainException(\sprintf('param "%s" must be resource', $key)); 158 | } 159 | $bind_values[$key] = [\PDO::PARAM_LOB, $value]; 160 | 161 | return $key; 162 | } 163 | 164 | if ($type === '' || $type === '@') { 165 | throw new \DomainException(\sprintf('type specifier for param "%s" not found', $key)); 166 | } else { 167 | throw new \DomainException(\sprintf('unexpected type "%s"', $type)); 168 | } 169 | } 170 | 171 | } 172 | -------------------------------------------------------------------------------- /src/ReplacerInterface.php: -------------------------------------------------------------------------------- 1 | 31 | * @param \PDO|PDOInterface $pdo 32 | * @phpstan-param T $pdo 33 | * @phpstan-param array $matches 34 | * @phpstan-param array $params 35 | * @phpstan-param array $bind_values 36 | * @return string|int Return a replaced part of query string 37 | */ 38 | public function replaceQuery($pdo, array $matches, array $params, array &$bind_values); 39 | } 40 | -------------------------------------------------------------------------------- /src/StaticQueryExecuteTrait.php: -------------------------------------------------------------------------------- 1 | 18 | * @param \PDO|PDOInterface|PDOAggregate $pdo 19 | * @phpstan-param T|PDOAggregate $pdo 20 | * @param string $sql 21 | * @phpstan-param non-empty-string $sql 22 | * @phpstan-param array $params 23 | * @return \PDOStatement|PDOStatementInterface 24 | * @phpstan-return ($pdo is \PDO ? \PDOStatement : S) 25 | */ 26 | public static function execute($pdo, $sql, array $params) 27 | { 28 | if ($pdo instanceof PDOAggregate) { 29 | /** @phpstan-var T $pdo */ 30 | $pdo = $pdo->getPDO(); 31 | } 32 | 33 | $stmt = static::build($pdo, $sql, $params); 34 | $stmt->execute(); 35 | 36 | return $stmt; 37 | } 38 | 39 | /** 40 | * Build SQL query and execute 41 | * 42 | * @template S of \PDOStatement|PDOStatementInterface 43 | * @template T of \PDO|PDOInterface 44 | * @param \PDO|PDOInterface|PDOAggregate $pdo 45 | * @phpstan-param T|PDOAggregate $pdo 46 | * @param string $sql 47 | * @phpstan-param non-empty-string $sql 48 | * @phpstan-param array $params 49 | * @param ?string $name 50 | * @return string 51 | */ 52 | public static function executeAndReturnInsertId($pdo, $sql, array $params, $name = null) 53 | { 54 | if ($pdo instanceof PDOAggregate) { 55 | /** @phpstan-var T $pdo */ 56 | $pdo = $pdo->getPDO(); 57 | } 58 | 59 | $stmt = static::build($pdo, $sql, $params); 60 | $stmt->execute(); 61 | 62 | $id = ($name === null) ? $pdo->lastInsertId() : $pdo->lastInsertId($name); 63 | assert($id !== false); 64 | 65 | return $id; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/Type/PgIdentifier.php: -------------------------------------------------------------------------------- 1 | */ 17 | protected $types = []; 18 | 19 | /** 20 | * @phpstan-param array{'@column'?: non-empty-string, '@column[]'?: non-empty-string, '@table'?: non-empty-string} $type_names 21 | */ 22 | public function __construct(array $type_names) 23 | { 24 | $types = []; 25 | 26 | foreach (['@column', '@column[]', '@table'] as $type) { 27 | $key = isset($type_names[$type]) ? $type_names[$type] : $type; 28 | $types[$key] = $type; 29 | } 30 | 31 | $this->types = $types; 32 | } 33 | 34 | public function escapeValue($pdo, $key, $type, $value, &$bind_values) 35 | { 36 | if ($type === '@bool') { 37 | if (\is_bool($value)) { 38 | return $value ? 'TRUE' : 'FALSE'; 39 | } 40 | throw new DomainException(\sprintf('param "%s" must be bool', $key)); 41 | } 42 | 43 | if (!isset($this->types[$type])) { 44 | throw new LogicException("Passed unexpected type '{$type}', please check your configuration."); 45 | } 46 | 47 | $replaced_type = $this->types[$type]; 48 | if ($replaced_type === '@column') { 49 | if (\is_string($value)) { 50 | return $this->quote($value); 51 | } 52 | 53 | throw new DomainException("Passed unexpected \$value as type '{$type}'. please check your query and parameters."); 54 | } 55 | 56 | if ($replaced_type === '@column[]') { 57 | $columns = []; 58 | if (!\is_array($value)) { 59 | throw new DomainException("Passed unexpected \$value as type '{$type}'. please check your query and parameters."); 60 | } 61 | foreach ($value as $k => $v) { 62 | if (\is_string($k)) { 63 | if ($v === null || $v === '') { 64 | $columns[] = $k; 65 | } else { 66 | $columns[] = "{$k} AS {$this->quote($v)}"; 67 | } 68 | continue; 69 | } elseif (\is_int($k)) { 70 | if (\is_string($v)) { 71 | $columns[] = $this->quote($v); 72 | continue; 73 | } 74 | throw new DomainException("Passed unexpected \$value[{$k}] as type '{$type}'. please check your query and parameters."); 75 | } 76 | 77 | throw new LogicException('Unreachable'); 78 | } 79 | 80 | return \implode(',', $columns); 81 | } 82 | 83 | if ($replaced_type === '@table') { 84 | if (\is_string($value)) { 85 | return $this->quote($value); 86 | } 87 | 88 | throw new DomainException("Passed unexpected \$value as type '{$type}'. please check your query and parameters."); 89 | } 90 | 91 | throw new LogicException("Unreachable, or {$type} is not implemented yet."); 92 | } 93 | 94 | /** 95 | * @phpstan-param string $value 96 | * @phpstan-return non-empty-string 97 | * @see https://github.com/postgres/postgres/blob/REL9_0_STABLE/src/interfaces/libpq/fe-exec.c#L3165-L3169 98 | * @see https://github.com/postgres/postgres/blob/REL_15_STABLE/src/interfaces/libpq/fe-exec.c#L4216-L4220 99 | */ 100 | public function quote($value) 101 | { 102 | return '"' . \strtr($value, ['"' => '""']) . '"'; 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/TypeInterface.php: -------------------------------------------------------------------------------- 1 | $pdo 11 | * @param string $key 12 | * @param string $type 13 | * @param mixed $value 14 | * @param array $bind_values 15 | * @return string|int 16 | */ 17 | public function escapeValue($pdo, $key, $type, $value, &$bind_values); 18 | } 19 | -------------------------------------------------------------------------------- /tests/DummyPDO.php: -------------------------------------------------------------------------------- 1 | 10 | * @copyright 2016 USAMI Kenta 11 | * @license https://github.com/BaguettePHP/TetoSQL/blob/master/LICENSE MPL-2.0 12 | * @implements PDOInterface 13 | */ 14 | final class DummyPDO implements PDOInterface 15 | { 16 | public function beginTransaction() 17 | { 18 | throw new BadMethodCallException('Not supported'); 19 | } 20 | 21 | public function commit() 22 | { 23 | throw new BadMethodCallException('Not supported'); 24 | } 25 | 26 | public function errorCode() 27 | { 28 | throw new BadMethodCallException('Not supported'); 29 | } 30 | 31 | public function errorInfo() 32 | { 33 | throw new BadMethodCallException('Not supported'); 34 | } 35 | 36 | public function exec($statement) 37 | { 38 | throw new BadMethodCallException('Not supported'); 39 | } 40 | 41 | public function getAttribute($attribute) 42 | { 43 | throw new BadMethodCallException('Not supported'); 44 | } 45 | 46 | public static function getAvailableDrivers() 47 | { 48 | throw new BadMethodCallException('Not supported'); 49 | } 50 | 51 | public function inTransaction() 52 | { 53 | throw new BadMethodCallException('Not supported'); 54 | } 55 | 56 | public function lastInsertId($name = null) 57 | { 58 | throw new BadMethodCallException('Not supported'); 59 | } 60 | 61 | public function prepare($statement, $driver_options = array()) 62 | { 63 | return new DummyPDOStatement($statement, $driver_options); 64 | } 65 | 66 | public function query($statement) 67 | { 68 | throw new BadMethodCallException('Not supported'); 69 | } 70 | 71 | /** 72 | * @param string $string 73 | * @param int $parameter_type 74 | * @return string 75 | * @pure 76 | */ 77 | public function quote($string, $parameter_type = \PDO::PARAM_STR) 78 | { 79 | if ($parameter_type === \PDO::PARAM_STR) { 80 | return '@' . strtr($string, ['@' => '@@']) . '@'; 81 | } 82 | if ($parameter_type === \PDO::PARAM_INT) { 83 | return $string; 84 | } 85 | 86 | throw new LogicException("{$parameter_type} is not supported type."); 87 | } 88 | 89 | /** 90 | * @return bool 91 | */ 92 | public function rollBack() 93 | { 94 | throw new BadMethodCallException('Not supported'); 95 | } 96 | 97 | /** 98 | * @param int $attribute 99 | * @param mixed $value 100 | * @return bool 101 | */ 102 | public function setAttribute($attribute, $value) 103 | { 104 | throw new BadMethodCallException('Not supported'); 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /tests/DummyPDOStatement.php: -------------------------------------------------------------------------------- 1 | */ 17 | private $driverOptions; 18 | 19 | /** 20 | * @param string $query 21 | * @param array $driver_options 22 | */ 23 | public function __construct($query, array $driver_options = array()) 24 | { 25 | $this->queryString = $query; 26 | $this->driverOptions = $driver_options; 27 | } 28 | 29 | /** 30 | * @param string $name 31 | * @return mixed 32 | */ 33 | public function __get($name) 34 | { 35 | return $this->$name; 36 | } 37 | 38 | public function bindColumn($column, &$param, $type = null, $maxlen = null, $driverdata = null) 39 | { 40 | throw new BadMethodCallException('Unexpected method call'); 41 | } 42 | 43 | public function bindParam($parameter, &$variable, $data_type = \PDO::PARAM_STR, $length = null, $driver_options = null) 44 | { 45 | throw new BadMethodCallException('Unexpected method call'); 46 | } 47 | 48 | public function bindValue($parameter, $value, $data_type = \PDO::PARAM_STR) 49 | { 50 | throw new BadMethodCallException('Unexpected method call'); 51 | } 52 | 53 | public function closeCursor() 54 | { 55 | throw new BadMethodCallException('Unexpected method call'); 56 | } 57 | 58 | public function columnCount() 59 | { 60 | throw new BadMethodCallException('Unexpected method call'); 61 | } 62 | 63 | public function debugDumpParams() 64 | { 65 | throw new BadMethodCallException('Unexpected method call'); 66 | } 67 | 68 | public function errorCode() 69 | { 70 | throw new BadMethodCallException('Unexpected method call'); 71 | } 72 | 73 | public function errorInfo() 74 | { 75 | throw new BadMethodCallException('Unexpected method call'); 76 | } 77 | 78 | public function execute($input_parameters) 79 | { 80 | throw new BadMethodCallException('Unexpected method call'); 81 | } 82 | 83 | public function fetch($fetch_style = \PDO::ATTR_DEFAULT_FETCH_MODE, $cursor_orientation = \PDO::FETCH_ORI_NEXT, $cursor_offset = 0) 84 | { 85 | throw new BadMethodCallException('Unexpected method call'); 86 | } 87 | 88 | public function fetchAll($fetch_style, $fetch_argument = null, $ctor_args = array()) 89 | { 90 | throw new BadMethodCallException('Unexpected method call'); 91 | } 92 | 93 | public function fetchColumn($column_number = 0) 94 | { 95 | throw new BadMethodCallException('Unexpected method call'); 96 | } 97 | 98 | public function fetchObject($class_name = 'stdClass', $ctor_args = array()) 99 | { 100 | throw new BadMethodCallException('Unexpected method call'); 101 | } 102 | 103 | public function getAttribute($attribute) 104 | { 105 | throw new BadMethodCallException('Unexpected method call'); 106 | } 107 | 108 | public function getColumnMeta($column) 109 | { 110 | throw new BadMethodCallException('Unexpected method call'); 111 | } 112 | 113 | public function nextRowset() 114 | { 115 | throw new BadMethodCallException('Unexpected method call'); 116 | } 117 | 118 | public function rowCount() 119 | { 120 | throw new BadMethodCallException('Unexpected method call'); 121 | } 122 | 123 | public function setAttribute($attribute, $value) 124 | { 125 | throw new BadMethodCallException('Unexpected method call'); 126 | } 127 | 128 | public function setFetchMode($mode, $colno_or_classname_or_object, array $ctorargs = null) 129 | { 130 | throw new BadMethodCallException('Unexpected method call'); 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /tests/Processor/CallbackProcessorTest.php: -------------------------------------------------------------------------------- 1 | 'bar', 'buz' => 'buz']); 22 | $called = true; 23 | $bind_values = $params; 24 | $bind_values[] = 'Bound!'; 25 | return 'Closure called!'; 26 | }); 27 | 28 | $params = ['foo' => 'bar', 'buz' => 'buz']; 29 | $actual = $subject->processQuery($pdo, 'before query', $params, $bind_values); 30 | 31 | $this->assertTrue($called); 32 | $this->assertEquals($bind_values, ['foo' => 'bar', 'buz' => 'buz', 'Bound!']); 33 | $this->assertSame($actual, 'Closure called!'); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /tests/Processor/IfBlockTest.php: -------------------------------------------------------------------------------- 1 | 13 | * @copyright 2022 Baguette HQ 14 | * @license https://github.com/BaguettePHP/TetoSQL/blob/master/LICENSE MPL-2.0 15 | */ 16 | final class IfBlockTest extends TestCase 17 | { 18 | use ExpectException; 19 | use ExpectPHPException; 20 | 21 | /** @var IfBlock */ 22 | private $subject; 23 | 24 | public function set_up() 25 | { 26 | $this->subject = new IfBlock(); 27 | } 28 | 29 | /** 30 | * @dataProvider acceptDataProvider 31 | * @param string $input 32 | * @phpstan-param array $params 33 | * @param string $expected 34 | * @return void 35 | */ 36 | public function test_accept($input, array $params, $expected) 37 | { 38 | $pdo = new DummyPDO(); 39 | 40 | $bind_values = []; 41 | $actual = $this->subject->processQuery($pdo, $input, $params, $bind_values); 42 | 43 | $this->assertSame($expected, $actual); 44 | $this->assertSame([], $bind_values); 45 | } 46 | 47 | /** 48 | * @phpstan-return iterable, string}> 49 | */ 50 | public function acceptDataProvider() 51 | { 52 | $query_has_if = '%if :cond 53 | Then! 54 | %endif 55 | Rest'; 56 | $query_has_if_single_line = '%if :cond Then! %endif Rest'; 57 | $query_has_if_else = '%if :cond 58 | Then! 59 | %else 60 | Else! 61 | %endif 62 | Rest'; 63 | $query_has_if_else_single_line = '%if :cond Then! %else Else! %endif Rest'; 64 | $query_has_2_if_else = '%if :cond_1 65 | 1st Then! 66 | %else 67 | 1st Else! 68 | %endif 69 | 70 | Between 71 | 72 | %if :cond_2 73 | 2nd Then! 74 | %else 75 | 2nd Else! 76 | %endif 77 | Rest'; 78 | $query_has_if_invalid_else = '%if :cond 79 | Then! 80 | "%else" 81 | Not else! 82 | %endif 83 | Rest'; 84 | 85 | return [ 86 | [ 87 | 'No condition', [], 'No condition', 88 | ], 89 | [ 90 | '"%if :cond" in literal', [], '"%if :cond" in literal', 91 | ], 92 | [ 93 | $query_has_if, 94 | [':cond' => true], 95 | " Then!\nRest", 96 | ], 97 | [ 98 | $query_has_if, 99 | [':cond' => false], 100 | "\nRest", 101 | ], 102 | [ 103 | $query_has_if_single_line, 104 | [':cond' => false], 105 | " Rest", 106 | ], 107 | [ 108 | $query_has_if_else, 109 | [':cond' => false], 110 | "\n Else!\nRest", 111 | ], 112 | [ 113 | $query_has_if_else_single_line, 114 | [':cond' => false], 115 | "Else! Rest", 116 | ], 117 | [ 118 | implode("\n", [$query_has_if_else, $query_has_if_else]), 119 | [':cond' => false], 120 | "\n Else!\nRest\n Else!\nRest", 121 | ], 122 | [ 123 | $query_has_2_if_else, 124 | [':cond_1' => true, ':cond_2' => true], 125 | " 1st Then!\n\nBetween\n 2nd Then!\nRest", 126 | ], 127 | [ 128 | $query_has_2_if_else, 129 | [':cond_1' => true, ':cond_2' => false], 130 | " 1st Then!\n\nBetween\n\n 2nd Else!\nRest", 131 | ], 132 | [ 133 | $query_has_2_if_else, 134 | [':cond_1' => false, ':cond_2' => true], 135 | "\n 1st Else!\n\nBetween\n 2nd Then!\nRest", 136 | ], 137 | [ 138 | $query_has_2_if_else, 139 | [':cond_1' => false, ':cond_2' => false], 140 | "\n 1st Else!\n\nBetween\n\n 2nd Else!\nRest", 141 | ], 142 | [ 143 | $query_has_if_invalid_else, 144 | [':cond' => true], 145 | " Then!\n\"%else\"\n Not else!\nRest", 146 | ], 147 | [ 148 | $query_has_if_invalid_else, 149 | [':cond' => false], 150 | "\nRest", 151 | ], 152 | 153 | ]; 154 | } 155 | 156 | /** 157 | * @dataProvider rejeceptDataProvider 158 | * @param string $input 159 | * @phpstan-param array $params 160 | * @param string $expected_message 161 | * @return void 162 | */ 163 | public function test_raise_exception($input, array $params, $expected_message) 164 | { 165 | $pdo = new DummyPDO(); 166 | 167 | $this->expectException('DomainException'); 168 | $this->expectExceptionMessage($expected_message); 169 | 170 | $bind_values = []; 171 | $actual = $this->subject->processQuery($pdo, $input, $params, $bind_values); 172 | } 173 | 174 | /** 175 | * @return iterable 176 | */ 177 | public function rejeceptDataProvider() 178 | { 179 | return [ 180 | [ 181 | ' 182 | %if :cond 183 | code 184 | %endif', 185 | [], 186 | 'Must be assigned parameter :cond', 187 | ], 188 | [ 189 | ' 190 | %if :cond 191 | %else 192 | %else 193 | %endif', 194 | [':cond' => true], 195 | 'Multiple else is not allowed for %if', 196 | ], 197 | ]; 198 | } 199 | } 200 | -------------------------------------------------------------------------------- /tests/QueryTest.php: -------------------------------------------------------------------------------- 1 | 9 | * @copyright 2016 USAMI Kenta 10 | * @license https://github.com/BaguettePHP/TetoSQL/blob/master/LICENSE MPL-2.0 11 | */ 12 | final class QueryTest extends TestCase 13 | { 14 | /** 15 | * @dataProvider queryProvider 16 | * @param non-empty-string $query 17 | * @param array $params 18 | * @param string $expected 19 | * @return void 20 | */ 21 | public function test($query, array $params, $expected) 22 | { 23 | $pdo = new DummyPDO(); 24 | $stmt = Query::build($pdo, $query, $params); 25 | 26 | $this->assertSame($expected, $stmt->queryString); 27 | } 28 | 29 | /** 30 | * @return array, string}> 31 | */ 32 | public function queryProvider() 33 | { 34 | return [ 35 | [ 36 | "string: :a@string\nint :b@int\nstring: :a@string\nint :b@int\nint :c1@int\nint :c1@int", 37 | [ 38 | ':a' => 'AAAA', 39 | ':b' => '2222', 40 | ':c1' => '0', 41 | ':c2' => 0, 42 | ], 43 | 'string: @AAAA@ int 2222 string: @AAAA@ int 2222 int 0 int 0', 44 | ], 45 | [ 46 | <<<'SQL' 47 | SELECT foo, bar, buz 48 | FROM hoge 49 | WHERE id = :id@int 50 | SQL 51 | , 52 | [ 53 | ':id' => 12345, 54 | ], 55 | 'SELECT foo, bar, buz FROM hoge WHERE id = 12345', 56 | ], 57 | [ 58 | <<<'SQL' 59 | SELECT foo, bar, buz 60 | FROM hoge 61 | WHERE id = :id@int 62 | %if :order 63 | ORDER BY id ASC 64 | %endif 65 | SQL 66 | , 67 | [ 68 | ':id' => 12345, 69 | ':order' => true, 70 | ], 71 | 'SELECT foo, bar, buz FROM hoge WHERE id = 12345 ORDER BY id ASC', 72 | ], 73 | [ 74 | <<<'SQL' 75 | SELECT foo, bar, buz 76 | FROM hoge 77 | WHERE id = :id@int 78 | %if :order 79 | ORDER BY id ASC 80 | %endif 81 | SQL 82 | , 83 | [ 84 | ':id' => 12345, 85 | ':order' => false, 86 | ], 87 | 'SELECT foo, bar, buz FROM hoge WHERE id = 12345', 88 | ], 89 | [ 90 | <<<'SQL' 91 | SELECT foo, bar, buz 92 | FROM hoge 93 | WHERE id IN (:ids@int[]) 94 | %if :order 95 | ORDER BY id ASC 96 | %endif 97 | SQL 98 | , 99 | [ 100 | ':ids' => [12345, 23456, 78901], 101 | ':order' => false, 102 | ], 103 | 'SELECT foo, bar, buz FROM hoge WHERE id IN (12345,23456,78901)', 104 | ], 105 | [ 106 | <<<'SQL' 107 | INSERT INTO hoge 108 | VALUES 109 | %for[,] :values 110 | (:id@int, :name@string) 111 | %endfor 112 | SQL 113 | , 114 | [ 115 | ':id' => 12345, 116 | ':values' => [ 117 | ['id' => 0, 'name' => 'hoge'], 118 | ['id' => 1, 'name' => 'fuga'], 119 | ['id' => 2, 'name' => 'piyo'], 120 | ], 121 | ], 122 | 'INSERT INTO hoge VALUES(0, @hoge@),(1, @fuga@),(2, @piyo@)', 123 | ], 124 | ]; 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /tests/Replacer/ForBlockTest.php: -------------------------------------------------------------------------------- 1 | 16 | * @copyright 2022 Baguette HQ 17 | * @license https://github.com/BaguettePHP/TetoSQL/blob/master/LICENSE MPL-2.0 18 | */ 19 | final class ForBlockTest extends TestCase 20 | { 21 | use ExpectException; 22 | use ExpectPHPException; 23 | 24 | /** @var PregCallbackReplacer */ 25 | private $subject; 26 | 27 | public function set_up() 28 | { 29 | $placeholder_replacer = new Placeholder(); 30 | $this->subject = new PregCallbackReplacer([ 31 | new ForBlock([new PregCallbackReplacer([$placeholder_replacer])]) 32 | ]); 33 | } 34 | 35 | /** 36 | * @dataProvider acceptDataProvider 37 | * @param string $input 38 | * @phpstan-param array $params 39 | * @param string $expected 40 | * @return void 41 | */ 42 | public function test_accept($input, array $params, $expected) 43 | { 44 | $pdo = new DummyPDO(); 45 | 46 | $bind_values = []; 47 | $actual = $this->subject->processQuery($pdo, $input, $params, $bind_values); 48 | 49 | $this->assertSame($expected, $actual); 50 | $this->assertSame([], $bind_values); 51 | } 52 | 53 | /** 54 | * @phpstan-return iterable, string}> 55 | */ 56 | public function acceptDataProvider() 57 | { 58 | $query_has_if = '%for :arr 59 | Then! 60 | %endif 61 | Rest'; 62 | 63 | return [ 64 | [ 65 | 'No collection', [], 'No collection', 66 | ], 67 | [ 68 | '"%for :arr" in literal', [], '"%for :arr" in literal', 69 | ], 70 | [ 71 | '%for :arr 72 | :a@string - :b@string 73 | %endfor', 74 | [ 75 | ':arr' => [ 76 | [':a' => 'A1', ':b' => 'B1'], 77 | [':a' => 'A2', ':b' => 'B2'], 78 | [':a' => 'A3', ':b' => 'B3'], 79 | ] 80 | ], 81 | '@A1@ - @B1@,@A2@ - @B2@,@A3@ - @B3@', 82 | ], 83 | [ 84 | '%for :arr :a@string - :b@string %endfor', 85 | [ 86 | ':arr' => [ 87 | [':a' => 'A1', ':b' => 'B1'], 88 | [':a' => 'A2', ':b' => 'B2'], 89 | [':a' => 'A3', ':b' => 'B3'], 90 | ] 91 | ], 92 | '@A1@ - @B1@,@A2@ - @B2@,@A3@ - @B3@', 93 | ], 94 | [ 95 | '%for[] :arr :a@string - :b@string %endfor ', 96 | [ 97 | ':arr' => [ 98 | [':a' => 'A1', ':b' => 'B1'], 99 | [':a' => 'A2', ':b' => 'B2'], 100 | [':a' => 'A3', ':b' => 'B3'], 101 | ] 102 | ], 103 | '@A1@ - @B1@,@A2@ - @B2@,@A3@ - @B3@', 104 | ], 105 | [ 106 | '%for[,] :arr :a@string - :b@string %endfor ', 107 | [ 108 | ':arr' => [ 109 | [':a' => 'A1', ':b' => 'B1'], 110 | [':a' => 'A2', ':b' => 'B2'], 111 | [':a' => 'A3', ':b' => 'B3'], 112 | ] 113 | ], 114 | '@A1@ - @B1@,@A2@ - @B2@,@A3@ - @B3@', 115 | ], 116 | ]; 117 | } 118 | 119 | /** 120 | * @dataProvider rejeceptDataProvider 121 | * @param string $input 122 | * @phpstan-param array $params 123 | * @phpstan-param array{class: class-string<\Exception>, message: string} $expected 124 | * @return void 125 | */ 126 | public function test_raise_exception($input, array $params, array $expected) 127 | { 128 | $pdo = new DummyPDO(); 129 | 130 | $this->expectException($expected['class']); 131 | $this->expectExceptionMessage($expected['message']); 132 | 133 | $bind_values = []; 134 | $actual = $this->subject->processQuery($pdo, $input, $params, $bind_values); 135 | } 136 | 137 | /** 138 | * @return iterable, array{class: class-string<\Exception>, message: string}}> 139 | */ 140 | public function rejeceptDataProvider() 141 | { 142 | return [ 143 | [ 144 | '%for :arr 145 | :a@string - :b@string 146 | %endfor', 147 | [], 148 | [ 149 | 'class' => get_class(new DomainException()), 150 | 'message' => 'Must be assigned parameter :arr.', 151 | ] 152 | ], 153 | [ 154 | '%for :arr 155 | :a@string - :b@string 156 | %endfor', 157 | [ 158 | ':arr' => [ 159 | [':a' => 'A1', ':b' => 'B1'], 160 | [':a' => 'A2'], 161 | [':a' => 'A3', ':b' => 'B3'], 162 | ] 163 | ], 164 | [ 165 | 'class' => get_class(new OutOfRangeException()), 166 | 'message' => 'param ":b" expected but not assigned', 167 | ] 168 | ], 169 | ]; 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /tests/Replacer/PlaceholderTest.php: -------------------------------------------------------------------------------- 1 | 13 | * @copyright 2019 USAMI Kenta 14 | * @license https://github.com/BaguettePHP/TetoSQL/blob/master/LICENSE MPL-2.0 15 | */ 16 | final class ReplaceHolderTest extends TestCase 17 | { 18 | use ExpectException; 19 | use ExpectPHPException; 20 | 21 | /** @var Placeholder */ 22 | private $subject; 23 | 24 | public function set_up() 25 | { 26 | $this->subject = new Placeholder( 27 | ':', 28 | [ 29 | '@dummy' => new Sample\DummyType(), 30 | ] 31 | ); 32 | } 33 | 34 | /** 35 | * @dataProvider acceptDataProvider 36 | * @param string $type 37 | * @param mixed $input 38 | * @param string|int $expected 39 | * @return void 40 | */ 41 | public function test_accept($type, $input, $expected) 42 | { 43 | $pdo = new DummyPDO(); 44 | 45 | $actual = $this->subject->replaceHolder($pdo, ':key', "@{$type}", $input, $bind_values);; 46 | 47 | $this->assertSame($expected, $actual); 48 | } 49 | 50 | /** 51 | * @return iterable 52 | */ 53 | public function acceptDataProvider() 54 | { 55 | return [ 56 | ['ascdesc', 'ASC', 'ASC'], 57 | ['ascdesc', 'DESC', 'DESC'], 58 | ['ascdesc', 'asc', 'asc'], 59 | ['ascdesc', 'desc', 'desc'], 60 | ['int', 1, 1], 61 | ['int', '1', 1], 62 | ['int', 0, 0], 63 | ['int', '0', 0], 64 | ['int', '9223372036854775807', 9223372036854775807], 65 | ['int', '-9223372036854775808', (int)'-9223372036854775808'], 66 | ['int[]', [0], '0'], 67 | ['int[]', ['0'], '0'], 68 | ['int[]', [0, 0], '0,0'], 69 | ['int[]', ['0', '0'], '0,0'], 70 | ['int[]', [1, 2, 3], '1,2,3'], 71 | ['int[]', ['1', '2', '3'], '1,2,3'], 72 | ['int[]', [0, 1, 2, 3], '0,1,2,3'], 73 | ['int[]', ['0', '1', '2', '3'], '0,1,2,3'], 74 | ['int[]', [1, 0, 2, 3], '1,0,2,3'], 75 | ['int[]', ['1', '0', '2', '3'], '1,0,2,3'], 76 | ['int[]', [1, 2, 0, 3], '1,2,0,3'], 77 | ['int[]', ['1', '2', '0', '3'], '1,2,0,3'], 78 | ['int[]', [1, 2, 3, 0], '1,2,3,0'], 79 | ['int[]', ['1', '2', '3', '0'], '1,2,3,0'], 80 | ['int[]', 81 | ['9223372036854775807', '-9223372036854775808'], 82 | '9223372036854775807,-9223372036854775808', 83 | ], 84 | ['string', 0, '@0@'], 85 | ['string', '0', '@0@'], 86 | ['string', '', '@@'], 87 | ['string[]', ['', ''], '@@,@@'], 88 | ['dummy', 'foo', '[foo] is a dummy value.'], 89 | ]; 90 | } 91 | 92 | /** 93 | * @dataProvider rejeceptDataProvider 94 | * @param string $type 95 | * @param mixed $input 96 | * @param string $expected_message 97 | * @return void 98 | */ 99 | public function test_raise_exception($type, $input, $expected_message) 100 | { 101 | $pdo = new DummyPDO(); 102 | 103 | $this->expectException('DomainException'); 104 | $this->expectExceptionMessage($expected_message); 105 | 106 | $this->subject->replaceHolder($pdo, ':key', $type, $input, $bind_values); 107 | } 108 | 109 | /** 110 | * @return iterable 111 | */ 112 | public function rejeceptDataProvider() 113 | { 114 | return [ 115 | ['', null, 'type specifier for param ":key" not found'], 116 | ['@', null, 'type specifier for param ":key" not found'], 117 | ['@piyo', null, 'unexpected type "@piyo"'], 118 | ['@ascdesc', 'foo', 'param ":key" must be "ASC", "DESC", "asc" or "desc"'], 119 | ['@int', '-0', 'param ":key" is unexpected integer notation'], 120 | ['@int', '00', 'param ":key" is unexpected integer notation'], 121 | ['@int', '9223372036854775808', 'param ":key" is integer out of range.'], 122 | ['@int', '-9223372036854775809', 'param ":key" is integer out of range.'], 123 | ['@int[]', 0, 'param ":key" must be int array'], 124 | ['@int[]', [], 'param ":key" must be not empty int array'], 125 | ['@int[]', ['1', 'a', '3'], 'param ":key[1]" is integer out of range.'], 126 | ['@int[]', ['00'], 'param ":key" must be int array'], 127 | ['@int[]', ['9223372036854775808'], 'param ":key[0]" is integer out of range.'], 128 | ['@int[]', ['-9223372036854775809'], 'param ":key[0]" is integer out of range.'], 129 | ['@string', [], 'param ":key" must be string or numeric'], 130 | ['@string[]', '', 'param ":key" must be string array'], 131 | ['@string[]', [], 'param ":key" must be not empty string array'], 132 | ['@string[]', ['', null], 'element of param ":key" must be string or numeric'], 133 | ]; 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /tests/Replacer/Sample/DummyType.php: -------------------------------------------------------------------------------- 1 | getPDO(); 18 | $pdo->exec(self::DROP_TABLE); 19 | $pdo->exec(self::CREATE_TABLE); 20 | } 21 | 22 | const DROP_TABLE = 'DROP TABLE IF EXISTS `books`'; 23 | const CREATE_TABLE = ' 24 | CREATE TABLE `books` ( 25 | `id` INTEGER PRIMARY KEY AUTOINCREMENT, 26 | `name` TEXT, 27 | `cover` BLOB 28 | ) 29 | '; 30 | const INSERT = 'INSERT INTO `books` (`name`, `cover`) VALUES (:name@string, :cover@lob)'; 31 | const SELECT_BY_ID = 'SELECT `id`, `name`, `cover` FROM `books` WHERE `id` = :id@int'; 32 | 33 | /** 34 | * @return \PDO 35 | */ 36 | public function getPDO() 37 | { 38 | if ($this->pdo === null) { 39 | $dsn = 'sqlite:/' . __DIR__ . '/db.sq3'; 40 | $this->pdo = new \PDO($dsn, null, null, [\PDO::ATTR_PERSISTENT => true]); 41 | } 42 | 43 | return $this->pdo; 44 | } 45 | 46 | /** 47 | * @return void 48 | */ 49 | public function test() 50 | { 51 | $pdo = $this->getPDO(); 52 | 53 | $img_file = dirname(__DIR__) . '/fuji36_01.jpg'; 54 | $id = Query::executeAndReturnInsertId($pdo, self::INSERT, [ 55 | ':name' => 'Thirty-six Views of Mount Fuji', 56 | ':cover' => fopen($img_file, 'rb'), 57 | ]); 58 | 59 | /** @var array{id:int, name:string, cover:string} $actual */ 60 | $actual = Query::execute($pdo, self::SELECT_BY_ID, [ 61 | ':id' => $id, 62 | ])->fetch(\PDO::FETCH_ASSOC); 63 | 64 | $this->assertEquals($id, $actual['id']); 65 | $this->assertSame('Thirty-six Views of Mount Fuji', $actual['name']); 66 | 67 | $blob = file_get_contents($img_file); 68 | $this->assertTrue($blob === $actual['cover']); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /tests/Type/PgIdentifierTest.php: -------------------------------------------------------------------------------- 1 | subject = new PgIdentifier([]); 20 | } 21 | 22 | /** 23 | * @dataProvider escapeValuesProvider 24 | * @phpstan-param string|array|bool $input 25 | * @param string $type 26 | * @param string $expected 27 | * @return void 28 | */ 29 | public function testEscapeValue($input, $type, $expected) 30 | { 31 | $pdo = new DummyPDO(); 32 | $bind_values = []; 33 | $this->assertSame($expected, $this->subject->escapeValue($pdo, ':key', $type, $input, $bind_values)); 34 | $this->assertEquals([], $bind_values); 35 | } 36 | 37 | /** 38 | * @return array|bool,string,string}> 39 | */ 40 | public function escapeValuesProvider() 41 | { 42 | return [ 43 | ['' , '@column', '""'], 44 | ['abc' , '@column', '"abc"'], 45 | ['ABC' , '@column', '"ABC"'], 46 | ['ABC\\ABC\'' , '@column', '"ABC\\ABC\'"'], 47 | ['ABC"ABC' , '@column', '"ABC""ABC"'], 48 | ['ABC"""ABC' , '@column', '"ABC""""""ABC"'], 49 | [['foo'] , '@column[]', '"foo"'], 50 | [['foo','bar'] , '@column[]', '"foo","bar"'], 51 | [['foo' => 'bar'] , '@column[]', 'foo AS "bar"'], 52 | [['"foo"' => 'bar'] , '@column[]', '"foo" AS "bar"'], 53 | [['foo' => null] , '@column[]', 'foo'], 54 | [['"foo"' => null] , '@column[]', '"foo"'], 55 | [['foo' => null, 'bar' => 'buz'] , '@column[]', 'foo,bar AS "buz"'], 56 | [['"foo"' => null, 'bar' => ''] , '@column[]', '"foo",bar'], 57 | [true, '@bool', 'TRUE'], 58 | [false, '@bool', 'FALSE'], 59 | ]; 60 | } 61 | 62 | /** 63 | * @dataProvider escapeValuesUnexpectedValuesProvider 64 | * @phpstan-param string|array $input 65 | * @param string $type 66 | * @phpstan-param array{class: class-string<\Exception>, message: non-empty-string} $expected 67 | * @return void 68 | */ 69 | public function testEscapeValue_raiseError($input, $type, array $expected) 70 | { 71 | $this->expectException($expected['class']); 72 | $this->expectExceptionMessage($expected['message']); 73 | 74 | $pdo = new DummyPDO(); 75 | $bind_values = []; 76 | $_ = $this->subject->escapeValue($pdo, ':key', $type, $input, $bind_values); 77 | } 78 | 79 | /** 80 | * @return array, message: non-empty-string}}> 81 | */ 82 | public function escapeValuesUnexpectedValuesProvider() 83 | { 84 | /** @phpstan-var class-string $DomainException */ 85 | $DomainException = 'DomainException'; 86 | 87 | return [ 88 | [1, '@bool', ['class' => $DomainException, 'message' => 'param ":key" must be bool']], 89 | ['', '@bool', ['class' => $DomainException, 'message' => 'param ":key" must be bool']], 90 | [null, '@bool', ['class' => $DomainException, 'message' => 'param ":key" must be bool']], 91 | ['true', '@bool', ['class' => $DomainException, 'message' => 'param ":key" must be bool']], 92 | [ 93 | ['"foo"' => null, 'bar' => ''], 94 | '@table', 95 | [ 96 | 'class' => $DomainException, 97 | 'message' => "Passed unexpected \$value as type '@table'. please check your query and parameters.", 98 | ], 99 | ], 100 | ]; 101 | } 102 | 103 | /** 104 | * @dataProvider quoteValueProvider 105 | * @param string $input 106 | * @param string $expected 107 | * @return void 108 | */ 109 | public function testQuote($input, $expected) 110 | { 111 | $this->assertSame($expected, $this->subject->quote($input)); 112 | } 113 | 114 | /** 115 | * @return array 116 | */ 117 | public function quoteValueProvider() 118 | { 119 | return [ 120 | ['' , '""'], 121 | ['foo' , '"foo"'], 122 | ]; 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /tests/bootstrap.php: -------------------------------------------------------------------------------- 1 | 7 | * @copyright 2016 USAMI Kenta 8 | * @license https://github.com/BaguettePHP/TetoSQL/blob/master/LICENSE MPL-2.0 9 | */ 10 | require_once dirname(__DIR__) . '/vendor/autoload.php'; 11 | 12 | error_reporting(E_ALL | E_STRICT); 13 | -------------------------------------------------------------------------------- /tests/fuji36_01.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BaguettePHP/TetoSQL/010a0146c92ebf4707fa7a47d3b2356e78e1243e/tests/fuji36_01.jpg -------------------------------------------------------------------------------- /tools/.infection/.gitignore: -------------------------------------------------------------------------------- 1 | /composer.lock 2 | /vendor/ 3 | -------------------------------------------------------------------------------- /tools/.infection/composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "require-dev": { 3 | "infection/infection": "^0.13.4" 4 | }, 5 | "config": { 6 | "optimize-autoloader": true, 7 | "classmap-authoritative": true, 8 | "discard-changes": true, 9 | "secure-http": false, 10 | "preferred-install": "dist" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /tools/.phpstan/composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "require": { 3 | "phpstan/phpstan": "^1.8", 4 | "phpstan/phpstan-phpunit": "^1.1", 5 | "phpstan/phpstan-strict-rules": "^1.4" 6 | }, 7 | "config": { 8 | "optimize-autoloader": true, 9 | "classmap-authoritative": true, 10 | "discard-changes": true, 11 | "secure-http": false, 12 | "preferred-install": "dist" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /tools/.phpstan/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -euxo pipefail 4 | 5 | cd $(dirname "${BASH_SOURCE:-$0}") 6 | composer install 7 | -------------------------------------------------------------------------------- /tools/infection: -------------------------------------------------------------------------------- 1 | .infection/vendor/bin/infection -------------------------------------------------------------------------------- /tools/phpstan: -------------------------------------------------------------------------------- 1 | .phpstan/vendor/bin/phpstan --------------------------------------------------------------------------------