├── .devcontainer.json ├── .docker └── php │ └── Dockerfile ├── .github └── workflows │ └── run-tests.yml ├── .gitignore ├── .phpunit.cache └── test-results ├── .phpunit.result.cache ├── README.md ├── composer.json ├── composer.lock ├── docker-compose.yaml ├── identifier.sqlite ├── src ├── AbstractBuilder.php ├── ConditionsTrait.php ├── CondsQueryBuild.php ├── CreateQueryBuilder.php ├── CrudManager.php ├── CrudModel.php ├── CrudModelInterface.php ├── CrudStore.php ├── CrudStoreInterface.php ├── Data.php ├── DataInterface.php ├── DeleteQueryBuilder.php ├── Mysql.php ├── MysqlException.php ├── MysqlQueryIterator.php ├── PDOConnector.php ├── QueryUtil.php ├── ReadQueryBuilder.php ├── SecurityUtil.php ├── StorageUtil.php ├── UniqueTokenOptions.php └── UpdateQueryBuilder.php └── tests ├── CreateQueryBuilderTest.php ├── CrudManagerTest.php ├── DeleteQueryBuilderTest.php ├── Models └── User.php ├── MysqlTest.php ├── ReadQueryBuilderTest.php ├── SecurityUtilTest.php ├── SqliteTestCase.php ├── Stores └── UserStore.php └── UpdateQueryBuilderTest.php /.devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "PHP 7.1 with Composer", 3 | "dockerFile": "Dockerfile", 4 | "settings": { 5 | "php.validate.executablePath": "/usr/local/bin/php" 6 | }, 7 | "extensions": [ 8 | "felixfbecker.php-debug", 9 | "bmewburn.vscode-intelephense-client", 10 | "mrmlnc.vscode-apache" 11 | ], 12 | "forwardPorts": [8080], 13 | "postCreateCommand": "composer install" 14 | } 15 | -------------------------------------------------------------------------------- /.docker/php/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM php:8.2-cli 2 | 3 | ARG USER_ID 4 | ARG GROUP_ID 5 | 6 | # Install system dependencies 7 | RUN apt-get update && apt-get install -y \ 8 | git \ 9 | unzip \ 10 | sqlite3 \ 11 | libsqlite3-dev 12 | 13 | # Install PHP extensions 14 | RUN docker-php-ext-install pdo pdo_mysql pdo_sqlite 15 | 16 | # Install Composer 17 | RUN curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer 18 | 19 | # Create user and group with provided IDs 20 | RUN groupadd -g ${GROUP_ID} appgroup || true \ 21 | && useradd -u ${USER_ID} -g ${GROUP_ID} -m appuser || true 22 | 23 | # Set alias for listing files 24 | RUN echo "alias ll='ls -la'" >> /home/appuser/.bashrc 25 | 26 | # Install dependencies 27 | RUN chown ${USER_ID}:${GROUP_ID} /usr/local/bin/composer 28 | 29 | # Set working directory 30 | WORKDIR /app 31 | 32 | # Ensure correct ownership regardless of user/group ID 33 | RUN chown -R ${USER_ID}:${GROUP_ID} /app 34 | 35 | # Switch to appuser for any subsequent commands 36 | USER ${USER_ID}:${GROUP_ID} 37 | 38 | # Install dependencies 39 | #RUN composer install 40 | 41 | # Command to keep the container running 42 | CMD ["tail", "-f", "/dev/null"] -------------------------------------------------------------------------------- /.github/workflows/run-tests.yml: -------------------------------------------------------------------------------- 1 | name: Run Tests 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | test: 11 | runs-on: ubuntu-latest 12 | 13 | strategy: 14 | matrix: 15 | php-versions: ['8.2'] 16 | 17 | steps: 18 | - uses: actions/checkout@v4 19 | 20 | - name: Setup PHP 21 | uses: shivammathur/setup-php@v2 22 | with: 23 | php-version: ${{ matrix.php-versions }} 24 | extensions: mbstring, intl, pdo_sqlite 25 | coverage: xdebug 26 | 27 | - name: Validate composer.json and composer.lock 28 | run: composer validate --strict 29 | 30 | - name: Cache Composer packages 31 | id: composer-cache 32 | uses: actions/cache@v4 33 | with: 34 | path: vendor 35 | key: ${{ runner.os }}-php-${{ hashFiles('**/composer.lock') }} 36 | restore-keys: | 37 | ${{ runner.os }}-php- 38 | 39 | - name: Install dependencies 40 | run: composer install --prefer-dist --no-progress 41 | 42 | - name: Run test suite 43 | run: vendor/bin/phpunit tests -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | vendor/ -------------------------------------------------------------------------------- /.phpunit.cache/test-results: -------------------------------------------------------------------------------- 1 | {"version":1,"defects":[],"times":{"Tests\\SqliteTestCase::testConnect":0.001}} -------------------------------------------------------------------------------- /.phpunit.result.cache: -------------------------------------------------------------------------------- 1 | {"version":1,"defects":[],"times":{"Tests\\SqliteTestCase::testConnect":0.001}} -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
  2 |      _                 _                                         _ 
  3 |  ___(_)_ __ ___  _ __ | | ___  _ __    _ __ ___  _   _ ___  __ _| |
  4 | / __| | '_ ` _ \| '_ \| |/ _ \| '_ \  | '_ ` _ \| | | / __|/ _` | |
  5 | \__ \ | | | | | | |_) | | (_) | | | | | | | | | | |_| \__ \ (_| | |
  6 | |___/_|_| |_| |_| .__/|_|\___/|_| |_| |_| |_| |_|\__, |___/\__, |_|
  7 |                 |_|                              |___/        |_|  
  8 | 
9 | 10 | # Simplon/Mysql 11 | 12 | ------------------------------------------------- 13 | 14 | 1. [__Installing__](#1-installing) 15 | 2. [__Direct vs. CRUD__](#2-direct-vs-crud) 16 | 3. [__Setup connection__](#3-setup-connection) 17 | 4. [__Usage: Direct access__](#4-usage-direct-access) 18 | 4.1. Query 19 | 4.2. Insert 20 | 4.3. Update 21 | 4.4. Replace 22 | 4.5. Delete 23 | 4.6. Execute 24 | 5. [__Usage: CRUD__](#5-usage-crud) 25 | 5.1. Setup store 26 | 5.2. Setup model 27 | 5.3. Connect to store 28 | 5.4. Query 29 | 5.5. Insert 30 | 5.6. Update 31 | 5.7. Delete 32 | 5.8. Custom queries 33 | 6. [__IN() Clause Handling__](#6-in-clause-handling) 34 | 6.1. The issue 35 | 6.2. The solution 36 | 7. [__Exceptions__](#7-exceptions) 37 | 38 | ------------------------------------------------- 39 | 40 | ### Dependecies 41 | 42 | - PHP >= 7.1 43 | - PDO 44 | 45 | ------------------------------------------------- 46 | 47 | ## 1. Installing 48 | 49 | Easy install via composer. Still no idea what composer is? Inform yourself [here](http://getcomposer.org). 50 | 51 | ```json 52 | { 53 | "require": { 54 | "simplon/mysql": "*" 55 | } 56 | } 57 | ``` 58 | 59 | ------------------------------------------------- 60 | 61 | ## 2. Direct vs. CRUD 62 | 63 | I implemented two different ways of interacting with MySQL. The first option is the usual one which interacts directly with the database. Following a straight forward example to show you what I mean: 64 | 65 | ```php 66 | $data = $dbConn->fetchRow('SELECT * FROM names WHERE name = :name', ['name' => 'Peter']); 67 | 68 | // 69 | // $data is an array with our result 70 | // 71 | ``` 72 | 73 | In constrast to the prior method CRUD is more structured with `store` and `model`. Further, it uses [Builder Patterns](http://sourcemaking.com/design_patterns/builder) to interact with the database. A quick example of how we would rewrite the above ```direct query```: 74 | 75 | ```php 76 | $store = new NamesStore($dbConn); 77 | 78 | $model = $store->read( 79 | ReadQueryBuilder::create()->addCondition(NameModel::COLUMN_NAME, 'Peter') 80 | ); 81 | 82 | // 83 | // $model is a class with our result data abstracted 84 | // 85 | ``` 86 | 87 | ------------------------------------------------- 88 | 89 | ## 3. Setup connection 90 | 91 | The library requires a config value object in order to instantiate a connection with MySQL. See how it's done: 92 | 93 | ```php 94 | $pdo = new PDOConnector( 95 | 'localhost', // server 96 | 'root', // user 97 | 'root', // password 98 | 'database' // database 99 | ); 100 | 101 | $pdoConn = $pdo->connect('utf8', []); // charset, options 102 | 103 | // 104 | // you could now interact with PDO for instance setting attributes etc: 105 | // $pdoConn->setAttribute($attribute, $value); 106 | // 107 | 108 | $dbConn = new Mysql($pdoConn); 109 | ``` 110 | 111 | ------------------------------------------------- 112 | 113 | ## 4. Usage: Direct access 114 | 115 | ### 4.1. Query 116 | 117 | #### FetchColumn 118 | 119 | Returns a selected column from the first match. The example below returns ```id``` or ```null``` if nothing was found. 120 | 121 | ```php 122 | $result = $dbConn->fetchColumn('SELECT id FROM names WHERE name = :name', ['name' => 'Peter']); 123 | 124 | // result 125 | var_dump($result); // '1' || null 126 | ``` 127 | 128 | #### FetchColumnMany 129 | 130 | Returns an array with the selected column from all matching datasets. In the example below an array with all ```ids``` will be returned or ```null``` if nothing was found. 131 | 132 | ```php 133 | $result = $dbConn->fetchColumnMany('SELECT id FROM names WHERE name = :name', ['name' => 'Peter']); 134 | 135 | // result 136 | var_dump($result); // ['1', '15', '30', ...] || null 137 | ``` 138 | 139 | #### FetchColumnManyCursor 140 | 141 | Returns one matching dataset at a time. It is resource efficient and therefore handy when your result has many data. In the example below you either iterate through the foreach loop in case you have matchings or nothing will happen. 142 | 143 | ```php 144 | $cursor = $dbConn->fetchColumnMany('SELECT id FROM names WHERE name = :name', ['name' => 'Peter']); 145 | 146 | foreach ($cursor as $result) 147 | { 148 | var_dump($result); // '1' 149 | } 150 | ``` 151 | 152 | #### FetchRow 153 | 154 | Returns all selected columns from a matched dataset. The example below returns ```id```, ```age``` for the matched dataset. If nothing got matched ```null``` will be returned. 155 | 156 | ```php 157 | $result = $dbConn->fetchRow('SELECT id, age FROM names WHERE name = :name', ['name' => 'Peter']); 158 | 159 | var_dump($result); // ['id' => '1', 'age' => '22'] || null 160 | ``` 161 | 162 | #### FetchRowMany 163 | 164 | Returns all selected columns from all matched dataset. The example below returns for each matched dataset ```id```, ```age```. If nothing got matched ```null``` will be returned. 165 | 166 | ```php 167 | $result = $dbConn->fetchRowMany('SELECT id, age FROM names WHERE name = :name', ['name' => 'Peter']); 168 | 169 | var_dump($result); // [ ['id' => '1', 'age' => '22'], ['id' => '15', 'age' => '40'], ... ] || null 170 | ``` 171 | 172 | #### FetchRowManyCursor 173 | 174 | Same explanation as for ```FetchColumnManyCursor``` except that we receive all selected columns. 175 | 176 | ```php 177 | $result = $dbConn->fetchRowMany('SELECT id, age FROM names WHERE name = :name', ['name' => 'Peter']); 178 | 179 | foreach ($cursor as $result) 180 | { 181 | var_dump($result); // ['id' => '1', 'age' => '22'] 182 | } 183 | ``` 184 | 185 | ------------------------------------------------- 186 | 187 | ### 4.2. Insert 188 | 189 | #### Single data 190 | 191 | Inserting data into the database is pretty straight forward. Follow the example below: 192 | 193 | ```php 194 | $data = [ 195 | 'id' => false, 196 | 'name' => 'Peter', 197 | 'age' => 45, 198 | ]; 199 | 200 | $id = $dbConn->insert('names', $data); 201 | 202 | var_dump($id); // 50 || bool 203 | ``` 204 | 205 | The result depends on the table. If the table holds an ```autoincrementing ID``` column you will receive the ID count for the inserted data. If the table does not hold such a field you will receive ```true``` for a successful insert. If anything went bogus you will receive ```false```. 206 | 207 | #### Many datasets 208 | 209 | Follow the example for inserting many datasets at once: 210 | 211 | ```php 212 | $data = [ 213 | [ 214 | 'id' => false, 215 | 'name' => 'Peter', 216 | 'age' => 45, 217 | ], 218 | [ 219 | 'id' => false, 220 | 'name' => 'Peter', 221 | 'age' => 16, 222 | ], 223 | ]; 224 | 225 | $id = $dbConn->insertMany('names', $data); 226 | 227 | var_dump($id); // 50 || bool 228 | ``` 229 | 230 | The result depends on the table. If the table holds an ```autoincrementing ID``` column you will receive the ID count for the inserted data. If the table does not hold such a field you will receive ```true``` for a successful insert. If anything went bogus you will receive ```false```. 231 | 232 | ------------------------------------------------- 233 | 234 | ### 4.3. Updating 235 | 236 | #### Simple update statement 237 | 238 | Same as for insert statements accounts for updates. Its easy to understand. If the update succeeded the response will be ```true```. If nothing has been updated you will receive ```null```. 239 | 240 | ```php 241 | $conds = [ 242 | 'id' => 50, 243 | ]; 244 | 245 | $data = [ 246 | 'name' => 'Peter', 247 | 'age' => 50, 248 | ]; 249 | 250 | $result = $dbConn->update('names', $conds, $data); 251 | 252 | var_dump($result); // true || null 253 | ``` 254 | 255 | #### Custom update conditions query 256 | 257 | Same as for insert statements accounts for updates. Its easy to understand. If the update succeeded the response will be ```true```. If nothing has been updated you will receive ```null```. 258 | 259 | ```php 260 | $conds = [ 261 | 'id' => 50, 262 | 'name' => 'Peter', 263 | ]; 264 | 265 | // custom conditions query 266 | $condsQuery = 'id = :id OR name =: name'; 267 | 268 | $data = [ 269 | 'name' => 'Peter', 270 | 'age' => 50, 271 | ]; 272 | 273 | $result = $dbConn->update('names', $conds, $data, $condsQuery); 274 | 275 | var_dump($result); // true || null 276 | ``` 277 | 278 | ------------------------------------------------- 279 | 280 | ### 4.4. Replace 281 | 282 | As MySQL states it: ```REPLACE``` works exactly like ```INSERT```, except that if an old row in the table has the same value as a new row for a ```PRIMARY KEY``` or a ```UNIQUE index```, the old row is deleted before the new row is inserted. 283 | 284 | #### Replace a single datasets 285 | 286 | As a result you will either receive the ```INSERT ID``` or ```false``` in case something went wrong. 287 | 288 | ```php 289 | $data = [ 290 | 'id' => 5, 291 | 'name' => 'Peter', 292 | 'age' => 16, 293 | ]; 294 | 295 | $result = $dbConn->replace('names', $data); 296 | 297 | var_dump($result); // 1 || false 298 | ``` 299 | 300 | #### Replace multiple datasets 301 | 302 | As a result you will either receive an array of ```INSERT IDs``` or ```false``` in case something went wrong. 303 | 304 | ```php 305 | $data = [ 306 | [ 307 | 'id' => 5, 308 | 'name' => 'Peter', 309 | 'age' => 16, 310 | ], 311 | [ 312 | 'id' => 10, 313 | 'name' => 'John', 314 | 'age' => 22, 315 | ], 316 | ]; 317 | 318 | $result = $dbConn->replaceMany('names', $data); 319 | 320 | var_dump($result); // [5, 10] || false 321 | ``` 322 | 323 | ------------------------------------------------- 324 | 325 | ### 4.5. Delete 326 | 327 | #### Simple delete conditions 328 | 329 | The following example demonstrates how to remove data. If the query succeeds we will receive ```true``` else ```false```. 330 | 331 | ```php 332 | $result = $dbConn->delete('names', ['id' => 50]); 333 | 334 | var_dump($result); // true || false 335 | ``` 336 | 337 | #### Custom delete conditions query 338 | 339 | The following example demonstrates how to remove data with a custom conditions query. If the query succeeds we will receive ```true``` else ```false```. 340 | 341 | ```php 342 | $conds = [ 343 | 'id' => 50, 344 | 'name' => 'John', 345 | ]; 346 | 347 | // custom conditions query 348 | $condsQuery = 'id = :id OR name =: name'; 349 | 350 | $result = $dbConn->delete('names', $conds, $condsQuery); 351 | 352 | var_dump($result); // true || false 353 | ``` 354 | 355 | ------------------------------------------------- 356 | 357 | ### 4.6. Execute 358 | 359 | This method is ment for calls which do not require any parameters such as ```TRUNCATE```. If the call succeeds you will receive ```true```. If it fails an ```MysqlException``` will be thrown. 360 | 361 | ```php 362 | $result = $dbConn->executeSql('TRUNCATE names'); 363 | 364 | var_dump($result); // true 365 | ``` 366 | 367 | ------------------------------------------------- 368 | 369 | ## 5. Usage: CRUD 370 | 371 | The following query examples will be a rewrite of the aforementioned ```direct access``` examples. For this we need a `Store` and a related `Model`. 372 | 373 | ### 5.1. Setup store 374 | 375 | ```php 376 | namespace Test\Crud; 377 | 378 | use Simplon\Mysql\CreateQueryBuilder;use Simplon\Mysql\CrudModelInterface;use Simplon\Mysql\CrudStore;use Simplon\Mysql\DeleteQueryBuilder;use Simplon\Mysql\MysqlException;use Simplon\Mysql\ReadQueryBuilder;use Simplon\Mysql\UpdateQueryBuilder; 379 | 380 | /** 381 | * @package Test\Crud 382 | */ 383 | class NamesStore extends CrudStore 384 | { 385 | /** 386 | * @return string 387 | */ 388 | public function getTableName(): string 389 | { 390 | return 'names'; 391 | } 392 | 393 | /** 394 | * @return CrudModelInterface 395 | */ 396 | public function getModel(): CrudModelInterface 397 | { 398 | return new NameModel(); 399 | } 400 | 401 | /** 402 | * @param CreateQueryBuilder $builder 403 | * 404 | * @return NameModel 405 | * @throws MysqlException 406 | */ 407 | public function create(CreateQueryBuilder $builder): NameModel 408 | { 409 | /** @var NameModel $model */ 410 | $model = $this->crudCreate($builder); 411 | 412 | return $model; 413 | } 414 | 415 | /** 416 | * @param ReadQueryBuilder|null $builder 417 | * 418 | * @return NameModel[]|null 419 | * @throws MysqlException 420 | */ 421 | public function read(?ReadQueryBuilder $builder = null): ?array 422 | { 423 | /** @var NameModel[]|null $response */ 424 | $response = $this->crudRead($builder); 425 | 426 | return $response; 427 | } 428 | 429 | /** 430 | * @param ReadQueryBuilder $builder 431 | * 432 | * @return null|NameModel 433 | * @throws MysqlException 434 | */ 435 | public function readOne(ReadQueryBuilder $builder): ?NameModel 436 | { 437 | /** @var NameModel|null $response */ 438 | $response = $this->crudReadOne($builder); 439 | 440 | return $response; 441 | } 442 | 443 | /** 444 | * @param UpdateQueryBuilder $builder 445 | * 446 | * @return NameModel 447 | * @throws MysqlException 448 | */ 449 | public function update(UpdateQueryBuilder $builder): NameModel 450 | { 451 | /** @var NameModel|null $model */ 452 | $model = $this->crudUpdate($builder); 453 | 454 | return $model; 455 | } 456 | 457 | /** 458 | * @param DeleteQueryBuilder $builder 459 | * 460 | * @return bool 461 | * @throws MysqlException 462 | */ 463 | public function delete(DeleteQueryBuilder $builder): bool 464 | { 465 | return $this->crudDelete($builder); 466 | } 467 | 468 | /** 469 | * @param int $id 470 | * 471 | * @return null|NameModel 472 | * @throws MysqlException 473 | */ 474 | public function customMethod(int $id): ?NameModel 475 | { 476 | $query = 'SELECT * FROM ' . $this->getTableName() . ' WHERE id=:id'; 477 | 478 | if ($result = $this->getCrudManager()->getMysql()->fetchRow($query, ['id' => $id])) 479 | { 480 | return (new NameModel())->fromArray($result); 481 | } 482 | 483 | return null; 484 | } 485 | } 486 | ``` 487 | 488 | ### 5.2. Setup model 489 | 490 | ```php 491 | id; 525 | } 526 | 527 | /** 528 | * @param int $id 529 | * 530 | * @return NameModel 531 | */ 532 | public function setId(int $id): NameModel 533 | { 534 | $this->id = $id; 535 | 536 | return $this; 537 | } 538 | 539 | /** 540 | * @return string 541 | */ 542 | public function getName(): string 543 | { 544 | return $this->name; 545 | } 546 | 547 | /** 548 | * @param string $name 549 | * 550 | * @return NameModel 551 | */ 552 | public function setName(string $name): NameModel 553 | { 554 | $this->name = $name; 555 | 556 | return $this; 557 | } 558 | 559 | /** 560 | * @return int 561 | */ 562 | public function getAge(): int 563 | { 564 | return (int)$this->age; 565 | } 566 | 567 | /** 568 | * @param int $age 569 | * 570 | * @return NameModel 571 | */ 572 | public function setAge(int $age): NameModel 573 | { 574 | $this->age = $age; 575 | 576 | return $this; 577 | } 578 | } 579 | ``` 580 | 581 | ### 5.3. Connect to store 582 | 583 | In order to interact with our store we need to create an instance. For the following points we will make use of this instance. 584 | 585 | ```php 586 | $store = new NamesStore($pdoConn); 587 | ``` 588 | 589 | ### 5.4. Query 590 | 591 | #### Fetch one item 592 | 593 | Returns a `name model` or `NULL` if nothing could be matched. 594 | 595 | ```php 596 | $model = $store->readOne( 597 | ReadQueryBuilder::create()->addCondition(NameModel::COLUMN_NAME, 'Peter') 598 | ); 599 | 600 | echo $model->getId(); // prints user id 601 | ``` 602 | 603 | You can make use of operators from the `addCondition`: 604 | 605 | ```php 606 | $model = $store->readOne( 607 | ReadQueryBuilder::create()->addCondition(NameModel::COLUMN_AGE, 20, '>') 608 | ); 609 | 610 | echo $model->getId(); // prints user id 611 | ``` 612 | 613 | #### Fetch many items 614 | 615 | Returns an array of `name models` or `NULL` if nothing could be matched. 616 | 617 | ```php 618 | $models = $store->read( 619 | ReadQueryBuilder::create()->addCondition(NameModel::COLUMN_NAME, 'Peter') 620 | ); 621 | 622 | echo $models[0]->getId(); // prints user id from first matched model 623 | ``` 624 | 625 | ### 5.5. Insert 626 | 627 | The following example shows how to create a new store entry. 628 | 629 | ```php 630 | $model = $store->create( 631 | CreateQueryBuilder::create()->setModel( 632 | (new NameModel()) 633 | ->setName('Johnny') 634 | ->setAge(22) 635 | ) 636 | ); 637 | ``` 638 | 639 | ### 5.6. Update 640 | 641 | The following example shows how to update an existing store entry. 642 | 643 | ```php 644 | // 645 | // fetch a model 646 | // 647 | 648 | $model = $store->readOne( 649 | ReadQueryBuilder::create()->addCondition(NameModel::COLUMN_NAME, 'Peter') 650 | ); 651 | 652 | // 653 | // update age 654 | // 655 | 656 | $model->setAge(36); 657 | 658 | // 659 | // persist change 660 | // 661 | 662 | $model = $store->update( 663 | UpdateQueryBuilder::create() 664 | ->setModel($model) 665 | ->addCondition(NameModel::COLUMN_ID, $model->getId()) 666 | ); 667 | ``` 668 | 669 | ### 5.7. Delete 670 | 671 | The following example shows how to delete an existing store entry. 672 | 673 | ```php 674 | // 675 | // fetch a model 676 | // 677 | 678 | $model = $store->readOne( 679 | ReadQueryBuilder::create()->addCondition(NameModel::COLUMN_NAME, 'Peter') 680 | ); 681 | 682 | // 683 | // delete model from store 684 | // 685 | 686 | $store->delete( 687 | DeleteQueryBuilder::create() 688 | ->addCondition(NameModel::COLUMN_ID, $model->getId()) 689 | ) 690 | ``` 691 | 692 | ### 5.8. Custom queries 693 | 694 | You also have the possibility to write custom queries/handlings for your store. I added a method to the store which demonstrates on how to implement custom handlings. 695 | 696 | ```php 697 | /** 698 | * @param int $id 699 | * 700 | * @return null|NameModel 701 | * @throws MysqlException 702 | */ 703 | public function customMethod(int $id): ?NameModel 704 | { 705 | $query = 'SELECT * FROM ' . $this->getTableName() . ' WHERE id=:id'; 706 | 707 | if ($result = $this->getCrudManager()->getMysql()->fetchRow($query, ['id' => $id])) 708 | { 709 | return (new NameModel())->fromArray($result); 710 | } 711 | 712 | return null; 713 | } 714 | ``` 715 | 716 | ------------------------------------------------- 717 | 718 | ## 6. IN() Clause Handling 719 | 720 | ### 6.1. The issue 721 | 722 | There is no way using an ```IN()``` clause via PDO. This functionality is simply not given. However, you could do something like the following: 723 | 724 | ```php 725 | $ids = array(1,2,3,4,5); 726 | $query = "SELECT * FROM users WHERE id IN (" . join(',', $ids) . ")"; 727 | ``` 728 | 729 | Looks good at first sight - not sexy but probably does the job, right? Wrong. This approach only works with ```INTEGERS``` and it does not ```ESCAPE``` the user's input - the reason why we use ```PDO``` in first place. 730 | 731 | Just for the record here is a string example which would not work: 732 | 733 | ```php 734 | $emails = array('johnny@me.com', 'peter@ibm.com'); 735 | $query = "SELECT * FROM users WHERE email IN (" . join(',', $emails) . ")"; 736 | ``` 737 | 738 | The only way how this would work is by wrapping each value like the following: ```'"email"'```. Way too much work. 739 | 740 | ### 6.2. The solution 741 | 742 | To take advantage of the built in ```IN() Clause``` with escaping and type handling do the following for the direct access. CRUD will do the query building automatically for you. 743 | 744 | ```php 745 | // integers 746 | $conds = array('ids' => array(1,2,3,4,5)); 747 | $query = "SELECT * FROM users WHERE id IN (:ids)"; 748 | 749 | // strings 750 | $conds = array('emails' => array('johnny@me.com', 'peter@ibm.com')); 751 | $query = "SELECT * FROM users WHERE email IN (:emails)"; 752 | ``` 753 | 754 | ------------------------------------------------- 755 | 756 | ## 7. Exceptions 757 | 758 | For both access methods (direct, sqlmanager) occuring exceptions will be wrapped by a ```MysqlException```. All essential exception information will be summarised as ```JSON``` within the ```Exception Message```. 759 | 760 | Here is an example of how that might look like: 761 | 762 | ```bash 763 | {"query":"SELECT pro_id FROM names WHERE connector_type = :connectorType","params":{"connectorType":"FB"},"errorInfo":{"sqlStateCode":"42S22","code":1054,"message":"Unknown column 'pro_id' in 'field list'"}} 764 | ``` 765 | 766 | ------------------------------------------------- 767 | 768 | # License 769 | 770 | Simplon/Mysql is freely distributable under the terms of the MIT license. 771 | 772 | Copyright (c) 2017 Tino Ehrich ([tino@bigpun.me](mailto:tino@bigpun.me)) 773 | 774 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 775 | 776 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 777 | 778 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 779 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "simplon/mysql", 3 | "description": "Simplon MySQL Library", 4 | "homepage": "https://github.com/fightbulc/simplon_mysql", 5 | "type": "library", 6 | "keywords": [ 7 | "db", 8 | "pdo", 9 | "mysql", 10 | "manager" 11 | ], 12 | "license": "MIT", 13 | "authors": [ 14 | { 15 | "name": "Tino Ehrich", 16 | "email": "opensource@ato.mozmail.com", 17 | "role": "developer" 18 | } 19 | ], 20 | "minimum-stability": "dev", 21 | "require": { 22 | "php": ">=8.2", 23 | "ext-pdo": "*" 24 | }, 25 | "autoload": { 26 | "psr-4": { 27 | "Simplon\\Mysql\\": "src/", 28 | "Tests\\": "tests/" 29 | } 30 | }, 31 | "require-dev": { 32 | "phpunit/phpunit": "^11.4@dev" 33 | }, 34 | "scripts": { 35 | "test": "phpunit --colors=always" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /composer.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_readme": [ 3 | "This file locks the dependencies of your project to a known state", 4 | "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", 5 | "This file is @generated automatically" 6 | ], 7 | "content-hash": "9d80748364563a488d7136070520fcdc", 8 | "packages": [], 9 | "packages-dev": [ 10 | { 11 | "name": "myclabs/deep-copy", 12 | "version": "1.x-dev", 13 | "source": { 14 | "type": "git", 15 | "url": "https://github.com/myclabs/DeepCopy.git", 16 | "reference": "3a6b9a42cd8f8771bd4295d13e1423fa7f3d942c" 17 | }, 18 | "dist": { 19 | "type": "zip", 20 | "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/3a6b9a42cd8f8771bd4295d13e1423fa7f3d942c", 21 | "reference": "3a6b9a42cd8f8771bd4295d13e1423fa7f3d942c", 22 | "shasum": "" 23 | }, 24 | "require": { 25 | "php": "^7.1 || ^8.0" 26 | }, 27 | "conflict": { 28 | "doctrine/collections": "<1.6.8", 29 | "doctrine/common": "<2.13.3 || >=3 <3.2.2" 30 | }, 31 | "require-dev": { 32 | "doctrine/collections": "^1.6.8", 33 | "doctrine/common": "^2.13.3 || ^3.2.2", 34 | "phpspec/prophecy": "^1.10", 35 | "phpunit/phpunit": "^7.5.20 || ^8.5.23 || ^9.5.13" 36 | }, 37 | "default-branch": true, 38 | "type": "library", 39 | "autoload": { 40 | "files": [ 41 | "src/DeepCopy/deep_copy.php" 42 | ], 43 | "psr-4": { 44 | "DeepCopy\\": "src/DeepCopy/" 45 | } 46 | }, 47 | "notification-url": "https://packagist.org/downloads/", 48 | "license": [ 49 | "MIT" 50 | ], 51 | "description": "Create deep copies (clones) of your objects", 52 | "keywords": [ 53 | "clone", 54 | "copy", 55 | "duplicate", 56 | "object", 57 | "object graph" 58 | ], 59 | "support": { 60 | "issues": "https://github.com/myclabs/DeepCopy/issues", 61 | "source": "https://github.com/myclabs/DeepCopy/tree/1.12.0" 62 | }, 63 | "funding": [ 64 | { 65 | "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy", 66 | "type": "tidelift" 67 | } 68 | ], 69 | "time": "2024-06-12T14:39:25+00:00" 70 | }, 71 | { 72 | "name": "nikic/php-parser", 73 | "version": "v5.1.0", 74 | "source": { 75 | "type": "git", 76 | "url": "https://github.com/nikic/PHP-Parser.git", 77 | "reference": "683130c2ff8c2739f4822ff7ac5c873ec529abd1" 78 | }, 79 | "dist": { 80 | "type": "zip", 81 | "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/683130c2ff8c2739f4822ff7ac5c873ec529abd1", 82 | "reference": "683130c2ff8c2739f4822ff7ac5c873ec529abd1", 83 | "shasum": "" 84 | }, 85 | "require": { 86 | "ext-ctype": "*", 87 | "ext-json": "*", 88 | "ext-tokenizer": "*", 89 | "php": ">=7.4" 90 | }, 91 | "require-dev": { 92 | "ircmaxell/php-yacc": "^0.0.7", 93 | "phpunit/phpunit": "^9.0" 94 | }, 95 | "bin": [ 96 | "bin/php-parse" 97 | ], 98 | "type": "library", 99 | "extra": { 100 | "branch-alias": { 101 | "dev-master": "5.0-dev" 102 | } 103 | }, 104 | "autoload": { 105 | "psr-4": { 106 | "PhpParser\\": "lib/PhpParser" 107 | } 108 | }, 109 | "notification-url": "https://packagist.org/downloads/", 110 | "license": [ 111 | "BSD-3-Clause" 112 | ], 113 | "authors": [ 114 | { 115 | "name": "Nikita Popov" 116 | } 117 | ], 118 | "description": "A PHP parser written in PHP", 119 | "keywords": [ 120 | "parser", 121 | "php" 122 | ], 123 | "support": { 124 | "issues": "https://github.com/nikic/PHP-Parser/issues", 125 | "source": "https://github.com/nikic/PHP-Parser/tree/v5.1.0" 126 | }, 127 | "time": "2024-07-01T20:03:41+00:00" 128 | }, 129 | { 130 | "name": "phar-io/manifest", 131 | "version": "dev-master", 132 | "source": { 133 | "type": "git", 134 | "url": "https://github.com/phar-io/manifest.git", 135 | "reference": "54750ef60c58e43759730615a392c31c80e23176" 136 | }, 137 | "dist": { 138 | "type": "zip", 139 | "url": "https://api.github.com/repos/phar-io/manifest/zipball/54750ef60c58e43759730615a392c31c80e23176", 140 | "reference": "54750ef60c58e43759730615a392c31c80e23176", 141 | "shasum": "" 142 | }, 143 | "require": { 144 | "ext-dom": "*", 145 | "ext-libxml": "*", 146 | "ext-phar": "*", 147 | "ext-xmlwriter": "*", 148 | "phar-io/version": "^3.0.1", 149 | "php": "^7.2 || ^8.0" 150 | }, 151 | "default-branch": true, 152 | "type": "library", 153 | "extra": { 154 | "branch-alias": { 155 | "dev-master": "2.0.x-dev" 156 | } 157 | }, 158 | "autoload": { 159 | "classmap": [ 160 | "src/" 161 | ] 162 | }, 163 | "notification-url": "https://packagist.org/downloads/", 164 | "license": [ 165 | "BSD-3-Clause" 166 | ], 167 | "authors": [ 168 | { 169 | "name": "Arne Blankerts", 170 | "email": "arne@blankerts.de", 171 | "role": "Developer" 172 | }, 173 | { 174 | "name": "Sebastian Heuer", 175 | "email": "sebastian@phpeople.de", 176 | "role": "Developer" 177 | }, 178 | { 179 | "name": "Sebastian Bergmann", 180 | "email": "sebastian@phpunit.de", 181 | "role": "Developer" 182 | } 183 | ], 184 | "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)", 185 | "support": { 186 | "issues": "https://github.com/phar-io/manifest/issues", 187 | "source": "https://github.com/phar-io/manifest/tree/2.0.4" 188 | }, 189 | "funding": [ 190 | { 191 | "url": "https://github.com/theseer", 192 | "type": "github" 193 | } 194 | ], 195 | "time": "2024-03-03T12:33:53+00:00" 196 | }, 197 | { 198 | "name": "phar-io/version", 199 | "version": "3.2.1", 200 | "source": { 201 | "type": "git", 202 | "url": "https://github.com/phar-io/version.git", 203 | "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74" 204 | }, 205 | "dist": { 206 | "type": "zip", 207 | "url": "https://api.github.com/repos/phar-io/version/zipball/4f7fd7836c6f332bb2933569e566a0d6c4cbed74", 208 | "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74", 209 | "shasum": "" 210 | }, 211 | "require": { 212 | "php": "^7.2 || ^8.0" 213 | }, 214 | "type": "library", 215 | "autoload": { 216 | "classmap": [ 217 | "src/" 218 | ] 219 | }, 220 | "notification-url": "https://packagist.org/downloads/", 221 | "license": [ 222 | "BSD-3-Clause" 223 | ], 224 | "authors": [ 225 | { 226 | "name": "Arne Blankerts", 227 | "email": "arne@blankerts.de", 228 | "role": "Developer" 229 | }, 230 | { 231 | "name": "Sebastian Heuer", 232 | "email": "sebastian@phpeople.de", 233 | "role": "Developer" 234 | }, 235 | { 236 | "name": "Sebastian Bergmann", 237 | "email": "sebastian@phpunit.de", 238 | "role": "Developer" 239 | } 240 | ], 241 | "description": "Library for handling version information and constraints", 242 | "support": { 243 | "issues": "https://github.com/phar-io/version/issues", 244 | "source": "https://github.com/phar-io/version/tree/3.2.1" 245 | }, 246 | "time": "2022-02-21T01:04:05+00:00" 247 | }, 248 | { 249 | "name": "phpunit/php-code-coverage", 250 | "version": "dev-main", 251 | "source": { 252 | "type": "git", 253 | "url": "https://github.com/sebastianbergmann/php-code-coverage.git", 254 | "reference": "ebdffc9e09585dafa71b9bffcdb0a229d4704c45" 255 | }, 256 | "dist": { 257 | "type": "zip", 258 | "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/ebdffc9e09585dafa71b9bffcdb0a229d4704c45", 259 | "reference": "ebdffc9e09585dafa71b9bffcdb0a229d4704c45", 260 | "shasum": "" 261 | }, 262 | "require": { 263 | "ext-dom": "*", 264 | "ext-libxml": "*", 265 | "ext-xmlwriter": "*", 266 | "nikic/php-parser": "^5.1.0", 267 | "php": ">=8.2", 268 | "phpunit/php-file-iterator": "^5.0.1", 269 | "phpunit/php-text-template": "^4.0.1", 270 | "sebastian/code-unit-reverse-lookup": "^4.0.1", 271 | "sebastian/complexity": "^4.0.1", 272 | "sebastian/environment": "^7.2.0", 273 | "sebastian/lines-of-code": "^3.0.1", 274 | "sebastian/version": "^5.0.1", 275 | "theseer/tokenizer": "^1.2.3" 276 | }, 277 | "require-dev": { 278 | "phpunit/phpunit": "^11.0" 279 | }, 280 | "suggest": { 281 | "ext-pcov": "PHP extension that provides line coverage", 282 | "ext-xdebug": "PHP extension that provides line coverage as well as branch and path coverage" 283 | }, 284 | "default-branch": true, 285 | "type": "library", 286 | "extra": { 287 | "branch-alias": { 288 | "dev-main": "11.0.x-dev" 289 | } 290 | }, 291 | "autoload": { 292 | "classmap": [ 293 | "src/" 294 | ] 295 | }, 296 | "notification-url": "https://packagist.org/downloads/", 297 | "license": [ 298 | "BSD-3-Clause" 299 | ], 300 | "authors": [ 301 | { 302 | "name": "Sebastian Bergmann", 303 | "email": "sebastian@phpunit.de", 304 | "role": "lead" 305 | } 306 | ], 307 | "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.", 308 | "homepage": "https://github.com/sebastianbergmann/php-code-coverage", 309 | "keywords": [ 310 | "coverage", 311 | "testing", 312 | "xunit" 313 | ], 314 | "support": { 315 | "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", 316 | "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy", 317 | "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/11.0.6" 318 | }, 319 | "funding": [ 320 | { 321 | "url": "https://github.com/sebastianbergmann", 322 | "type": "github" 323 | } 324 | ], 325 | "time": "2024-08-22T04:37:56+00:00" 326 | }, 327 | { 328 | "name": "phpunit/php-file-iterator", 329 | "version": "5.1.0", 330 | "source": { 331 | "type": "git", 332 | "url": "https://github.com/sebastianbergmann/php-file-iterator.git", 333 | "reference": "118cfaaa8bc5aef3287bf315b6060b1174754af6" 334 | }, 335 | "dist": { 336 | "type": "zip", 337 | "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/118cfaaa8bc5aef3287bf315b6060b1174754af6", 338 | "reference": "118cfaaa8bc5aef3287bf315b6060b1174754af6", 339 | "shasum": "" 340 | }, 341 | "require": { 342 | "php": ">=8.2" 343 | }, 344 | "require-dev": { 345 | "phpunit/phpunit": "^11.0" 346 | }, 347 | "type": "library", 348 | "extra": { 349 | "branch-alias": { 350 | "dev-main": "5.0-dev" 351 | } 352 | }, 353 | "autoload": { 354 | "classmap": [ 355 | "src/" 356 | ] 357 | }, 358 | "notification-url": "https://packagist.org/downloads/", 359 | "license": [ 360 | "BSD-3-Clause" 361 | ], 362 | "authors": [ 363 | { 364 | "name": "Sebastian Bergmann", 365 | "email": "sebastian@phpunit.de", 366 | "role": "lead" 367 | } 368 | ], 369 | "description": "FilterIterator implementation that filters files based on a list of suffixes.", 370 | "homepage": "https://github.com/sebastianbergmann/php-file-iterator/", 371 | "keywords": [ 372 | "filesystem", 373 | "iterator" 374 | ], 375 | "support": { 376 | "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues", 377 | "security": "https://github.com/sebastianbergmann/php-file-iterator/security/policy", 378 | "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/5.1.0" 379 | }, 380 | "funding": [ 381 | { 382 | "url": "https://github.com/sebastianbergmann", 383 | "type": "github" 384 | } 385 | ], 386 | "time": "2024-08-27T05:02:59+00:00" 387 | }, 388 | { 389 | "name": "phpunit/php-invoker", 390 | "version": "dev-main", 391 | "source": { 392 | "type": "git", 393 | "url": "https://github.com/sebastianbergmann/php-invoker.git", 394 | "reference": "e74ab00a7757bad36291f8f40fcb4e4f68f4ab62" 395 | }, 396 | "dist": { 397 | "type": "zip", 398 | "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/e74ab00a7757bad36291f8f40fcb4e4f68f4ab62", 399 | "reference": "e74ab00a7757bad36291f8f40fcb4e4f68f4ab62", 400 | "shasum": "" 401 | }, 402 | "require": { 403 | "php": ">=8.2" 404 | }, 405 | "require-dev": { 406 | "ext-pcntl": "*", 407 | "phpunit/phpunit": "^11.0" 408 | }, 409 | "suggest": { 410 | "ext-pcntl": "*" 411 | }, 412 | "default-branch": true, 413 | "type": "library", 414 | "extra": { 415 | "branch-alias": { 416 | "dev-main": "5.0-dev" 417 | } 418 | }, 419 | "autoload": { 420 | "classmap": [ 421 | "src/" 422 | ] 423 | }, 424 | "notification-url": "https://packagist.org/downloads/", 425 | "license": [ 426 | "BSD-3-Clause" 427 | ], 428 | "authors": [ 429 | { 430 | "name": "Sebastian Bergmann", 431 | "email": "sebastian@phpunit.de", 432 | "role": "lead" 433 | } 434 | ], 435 | "description": "Invoke callables with a timeout", 436 | "homepage": "https://github.com/sebastianbergmann/php-invoker/", 437 | "keywords": [ 438 | "process" 439 | ], 440 | "support": { 441 | "issues": "https://github.com/sebastianbergmann/php-invoker/issues", 442 | "security": "https://github.com/sebastianbergmann/php-invoker/security/policy", 443 | "source": "https://github.com/sebastianbergmann/php-invoker/tree/main" 444 | }, 445 | "funding": [ 446 | { 447 | "url": "https://github.com/sebastianbergmann", 448 | "type": "github" 449 | } 450 | ], 451 | "time": "2024-07-17T06:35:19+00:00" 452 | }, 453 | { 454 | "name": "phpunit/php-text-template", 455 | "version": "dev-main", 456 | "source": { 457 | "type": "git", 458 | "url": "https://github.com/sebastianbergmann/php-text-template.git", 459 | "reference": "8fa45833790f371a8aded71ef663391c7c98f53d" 460 | }, 461 | "dist": { 462 | "type": "zip", 463 | "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/8fa45833790f371a8aded71ef663391c7c98f53d", 464 | "reference": "8fa45833790f371a8aded71ef663391c7c98f53d", 465 | "shasum": "" 466 | }, 467 | "require": { 468 | "php": ">=8.2" 469 | }, 470 | "require-dev": { 471 | "phpunit/phpunit": "^11.0" 472 | }, 473 | "default-branch": true, 474 | "type": "library", 475 | "extra": { 476 | "branch-alias": { 477 | "dev-main": "4.0-dev" 478 | } 479 | }, 480 | "autoload": { 481 | "classmap": [ 482 | "src/" 483 | ] 484 | }, 485 | "notification-url": "https://packagist.org/downloads/", 486 | "license": [ 487 | "BSD-3-Clause" 488 | ], 489 | "authors": [ 490 | { 491 | "name": "Sebastian Bergmann", 492 | "email": "sebastian@phpunit.de", 493 | "role": "lead" 494 | } 495 | ], 496 | "description": "Simple template engine.", 497 | "homepage": "https://github.com/sebastianbergmann/php-text-template/", 498 | "keywords": [ 499 | "template" 500 | ], 501 | "support": { 502 | "issues": "https://github.com/sebastianbergmann/php-text-template/issues", 503 | "security": "https://github.com/sebastianbergmann/php-text-template/security/policy", 504 | "source": "https://github.com/sebastianbergmann/php-text-template/tree/main" 505 | }, 506 | "funding": [ 507 | { 508 | "url": "https://github.com/sebastianbergmann", 509 | "type": "github" 510 | } 511 | ], 512 | "time": "2024-08-13T09:34:41+00:00" 513 | }, 514 | { 515 | "name": "phpunit/php-timer", 516 | "version": "dev-main", 517 | "source": { 518 | "type": "git", 519 | "url": "https://github.com/sebastianbergmann/php-timer.git", 520 | "reference": "5df799fe4111e872386df7e7f3db055a1a0f6852" 521 | }, 522 | "dist": { 523 | "type": "zip", 524 | "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/5df799fe4111e872386df7e7f3db055a1a0f6852", 525 | "reference": "5df799fe4111e872386df7e7f3db055a1a0f6852", 526 | "shasum": "" 527 | }, 528 | "require": { 529 | "php": ">=8.2" 530 | }, 531 | "require-dev": { 532 | "phpunit/phpunit": "^11.0" 533 | }, 534 | "default-branch": true, 535 | "type": "library", 536 | "extra": { 537 | "branch-alias": { 538 | "dev-main": "7.0-dev" 539 | } 540 | }, 541 | "autoload": { 542 | "classmap": [ 543 | "src/" 544 | ] 545 | }, 546 | "notification-url": "https://packagist.org/downloads/", 547 | "license": [ 548 | "BSD-3-Clause" 549 | ], 550 | "authors": [ 551 | { 552 | "name": "Sebastian Bergmann", 553 | "email": "sebastian@phpunit.de", 554 | "role": "lead" 555 | } 556 | ], 557 | "description": "Utility class for timing", 558 | "homepage": "https://github.com/sebastianbergmann/php-timer/", 559 | "keywords": [ 560 | "timer" 561 | ], 562 | "support": { 563 | "issues": "https://github.com/sebastianbergmann/php-timer/issues", 564 | "security": "https://github.com/sebastianbergmann/php-timer/security/policy", 565 | "source": "https://github.com/sebastianbergmann/php-timer/tree/main" 566 | }, 567 | "funding": [ 568 | { 569 | "url": "https://github.com/sebastianbergmann", 570 | "type": "github" 571 | } 572 | ], 573 | "time": "2024-08-13T09:36:07+00:00" 574 | }, 575 | { 576 | "name": "phpunit/phpunit", 577 | "version": "dev-main", 578 | "source": { 579 | "type": "git", 580 | "url": "https://github.com/sebastianbergmann/phpunit.git", 581 | "reference": "11bbe1268a8c3d3c8dda3a1bb4ae44cd61d08f8b" 582 | }, 583 | "dist": { 584 | "type": "zip", 585 | "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/11bbe1268a8c3d3c8dda3a1bb4ae44cd61d08f8b", 586 | "reference": "11bbe1268a8c3d3c8dda3a1bb4ae44cd61d08f8b", 587 | "shasum": "" 588 | }, 589 | "require": { 590 | "ext-dom": "*", 591 | "ext-json": "*", 592 | "ext-libxml": "*", 593 | "ext-mbstring": "*", 594 | "ext-xml": "*", 595 | "ext-xmlwriter": "*", 596 | "myclabs/deep-copy": "^1.12.0", 597 | "phar-io/manifest": "^2.0.4", 598 | "phar-io/version": "^3.2.1", 599 | "php": ">=8.2", 600 | "phpunit/php-code-coverage": "^11.0.6", 601 | "phpunit/php-file-iterator": "^5.1.0", 602 | "phpunit/php-invoker": "^5.0.1", 603 | "phpunit/php-text-template": "^4.0.1", 604 | "phpunit/php-timer": "^7.0.1", 605 | "sebastian/cli-parser": "^3.0.2", 606 | "sebastian/code-unit": "^3.0.1", 607 | "sebastian/comparator": "^6.0.2", 608 | "sebastian/diff": "^6.0.2", 609 | "sebastian/environment": "^7.2.0", 610 | "sebastian/exporter": "^6.1.3", 611 | "sebastian/global-state": "^7.0.2", 612 | "sebastian/object-enumerator": "^6.0.1", 613 | "sebastian/type": "^5.0.1", 614 | "sebastian/version": "^5.0.1" 615 | }, 616 | "suggest": { 617 | "ext-soap": "To be able to generate mocks based on WSDL files" 618 | }, 619 | "default-branch": true, 620 | "bin": [ 621 | "phpunit" 622 | ], 623 | "type": "library", 624 | "extra": { 625 | "branch-alias": { 626 | "dev-main": "11.4-dev" 627 | } 628 | }, 629 | "autoload": { 630 | "files": [ 631 | "src/Framework/Assert/Functions.php" 632 | ], 633 | "classmap": [ 634 | "src/" 635 | ] 636 | }, 637 | "notification-url": "https://packagist.org/downloads/", 638 | "license": [ 639 | "BSD-3-Clause" 640 | ], 641 | "authors": [ 642 | { 643 | "name": "Sebastian Bergmann", 644 | "email": "sebastian@phpunit.de", 645 | "role": "lead" 646 | } 647 | ], 648 | "description": "The PHP Unit Testing framework.", 649 | "homepage": "https://phpunit.de/", 650 | "keywords": [ 651 | "phpunit", 652 | "testing", 653 | "xunit" 654 | ], 655 | "support": { 656 | "issues": "https://github.com/sebastianbergmann/phpunit/issues", 657 | "security": "https://github.com/sebastianbergmann/phpunit/security/policy", 658 | "source": "https://github.com/sebastianbergmann/phpunit/tree/main" 659 | }, 660 | "funding": [ 661 | { 662 | "url": "https://phpunit.de/sponsors.html", 663 | "type": "custom" 664 | }, 665 | { 666 | "url": "https://github.com/sebastianbergmann", 667 | "type": "github" 668 | }, 669 | { 670 | "url": "https://tidelift.com/funding/github/packagist/phpunit/phpunit", 671 | "type": "tidelift" 672 | } 673 | ], 674 | "time": "2024-08-29T09:26:22+00:00" 675 | }, 676 | { 677 | "name": "sebastian/cli-parser", 678 | "version": "dev-main", 679 | "source": { 680 | "type": "git", 681 | "url": "https://github.com/sebastianbergmann/cli-parser.git", 682 | "reference": "80a2b6ef3b849ccdc7d1459f54d08eccb0df1b1b" 683 | }, 684 | "dist": { 685 | "type": "zip", 686 | "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/80a2b6ef3b849ccdc7d1459f54d08eccb0df1b1b", 687 | "reference": "80a2b6ef3b849ccdc7d1459f54d08eccb0df1b1b", 688 | "shasum": "" 689 | }, 690 | "require": { 691 | "php": ">=8.2" 692 | }, 693 | "require-dev": { 694 | "phpunit/phpunit": "^11.0" 695 | }, 696 | "default-branch": true, 697 | "type": "library", 698 | "extra": { 699 | "branch-alias": { 700 | "dev-main": "3.0-dev" 701 | } 702 | }, 703 | "autoload": { 704 | "classmap": [ 705 | "src/" 706 | ] 707 | }, 708 | "notification-url": "https://packagist.org/downloads/", 709 | "license": [ 710 | "BSD-3-Clause" 711 | ], 712 | "authors": [ 713 | { 714 | "name": "Sebastian Bergmann", 715 | "email": "sebastian@phpunit.de", 716 | "role": "lead" 717 | } 718 | ], 719 | "description": "Library for parsing CLI options", 720 | "homepage": "https://github.com/sebastianbergmann/cli-parser", 721 | "support": { 722 | "issues": "https://github.com/sebastianbergmann/cli-parser/issues", 723 | "security": "https://github.com/sebastianbergmann/cli-parser/security/policy", 724 | "source": "https://github.com/sebastianbergmann/cli-parser/tree/main" 725 | }, 726 | "funding": [ 727 | { 728 | "url": "https://github.com/sebastianbergmann", 729 | "type": "github" 730 | } 731 | ], 732 | "time": "2024-08-13T08:32:45+00:00" 733 | }, 734 | { 735 | "name": "sebastian/code-unit", 736 | "version": "dev-main", 737 | "source": { 738 | "type": "git", 739 | "url": "https://github.com/sebastianbergmann/code-unit.git", 740 | "reference": "576474f9bd38167e0cb5b0cc366ebd9ec0b153ab" 741 | }, 742 | "dist": { 743 | "type": "zip", 744 | "url": "https://api.github.com/repos/sebastianbergmann/code-unit/zipball/576474f9bd38167e0cb5b0cc366ebd9ec0b153ab", 745 | "reference": "576474f9bd38167e0cb5b0cc366ebd9ec0b153ab", 746 | "shasum": "" 747 | }, 748 | "require": { 749 | "php": ">=8.2" 750 | }, 751 | "require-dev": { 752 | "phpunit/phpunit": "^11.0" 753 | }, 754 | "default-branch": true, 755 | "type": "library", 756 | "extra": { 757 | "branch-alias": { 758 | "dev-main": "3.0-dev" 759 | } 760 | }, 761 | "autoload": { 762 | "classmap": [ 763 | "src/" 764 | ] 765 | }, 766 | "notification-url": "https://packagist.org/downloads/", 767 | "license": [ 768 | "BSD-3-Clause" 769 | ], 770 | "authors": [ 771 | { 772 | "name": "Sebastian Bergmann", 773 | "email": "sebastian@phpunit.de", 774 | "role": "lead" 775 | } 776 | ], 777 | "description": "Collection of value objects that represent the PHP code units", 778 | "homepage": "https://github.com/sebastianbergmann/code-unit", 779 | "support": { 780 | "issues": "https://github.com/sebastianbergmann/code-unit/issues", 781 | "security": "https://github.com/sebastianbergmann/code-unit/security/policy", 782 | "source": "https://github.com/sebastianbergmann/code-unit/tree/main" 783 | }, 784 | "funding": [ 785 | { 786 | "url": "https://github.com/sebastianbergmann", 787 | "type": "github" 788 | } 789 | ], 790 | "time": "2024-08-13T08:46:39+00:00" 791 | }, 792 | { 793 | "name": "sebastian/code-unit-reverse-lookup", 794 | "version": "dev-main", 795 | "source": { 796 | "type": "git", 797 | "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git", 798 | "reference": "92f1962643bed6cd4511f4064c0dc1c90cd478c8" 799 | }, 800 | "dist": { 801 | "type": "zip", 802 | "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/92f1962643bed6cd4511f4064c0dc1c90cd478c8", 803 | "reference": "92f1962643bed6cd4511f4064c0dc1c90cd478c8", 804 | "shasum": "" 805 | }, 806 | "require": { 807 | "php": ">=8.2" 808 | }, 809 | "require-dev": { 810 | "phpunit/phpunit": "^11.0" 811 | }, 812 | "default-branch": true, 813 | "type": "library", 814 | "extra": { 815 | "branch-alias": { 816 | "dev-main": "4.0-dev" 817 | } 818 | }, 819 | "autoload": { 820 | "classmap": [ 821 | "src/" 822 | ] 823 | }, 824 | "notification-url": "https://packagist.org/downloads/", 825 | "license": [ 826 | "BSD-3-Clause" 827 | ], 828 | "authors": [ 829 | { 830 | "name": "Sebastian Bergmann", 831 | "email": "sebastian@phpunit.de" 832 | } 833 | ], 834 | "description": "Looks up which function or method a line of code belongs to", 835 | "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/", 836 | "support": { 837 | "issues": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/issues", 838 | "security": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/security/policy", 839 | "source": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/tree/main" 840 | }, 841 | "funding": [ 842 | { 843 | "url": "https://github.com/sebastianbergmann", 844 | "type": "github" 845 | } 846 | ], 847 | "time": "2024-08-13T08:48:36+00:00" 848 | }, 849 | { 850 | "name": "sebastian/comparator", 851 | "version": "dev-main", 852 | "source": { 853 | "type": "git", 854 | "url": "https://github.com/sebastianbergmann/comparator.git", 855 | "reference": "33a9d77ad7df2399e67942dbbe0fb4e328f06b76" 856 | }, 857 | "dist": { 858 | "type": "zip", 859 | "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/33a9d77ad7df2399e67942dbbe0fb4e328f06b76", 860 | "reference": "33a9d77ad7df2399e67942dbbe0fb4e328f06b76", 861 | "shasum": "" 862 | }, 863 | "require": { 864 | "ext-dom": "*", 865 | "ext-mbstring": "*", 866 | "php": ">=8.2", 867 | "sebastian/diff": "^6.0", 868 | "sebastian/exporter": "^6.0" 869 | }, 870 | "require-dev": { 871 | "phpunit/phpunit": "^11.0" 872 | }, 873 | "default-branch": true, 874 | "type": "library", 875 | "extra": { 876 | "branch-alias": { 877 | "dev-main": "6.0-dev" 878 | } 879 | }, 880 | "autoload": { 881 | "classmap": [ 882 | "src/" 883 | ] 884 | }, 885 | "notification-url": "https://packagist.org/downloads/", 886 | "license": [ 887 | "BSD-3-Clause" 888 | ], 889 | "authors": [ 890 | { 891 | "name": "Sebastian Bergmann", 892 | "email": "sebastian@phpunit.de" 893 | }, 894 | { 895 | "name": "Jeff Welch", 896 | "email": "whatthejeff@gmail.com" 897 | }, 898 | { 899 | "name": "Volker Dusch", 900 | "email": "github@wallbash.com" 901 | }, 902 | { 903 | "name": "Bernhard Schussek", 904 | "email": "bschussek@2bepublished.at" 905 | } 906 | ], 907 | "description": "Provides the functionality to compare PHP values for equality", 908 | "homepage": "https://github.com/sebastianbergmann/comparator", 909 | "keywords": [ 910 | "comparator", 911 | "compare", 912 | "equality" 913 | ], 914 | "support": { 915 | "issues": "https://github.com/sebastianbergmann/comparator/issues", 916 | "security": "https://github.com/sebastianbergmann/comparator/security/policy", 917 | "source": "https://github.com/sebastianbergmann/comparator/tree/main" 918 | }, 919 | "funding": [ 920 | { 921 | "url": "https://github.com/sebastianbergmann", 922 | "type": "github" 923 | } 924 | ], 925 | "time": "2024-08-13T08:49:56+00:00" 926 | }, 927 | { 928 | "name": "sebastian/complexity", 929 | "version": "dev-main", 930 | "source": { 931 | "type": "git", 932 | "url": "https://github.com/sebastianbergmann/complexity.git", 933 | "reference": "8954c06fc9ba67e695b65ba17efb4c10a2edd063" 934 | }, 935 | "dist": { 936 | "type": "zip", 937 | "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/8954c06fc9ba67e695b65ba17efb4c10a2edd063", 938 | "reference": "8954c06fc9ba67e695b65ba17efb4c10a2edd063", 939 | "shasum": "" 940 | }, 941 | "require": { 942 | "nikic/php-parser": "^5.0", 943 | "php": ">=8.2" 944 | }, 945 | "require-dev": { 946 | "phpunit/phpunit": "^11.0" 947 | }, 948 | "default-branch": true, 949 | "type": "library", 950 | "extra": { 951 | "branch-alias": { 952 | "dev-main": "4.0-dev" 953 | } 954 | }, 955 | "autoload": { 956 | "classmap": [ 957 | "src/" 958 | ] 959 | }, 960 | "notification-url": "https://packagist.org/downloads/", 961 | "license": [ 962 | "BSD-3-Clause" 963 | ], 964 | "authors": [ 965 | { 966 | "name": "Sebastian Bergmann", 967 | "email": "sebastian@phpunit.de", 968 | "role": "lead" 969 | } 970 | ], 971 | "description": "Library for calculating the complexity of PHP code units", 972 | "homepage": "https://github.com/sebastianbergmann/complexity", 973 | "support": { 974 | "issues": "https://github.com/sebastianbergmann/complexity/issues", 975 | "security": "https://github.com/sebastianbergmann/complexity/security/policy", 976 | "source": "https://github.com/sebastianbergmann/complexity/tree/main" 977 | }, 978 | "funding": [ 979 | { 980 | "url": "https://github.com/sebastianbergmann", 981 | "type": "github" 982 | } 983 | ], 984 | "time": "2024-08-13T08:51:09+00:00" 985 | }, 986 | { 987 | "name": "sebastian/diff", 988 | "version": "dev-main", 989 | "source": { 990 | "type": "git", 991 | "url": "https://github.com/sebastianbergmann/diff.git", 992 | "reference": "6af04a161091265daf4791511c37158c837be274" 993 | }, 994 | "dist": { 995 | "type": "zip", 996 | "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/6af04a161091265daf4791511c37158c837be274", 997 | "reference": "6af04a161091265daf4791511c37158c837be274", 998 | "shasum": "" 999 | }, 1000 | "require": { 1001 | "php": ">=8.2" 1002 | }, 1003 | "require-dev": { 1004 | "phpunit/phpunit": "^11.0", 1005 | "symfony/process": "^4.2 || ^5" 1006 | }, 1007 | "default-branch": true, 1008 | "type": "library", 1009 | "extra": { 1010 | "branch-alias": { 1011 | "dev-main": "6.0-dev" 1012 | } 1013 | }, 1014 | "autoload": { 1015 | "classmap": [ 1016 | "src/" 1017 | ] 1018 | }, 1019 | "notification-url": "https://packagist.org/downloads/", 1020 | "license": [ 1021 | "BSD-3-Clause" 1022 | ], 1023 | "authors": [ 1024 | { 1025 | "name": "Sebastian Bergmann", 1026 | "email": "sebastian@phpunit.de" 1027 | }, 1028 | { 1029 | "name": "Kore Nordmann", 1030 | "email": "mail@kore-nordmann.de" 1031 | } 1032 | ], 1033 | "description": "Diff implementation", 1034 | "homepage": "https://github.com/sebastianbergmann/diff", 1035 | "keywords": [ 1036 | "diff", 1037 | "udiff", 1038 | "unidiff", 1039 | "unified diff" 1040 | ], 1041 | "support": { 1042 | "issues": "https://github.com/sebastianbergmann/diff/issues", 1043 | "security": "https://github.com/sebastianbergmann/diff/security/policy", 1044 | "source": "https://github.com/sebastianbergmann/diff/tree/main" 1045 | }, 1046 | "funding": [ 1047 | { 1048 | "url": "https://github.com/sebastianbergmann", 1049 | "type": "github" 1050 | } 1051 | ], 1052 | "time": "2024-08-21T07:25:16+00:00" 1053 | }, 1054 | { 1055 | "name": "sebastian/environment", 1056 | "version": "dev-main", 1057 | "source": { 1058 | "type": "git", 1059 | "url": "https://github.com/sebastianbergmann/environment.git", 1060 | "reference": "2eb35ad543ca38b89b37eda4bdc1983ac7e64dd0" 1061 | }, 1062 | "dist": { 1063 | "type": "zip", 1064 | "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/2eb35ad543ca38b89b37eda4bdc1983ac7e64dd0", 1065 | "reference": "2eb35ad543ca38b89b37eda4bdc1983ac7e64dd0", 1066 | "shasum": "" 1067 | }, 1068 | "require": { 1069 | "php": ">=8.2" 1070 | }, 1071 | "require-dev": { 1072 | "phpunit/phpunit": "^11.0" 1073 | }, 1074 | "suggest": { 1075 | "ext-posix": "*" 1076 | }, 1077 | "default-branch": true, 1078 | "type": "library", 1079 | "extra": { 1080 | "branch-alias": { 1081 | "dev-main": "7.2-dev" 1082 | } 1083 | }, 1084 | "autoload": { 1085 | "classmap": [ 1086 | "src/" 1087 | ] 1088 | }, 1089 | "notification-url": "https://packagist.org/downloads/", 1090 | "license": [ 1091 | "BSD-3-Clause" 1092 | ], 1093 | "authors": [ 1094 | { 1095 | "name": "Sebastian Bergmann", 1096 | "email": "sebastian@phpunit.de" 1097 | } 1098 | ], 1099 | "description": "Provides functionality to handle HHVM/PHP environments", 1100 | "homepage": "https://github.com/sebastianbergmann/environment", 1101 | "keywords": [ 1102 | "Xdebug", 1103 | "environment", 1104 | "hhvm" 1105 | ], 1106 | "support": { 1107 | "issues": "https://github.com/sebastianbergmann/environment/issues", 1108 | "security": "https://github.com/sebastianbergmann/environment/security/policy", 1109 | "source": "https://github.com/sebastianbergmann/environment/tree/main" 1110 | }, 1111 | "funding": [ 1112 | { 1113 | "url": "https://github.com/sebastianbergmann", 1114 | "type": "github" 1115 | } 1116 | ], 1117 | "time": "2024-08-13T08:54:06+00:00" 1118 | }, 1119 | { 1120 | "name": "sebastian/exporter", 1121 | "version": "dev-main", 1122 | "source": { 1123 | "type": "git", 1124 | "url": "https://github.com/sebastianbergmann/exporter.git", 1125 | "reference": "af335564dae7bab3c77fbe6e82759c5bdf46cdf0" 1126 | }, 1127 | "dist": { 1128 | "type": "zip", 1129 | "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/af335564dae7bab3c77fbe6e82759c5bdf46cdf0", 1130 | "reference": "af335564dae7bab3c77fbe6e82759c5bdf46cdf0", 1131 | "shasum": "" 1132 | }, 1133 | "require": { 1134 | "ext-mbstring": "*", 1135 | "php": ">=8.2", 1136 | "sebastian/recursion-context": "^6.0" 1137 | }, 1138 | "require-dev": { 1139 | "phpunit/phpunit": "^11.2" 1140 | }, 1141 | "default-branch": true, 1142 | "type": "library", 1143 | "extra": { 1144 | "branch-alias": { 1145 | "dev-main": "6.1-dev" 1146 | } 1147 | }, 1148 | "autoload": { 1149 | "classmap": [ 1150 | "src/" 1151 | ] 1152 | }, 1153 | "notification-url": "https://packagist.org/downloads/", 1154 | "license": [ 1155 | "BSD-3-Clause" 1156 | ], 1157 | "authors": [ 1158 | { 1159 | "name": "Sebastian Bergmann", 1160 | "email": "sebastian@phpunit.de" 1161 | }, 1162 | { 1163 | "name": "Jeff Welch", 1164 | "email": "whatthejeff@gmail.com" 1165 | }, 1166 | { 1167 | "name": "Volker Dusch", 1168 | "email": "github@wallbash.com" 1169 | }, 1170 | { 1171 | "name": "Adam Harvey", 1172 | "email": "aharvey@php.net" 1173 | }, 1174 | { 1175 | "name": "Bernhard Schussek", 1176 | "email": "bschussek@gmail.com" 1177 | } 1178 | ], 1179 | "description": "Provides the functionality to export PHP variables for visualization", 1180 | "homepage": "https://www.github.com/sebastianbergmann/exporter", 1181 | "keywords": [ 1182 | "export", 1183 | "exporter" 1184 | ], 1185 | "support": { 1186 | "issues": "https://github.com/sebastianbergmann/exporter/issues", 1187 | "security": "https://github.com/sebastianbergmann/exporter/security/policy", 1188 | "source": "https://github.com/sebastianbergmann/exporter/tree/main" 1189 | }, 1190 | "funding": [ 1191 | { 1192 | "url": "https://github.com/sebastianbergmann", 1193 | "type": "github" 1194 | } 1195 | ], 1196 | "time": "2024-08-13T08:55:40+00:00" 1197 | }, 1198 | { 1199 | "name": "sebastian/global-state", 1200 | "version": "dev-main", 1201 | "source": { 1202 | "type": "git", 1203 | "url": "https://github.com/sebastianbergmann/global-state.git", 1204 | "reference": "d00442e04e8ab6d9b95a7aa5da01aa1081483b2e" 1205 | }, 1206 | "dist": { 1207 | "type": "zip", 1208 | "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/d00442e04e8ab6d9b95a7aa5da01aa1081483b2e", 1209 | "reference": "d00442e04e8ab6d9b95a7aa5da01aa1081483b2e", 1210 | "shasum": "" 1211 | }, 1212 | "require": { 1213 | "php": ">=8.2", 1214 | "sebastian/object-reflector": "^4.0", 1215 | "sebastian/recursion-context": "^6.0" 1216 | }, 1217 | "require-dev": { 1218 | "ext-dom": "*", 1219 | "phpunit/phpunit": "^11.0" 1220 | }, 1221 | "default-branch": true, 1222 | "type": "library", 1223 | "extra": { 1224 | "branch-alias": { 1225 | "dev-main": "7.0-dev" 1226 | } 1227 | }, 1228 | "autoload": { 1229 | "classmap": [ 1230 | "src/" 1231 | ] 1232 | }, 1233 | "notification-url": "https://packagist.org/downloads/", 1234 | "license": [ 1235 | "BSD-3-Clause" 1236 | ], 1237 | "authors": [ 1238 | { 1239 | "name": "Sebastian Bergmann", 1240 | "email": "sebastian@phpunit.de" 1241 | } 1242 | ], 1243 | "description": "Snapshotting of global state", 1244 | "homepage": "https://www.github.com/sebastianbergmann/global-state", 1245 | "keywords": [ 1246 | "global state" 1247 | ], 1248 | "support": { 1249 | "issues": "https://github.com/sebastianbergmann/global-state/issues", 1250 | "security": "https://github.com/sebastianbergmann/global-state/security/policy", 1251 | "source": "https://github.com/sebastianbergmann/global-state/tree/main" 1252 | }, 1253 | "funding": [ 1254 | { 1255 | "url": "https://github.com/sebastianbergmann", 1256 | "type": "github" 1257 | } 1258 | ], 1259 | "time": "2024-08-13T08:56:37+00:00" 1260 | }, 1261 | { 1262 | "name": "sebastian/lines-of-code", 1263 | "version": "dev-main", 1264 | "source": { 1265 | "type": "git", 1266 | "url": "https://github.com/sebastianbergmann/lines-of-code.git", 1267 | "reference": "0b35938b56a0eebf3f09e5525bfaefab5f085b82" 1268 | }, 1269 | "dist": { 1270 | "type": "zip", 1271 | "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/0b35938b56a0eebf3f09e5525bfaefab5f085b82", 1272 | "reference": "0b35938b56a0eebf3f09e5525bfaefab5f085b82", 1273 | "shasum": "" 1274 | }, 1275 | "require": { 1276 | "nikic/php-parser": "^5.0", 1277 | "php": ">=8.2" 1278 | }, 1279 | "require-dev": { 1280 | "phpunit/phpunit": "^11.0" 1281 | }, 1282 | "default-branch": true, 1283 | "type": "library", 1284 | "extra": { 1285 | "branch-alias": { 1286 | "dev-main": "3.0-dev" 1287 | } 1288 | }, 1289 | "autoload": { 1290 | "classmap": [ 1291 | "src/" 1292 | ] 1293 | }, 1294 | "notification-url": "https://packagist.org/downloads/", 1295 | "license": [ 1296 | "BSD-3-Clause" 1297 | ], 1298 | "authors": [ 1299 | { 1300 | "name": "Sebastian Bergmann", 1301 | "email": "sebastian@phpunit.de", 1302 | "role": "lead" 1303 | } 1304 | ], 1305 | "description": "Library for counting the lines of code in PHP source code", 1306 | "homepage": "https://github.com/sebastianbergmann/lines-of-code", 1307 | "support": { 1308 | "issues": "https://github.com/sebastianbergmann/lines-of-code/issues", 1309 | "security": "https://github.com/sebastianbergmann/lines-of-code/security/policy", 1310 | "source": "https://github.com/sebastianbergmann/lines-of-code/tree/main" 1311 | }, 1312 | "funding": [ 1313 | { 1314 | "url": "https://github.com/sebastianbergmann", 1315 | "type": "github" 1316 | } 1317 | ], 1318 | "time": "2024-08-13T08:58:02+00:00" 1319 | }, 1320 | { 1321 | "name": "sebastian/object-enumerator", 1322 | "version": "dev-main", 1323 | "source": { 1324 | "type": "git", 1325 | "url": "https://github.com/sebastianbergmann/object-enumerator.git", 1326 | "reference": "d3a93120467b7a2ab78dab2bca8bfb486cdb66c4" 1327 | }, 1328 | "dist": { 1329 | "type": "zip", 1330 | "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/d3a93120467b7a2ab78dab2bca8bfb486cdb66c4", 1331 | "reference": "d3a93120467b7a2ab78dab2bca8bfb486cdb66c4", 1332 | "shasum": "" 1333 | }, 1334 | "require": { 1335 | "php": ">=8.2", 1336 | "sebastian/object-reflector": "^4.0", 1337 | "sebastian/recursion-context": "^6.0" 1338 | }, 1339 | "require-dev": { 1340 | "phpunit/phpunit": "^11.0" 1341 | }, 1342 | "default-branch": true, 1343 | "type": "library", 1344 | "extra": { 1345 | "branch-alias": { 1346 | "dev-main": "6.0-dev" 1347 | } 1348 | }, 1349 | "autoload": { 1350 | "classmap": [ 1351 | "src/" 1352 | ] 1353 | }, 1354 | "notification-url": "https://packagist.org/downloads/", 1355 | "license": [ 1356 | "BSD-3-Clause" 1357 | ], 1358 | "authors": [ 1359 | { 1360 | "name": "Sebastian Bergmann", 1361 | "email": "sebastian@phpunit.de" 1362 | } 1363 | ], 1364 | "description": "Traverses array structures and object graphs to enumerate all referenced objects", 1365 | "homepage": "https://github.com/sebastianbergmann/object-enumerator/", 1366 | "support": { 1367 | "issues": "https://github.com/sebastianbergmann/object-enumerator/issues", 1368 | "security": "https://github.com/sebastianbergmann/object-enumerator/security/policy", 1369 | "source": "https://github.com/sebastianbergmann/object-enumerator/tree/main" 1370 | }, 1371 | "funding": [ 1372 | { 1373 | "url": "https://github.com/sebastianbergmann", 1374 | "type": "github" 1375 | } 1376 | ], 1377 | "time": "2024-08-13T09:28:02+00:00" 1378 | }, 1379 | { 1380 | "name": "sebastian/object-reflector", 1381 | "version": "dev-main", 1382 | "source": { 1383 | "type": "git", 1384 | "url": "https://github.com/sebastianbergmann/object-reflector.git", 1385 | "reference": "1798c980c92eca58f241dcd4526ae150734a3500" 1386 | }, 1387 | "dist": { 1388 | "type": "zip", 1389 | "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/1798c980c92eca58f241dcd4526ae150734a3500", 1390 | "reference": "1798c980c92eca58f241dcd4526ae150734a3500", 1391 | "shasum": "" 1392 | }, 1393 | "require": { 1394 | "php": ">=8.2" 1395 | }, 1396 | "require-dev": { 1397 | "phpunit/phpunit": "^11.0" 1398 | }, 1399 | "default-branch": true, 1400 | "type": "library", 1401 | "extra": { 1402 | "branch-alias": { 1403 | "dev-main": "4.0-dev" 1404 | } 1405 | }, 1406 | "autoload": { 1407 | "classmap": [ 1408 | "src/" 1409 | ] 1410 | }, 1411 | "notification-url": "https://packagist.org/downloads/", 1412 | "license": [ 1413 | "BSD-3-Clause" 1414 | ], 1415 | "authors": [ 1416 | { 1417 | "name": "Sebastian Bergmann", 1418 | "email": "sebastian@phpunit.de" 1419 | } 1420 | ], 1421 | "description": "Allows reflection of object attributes, including inherited and non-public ones", 1422 | "homepage": "https://github.com/sebastianbergmann/object-reflector/", 1423 | "support": { 1424 | "issues": "https://github.com/sebastianbergmann/object-reflector/issues", 1425 | "security": "https://github.com/sebastianbergmann/object-reflector/security/policy", 1426 | "source": "https://github.com/sebastianbergmann/object-reflector/tree/main" 1427 | }, 1428 | "funding": [ 1429 | { 1430 | "url": "https://github.com/sebastianbergmann", 1431 | "type": "github" 1432 | } 1433 | ], 1434 | "time": "2024-08-13T09:31:16+00:00" 1435 | }, 1436 | { 1437 | "name": "sebastian/recursion-context", 1438 | "version": "dev-main", 1439 | "source": { 1440 | "type": "git", 1441 | "url": "https://github.com/sebastianbergmann/recursion-context.git", 1442 | "reference": "4e2444033483855972dabcfb1981c09fdb8f5ab2" 1443 | }, 1444 | "dist": { 1445 | "type": "zip", 1446 | "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/4e2444033483855972dabcfb1981c09fdb8f5ab2", 1447 | "reference": "4e2444033483855972dabcfb1981c09fdb8f5ab2", 1448 | "shasum": "" 1449 | }, 1450 | "require": { 1451 | "php": ">=8.2" 1452 | }, 1453 | "require-dev": { 1454 | "phpunit/phpunit": "^11.0" 1455 | }, 1456 | "default-branch": true, 1457 | "type": "library", 1458 | "extra": { 1459 | "branch-alias": { 1460 | "dev-main": "6.0-dev" 1461 | } 1462 | }, 1463 | "autoload": { 1464 | "classmap": [ 1465 | "src/" 1466 | ] 1467 | }, 1468 | "notification-url": "https://packagist.org/downloads/", 1469 | "license": [ 1470 | "BSD-3-Clause" 1471 | ], 1472 | "authors": [ 1473 | { 1474 | "name": "Sebastian Bergmann", 1475 | "email": "sebastian@phpunit.de" 1476 | }, 1477 | { 1478 | "name": "Jeff Welch", 1479 | "email": "whatthejeff@gmail.com" 1480 | }, 1481 | { 1482 | "name": "Adam Harvey", 1483 | "email": "aharvey@php.net" 1484 | } 1485 | ], 1486 | "description": "Provides functionality to recursively process PHP variables", 1487 | "homepage": "https://github.com/sebastianbergmann/recursion-context", 1488 | "support": { 1489 | "issues": "https://github.com/sebastianbergmann/recursion-context/issues", 1490 | "security": "https://github.com/sebastianbergmann/recursion-context/security/policy", 1491 | "source": "https://github.com/sebastianbergmann/recursion-context/tree/main" 1492 | }, 1493 | "funding": [ 1494 | { 1495 | "url": "https://github.com/sebastianbergmann", 1496 | "type": "github" 1497 | } 1498 | ], 1499 | "time": "2024-08-13T09:36:42+00:00" 1500 | }, 1501 | { 1502 | "name": "sebastian/type", 1503 | "version": "dev-main", 1504 | "source": { 1505 | "type": "git", 1506 | "url": "https://github.com/sebastianbergmann/type.git", 1507 | "reference": "83156d9d27ce1cdef816fa4e6b5794facabed3dc" 1508 | }, 1509 | "dist": { 1510 | "type": "zip", 1511 | "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/83156d9d27ce1cdef816fa4e6b5794facabed3dc", 1512 | "reference": "83156d9d27ce1cdef816fa4e6b5794facabed3dc", 1513 | "shasum": "" 1514 | }, 1515 | "require": { 1516 | "php": ">=8.2" 1517 | }, 1518 | "require-dev": { 1519 | "phpunit/phpunit": "^11.0" 1520 | }, 1521 | "default-branch": true, 1522 | "type": "library", 1523 | "extra": { 1524 | "branch-alias": { 1525 | "dev-main": "5.0-dev" 1526 | } 1527 | }, 1528 | "autoload": { 1529 | "classmap": [ 1530 | "src/" 1531 | ] 1532 | }, 1533 | "notification-url": "https://packagist.org/downloads/", 1534 | "license": [ 1535 | "BSD-3-Clause" 1536 | ], 1537 | "authors": [ 1538 | { 1539 | "name": "Sebastian Bergmann", 1540 | "email": "sebastian@phpunit.de", 1541 | "role": "lead" 1542 | } 1543 | ], 1544 | "description": "Collection of value objects that represent the types of the PHP type system", 1545 | "homepage": "https://github.com/sebastianbergmann/type", 1546 | "support": { 1547 | "issues": "https://github.com/sebastianbergmann/type/issues", 1548 | "security": "https://github.com/sebastianbergmann/type/security/policy", 1549 | "source": "https://github.com/sebastianbergmann/type/tree/main" 1550 | }, 1551 | "funding": [ 1552 | { 1553 | "url": "https://github.com/sebastianbergmann", 1554 | "type": "github" 1555 | } 1556 | ], 1557 | "time": "2024-08-13T09:37:11+00:00" 1558 | }, 1559 | { 1560 | "name": "sebastian/version", 1561 | "version": "dev-main", 1562 | "source": { 1563 | "type": "git", 1564 | "url": "https://github.com/sebastianbergmann/version.git", 1565 | "reference": "f63f82ca485b64b8db20bae6bc2817dc44cae324" 1566 | }, 1567 | "dist": { 1568 | "type": "zip", 1569 | "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/f63f82ca485b64b8db20bae6bc2817dc44cae324", 1570 | "reference": "f63f82ca485b64b8db20bae6bc2817dc44cae324", 1571 | "shasum": "" 1572 | }, 1573 | "require": { 1574 | "php": ">=8.2" 1575 | }, 1576 | "default-branch": true, 1577 | "type": "library", 1578 | "extra": { 1579 | "branch-alias": { 1580 | "dev-main": "5.0-dev" 1581 | } 1582 | }, 1583 | "autoload": { 1584 | "classmap": [ 1585 | "src/" 1586 | ] 1587 | }, 1588 | "notification-url": "https://packagist.org/downloads/", 1589 | "license": [ 1590 | "BSD-3-Clause" 1591 | ], 1592 | "authors": [ 1593 | { 1594 | "name": "Sebastian Bergmann", 1595 | "email": "sebastian@phpunit.de", 1596 | "role": "lead" 1597 | } 1598 | ], 1599 | "description": "Library that helps with managing the version number of Git-hosted PHP projects", 1600 | "homepage": "https://github.com/sebastianbergmann/version", 1601 | "support": { 1602 | "issues": "https://github.com/sebastianbergmann/version/issues", 1603 | "security": "https://github.com/sebastianbergmann/version/security/policy", 1604 | "source": "https://github.com/sebastianbergmann/version/tree/main" 1605 | }, 1606 | "funding": [ 1607 | { 1608 | "url": "https://github.com/sebastianbergmann", 1609 | "type": "github" 1610 | } 1611 | ], 1612 | "time": "2024-07-15T07:12:03+00:00" 1613 | }, 1614 | { 1615 | "name": "theseer/tokenizer", 1616 | "version": "1.2.3", 1617 | "source": { 1618 | "type": "git", 1619 | "url": "https://github.com/theseer/tokenizer.git", 1620 | "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2" 1621 | }, 1622 | "dist": { 1623 | "type": "zip", 1624 | "url": "https://api.github.com/repos/theseer/tokenizer/zipball/737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2", 1625 | "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2", 1626 | "shasum": "" 1627 | }, 1628 | "require": { 1629 | "ext-dom": "*", 1630 | "ext-tokenizer": "*", 1631 | "ext-xmlwriter": "*", 1632 | "php": "^7.2 || ^8.0" 1633 | }, 1634 | "type": "library", 1635 | "autoload": { 1636 | "classmap": [ 1637 | "src/" 1638 | ] 1639 | }, 1640 | "notification-url": "https://packagist.org/downloads/", 1641 | "license": [ 1642 | "BSD-3-Clause" 1643 | ], 1644 | "authors": [ 1645 | { 1646 | "name": "Arne Blankerts", 1647 | "email": "arne@blankerts.de", 1648 | "role": "Developer" 1649 | } 1650 | ], 1651 | "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", 1652 | "support": { 1653 | "issues": "https://github.com/theseer/tokenizer/issues", 1654 | "source": "https://github.com/theseer/tokenizer/tree/1.2.3" 1655 | }, 1656 | "funding": [ 1657 | { 1658 | "url": "https://github.com/theseer", 1659 | "type": "github" 1660 | } 1661 | ], 1662 | "time": "2024-03-03T12:36:25+00:00" 1663 | } 1664 | ], 1665 | "aliases": [], 1666 | "minimum-stability": "dev", 1667 | "stability-flags": { 1668 | "phpunit/phpunit": 20 1669 | }, 1670 | "prefer-stable": false, 1671 | "prefer-lowest": false, 1672 | "platform": { 1673 | "php": ">=8.2", 1674 | "ext-pdo": "*" 1675 | }, 1676 | "platform-dev": [], 1677 | "plugin-api-version": "2.6.0" 1678 | } 1679 | -------------------------------------------------------------------------------- /docker-compose.yaml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | # mysql: 4 | # image: mysql:latest 5 | # environment: 6 | # MYSQL_ROOT_PASSWORD: root 7 | # MYSQL_DATABASE: simplon 8 | # ports: 9 | # - "33306:33306" 10 | # volumes: 11 | # - mysql_data:/var/lib/mysql 12 | 13 | php: 14 | container_name: "php" 15 | build: 16 | context: .docker/php/ 17 | args: 18 | USER_ID: ${UID:-1000} 19 | GROUP_ID: ${GID:-1000} 20 | volumes: 21 | - ./:/app 22 | ports: 23 | - "8000:8000" 24 | # depends_on: 25 | # - mysql 26 | 27 | volumes: 28 | mysql_data: 29 | -------------------------------------------------------------------------------- /identifier.sqlite: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fightbulc/simplon_mysql/6f52341ae16890671fa50b9bff5057054e9ffe83/identifier.sqlite -------------------------------------------------------------------------------- /src/AbstractBuilder.php: -------------------------------------------------------------------------------- 1 | conditions; 22 | } 23 | 24 | /** 25 | * @param string $key 26 | * @param mixed $val 27 | * @param string $operator 28 | * 29 | * @return static 30 | */ 31 | public function addCondition(string $key, $val, string $operator = '=') 32 | { 33 | $this->conditions[$key] = [ 34 | 'value' => $val, 35 | 'operator' => $operator, 36 | ]; 37 | 38 | return $this; 39 | } 40 | 41 | /** 42 | * @param array $conditions 43 | * 44 | * @return static 45 | */ 46 | public function setConditions(array $conditions) 47 | { 48 | foreach ($conditions as $key => $value) 49 | { 50 | $this->addCondition($key, $value); 51 | } 52 | 53 | return $this; 54 | } 55 | 56 | /** 57 | * @return null|string 58 | */ 59 | public function getCondsQuery(): ?string 60 | { 61 | return $this->condsQuery; 62 | } 63 | 64 | /** 65 | * @param string $condsQuery 66 | * 67 | * @return static 68 | */ 69 | public function setCondsQuery(string $condsQuery) 70 | { 71 | $this->condsQuery = $condsQuery; 72 | 73 | return $this; 74 | } 75 | } -------------------------------------------------------------------------------- /src/CondsQueryBuild.php: -------------------------------------------------------------------------------- 1 | strippedConds = $strippedConds; 23 | $this->condPairs = $condPairs; 24 | } 25 | 26 | /** 27 | * @return array 28 | */ 29 | public function getStrippedConds(): array 30 | { 31 | return $this->strippedConds; 32 | } 33 | 34 | /** 35 | * @return array 36 | */ 37 | public function getCondPairs(): array 38 | { 39 | return $this->condPairs; 40 | } 41 | } -------------------------------------------------------------------------------- /src/CreateQueryBuilder.php: -------------------------------------------------------------------------------- 1 | model; 30 | } 31 | 32 | /** 33 | * @param CrudModelInterface $model 34 | * 35 | * @return CreateQueryBuilder 36 | */ 37 | public function setModel(CrudModelInterface $model): self 38 | { 39 | $this->model = $model; 40 | 41 | return $this; 42 | } 43 | 44 | /** 45 | * @return string 46 | */ 47 | public function getTableName(): string 48 | { 49 | return $this->tableName; 50 | } 51 | 52 | /** 53 | * @param string $tableName 54 | * 55 | * @return CreateQueryBuilder 56 | */ 57 | public function setTableName(string $tableName): self 58 | { 59 | $this->tableName = $tableName; 60 | 61 | return $this; 62 | } 63 | 64 | /** 65 | * @return boolean 66 | */ 67 | public function isInsertIgnore(): bool 68 | { 69 | return $this->insertIgnore; 70 | } 71 | 72 | /** 73 | * @return CreateQueryBuilder 74 | */ 75 | public function setInsertIgnore(): self 76 | { 77 | $this->insertIgnore = true; 78 | 79 | return $this; 80 | } 81 | 82 | /** 83 | * @return array 84 | */ 85 | public function getData(): array 86 | { 87 | if ($this->getModel() instanceof CrudModelInterface) 88 | { 89 | return $this->getModel()->toArray(); 90 | } 91 | 92 | return $this->data; 93 | } 94 | 95 | /** 96 | * @param array $data 97 | * 98 | * @return CreateQueryBuilder 99 | */ 100 | public function setData(array $data): self 101 | { 102 | $this->data = $data; 103 | 104 | return $this; 105 | } 106 | } -------------------------------------------------------------------------------- /src/CrudManager.php: -------------------------------------------------------------------------------- 1 | mysql = $mysql; 18 | } 19 | 20 | /** 21 | * @return Mysql 22 | */ 23 | public function getMysql(): Mysql 24 | { 25 | return $this->mysql; 26 | } 27 | 28 | /** 29 | * @param CreateQueryBuilder $builder 30 | * 31 | * @return CrudModelInterface 32 | * @throws MysqlException 33 | */ 34 | public function create(CreateQueryBuilder $builder): CrudModelInterface 35 | { 36 | $builder->getModel()->beforeSave(); 37 | 38 | $insertId = $this->getMysql()->insert( 39 | $builder->getTableName(), 40 | $builder->getData(), 41 | $builder->isInsertIgnore() 42 | ) 43 | ; 44 | 45 | if ($insertId === false) 46 | { 47 | throw new MysqlException('Could not create dataset'); 48 | } 49 | 50 | if (is_bool($insertId) !== true && method_exists($builder->getModel(), 'setId')) 51 | { 52 | $builder->getModel()->setId($insertId); 53 | } 54 | 55 | return $builder->getModel(); 56 | } 57 | 58 | /** 59 | * @param ReadQueryBuilder $builder 60 | * 61 | * @return null|MysqlQueryIterator 62 | * @throws MysqlException 63 | */ 64 | public function read(ReadQueryBuilder $builder): ?MysqlQueryIterator 65 | { 66 | return $this 67 | ->getMysql() 68 | ->fetchRowManyCursor( 69 | $builder->renderQuery(), 70 | $this->filterCondsNullValues( 71 | $this->filterCondsValues($builder->getConditions()) 72 | ) 73 | ) 74 | ; 75 | } 76 | 77 | /** 78 | * @param ReadQueryBuilder $builder 79 | * 80 | * @return array|null 81 | * @throws MysqlException 82 | */ 83 | public function readOne(ReadQueryBuilder $builder): ?array 84 | { 85 | return $this 86 | ->getMysql() 87 | ->fetchRow( 88 | $builder->renderQuery(), 89 | $this->filterCondsNullValues( 90 | $this->filterCondsValues($builder->getConditions()) 91 | ) 92 | ) 93 | ; 94 | } 95 | 96 | /** 97 | * @param UpdateQueryBuilder $builder 98 | * 99 | * @return CrudModelInterface 100 | * @throws MysqlException 101 | */ 102 | public function update(UpdateQueryBuilder $builder): CrudModelInterface 103 | { 104 | $builder->getModel()->beforeUpdate(); 105 | 106 | $conds = []; 107 | $condsQuery = null; 108 | 109 | if (!empty($builder->getConditions())) 110 | { 111 | $condsQuery = $this->buildCondsQuery($builder->getConditions(), $builder->getCondsQuery()); 112 | $conds = $this->filterCondsNullValues( 113 | $this->filterCondsValues($builder->getConditions()) 114 | ); 115 | } 116 | 117 | $this->getMysql()->update( 118 | $builder->getTableName(), 119 | $conds, 120 | $builder->getData(), 121 | $condsQuery 122 | ) 123 | ; 124 | 125 | return $builder->getModel(); 126 | } 127 | 128 | /** 129 | * @param DeleteQueryBuilder $builder 130 | * 131 | * @return bool 132 | * @throws MysqlException 133 | */ 134 | public function delete(DeleteQueryBuilder $builder): bool 135 | { 136 | $conds = []; 137 | $condsQuery = null; 138 | 139 | if (!empty($builder->getConditions())) 140 | { 141 | $condsQuery = $this->buildCondsQuery($builder->getConditions(), $builder->getCondsQuery()); 142 | $conds = $this->filterCondsNullValues( 143 | $this->filterCondsValues($builder->getConditions()) 144 | ); 145 | } 146 | 147 | return $this->getMysql()->delete($builder->getTableName(), $conds, $condsQuery); 148 | } 149 | 150 | /** 151 | * @param array $conds 152 | * @param null|string $condsQuery 153 | * 154 | * @return string 155 | */ 156 | private function buildCondsQuery(array $conds, ?string $condsQuery = null): string 157 | { 158 | if ($condsQuery !== null) 159 | { 160 | return (string)$condsQuery; 161 | } 162 | 163 | $condsQueryBuild = QueryUtil::buildCondsQuery($conds); 164 | 165 | return join(' AND ', $condsQueryBuild->getCondPairs()); 166 | } 167 | 168 | /** 169 | * @param array $conds 170 | * 171 | * @return array 172 | */ 173 | private function filterCondsValues(array $conds): array 174 | { 175 | $values = []; 176 | 177 | foreach ($conds as $key => $data) 178 | { 179 | $values[$key] = $data['value']; 180 | } 181 | 182 | return $values; 183 | } 184 | 185 | /** 186 | * @param array $filteredCondsValues 187 | * 188 | * @return array 189 | */ 190 | private function filterCondsNullValues(array $filteredCondsValues): array 191 | { 192 | $new = []; 193 | 194 | foreach ($filteredCondsValues as $key => $value) 195 | { 196 | if ($value !== null) 197 | { 198 | $new[$key] = $value; 199 | } 200 | } 201 | 202 | return $new; 203 | } 204 | } -------------------------------------------------------------------------------- /src/CrudModel.php: -------------------------------------------------------------------------------- 1 | fromArray($data, false); 14 | } 15 | 16 | /** 17 | * @return static 18 | */ 19 | public function beforeSave(): static 20 | { 21 | return $this; 22 | } 23 | 24 | /** 25 | * @return static 26 | */ 27 | public function beforeUpdate(): static 28 | { 29 | return $this; 30 | } 31 | } -------------------------------------------------------------------------------- /src/CrudModelInterface.php: -------------------------------------------------------------------------------- 1 | crudManager = new CrudManager($mysql); 30 | } 31 | 32 | /** 33 | * @return CrudManager 34 | */ 35 | public function getCrudManager(): CrudManager 36 | { 37 | return $this->crudManager; 38 | } 39 | 40 | /** 41 | * @param callable $callable 42 | * 43 | * @return static 44 | */ 45 | public function setAfterCreateBehaviour(callable $callable) 46 | { 47 | $this->afterCreateBehaviour = $callable; 48 | 49 | return $this; 50 | } 51 | 52 | /** 53 | * @param callable $callable 54 | * 55 | * @return static 56 | */ 57 | public function setAfterUpdateBehaviour(callable $callable) 58 | { 59 | $this->afterUpdateBehaviour = $callable; 60 | 61 | return $this; 62 | } 63 | 64 | /** 65 | * @param callable $callable 66 | * 67 | * @return static 68 | */ 69 | public function setAfterDeleteBehaviour(callable $callable) 70 | { 71 | $this->afterDeleteBehaviour = $callable; 72 | 73 | return $this; 74 | } 75 | 76 | /** 77 | * @param CreateQueryBuilder $builder 78 | * 79 | * @return CrudModelInterface 80 | * @throws MysqlException 81 | */ 82 | protected function crudCreate(CreateQueryBuilder $builder): CrudModelInterface 83 | { 84 | $model = $this->crudManager->create( 85 | $builder->setTableName($this->getTableName()) 86 | ); 87 | 88 | $this->runBehaviour($model, $this->afterCreateBehaviour); 89 | 90 | return $model; 91 | } 92 | 93 | /** 94 | * @param ReadQueryBuilder|null $builder 95 | * 96 | * @return CrudModelInterface[]|null 97 | * @throws MysqlException 98 | */ 99 | protected function crudRead(?ReadQueryBuilder $builder = null): ?array 100 | { 101 | if (!$builder) 102 | { 103 | $builder = new ReadQueryBuilder(); 104 | } 105 | 106 | $response = $this->crudManager->read( 107 | $builder->setFrom($this->getTableName()) 108 | ); 109 | 110 | if ($response) 111 | { 112 | $models = []; 113 | 114 | foreach ($response as $row) 115 | { 116 | $models[] = $this->getModel()->fromArray($row); 117 | } 118 | 119 | return $models; 120 | } 121 | 122 | return null; 123 | } 124 | 125 | /** 126 | * @param ReadQueryBuilder $builder 127 | * 128 | * @return null|CrudModelInterface 129 | * @throws MysqlException 130 | */ 131 | protected function crudReadOne(ReadQueryBuilder $builder): ?CrudModelInterface 132 | { 133 | $response = $this->crudManager->readOne( 134 | $builder->setFrom($this->getTableName()) 135 | ); 136 | 137 | if ($response) 138 | { 139 | return $this->getModel()->fromArray($response); 140 | } 141 | 142 | return null; 143 | } 144 | 145 | /** 146 | * @param UpdateQueryBuilder $builder 147 | * 148 | * @return CrudModelInterface 149 | * @throws MysqlException 150 | */ 151 | protected function crudUpdate(UpdateQueryBuilder $builder): CrudModelInterface 152 | { 153 | if ($builder->getModel()->isChanged()) 154 | { 155 | $model = $this->crudManager->update( 156 | $this->buildIdConditionFallback( 157 | $builder->setTableName($this->getTableName()) 158 | ) 159 | ); 160 | 161 | $this->runBehaviour($model, $this->afterUpdateBehaviour); 162 | 163 | return $model; 164 | } 165 | 166 | return $builder->getModel(); 167 | } 168 | 169 | /** 170 | * @param DeleteQueryBuilder $builder 171 | * 172 | * @return bool 173 | * @throws MysqlException 174 | */ 175 | protected function crudDelete(DeleteQueryBuilder $builder): bool 176 | { 177 | $response = $this->crudManager->delete( 178 | $this->buildIdConditionFallback( 179 | $builder->setTableName($this->getTableName()) 180 | ) 181 | ); 182 | 183 | if ($response && $builder->getModel()) 184 | { 185 | $this->runBehaviour($builder->getModel(), $this->afterDeleteBehaviour); 186 | } 187 | 188 | return $response; 189 | } 190 | 191 | /** 192 | * @param UpdateQueryBuilder|DeleteQueryBuilder $builder 193 | * 194 | * @return UpdateQueryBuilder|DeleteQueryBuilder 195 | */ 196 | private function buildIdConditionFallback($builder) 197 | { 198 | if (!$builder->getConditions()) 199 | { 200 | if (method_exists($builder->getModel(), 'getId')) 201 | { 202 | $builder->addCondition('id', $builder->getModel()->getId()); 203 | } 204 | } 205 | 206 | return $builder; 207 | } 208 | 209 | /** 210 | * @param CrudModelInterface $model 211 | * @param null|callable $behaviour 212 | */ 213 | private function runBehaviour(CrudModelInterface $model, ?callable $behaviour = null): void 214 | { 215 | if ($behaviour) 216 | { 217 | $behaviour($model); 218 | } 219 | } 220 | } -------------------------------------------------------------------------------- /src/CrudStoreInterface.php: -------------------------------------------------------------------------------- 1 | fromArray($data, false); 20 | } 21 | 22 | $this->internalChecksum = $this->calcMd5($this->toRawArray()); 23 | } 24 | 25 | /** 26 | * @return bool 27 | */ 28 | public function isChanged(): bool 29 | { 30 | return $this->internalChecksum !== $this->calcMd5($this->toRawArray()); 31 | } 32 | 33 | /** 34 | * @param array $data 35 | * @param bool $buildChecksum Build checksum of data. Use FALSE in case you're rebuilding existing object. 36 | * 37 | * @return static 38 | */ 39 | public function fromArray(array $data, bool $buildChecksum = true): static 40 | { 41 | if ($data) 42 | { 43 | foreach ($data as $fieldName => $val) 44 | { 45 | // format field name 46 | if (str_contains($fieldName, '_')) 47 | { 48 | $fieldName = self::camelCaseString($fieldName); 49 | } 50 | 51 | $setMethodName = 'set' . ucfirst($fieldName); 52 | 53 | // set on setter 54 | if (method_exists($this, $setMethodName)) 55 | { 56 | $this->$setMethodName($val); 57 | continue; 58 | } 59 | 60 | // set on field 61 | if (property_exists($this, $fieldName)) 62 | { 63 | $this->$fieldName = $val; 64 | } 65 | } 66 | 67 | if ($buildChecksum) 68 | { 69 | $this->internalChecksum = $this->calcMd5($this->toRawArray()); 70 | } 71 | } 72 | 73 | return $this; 74 | } 75 | 76 | /** 77 | * @param bool $snakeCase 78 | * 79 | * @return array 80 | */ 81 | public function toArray(bool $snakeCase = true): array 82 | { 83 | $result = []; 84 | 85 | $visibleFields = get_class_vars(get_called_class()); 86 | 87 | // render column names 88 | foreach ($visibleFields as $fieldName => $value) 89 | { 90 | $propertyName = $fieldName; 91 | $getMethodName = 'get' . ucfirst($fieldName); 92 | 93 | // format field name 94 | if ($snakeCase === true && !str_contains($fieldName, '_')) 95 | { 96 | $fieldName = self::snakeCaseString($fieldName); 97 | } 98 | 99 | // get from getter 100 | if (method_exists($this, $getMethodName)) 101 | { 102 | $result[$fieldName] = $this->$getMethodName(); 103 | continue; 104 | } 105 | 106 | // get from field 107 | if (property_exists($this, $propertyName)) 108 | { 109 | if ($propertyName !== 'internalChecksum') 110 | { 111 | $result[$fieldName] = $this->$propertyName; 112 | } 113 | } 114 | } 115 | 116 | return $result; 117 | } 118 | 119 | /** 120 | * @param bool $snakeCase 121 | * 122 | * @return string 123 | */ 124 | public function toJson(bool $snakeCase = true): string 125 | { 126 | return json_encode( 127 | $this->toArray($snakeCase) 128 | ); 129 | } 130 | 131 | /** 132 | * @param string $json 133 | * @param bool $buildChecksum Build checksum of data. Use FALSE in case you're rebuilding existing object. 134 | * 135 | * @return static 136 | */ 137 | public function fromJson(string $json, bool $buildChecksum = true): static 138 | { 139 | return $this->fromArray(json_decode($json, true), $buildChecksum); 140 | } 141 | 142 | /** 143 | * @param string $string 144 | * 145 | * @return string 146 | */ 147 | protected static function snakeCaseString(string $string): string 148 | { 149 | return strtolower(preg_replace('/([A-Z1-9])/', '_\\1', $string)); 150 | } 151 | 152 | /** 153 | * @param string $string 154 | * 155 | * @return string 156 | */ 157 | protected static function camelCaseString(string $string): string 158 | { 159 | $string = strtolower($string); 160 | $string = ucwords(str_replace('_', ' ', $string)); 161 | 162 | return lcfirst(str_replace(' ', '', $string)); 163 | } 164 | 165 | /** 166 | * @param array $data 167 | * 168 | * @return string 169 | */ 170 | private function calcMd5(array $data): string 171 | { 172 | ksort($data); 173 | 174 | return md5(json_encode($data)); 175 | } 176 | 177 | /** 178 | * @param bool $snakeCase 179 | * 180 | * @return array 181 | */ 182 | private function toRawArray(bool $snakeCase = true): array 183 | { 184 | $result = []; 185 | 186 | $visibleFields = get_class_vars(get_called_class()); 187 | 188 | // render column names 189 | foreach ($visibleFields as $fieldName => $value) 190 | { 191 | $propertyName = $fieldName; 192 | 193 | // format field name 194 | if ($snakeCase === true && !str_contains($fieldName, '_')) 195 | { 196 | $fieldName = self::snakeCaseString($fieldName); 197 | } 198 | 199 | // get from field 200 | if (property_exists($this, $propertyName)) 201 | { 202 | if ($propertyName !== 'internalChecksum') 203 | { 204 | $value = null; 205 | 206 | if(isset($this->$propertyName)) { 207 | $value = $this->$propertyName; 208 | } 209 | 210 | $result[$fieldName] = $value; 211 | } 212 | } 213 | } 214 | 215 | return $result; 216 | } 217 | } -------------------------------------------------------------------------------- /src/DataInterface.php: -------------------------------------------------------------------------------- 1 | model; 28 | } 29 | 30 | /** 31 | * @param CrudModelInterface $model 32 | * 33 | * @return DeleteQueryBuilder 34 | */ 35 | public function setModel(CrudModelInterface $model): self 36 | { 37 | $this->model = $model; 38 | 39 | return $this; 40 | } 41 | 42 | /** 43 | * @return string 44 | */ 45 | public function getTableName(): string 46 | { 47 | return $this->tableName; 48 | } 49 | 50 | /** 51 | * @param string $tableName 52 | * 53 | * @return DeleteQueryBuilder 54 | */ 55 | public function setTableName(string $tableName): self 56 | { 57 | $this->tableName = $tableName; 58 | 59 | return $this; 60 | } 61 | 62 | /** 63 | * @return string 64 | */ 65 | public function getQuery(): string 66 | { 67 | return $this->query; 68 | } 69 | 70 | /** 71 | * @param string $query 72 | * 73 | * @return DeleteQueryBuilder 74 | */ 75 | public function setQuery(string $query): self 76 | { 77 | $this->query = $query; 78 | 79 | return $this; 80 | } 81 | } -------------------------------------------------------------------------------- /src/Mysql.php: -------------------------------------------------------------------------------- 1 | pdo = $pdo; 26 | } 27 | 28 | /** 29 | * @return Mysql 30 | */ 31 | public function close(): self 32 | { 33 | $this->pdo = null; 34 | 35 | return $this; 36 | } 37 | 38 | /** 39 | * @param int $fetchMode 40 | * 41 | * @return Mysql 42 | */ 43 | public function setFetchMode(int $fetchMode): self 44 | { 45 | $this->fetchMode = $fetchMode; 46 | 47 | return $this; 48 | } 49 | 50 | /** 51 | * @return int 52 | */ 53 | public function getRowCount(): int 54 | { 55 | if ($this->hasLastStatement() === false) 56 | { 57 | return 0; 58 | } 59 | 60 | return $this->getLastStatement()->rowCount(); 61 | } 62 | 63 | /** 64 | * @param string $query 65 | * 66 | * @return bool 67 | * @throws MysqlException 68 | */ 69 | public function executeSql(string $query): bool 70 | { 71 | $dbh = $this->getPdo(); 72 | 73 | $response = $dbh->exec($query); 74 | 75 | if ($response !== false) 76 | { 77 | return true; 78 | } 79 | 80 | $error = [ 81 | 'query' => $query, 82 | 'errorInfo' => $this->prepareErrorInfo($dbh->errorInfo()), 83 | ]; 84 | 85 | $errorInfo = json_encode($error); 86 | 87 | throw new MysqlException($errorInfo); 88 | } 89 | 90 | /** 91 | * @param string $dbName 92 | * 93 | * @return bool 94 | * @throws MysqlException 95 | */ 96 | public function selectDb(string $dbName): bool 97 | { 98 | return $this->executeSql('use ' . $dbName); 99 | } 100 | 101 | /** 102 | * @return bool 103 | */ 104 | public function transactionBegin(): bool 105 | { 106 | return $this->pdo->beginTransaction(); 107 | } 108 | 109 | /** 110 | * @return bool 111 | */ 112 | public function transactionCommit(): bool 113 | { 114 | return $this->pdo->commit(); 115 | } 116 | 117 | /** 118 | * @return bool 119 | */ 120 | public function transactionRollback(): bool 121 | { 122 | return $this->pdo->rollBack(); 123 | } 124 | 125 | /** 126 | * @param string $query 127 | * @param array $conds 128 | * 129 | * @return null|string 130 | * @throws MysqlException 131 | */ 132 | public function fetchColumn(string $query, array $conds = []): ?string 133 | { 134 | $response = $this->prepareSelect($query, $conds)->fetchColumn(); 135 | 136 | if ($response === false) 137 | { 138 | return null; 139 | } 140 | 141 | return (string)$response; 142 | } 143 | 144 | /** 145 | * @param string $query 146 | * @param array $conds 147 | * 148 | * @return array|null 149 | * @throws MysqlException 150 | */ 151 | public function fetchColumnMany(string $query, array $conds = []): ?array 152 | { 153 | $responsesMany = []; 154 | $pdoStatment = $this->prepareSelect($query, $conds); 155 | 156 | while ($response = $pdoStatment->fetchColumn()) 157 | { 158 | $responsesMany[] = $response; 159 | } 160 | 161 | if (empty($responsesMany)) 162 | { 163 | return null; 164 | } 165 | 166 | return (array)$responsesMany; 167 | } 168 | 169 | /** 170 | * @param string $query 171 | * @param array $conds 172 | * 173 | * @return null|MysqlQueryIterator 174 | * @throws MysqlException 175 | */ 176 | public function fetchColumnManyCursor(string $query, array $conds = []): ?MysqlQueryIterator 177 | { 178 | $this->prepareSelect($query, $conds); 179 | 180 | $cursor = new MysqlQueryIterator($this->getLastStatement(), 'fetchColumn'); 181 | 182 | if ($cursor === false) 183 | { 184 | return null; 185 | } 186 | 187 | return $cursor; 188 | } 189 | 190 | /** 191 | * @param string $query 192 | * @param array $conds 193 | * 194 | * @return array|null 195 | * @throws MysqlException 196 | */ 197 | public function fetchRow(string $query, array $conds = []): ?array 198 | { 199 | $response = $this->prepareSelect($query, $conds)->fetch($this->getFetchMode()); 200 | 201 | if ($response === false) 202 | { 203 | return null; 204 | } 205 | 206 | return $response; 207 | } 208 | 209 | /** 210 | * @param string $query 211 | * @param array $conds 212 | * 213 | * @return array|null 214 | * @throws MysqlException 215 | */ 216 | public function fetchRowMany(string $query, array $conds = []): ?array 217 | { 218 | $responsesMany = []; 219 | $pdoStatment = $this->prepareSelect($query, $conds); 220 | 221 | while ($response = $pdoStatment->fetch($this->getFetchMode())) 222 | { 223 | $responsesMany[] = $response; 224 | } 225 | 226 | if (empty($responsesMany)) 227 | { 228 | return null; 229 | } 230 | 231 | return (array)$responsesMany; 232 | } 233 | 234 | /** 235 | * @param string $query 236 | * @param array $conds 237 | * 238 | * @return null|MysqlQueryIterator 239 | * @throws MysqlException 240 | */ 241 | public function fetchRowManyCursor(string $query, array $conds = []): ?MysqlQueryIterator 242 | { 243 | $this->prepareSelect($query, $conds); 244 | 245 | $cursor = new MysqlQueryIterator($this->getLastStatement(), 'fetch'); 246 | 247 | if ($cursor === false) 248 | { 249 | return null; 250 | } 251 | 252 | return $cursor; 253 | } 254 | 255 | /** 256 | * @param string $tableName 257 | * @param array $data 258 | * @param bool $insertIgnore 259 | * 260 | * @return int|bool 261 | * @throws MysqlException 262 | */ 263 | public function insert(string $tableName, array $data, bool $insertIgnore = false) 264 | { 265 | if (isset($data[0])) 266 | { 267 | throw new MysqlException("Multi-dimensional datasets are not allowed. Use 'Mysql::insertMany()' instead"); 268 | } 269 | 270 | $response = $this->insertMany($tableName, [$data], $insertIgnore); 271 | 272 | if ($response === false) 273 | { 274 | return false; 275 | } 276 | 277 | return array_pop($response); 278 | } 279 | 280 | /** 281 | * @param string $tableName 282 | * @param array $data 283 | * @param bool $insertIgnore 284 | * 285 | * @return array|bool 286 | * @throws MysqlException 287 | */ 288 | public function insertMany(string $tableName, array $data, bool $insertIgnore = false) 289 | { 290 | if (!isset($data[0])) 291 | { 292 | throw new MysqlException("One-dimensional datasets are not allowed. Use 'Mysql::insert()' instead"); 293 | } 294 | 295 | $query = 'INSERT' . ($insertIgnore === true ? ' IGNORE ' : null) . ' INTO ' . $tableName . ' (:COLUMN_NAMES) VALUES (:PARAM_NAMES)'; 296 | 297 | $placeholder = [ 298 | 'column_names' => [], 299 | 'param_names' => [], 300 | ]; 301 | 302 | foreach ($data[0] as $columnName => $value) 303 | { 304 | $placeholder['column_names'][] = '`' . $columnName . '`'; 305 | $placeholder['param_names'][] = ':' . $columnName; 306 | } 307 | 308 | $query = str_replace(':COLUMN_NAMES', join(', ', $placeholder['column_names']), $query); 309 | $query = str_replace(':PARAM_NAMES', join(', ', $placeholder['param_names']), $query); 310 | 311 | $response = $this->prepareInsertReplace($query, $data); 312 | 313 | if (empty($response)) 314 | { 315 | return false; 316 | } 317 | 318 | return (array)$response; 319 | } 320 | 321 | /** 322 | * @param string $tableName 323 | * @param array $data 324 | * 325 | * @return array|bool 326 | * @throws MysqlException 327 | */ 328 | public function replace(string $tableName, array $data) 329 | { 330 | if (isset($data[0])) 331 | { 332 | throw new MysqlException("Multi-dimensional datasets are not allowed. Use 'Mysql::replaceMany()' instead"); 333 | } 334 | 335 | return $this->replaceMany($tableName, [$data]); 336 | } 337 | 338 | /** 339 | * @param string $tableName 340 | * @param array $data 341 | * 342 | * @return array|bool 343 | * @throws MysqlException 344 | */ 345 | public function replaceMany(string $tableName, array $data) 346 | { 347 | if (!isset($data[0])) 348 | { 349 | throw new MysqlException("One-dimensional datasets are not allowed. Use 'Mysql::replace()' instead"); 350 | } 351 | 352 | $query = 'REPLACE INTO ' . $tableName . ' (:COLUMN_NAMES) VALUES (:PARAM_NAMES)'; 353 | 354 | $placeholder = [ 355 | 'column_names' => [], 356 | 'param_names' => [], 357 | ]; 358 | 359 | foreach ($data[0] as $columnName => $value) 360 | { 361 | $placeholder['column_names'][] = '`' . $columnName . '`'; 362 | $placeholder['param_names'][] = ':' . $columnName; 363 | } 364 | 365 | $query = str_replace(':COLUMN_NAMES', join(', ', $placeholder['column_names']), $query); 366 | $query = str_replace(':PARAM_NAMES', join(', ', $placeholder['param_names']), $query); 367 | 368 | $response = $this->prepareInsertReplace($query, $data); 369 | 370 | if (empty($response)) 371 | { 372 | return false; 373 | } 374 | 375 | return (array)$response; 376 | } 377 | 378 | /** 379 | * @param string $tableName 380 | * @param array $conds 381 | * @param array $data 382 | * @param null|string $condsQuery 383 | * 384 | * @return bool 385 | * @throws MysqlException 386 | */ 387 | public function update(string $tableName, array $conds, array $data, ?string $condsQuery = null): bool 388 | { 389 | if (isset($data[0])) 390 | { 391 | throw new MysqlException("Multi-dimensional datasets are not allowed."); 392 | } 393 | 394 | $query = 'UPDATE ' . $tableName . ' SET :PARAMS WHERE :CONDS'; 395 | 396 | $placeholder = [ 397 | 'params' => [], 398 | 'conds' => [], 399 | ]; 400 | 401 | foreach ($data as $columnName => $value) 402 | { 403 | $placeholder['params'][] = '`' . $columnName . '` = :DATA_' . $columnName; 404 | 405 | // mark data keys in case CONDS and DATA hold the same keys 406 | unset($data[$columnName]); 407 | $data['DATA_' . $columnName] = $value; 408 | } 409 | 410 | $query = str_replace(':PARAMS', join(', ', $placeholder['params']), $query); 411 | $query = $this->buildCondsQuery($query, $conds, $condsQuery); 412 | 413 | return $this->prepareUpdate($query, $conds, $data); 414 | } 415 | 416 | /** 417 | * @param string $tableName 418 | * @param array $conds 419 | * @param null|string $condsQuery 420 | * 421 | * @return bool 422 | * @throws MysqlException 423 | */ 424 | public function delete(string $tableName, array $conds = [], ?string $condsQuery = null): bool 425 | { 426 | $query = $this->buildCondsQuery('DELETE FROM ' . $tableName . ' WHERE :CONDS', $conds, $condsQuery); 427 | $response = $this->prepareDelete($query, $conds); 428 | 429 | if ($response === true) 430 | { 431 | return true; 432 | } 433 | 434 | return false; 435 | } 436 | 437 | /** 438 | * @return \PDO 439 | */ 440 | protected function getPdo(): \PDO 441 | { 442 | return $this->pdo; 443 | } 444 | 445 | /** 446 | * @return int 447 | */ 448 | protected function getFetchMode(): int 449 | { 450 | return $this->fetchMode; 451 | } 452 | 453 | /** 454 | * @param array $errorInfo 455 | * 456 | * @return array 457 | */ 458 | protected function prepareErrorInfo(array $errorInfo): array 459 | { 460 | return [ 461 | 'sqlStateCode' => $errorInfo[0], 462 | 'code' => $errorInfo[1], 463 | 'message' => $errorInfo[2], 464 | ]; 465 | } 466 | 467 | /** 468 | * @param \PDOStatement $cursor 469 | * 470 | * @return Mysql 471 | */ 472 | protected function setLastStatement(\PDOStatement $cursor): self 473 | { 474 | $this->lastStatement = $cursor; 475 | 476 | return $this; 477 | } 478 | 479 | /** 480 | * @return \PDOStatement 481 | */ 482 | protected function getLastStatement(): \PDOStatement 483 | { 484 | return $this->lastStatement; 485 | } 486 | 487 | /** 488 | * @return bool 489 | */ 490 | protected function hasLastStatement(): bool 491 | { 492 | return $this->lastStatement !== null; 493 | } 494 | 495 | /** 496 | * @return Mysql 497 | */ 498 | protected function clearLastStatement(): self 499 | { 500 | $this->lastStatement = null; 501 | 502 | return $this; 503 | } 504 | 505 | /** 506 | * @param mixed $paramValue 507 | * 508 | * @return int 509 | * @throws MysqlException 510 | */ 511 | protected function getParamType($paramValue): int 512 | { 513 | if (is_null($paramValue)) 514 | { 515 | return \PDO::PARAM_NULL; 516 | } 517 | elseif (is_int($paramValue)) 518 | { 519 | return \PDO::PARAM_INT; 520 | } 521 | elseif (is_bool($paramValue)) 522 | { 523 | return \PDO::PARAM_INT; 524 | } 525 | elseif (is_string($paramValue)) 526 | { 527 | return \PDO::PARAM_STR; 528 | } 529 | elseif (is_float($paramValue)) 530 | { 531 | return \PDO::PARAM_STR; 532 | } 533 | elseif (is_double($paramValue)) 534 | { 535 | return \PDO::PARAM_STR; 536 | } 537 | 538 | throw new MysqlException("Invalid param type: {$paramValue} with type {gettype($paramValue)}"); 539 | } 540 | 541 | /** 542 | * @param \PDOStatement $pdoStatement 543 | * @param array $params 544 | * 545 | * @return \PDOStatement 546 | * @throws MysqlException 547 | */ 548 | protected function setParams(\PDOStatement $pdoStatement, array $params): \PDOStatement 549 | { 550 | foreach ($params as $key => &$val) 551 | { 552 | $pdoStatement->bindParam($key, $val, $this->getParamType($val)); 553 | } 554 | 555 | return $pdoStatement; 556 | } 557 | 558 | /** 559 | * @param string $query 560 | * @param array $params 561 | */ 562 | protected function handleInCondition(string &$query, array &$params): void 563 | { 564 | if (!empty($params)) 565 | { 566 | foreach ($params as $key => $val) 567 | { 568 | if (is_array($val)) 569 | { 570 | $keys = []; 571 | 572 | foreach ($val as $k => $v) 573 | { 574 | // new param name 575 | $keyString = ':' . $key . $k; 576 | 577 | // cache new params 578 | $keys[] = $keyString; 579 | 580 | // add new params 581 | $params[$keyString] = $v; 582 | } 583 | 584 | // include new params 585 | $query = str_replace(':' . $key, join(',', $keys), $query); 586 | 587 | // remove actual param 588 | unset($params[$key]); 589 | } 590 | } 591 | } 592 | } 593 | 594 | /** 595 | * @param string $query 596 | * @param array $conds 597 | * 598 | * @return \PDOStatement 599 | * @throws MysqlException 600 | */ 601 | protected function prepareSelect(string $query, array $conds): \PDOStatement 602 | { 603 | // clear last statement 604 | $this->clearLastStatement(); 605 | 606 | // handle "in" condition 607 | $this->handleInCondition($query, $conds); 608 | 609 | // set query 610 | $pdoStatement = $this->getPdo()->prepare($query); 611 | 612 | // bind named params 613 | $pdoStatement = $this->setParams($pdoStatement, $conds); 614 | 615 | try 616 | { 617 | $pdoStatement->execute(); 618 | 619 | if ($pdoStatement->errorCode() === '00000') 620 | { 621 | $this->setLastStatement($pdoStatement); 622 | 623 | return $pdoStatement; 624 | } 625 | 626 | $errorInfo = $this->prepareErrorInfo($pdoStatement->errorInfo()); 627 | } 628 | catch (\Exception $e) 629 | { 630 | $errorInfo = [ 631 | 'message' => $e->getMessage(), 632 | 'file' => $e->getFile(), 633 | 'line' => $e->getLine(), 634 | ]; 635 | } 636 | 637 | throw new MysqlException(json_encode([ 638 | 'query' => $query, 639 | 'params' => $conds, 640 | 'errorInfo' => $errorInfo, 641 | ])); 642 | } 643 | 644 | /** 645 | * @param string $query 646 | * @param array $rowsMany 647 | * 648 | * @return array 649 | * @throws MysqlException 650 | */ 651 | protected function prepareInsertReplace(string $query, array $rowsMany): array 652 | { 653 | $dbh = $this->getPdo(); 654 | $responses = []; 655 | 656 | // clear last statement 657 | $this->clearLastStatement(); 658 | 659 | // set query 660 | $pdoStatement = $dbh->prepare($query); 661 | 662 | // loop through rows 663 | while ($row = array_shift($rowsMany)) 664 | { 665 | // bind params 666 | $pdoStatement = $this->setParams($pdoStatement, $row); 667 | 668 | // execute 669 | $pdoStatement->execute(); 670 | 671 | // throw errors 672 | if ($pdoStatement->errorCode() !== '00000') 673 | { 674 | $error = [ 675 | 'query' => $query, 676 | 'errorInfo' => $this->prepareErrorInfo($pdoStatement->errorInfo()), 677 | ]; 678 | 679 | $errorInfo = json_encode($error); 680 | 681 | throw new MysqlException($errorInfo); 682 | } 683 | 684 | // last insert|null 685 | $lastInsert = $dbh->lastInsertId(); 686 | 687 | // cache response 688 | $responses[] = $lastInsert ? (int)$lastInsert : true; 689 | } 690 | 691 | return $responses; 692 | } 693 | 694 | /** 695 | * @param string $query 696 | * @param array $conds 697 | * @param array $data 698 | * 699 | * @return bool 700 | * @throws MysqlException 701 | */ 702 | protected function prepareUpdate(string $query, array $conds, array $data): bool 703 | { 704 | // clear last statement 705 | $this->clearLastStatement(); 706 | 707 | // handle "in" condition 708 | $this->handleInCondition($query, $conds); 709 | 710 | // set query 711 | $pdoStatement = $this->getPdo()->prepare($query); 712 | 713 | // bind conds params 714 | $pdoStatement = $this->setParams($pdoStatement, $conds); 715 | 716 | // bind data params 717 | $pdoStatement = $this->setParams($pdoStatement, $data); 718 | 719 | // execute 720 | $pdoStatement->execute(); 721 | 722 | // cache statement 723 | $this->setLastStatement($pdoStatement); 724 | 725 | // throw errors 726 | if ($pdoStatement->errorCode() !== '00000') 727 | { 728 | $error = [ 729 | 'query' => $query, 730 | 'conds' => $conds, 731 | 'errorInfo' => $this->prepareErrorInfo($pdoStatement->errorInfo()), 732 | ]; 733 | 734 | $errorInfo = json_encode($error); 735 | 736 | throw new MysqlException($errorInfo); 737 | } 738 | 739 | return $this->getRowCount() === 0 ? false : true; 740 | } 741 | 742 | /** 743 | * @param string $query 744 | * @param array $conds 745 | * 746 | * @return bool 747 | * @throws MysqlException 748 | */ 749 | protected function prepareDelete(string $query, array $conds): bool 750 | { 751 | // clear last statement 752 | $this->clearLastStatement(); 753 | 754 | // handle "in" condition 755 | $this->handleInCondition($query, $conds); 756 | 757 | // set query 758 | $pdoStatement = $this->getPdo()->prepare($query); 759 | 760 | // bind conds params 761 | $pdoStatement = $this->setParams($pdoStatement, $conds); 762 | 763 | // execute 764 | $pdoStatement->execute(); 765 | 766 | // cache statement 767 | $this->setLastStatement($pdoStatement); 768 | 769 | // throw errors 770 | if ($pdoStatement->errorCode() !== '00000') 771 | { 772 | $error = [ 773 | 'query' => $query, 774 | 'conds' => $conds, 775 | 'errorInfo' => $this->prepareErrorInfo($pdoStatement->errorInfo()), 776 | ]; 777 | 778 | $errorInfo = json_encode($error); 779 | 780 | throw new MysqlException($errorInfo); 781 | } 782 | 783 | return $this->getRowCount() === 0 ? false : true; 784 | } 785 | 786 | /** 787 | * @param string $query 788 | * @param array $conds 789 | * @param null|string $condsQuery 790 | * 791 | * @return string 792 | */ 793 | private function buildCondsQuery(string $query, array $conds, ?string $condsQuery = null): string 794 | { 795 | if (!empty($conds)) 796 | { 797 | if ($condsQuery === null) 798 | { 799 | $placeholder = []; 800 | 801 | foreach ($conds as $columnName => $value) 802 | { 803 | if ($this->isColum($columnName)) 804 | { 805 | $placeholder[] = '`' . $columnName . '` = :' . $columnName; 806 | } 807 | } 808 | 809 | $query = str_replace(':CONDS', join(' AND ', $placeholder), $query); 810 | } 811 | else 812 | { 813 | $query = str_replace(':CONDS', $condsQuery, $query); 814 | } 815 | } 816 | else 817 | { 818 | $query = str_replace(' WHERE :CONDS', '', $query); 819 | } 820 | 821 | return $query; 822 | } 823 | 824 | /** 825 | * @param string $key 826 | * 827 | * @return bool 828 | */ 829 | private function isColum(string $key): bool 830 | { 831 | return substr($key, 0, 1) !== '_'; 832 | } 833 | } -------------------------------------------------------------------------------- /src/MysqlException.php: -------------------------------------------------------------------------------- 1 | pdoStatement = $pdoStatement; 36 | $this->fetchType = $fetchType; 37 | $this->fetchMode = $fetchMode; 38 | } 39 | 40 | function rewind(): void 41 | { 42 | $this->position = 0; 43 | $this->data = $this->fetchData(); 44 | } 45 | 46 | /** 47 | * @return mixed 48 | */ 49 | function current(): mixed 50 | { 51 | return $this->data; 52 | } 53 | 54 | /** 55 | * @return int 56 | */ 57 | function key(): int 58 | { 59 | return $this->position; 60 | } 61 | 62 | function next(): void 63 | { 64 | $this->data = $this->fetchData(); 65 | ++$this->position; 66 | } 67 | 68 | /** 69 | * @return bool 70 | */ 71 | function valid(): bool 72 | { 73 | return $this->data !== false; 74 | } 75 | 76 | /** 77 | * @return mixed 78 | */ 79 | private function fetchData() 80 | { 81 | if ($this->fetchType === 'fetch') 82 | { 83 | return $this->pdoStatement->fetch($this->fetchMode); 84 | } 85 | 86 | return $this->pdoStatement->fetchColumn(); 87 | } 88 | } -------------------------------------------------------------------------------- /src/PDOConnector.php: -------------------------------------------------------------------------------- 1 | host = $host; 37 | $this->user = $user; 38 | $this->password = $password; 39 | $this->database = $database; 40 | } 41 | 42 | /** 43 | * @param string $charset 44 | * @param array $options 45 | * 46 | * @return \PDO 47 | * @throws \Exception 48 | */ 49 | public function connect(string $charset = 'utf8', array $options = []): \PDO 50 | { 51 | try 52 | { 53 | if (!$this->pdo) 54 | { 55 | $dns = $this->buildDns($this->host, $this->database, $charset, $options); 56 | 57 | if (empty($options['pdo'])) 58 | { 59 | $options['pdo'] = []; 60 | } 61 | 62 | $this->pdo = new \PDO($dns, $this->user, $this->password, $options['pdo']); 63 | } 64 | 65 | return $this->pdo; 66 | } 67 | catch (\PDOException $e) 68 | { 69 | $message = str_replace($this->password, '********', $e->getMessage()); 70 | throw new \Exception($message, $e->getCode()); 71 | } 72 | } 73 | 74 | /** 75 | * @param string $host 76 | * @param string $database 77 | * @param string $charset 78 | * @param array $options 79 | * 80 | * @return string 81 | */ 82 | private function buildDns(string $host, string $database, string $charset, array $options): string 83 | { 84 | $dns = 'mysql:host=' . $host; 85 | 86 | if (isset($options['port'])) 87 | { 88 | $dns .= ';port=' . $options['port']; 89 | } 90 | 91 | // use unix socket 92 | if (isset($options['unixSocket'])) 93 | { 94 | $dns = 'mysql:unix_socket=' . $options['unixSocket']; 95 | } 96 | 97 | $dns .= ';dbname=' . $database; 98 | $dns .= ';charset=' . $charset; 99 | 100 | return $dns; 101 | } 102 | 103 | } -------------------------------------------------------------------------------- /src/QueryUtil.php: -------------------------------------------------------------------------------- 1 | $data) 20 | { 21 | $value = $data['value']; 22 | $operator = $data['operator']; 23 | 24 | // handle db named columns e.g. "db.id" 25 | $strippedKey = str_replace('.', '', $key); 26 | 27 | $strippedConds[$strippedKey] = $data; 28 | 29 | // handle only columns (non-column conds are prepend with "_") 30 | if (!str_starts_with($key, '_')) 31 | { 32 | $key = str_contains($key, '.') ? $key : '`' . $key . '`'; 33 | $query = $key . ' ' . $operator . ' :' . $strippedKey; 34 | 35 | if ($value === null) 36 | { 37 | $not = ' IS NULL'; 38 | 39 | if ($operator === self::OP_NOT) 40 | { 41 | $not = ' IS NOT NULL'; 42 | } 43 | 44 | $query = $key . $not; 45 | } 46 | elseif (is_array($value)) 47 | { 48 | $not = null; 49 | 50 | if ($operator === self::OP_NOT) 51 | { 52 | $not = ' NOT'; 53 | } 54 | 55 | $query = $key . $not . ' IN(:' . $strippedKey . ')'; 56 | } 57 | 58 | $pairs[] = $query; 59 | } 60 | } 61 | 62 | return new CondsQueryBuild($strippedConds, $pairs); 63 | } 64 | } -------------------------------------------------------------------------------- /src/ReadQueryBuilder.php: -------------------------------------------------------------------------------- 1 | from; 43 | } 44 | 45 | /** 46 | * @param string $from 47 | * @param string $alias 48 | * 49 | * @return ReadQueryBuilder 50 | */ 51 | public function setFrom(string $from, ?string $alias = null): self 52 | { 53 | if ($alias !== null) 54 | { 55 | $from = $from . ' AS ' . $alias; 56 | } 57 | 58 | $this->from = $from; 59 | 60 | return $this; 61 | } 62 | 63 | /** 64 | * @deprecated Use getSelect 65 | * 66 | * @return string 67 | */ 68 | public function getColumns(): string 69 | { 70 | return implode(', ', $this->getSelect()); 71 | } 72 | 73 | /** 74 | * @deprecated Use setSelect or addSelect 75 | * 76 | * @param string $fields 77 | * 78 | * @return ReadQueryBuilder 79 | */ 80 | public function setColumns(string $fields): self 81 | { 82 | return $this->setSelect([$fields]); 83 | } 84 | 85 | /** 86 | * @return array 87 | */ 88 | public function getSelect(): array 89 | { 90 | return empty($this->select) ? ['*'] : $this->select; 91 | } 92 | 93 | /** 94 | * @param string $column 95 | * 96 | * @return ReadQueryBuilder 97 | */ 98 | public function addSelect(string $column): self 99 | { 100 | $this->select[] = $column; 101 | 102 | return $this; 103 | } 104 | 105 | /** 106 | * @param array $select 107 | * 108 | * @return ReadQueryBuilder 109 | */ 110 | public function setSelect(array $select): self 111 | { 112 | foreach ($select as $column) 113 | { 114 | $this->addSelect($column); 115 | } 116 | 117 | return $this; 118 | } 119 | 120 | /** 121 | * @param string $tableName 122 | * @param string $alias 123 | * @param string $condsQuery 124 | * 125 | * @return ReadQueryBuilder 126 | */ 127 | public function addInnerJoin(string $tableName, string $alias, string $condsQuery): self 128 | { 129 | return $this->addJoin('INNER', $tableName, $alias, $condsQuery); 130 | } 131 | 132 | /** 133 | * @param string $tableName 134 | * @param string $alias 135 | * @param string $condsQuery 136 | * 137 | * @return ReadQueryBuilder 138 | */ 139 | public function addLeftJoin(string $tableName, string $alias, string $condsQuery): self 140 | { 141 | return $this->addJoin('LEFT', $tableName, $alias, $condsQuery); 142 | } 143 | 144 | /** 145 | * @return array|null 146 | */ 147 | public function getJoins(): ?array 148 | { 149 | return $this->joins; 150 | } 151 | 152 | /** 153 | * @return array|null 154 | */ 155 | public function getSorting(): ?array 156 | { 157 | return $this->sorting; 158 | } 159 | 160 | /** 161 | * @param string $field 162 | * @param string $direction 163 | * 164 | * @return ReadQueryBuilder 165 | */ 166 | public function addSorting(string $field, string $direction): self 167 | { 168 | $field = strpos($field, '.') !== false ? $field : '`' . $field . '`'; 169 | $this->sorting[] = $field . ' ' . $direction; 170 | 171 | return $this; 172 | } 173 | 174 | /** 175 | * @param array $sorting 176 | * 177 | * @return ReadQueryBuilder 178 | */ 179 | public function setSorting(array $sorting): self 180 | { 181 | foreach ($sorting as $val) 182 | { 183 | list($field, $direction) = explode(' ', $val); 184 | $this->addSorting($field, $direction); 185 | } 186 | 187 | return $this; 188 | } 189 | 190 | /** 191 | * @return array|null 192 | */ 193 | public function getGroup(): ?array 194 | { 195 | return $this->group; 196 | } 197 | 198 | /** 199 | * @param string $column 200 | * 201 | * @return ReadQueryBuilder 202 | */ 203 | public function addGroup(string $column): self 204 | { 205 | $this->group[] = strpos($column, '.') !== false ? $column : '`' . $column . '`'; 206 | 207 | return $this; 208 | } 209 | 210 | /** 211 | * @param array $columns 212 | * 213 | * @return ReadQueryBuilder 214 | */ 215 | public function setGroup(array $columns): self 216 | { 217 | foreach ($columns as $column) 218 | { 219 | $this->addGroup($column); 220 | } 221 | 222 | return $this; 223 | } 224 | 225 | /** 226 | * @return null|string 227 | */ 228 | public function getLimit(): ?string 229 | { 230 | return $this->limit; 231 | } 232 | 233 | /** 234 | * @param int $rows 235 | * @param int $offset 236 | * 237 | * @return ReadQueryBuilder 238 | */ 239 | public function setLimit(int $rows, int $offset = 0): self 240 | { 241 | $this->limit = $offset . ', ' . $rows; 242 | 243 | return $this; 244 | } 245 | 246 | /** 247 | * @return string 248 | */ 249 | public function renderQuery(): string 250 | { 251 | $query = ['SELECT', join(', ', $this->getSelect()), 'FROM ' . $this->getFrom()]; 252 | 253 | if ($this->getJoins()) 254 | { 255 | $query = array_merge($query, $this->getJoins()); 256 | } 257 | 258 | if ($this->getConditions() || $this->getCondsQuery()) 259 | { 260 | $condPairs = []; 261 | 262 | if ($this->getCondsQuery()) 263 | { 264 | $condPairs[] = $this->getCondsQuery(); 265 | } 266 | else 267 | { 268 | $condsQueryBuild = QueryUtil::buildCondsQuery($this->getConditions()); 269 | 270 | foreach ($condsQueryBuild->getStrippedConds() as $key => $data) 271 | { 272 | $this->addCondition($key, $data['value'], $data['operator']); 273 | } 274 | 275 | $condPairs = $condsQueryBuild->getCondPairs(); 276 | } 277 | 278 | if (empty($condPairs) === false) 279 | { 280 | $query[] = 'WHERE ' . join(' AND ', $condPairs); 281 | } 282 | } 283 | 284 | if ($this->getGroup()) 285 | { 286 | $query[] = 'GROUP BY ' . join(', ', $this->getGroup()); 287 | } 288 | 289 | if ($this->getSorting()) 290 | { 291 | $query[] = 'ORDER BY ' . join(', ', $this->getSorting()); 292 | } 293 | 294 | if ($this->getLimit()) 295 | { 296 | $query[] = 'LIMIT ' . $this->getLimit(); 297 | } 298 | 299 | return join(' ', $query); 300 | } 301 | 302 | /** 303 | * @param string $type 304 | * @param string $tableName 305 | * @param string $alias 306 | * @param string $conds 307 | * 308 | * @return ReadQueryBuilder 309 | */ 310 | private function addJoin(string $type, string $tableName, string $alias, string $conds): self 311 | { 312 | if ($this->joins === null) 313 | { 314 | $this->joins = []; 315 | } 316 | 317 | $this->joins[] = $type . ' JOIN ' . $tableName . ' AS ' . $alias . ' ON ' . $conds; 318 | 319 | return $this; 320 | } 321 | } -------------------------------------------------------------------------------- /src/SecurityUtil.php: -------------------------------------------------------------------------------- 1 | 12, 23 | ]; 24 | } 25 | 26 | if ($hash = password_hash($password, $algo, $options)) 27 | { 28 | return $hash; 29 | } 30 | 31 | return null; 32 | } 33 | 34 | /** 35 | * @param string $password 36 | * @param string $passwordHash 37 | * 38 | * @return bool 39 | */ 40 | public static function verifyPasswordHash(string $password, string $passwordHash): bool 41 | { 42 | return password_verify($password, $passwordHash); 43 | } 44 | 45 | /** 46 | * @param int $length 47 | * @param null|string $prefix 48 | * @param null|string $customCharacters 49 | * 50 | * @return string 51 | */ 52 | public static function createRandomToken(int $length = 12, ?string $prefix = null, ?string $customCharacters = null): string 53 | { 54 | $randomString = ''; 55 | $characters = self::TOKEN_ALL_CASE_LETTERS_NUMBERS; 56 | 57 | // set custom characters 58 | if (empty($customCharacters) === false) 59 | { 60 | $characters = $customCharacters; 61 | } 62 | 63 | // handle prefix 64 | if ($prefix !== null) 65 | { 66 | $prefixLength = strlen($prefix); 67 | $length -= $prefixLength; 68 | 69 | if ($length < 0) 70 | { 71 | throw new \InvalidArgumentException('Prefix length is too long.'); 72 | } 73 | } 74 | 75 | // generate token 76 | for ($i = 0; $i < $length; $i++) 77 | { 78 | $randomString .= $characters[rand(0, strlen($characters) - 1)]; 79 | } 80 | 81 | return $prefix . $randomString; 82 | } 83 | 84 | /** 85 | * @param int $length 86 | * 87 | * @return string 88 | */ 89 | public static function createSessionId(int $length = 36): string 90 | { 91 | return self::createRandomToken($length); 92 | } 93 | } -------------------------------------------------------------------------------- /src/StorageUtil.php: -------------------------------------------------------------------------------- 1 | getLength(), $options->getPrefix(), $options->getCharacters()); 26 | 27 | $query = $options->mergeReadQuery( 28 | (new ReadQueryBuilder())->addCondition($options->getColumn(), $token) 29 | ); 30 | 31 | $isUnique = $storage->readOne($query) === null; 32 | } 33 | 34 | return $token; 35 | } 36 | 37 | /** 38 | * @param CreateQueryBuilder $builder 39 | * @param string $token 40 | * 41 | * @return CreateQueryBuilder 42 | */ 43 | public static function autosetEmptyToken(CreateQueryBuilder $builder, string $token): CreateQueryBuilder 44 | { 45 | $model = $builder->getModel(); 46 | 47 | if (method_exists($model, 'getToken') && method_exists($model, 'setToken')) 48 | { 49 | if (!$model->getToken()) 50 | { 51 | $builder->setModel( 52 | $model->setToken($token) 53 | ); 54 | } 55 | } 56 | 57 | return $builder; 58 | } 59 | } -------------------------------------------------------------------------------- /src/UniqueTokenOptions.php: -------------------------------------------------------------------------------- 1 | readQuery) 39 | { 40 | foreach ($builder->getConditions() as $key => $val) 41 | { 42 | $this->readQuery->addCondition($key, $val); 43 | } 44 | 45 | return $this->readQuery; 46 | } 47 | 48 | return $builder; 49 | } 50 | 51 | /** 52 | * @param ReadQueryBuilder $readQuery 53 | * 54 | * @return UniqueTokenOptions 55 | */ 56 | public function presetReadQuery(ReadQueryBuilder $readQuery): UniqueTokenOptions 57 | { 58 | $this->readQuery = $readQuery; 59 | 60 | return $this; 61 | } 62 | 63 | /** 64 | * @return string 65 | */ 66 | public function getColumn(): string 67 | { 68 | return $this->column; 69 | } 70 | 71 | /** 72 | * @param string $column 73 | * 74 | * @return UniqueTokenOptions 75 | */ 76 | public function setColumn(string $column): UniqueTokenOptions 77 | { 78 | $this->column = $column; 79 | 80 | return $this; 81 | } 82 | 83 | /** 84 | * @return int 85 | */ 86 | public function getLength(): int 87 | { 88 | return $this->length; 89 | } 90 | 91 | /** 92 | * @param int $length 93 | * 94 | * @return UniqueTokenOptions 95 | */ 96 | public function setLength(int $length): UniqueTokenOptions 97 | { 98 | $this->length = $length; 99 | 100 | return $this; 101 | } 102 | 103 | /** 104 | * @return null|string 105 | */ 106 | public function getPrefix(): ?string 107 | { 108 | return $this->prefix; 109 | } 110 | 111 | /** 112 | * @param string $prefix 113 | * 114 | * @return UniqueTokenOptions 115 | */ 116 | public function setPrefix(string $prefix): UniqueTokenOptions 117 | { 118 | $this->prefix = $prefix; 119 | 120 | return $this; 121 | } 122 | 123 | /** 124 | * @return string 125 | */ 126 | public function getCharacters(): string 127 | { 128 | return $this->characters; 129 | } 130 | 131 | /** 132 | * @param string $characters 133 | * 134 | * @return UniqueTokenOptions 135 | */ 136 | public function setCharacters(string $characters): UniqueTokenOptions 137 | { 138 | $this->characters = $characters; 139 | 140 | return $this; 141 | } 142 | } -------------------------------------------------------------------------------- /src/UpdateQueryBuilder.php: -------------------------------------------------------------------------------- 1 | model; 32 | } 33 | 34 | /** 35 | * @param CrudModelInterface $model 36 | * 37 | * @return UpdateQueryBuilder 38 | */ 39 | public function setModel(CrudModelInterface $model): self 40 | { 41 | $this->model = $model; 42 | 43 | return $this; 44 | } 45 | 46 | /** 47 | * @return string 48 | */ 49 | public function getTableName(): string 50 | { 51 | return $this->tableName; 52 | } 53 | 54 | /** 55 | * @param string $tableName 56 | * 57 | * @return UpdateQueryBuilder 58 | */ 59 | public function setTableName(string $tableName): self 60 | { 61 | $this->tableName = $tableName; 62 | 63 | return $this; 64 | } 65 | 66 | /** 67 | * @return string 68 | */ 69 | public function getQuery(): string 70 | { 71 | return $this->query; 72 | } 73 | 74 | /** 75 | * @param string $query 76 | * 77 | * @return UpdateQueryBuilder 78 | */ 79 | public function setQuery(string $query): self 80 | { 81 | $this->query = $query; 82 | 83 | return $this; 84 | } 85 | 86 | /** 87 | * @return array 88 | */ 89 | public function getData(): array 90 | { 91 | if ($this->getModel() instanceof CrudModelInterface) 92 | { 93 | return $this->getModel()->toArray(); 94 | } 95 | 96 | return $this->data; 97 | } 98 | 99 | /** 100 | * @param array $data 101 | * 102 | * @return UpdateQueryBuilder 103 | */ 104 | public function setData(array $data): self 105 | { 106 | $this->data = $data; 107 | 108 | return $this; 109 | } 110 | } -------------------------------------------------------------------------------- /tests/CreateQueryBuilderTest.php: -------------------------------------------------------------------------------- 1 | mysql = new Mysql($pdo); 21 | 22 | // Create the users table 23 | $this->mysql->executeSql(' 24 | CREATE TABLE users ( 25 | id INTEGER PRIMARY KEY, 26 | name TEXT, 27 | email TEXT 28 | ) 29 | '); 30 | 31 | $this->userStore = new UserStore($this->mysql); 32 | } 33 | 34 | public function testCreateQueryBuilder() 35 | { 36 | $user1 = new User(); 37 | $user1->setName('John Doe')->setEmail('john@example.com'); 38 | 39 | $builder1 = CreateQueryBuilder::create() 40 | ->setTableName('users') 41 | ->setModel($user1); 42 | 43 | $createdUser1 = $this->userStore->create($builder1); 44 | 45 | $this->assertEquals(1, $createdUser1->getId()); 46 | 47 | $user2 = new User(); 48 | $user2->setName('Jane Doe')->setEmail('jane@example.com'); 49 | 50 | $builder2 = CreateQueryBuilder::create() 51 | ->setTableName('users') 52 | ->setModel($user2); 53 | 54 | $createdUser2 = $this->userStore->create($builder2); 55 | 56 | $this->assertEquals(2, $createdUser2->getId()); 57 | 58 | // Verify both users were inserted with auto-incremented IDs 59 | $readBuilder = ReadQueryBuilder::create()->setFrom('users'); 60 | $users = $this->userStore->read($readBuilder); 61 | 62 | $this->assertCount(2, $users); 63 | $this->assertEquals(1, $users[0]->getId()); 64 | $this->assertEquals(2, $users[1]->getId()); 65 | } 66 | } -------------------------------------------------------------------------------- /tests/CrudManagerTest.php: -------------------------------------------------------------------------------- 1 | crudManager = new CrudManager($mysql); 23 | 24 | // Create test table 25 | $mysql->executeSql('CREATE TABLE users (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT, email TEXT)'); 26 | } 27 | 28 | public function testCreate() 29 | { 30 | $user = new User(); 31 | $user->setName('John Doe')->setEmail('john@example.com'); 32 | 33 | $builder = CreateQueryBuilder::create() 34 | ->setTableName('users') 35 | ->setModel($user); 36 | 37 | $result = $this->crudManager->create($builder); 38 | 39 | $this->assertInstanceOf(User::class, $result); 40 | $this->assertNotNull($result->getId()); 41 | $this->assertEquals('John Doe', $result->getName()); 42 | $this->assertEquals('john@example.com', $result->getEmail()); 43 | } 44 | 45 | public function testRead() 46 | { 47 | // Insert test data 48 | $user = new User(); 49 | $user->setName('John Doe')->setEmail('john@example.com'); 50 | $createdUser = $this->crudManager->create(CreateQueryBuilder::create()->setTableName('users')->setModel($user)); 51 | 52 | $builder = ReadQueryBuilder::create() 53 | ->setFrom('users') 54 | ->addCondition('id', $createdUser->getId()); 55 | 56 | $result = $this->crudManager->read($builder); 57 | 58 | $this->assertInstanceOf('Simplon\Mysql\MysqlQueryIterator', $result); 59 | $resultArray = iterator_to_array($result); 60 | $this->assertCount(1, $resultArray); 61 | $this->assertEquals('John Doe', $resultArray[0]['name']); 62 | $this->assertEquals('john@example.com', $resultArray[0]['email']); 63 | } 64 | 65 | public function testUpdate() 66 | { 67 | // Insert test data 68 | $user = new User(); 69 | $user->setName('John Doe')->setEmail('john@example.com'); 70 | $createdUser = $this->crudManager->create(CreateQueryBuilder::create()->setTableName('users')->setModel($user)); 71 | 72 | $createdUser->setName('Jane Doe'); 73 | $builder = UpdateQueryBuilder::create() 74 | ->setTableName('users') 75 | ->setModel($createdUser) 76 | ->addCondition('id', $createdUser->getId()); 77 | 78 | $result = $this->crudManager->update($builder); 79 | 80 | $this->assertInstanceOf(User::class, $result); 81 | $this->assertEquals('Jane Doe', $result->getName()); 82 | $this->assertEquals('john@example.com', $result->getEmail()); 83 | } 84 | 85 | public function testDelete() 86 | { 87 | // Insert test data 88 | $user = new User(); 89 | $user->setName('John Doe')->setEmail('john@example.com'); 90 | $createdUser = $this->crudManager->create(CreateQueryBuilder::create()->setTableName('users')->setModel($user)); 91 | 92 | $builder = DeleteQueryBuilder::create() 93 | ->setTableName('users') 94 | ->setModel($createdUser) 95 | ->addCondition('id', $createdUser->getId()); 96 | 97 | $result = $this->crudManager->delete($builder); 98 | 99 | $this->assertTrue($result); 100 | 101 | // Verify deletion 102 | $readBuilder = ReadQueryBuilder::create() 103 | ->setFrom('users') 104 | ->addCondition('id', $createdUser->getId()); 105 | $readResult = $this->crudManager->read($readBuilder); 106 | $this->assertEmpty(iterator_to_array($readResult)); 107 | } 108 | } -------------------------------------------------------------------------------- /tests/DeleteQueryBuilderTest.php: -------------------------------------------------------------------------------- 1 | mysql = new Mysql($pdo); 22 | 23 | // Create the users table 24 | $this->mysql->executeSql(' 25 | CREATE TABLE users ( 26 | id INTEGER PRIMARY KEY, 27 | name TEXT, 28 | email TEXT 29 | ) 30 | '); 31 | 32 | $this->userStore = new UserStore($this->mysql); 33 | 34 | // Insert sample data 35 | $user1 = new User(); 36 | $user1->setName('John Doe')->setEmail('john@example.com'); 37 | $this->userStore->create(CreateQueryBuilder::create()->setModel($user1)); 38 | 39 | $user2 = new User(); 40 | $user2->setName('Jane Doe')->setEmail('jane@example.com'); 41 | $this->userStore->create(CreateQueryBuilder::create()->setModel($user2)); 42 | } 43 | 44 | public function testDeleteQueryBuilder() 45 | { 46 | // First, retrieve a user to delete 47 | $readBuilder = ReadQueryBuilder::create() 48 | ->setFrom('users') 49 | ->addCondition('name', 'John Doe'); 50 | $user = $this->userStore->readOne($readBuilder); 51 | 52 | $this->assertInstanceOf(User::class, $user); 53 | $this->assertEquals('John Doe', $user->getName()); 54 | 55 | // Create the DeleteQueryBuilder 56 | $deleteBuilder = DeleteQueryBuilder::create() 57 | ->setTableName('users') 58 | ->addCondition('id', $user->getId()); 59 | 60 | // Test the builder properties 61 | $this->assertEquals('users', $deleteBuilder->getTableName()); 62 | $this->assertEquals([ 63 | 'id' => [ 64 | 'value' => $user->getId(), 65 | 'operator' => '=' 66 | ] 67 | ], $deleteBuilder->getConditions()); 68 | 69 | // Perform the delete operation 70 | $result = $this->userStore->delete($deleteBuilder); 71 | 72 | // Verify the delete was successful 73 | $this->assertTrue($result); 74 | 75 | // Try to fetch the deleted user 76 | $readBuilder = ReadQueryBuilder::create() 77 | ->setFrom('users') 78 | ->addCondition('id', $user->getId()); 79 | $deletedUser = $this->userStore->readOne($readBuilder); 80 | 81 | // Verify that the user no longer exists 82 | $this->assertNull($deletedUser); 83 | 84 | // Verify that other users still exist 85 | $allUsers = $this->userStore->read(ReadQueryBuilder::create()->setFrom('users')); 86 | $this->assertCount(1, $allUsers); 87 | $this->assertEquals('Jane Doe', $allUsers[0]->getName()); 88 | } 89 | } -------------------------------------------------------------------------------- /tests/Models/User.php: -------------------------------------------------------------------------------- 1 | id; 16 | } 17 | 18 | public function setId(int $id): self 19 | { 20 | $this->id = $id; 21 | return $this; 22 | } 23 | 24 | public function getName(): ?string 25 | { 26 | return $this->name; 27 | } 28 | 29 | public function setName(string $name): self 30 | { 31 | $this->name = $name; 32 | return $this; 33 | } 34 | 35 | public function getEmail(): ?string 36 | { 37 | return $this->email; 38 | } 39 | 40 | public function setEmail(string $email): self 41 | { 42 | $this->email = $email; 43 | return $this; 44 | } 45 | } -------------------------------------------------------------------------------- /tests/MysqlTest.php: -------------------------------------------------------------------------------- 1 | 'John Doe', 'email' => 'john@example.com']; 10 | $result = $this->mysql->insert('users', $data); 11 | 12 | $this->assertIsInt($result); 13 | $this->assertGreaterThan(0, $result); 14 | } 15 | 16 | public function testFetchRow() 17 | { 18 | $this->mysql->insert('users', ['name' => 'John Doe', 'email' => 'john@example.com']); 19 | 20 | $result = $this->mysql->fetchRow('SELECT * FROM users WHERE id = :id', ['id' => 1]); 21 | 22 | $this->assertIsArray($result); 23 | $this->assertArrayHasKey('name', $result); 24 | $this->assertArrayHasKey('email', $result); 25 | $this->assertEquals('John Doe', $result['name']); 26 | } 27 | 28 | public function testUpdate() 29 | { 30 | $this->mysql->insert('users', ['name' => 'John Doe', 'email' => 'john@example.com']); 31 | 32 | $data = ['name' => 'Jane Doe']; 33 | $result = $this->mysql->update('users', ['id' => 1], $data); 34 | 35 | $this->assertTrue($result); 36 | 37 | $updated = $this->mysql->fetchRow('SELECT * FROM users WHERE id = :id', ['id' => 1]); 38 | $this->assertEquals('Jane Doe', $updated['name']); 39 | } 40 | 41 | public function testDelete() 42 | { 43 | $this->mysql->insert('users', ['name' => 'John Doe', 'email' => 'john@example.com']); 44 | 45 | $result = $this->mysql->delete('users', ['id' => 1]); 46 | 47 | $this->assertTrue($result); 48 | 49 | $deleted = $this->mysql->fetchRow('SELECT * FROM users WHERE id = :id', ['id' => 1]); 50 | $this->assertNull($deleted); 51 | } 52 | } -------------------------------------------------------------------------------- /tests/ReadQueryBuilderTest.php: -------------------------------------------------------------------------------- 1 | mysql = new Mysql($pdo); 21 | 22 | // Create the users table 23 | $this->mysql->executeSql(' 24 | CREATE TABLE users ( 25 | id INTEGER PRIMARY KEY, 26 | name TEXT, 27 | email TEXT 28 | ) 29 | '); 30 | 31 | $this->userStore = new UserStore($this->mysql); 32 | 33 | // Insert sample data 34 | $user1 = new User(); 35 | $user1->setName('John Doe')->setEmail('john@example.com'); 36 | $this->userStore->create(CreateQueryBuilder::create()->setModel($user1)); 37 | 38 | $user2 = new User(); 39 | $user2->setName('Jane Doe')->setEmail('jane@example.com'); 40 | $this->userStore->create(CreateQueryBuilder::create()->setModel($user2)); 41 | } 42 | 43 | public function testBuildSimpleQuery() 44 | { 45 | $builder = ReadQueryBuilder::create() 46 | ->setFrom('users') 47 | ->addSelect('name') 48 | ->addSelect('email') 49 | ->addCondition('id', 1); 50 | 51 | $expectedQuery = 'SELECT name, email FROM users WHERE `id` = :id'; 52 | $this->assertEquals($expectedQuery, $builder->renderQuery()); 53 | 54 | // Test actual database query 55 | $result = $this->userStore->readOne($builder); 56 | $this->assertInstanceOf(User::class, $result); 57 | $this->assertEquals('John Doe', $result->getName()); 58 | $this->assertEquals('john@example.com', $result->getEmail()); 59 | } 60 | 61 | public function testBuildComplexQuery() 62 | { 63 | // For this test, we'll just check the query structure without executing it, 64 | // as we don't have an 'orders' table in our test setup 65 | $builder = ReadQueryBuilder::create() 66 | ->setFrom('users') 67 | ->addSelect('users.name') 68 | ->addSelect('users.email') 69 | ->addSelect('orders.total') 70 | ->addInnerJoin('orders', 'o', 'o.user_id = users.id') 71 | ->addCondition('users.id', 1) 72 | ->addSorting('orders.total', ReadQueryBuilder::ORDER_DESC) 73 | ->setLimit(10, 0); 74 | 75 | $expectedQuery = 'SELECT users.name, users.email, orders.total FROM users INNER JOIN orders AS o ON o.user_id = users.id WHERE users.id = :usersid ORDER BY orders.total DESC LIMIT 0, 10'; 76 | $this->assertEquals($expectedQuery, $builder->renderQuery()); 77 | } 78 | 79 | public function testReadMultipleUsers() 80 | { 81 | $builder = ReadQueryBuilder::create() 82 | ->setFrom('users') 83 | ->addSelect('*') 84 | ->addSorting('name', ReadQueryBuilder::ORDER_ASC); 85 | 86 | $results = $this->userStore->read($builder); 87 | 88 | $this->assertIsArray($results); 89 | $this->assertCount(2, $results); 90 | $this->assertInstanceOf(User::class, $results[0]); 91 | $this->assertInstanceOf(User::class, $results[1]); 92 | $this->assertEquals('Jane Doe', $results[0]->getName()); 93 | $this->assertEquals('John Doe', $results[1]->getName()); 94 | } 95 | } -------------------------------------------------------------------------------- /tests/SecurityUtilTest.php: -------------------------------------------------------------------------------- 1 | assertNotNull($hash); 16 | $this->assertIsString($hash); 17 | $this->assertNotEquals($password, $hash); 18 | } 19 | 20 | public function testVerifyPasswordHash() 21 | { 22 | $password = 'testPassword123'; 23 | $hash = SecurityUtil::createPasswordHash($password); 24 | 25 | $this->assertTrue(SecurityUtil::verifyPasswordHash($password, $hash)); 26 | $this->assertFalse(SecurityUtil::verifyPasswordHash('wrongPassword', $hash)); 27 | } 28 | 29 | public function testCreateRandomToken() 30 | { 31 | $token = SecurityUtil::createRandomToken(); 32 | $this->assertEquals(12, strlen($token)); 33 | 34 | $token = SecurityUtil::createRandomToken(20); 35 | $this->assertEquals(20, strlen($token)); 36 | 37 | $token = SecurityUtil::createRandomToken(15, 'prefix_'); 38 | var_dump($token); 39 | $this->assertEquals(15, strlen($token)); 40 | $this->assertStringStartsWith('prefix_', $token); 41 | 42 | $token = SecurityUtil::createRandomToken(10, null, 'ABC123'); 43 | $this->assertEquals(10, strlen($token)); 44 | $this->assertMatchesRegularExpression('/^[ABC123]+$/', $token); 45 | } 46 | 47 | public function testCreateSessionId() 48 | { 49 | $sessionId = SecurityUtil::createSessionId(); 50 | $this->assertEquals(36, strlen($sessionId)); 51 | 52 | $sessionId = SecurityUtil::createSessionId(50); 53 | $this->assertEquals(50, strlen($sessionId)); 54 | } 55 | 56 | public function testCreateRandomTokenWithLongPrefix() 57 | { 58 | $this->expectException(\InvalidArgumentException::class); 59 | SecurityUtil::createRandomToken(10, 'very_long_prefix_'); 60 | } 61 | 62 | public function testCreatePasswordHashWithCustomAlgorithm() 63 | { 64 | $password = 'testPassword123'; 65 | $hash = SecurityUtil::createPasswordHash($password, PASSWORD_ARGON2I); 66 | 67 | $this->assertNotNull($hash); 68 | $this->assertIsString($hash); 69 | $this->assertStringStartsWith('$argon2i$', $hash); 70 | } 71 | 72 | public function testCreatePasswordHashWithCustomOptions() 73 | { 74 | $password = 'testPassword123'; 75 | $options = ['cost' => 10]; 76 | $hash = SecurityUtil::createPasswordHash($password, PASSWORD_BCRYPT, $options); 77 | 78 | $this->assertNotNull($hash); 79 | $this->assertIsString($hash); 80 | } 81 | } -------------------------------------------------------------------------------- /tests/SqliteTestCase.php: -------------------------------------------------------------------------------- 1 | mysql = new Mysql($pdo); 16 | 17 | $this->createTestTables(); 18 | } 19 | 20 | private function createTestTables(): void 21 | { 22 | $this->mysql->executeSql(' 23 | CREATE TABLE users ( 24 | id INTEGER PRIMARY KEY AUTOINCREMENT, 25 | name TEXT, 26 | email TEXT 27 | ) 28 | '); 29 | 30 | $this->mysql->executeSql(' 31 | CREATE TABLE orders ( 32 | id INTEGER PRIMARY KEY AUTOINCREMENT, 33 | user_id INTEGER, 34 | total REAL 35 | ) 36 | '); 37 | } 38 | 39 | protected function tearDown(): void 40 | { 41 | $this->mysql->close(); 42 | } 43 | } -------------------------------------------------------------------------------- /tests/Stores/UserStore.php: -------------------------------------------------------------------------------- 1 | crudCreate($builder); 28 | } 29 | 30 | public function read(?ReadQueryBuilder $builder = null): ?array 31 | { 32 | if ($builder === null) { 33 | $builder = new ReadQueryBuilder(); 34 | } 35 | return $this->crudRead($builder); 36 | } 37 | 38 | public function readOne(ReadQueryBuilder $builder) 39 | { 40 | return $this->crudReadOne($builder); 41 | } 42 | 43 | public function update(UpdateQueryBuilder $builder) 44 | { 45 | return $this->crudUpdate($builder); 46 | } 47 | 48 | public function delete(DeleteQueryBuilder $builder) 49 | { 50 | return $this->crudDelete($builder); 51 | } 52 | } -------------------------------------------------------------------------------- /tests/UpdateQueryBuilderTest.php: -------------------------------------------------------------------------------- 1 | mysql = new Mysql($pdo); 22 | 23 | // Create the users table 24 | $this->mysql->executeSql(' 25 | CREATE TABLE users ( 26 | id INTEGER PRIMARY KEY, 27 | name TEXT, 28 | email TEXT 29 | ) 30 | '); 31 | 32 | $this->userStore = new UserStore($this->mysql); 33 | 34 | // Insert sample data 35 | $user = new User(); 36 | $user->setName('John Doe')->setEmail('john@example.com'); 37 | $this->userStore->create(CreateQueryBuilder::create()->setModel($user)); 38 | } 39 | 40 | public function testUpdateQueryBuilder() 41 | { 42 | // First, retrieve the user we just created 43 | $readBuilder = ReadQueryBuilder::create() 44 | ->setFrom('users') 45 | ->addCondition('name', 'John Doe'); 46 | $user = $this->userStore->readOne($readBuilder); 47 | 48 | $this->assertInstanceOf(User::class, $user); 49 | $this->assertEquals('John Doe', $user->getName()); 50 | 51 | // Now, let's update this user 52 | $user->setName('Jane Doe')->setEmail('jane@example.com'); 53 | 54 | $updateBuilder = UpdateQueryBuilder::create() 55 | ->setTableName('users') 56 | ->setModel($user) 57 | ->addCondition('id', $user->getId()); 58 | 59 | // Test the builder properties 60 | $this->assertEquals('users', $updateBuilder->getTableName()); 61 | $this->assertInstanceOf(User::class, $updateBuilder->getModel()); 62 | $this->assertEquals([ 63 | 'id' => $user->getId(), 64 | 'name' => 'Jane Doe', 65 | 'email' => 'jane@example.com' 66 | ], $updateBuilder->getData()); 67 | $this->assertEquals([ 68 | 'id' => [ 69 | 'value' => $user->getId(), 70 | 'operator' => '=' 71 | ] 72 | ], $updateBuilder->getConditions()); 73 | 74 | // Perform the update 75 | $updatedUser = $this->userStore->update($updateBuilder); 76 | 77 | // Verify the update was successful 78 | $this->assertInstanceOf(User::class, $updatedUser); 79 | $this->assertEquals('Jane Doe', $updatedUser->getName()); 80 | $this->assertEquals('jane@example.com', $updatedUser->getEmail()); 81 | 82 | // Double-check by reading from the database again 83 | $readBuilder = ReadQueryBuilder::create() 84 | ->setFrom('users') 85 | ->addCondition('id', $user->getId()); 86 | $fetchedUser = $this->userStore->readOne($readBuilder); 87 | 88 | $this->assertInstanceOf(User::class, $fetchedUser); 89 | $this->assertEquals('Jane Doe', $fetchedUser->getName()); 90 | $this->assertEquals('jane@example.com', $fetchedUser->getEmail()); 91 | } 92 | } --------------------------------------------------------------------------------