├── codeception-logo.png ├── CHANGELOG.md ├── src ├── Config.php ├── Module.php └── DatabasePopulator.php ├── composer.json ├── LICENSE.md └── README.md /codeception-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vjik/codeception-db-populator/HEAD/codeception-logo.png -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Database Populator for Codeception DB Module Change Log 2 | 3 | ## 1.1.1 July 18, 2024 4 | 5 | - Bug #5: Fix column and table names quotation. 6 | 7 | ## 1.1.0 August 4, 2022 8 | 9 | - Enh: Raise minimum required versions: PHP to `^8.0`, `codeception/codeception` to `^5.0` and 10 | `codeception/module-db` to `^3.0`. 11 | 12 | ## 1.0.1 December 27, 2021 13 | 14 | - Enh: Add support for `codeception/module-db` version `^2.0`. 15 | 16 | ## 1.0.0 September 18, 2021 17 | 18 | - Initial release. 19 | -------------------------------------------------------------------------------- /src/Config.php: -------------------------------------------------------------------------------- 1 | dumpsPath = codecept_absolute_path($config['dumpsPath']); 23 | $this->rowsPath = codecept_absolute_path($config['rowsPath']); 24 | } 25 | 26 | public function dumpsPath(): string 27 | { 28 | return $this->dumpsPath; 29 | } 30 | 31 | public function rowsPath(): string 32 | { 33 | return $this->rowsPath; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vjik/codeception-db-populator", 3 | "type": "library", 4 | "description": "Database populator for Codeception DB module", 5 | "keywords": [ 6 | "codeception", 7 | "codeception-module", 8 | "db-testing", 9 | "database-testing" 10 | ], 11 | "license": "BSD-3-Clause", 12 | "support": { 13 | "issues": "https://github.com/vjik/codeception-db-populator/issues?state=open", 14 | "source": "https://github.com/vjik/codeception-db-populator" 15 | }, 16 | "minimum-stability": "stable", 17 | "require": { 18 | "php": "^8.0", 19 | "ext-pdo": "*", 20 | "codeception/codeception": "^5.0", 21 | "codeception/module-db": "^3.0" 22 | }, 23 | "require-dev": { 24 | "vimeo/psalm": "^5.25", 25 | "vlucas/phpdotenv": "^5.4" 26 | }, 27 | "autoload": { 28 | "psr-4": { 29 | "Vjik\\Codeception\\DatabasePopulator\\": "src" 30 | } 31 | }, 32 | "autoload-dev": { 33 | "psr-4": { 34 | "Vjik\\Codeception\\DatabasePopulator\\Tests\\": "tests" 35 | } 36 | }, 37 | "config": { 38 | "sort-packages": true 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright © 2021 by Sergei Predvoditelev (https://predvoditelev.ru) 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions 6 | are met: 7 | 8 | * Redistributions of source code must retain the above copyright 9 | notice, this list of conditions and the following disclaimer. 10 | * Redistributions in binary form must reproduce the above copyright 11 | notice, this list of conditions and the following disclaimer in 12 | the documentation and/or other materials provided with the 13 | distribution. 14 | * Neither the name of the copyright holder nor the names of its 15 | contributors may be used to endorse or promote products derived 16 | from this software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 19 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 20 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 21 | FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 22 | COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 23 | INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 24 | BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 25 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 26 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 27 | LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN 28 | ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 29 | POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /src/Module.php: -------------------------------------------------------------------------------- 1 | &array{ 11 | * preloadDump: string|string[]|null, 12 | * preloadRows: string|string[]|null, 13 | * dumpsPath: string, 14 | * rowsPath: string, 15 | * } 16 | */ 17 | final class Module extends \Codeception\Module 18 | { 19 | /** 20 | * @psalm-suppress NonInvariantDocblockPropertyType 21 | * @psalm-suppress InvalidPropertyAssignmentValue 22 | * @psalm-var ModuleConfigArray 23 | */ 24 | protected array $config = [ 25 | 'preloadDump' => null, 26 | 'preloadRows' => null, 27 | ]; 28 | 29 | /** 30 | * @psalm-suppress NonInvariantDocblockPropertyType 31 | * @var string[] 32 | */ 33 | protected array $requiredFields = [ 34 | 'dumpsPath', 35 | 'rowsPath', 36 | ]; 37 | 38 | private ?DatabasePopulator $popualtor = null; 39 | 40 | public function _beforeSuite(array $settings = []): void 41 | { 42 | if ($this->config['preloadDump'] !== null) { 43 | $dumps = is_array($this->config['preloadDump']) 44 | ? $this->config['preloadDump'] 45 | : [$this->config['preloadDump']]; 46 | $this->getPopulator()->loadDump(...$dumps); 47 | } 48 | if ($this->config['preloadRows'] !== null) { 49 | $sets = is_array($this->config['preloadRows']) 50 | ? $this->config['preloadRows'] 51 | : [$this->config['preloadRows']]; 52 | $this->getPopulator()->loadRows(...$sets); 53 | } 54 | } 55 | 56 | public function loadDump(string ...$dumps): void 57 | { 58 | $this->getPopulator()->loadDump(...$dumps); 59 | } 60 | 61 | public function loadRows(string ...$sets): void 62 | { 63 | $this->getPopulator()->loadRows(...$sets); 64 | } 65 | 66 | private function getPopulator(): DatabasePopulator 67 | { 68 | if ($this->popualtor === null) { 69 | /** @psalm-suppress ArgumentTypeCoercion */ 70 | $this->popualtor = new DatabasePopulator( 71 | $this->getModule('Db'), 72 | new Config($this->config) 73 | ); 74 | } 75 | return $this->popualtor; 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/DatabasePopulator.php: -------------------------------------------------------------------------------- 1 | dbDriver = $db->_getDriver(); 25 | $this->dbh = $db->_getDbh(); 26 | $this->config = $config; 27 | } 28 | 29 | public function loadDump(string ...$dumps): void 30 | { 31 | // Clear database 32 | $this->dbDriver->cleanup(); 33 | 34 | // Load dumps 35 | foreach ($dumps as $dump) { 36 | $this->dbDriver->load($this->readDump($dump)); 37 | } 38 | } 39 | 40 | public function loadRows(string ...$sets): void 41 | { 42 | $this->beforeLoadRows(); 43 | 44 | foreach ($sets as $set) { 45 | /** 46 | * @psalm-suppress UnresolvableInclude 47 | * @psalm-var array>> $data 48 | */ 49 | $data = require $this->getRowsFilePath($set); 50 | foreach ($data as $table => $rows) { 51 | $this->insertRows($table, $rows); 52 | } 53 | } 54 | 55 | $this->afterLoadRows(); 56 | } 57 | 58 | /** 59 | * @param array[] $rows 60 | * @psalm-param list> $rows 61 | */ 62 | private function insertRows(string $table, array $rows): void 63 | { 64 | $requests = []; 65 | foreach ($rows as $row) { 66 | $columns = array_keys($row); 67 | $key = implode('~', $columns); 68 | if (isset($requests[$key])) { 69 | $requests[$key]['rows'][] = $row; 70 | } else { 71 | $requests[$key] = [ 72 | 'columns' => $columns, 73 | 'rows' => [$row], 74 | ]; 75 | } 76 | } 77 | 78 | foreach ($requests as $request) { 79 | $columns = array_map( 80 | fn($c) => $this->dbDriver->getQuotedName($c), 81 | $request['columns'] 82 | ); 83 | $sql = sprintf( 84 | 'INSERT INTO %s (%s) VALUES ', 85 | $this->dbDriver->getQuotedName($table), 86 | implode(',', $columns), 87 | ); 88 | 89 | $insertQuery = []; 90 | $insertData = []; 91 | $n = 0; 92 | foreach ($request['rows'] as $row) { 93 | $insertQueryData = []; 94 | 95 | /** @var mixed $value */ 96 | foreach ($row as $key => $value) { 97 | $insertQueryData[] = ':' . $key . $n; 98 | /** @var mixed */ 99 | $insertData[$key . $n] = $value; 100 | } 101 | 102 | $insertQuery[] = '(' . implode(',', $insertQueryData) . ')'; 103 | $n++; 104 | } 105 | 106 | $sql .= implode(', ', $insertQuery); 107 | $this->dbh->prepare($sql)->execute($insertData); 108 | } 109 | } 110 | 111 | private function beforeLoadRows(): void 112 | { 113 | if ($this->dbDriver instanceof MySqlDriver) { 114 | $this->dbh->prepare('SET FOREIGN_KEY_CHECKS = 0')->execute(); 115 | } 116 | } 117 | 118 | private function afterLoadRows(): void 119 | { 120 | if ($this->dbDriver instanceof MySqlDriver) { 121 | $this->dbh->prepare('SET FOREIGN_KEY_CHECKS = 1')->execute(); 122 | } 123 | } 124 | 125 | /** 126 | * @see Db::readSqlFile 127 | * @see Db::readSql 128 | * 129 | * @return string[] 130 | */ 131 | private function readDump(string $dump): array 132 | { 133 | $file = $this->getDumpFilePath($dump); 134 | 135 | if (!file_exists($file)) { 136 | throw new ModuleException( 137 | Module::class, 138 | "\nFile with dump doesn't exist.\nPlease, check path for SQL-file: $file" 139 | ); 140 | } 141 | 142 | $sql = file_get_contents($file); 143 | 144 | // Remove C-style comments (except MySQL directives) 145 | $sql = preg_replace('%/\*(?!!\d+).*?\*/%s', '', $sql); 146 | 147 | if (empty($sql)) { 148 | return []; 149 | } 150 | 151 | // Split SQL dump into lines 152 | return preg_split('/\r\n|\n|\r/', $sql, -1, PREG_SPLIT_NO_EMPTY); 153 | } 154 | 155 | private function getDumpFilePath(string $dump): string 156 | { 157 | return $this->config->dumpsPath() . '/' . $dump . '.sql'; 158 | } 159 | 160 | private function getRowsFilePath(string $set): string 161 | { 162 | return $this->config->rowsPath() . '/' . $set . '.php'; 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

Database Populator for Codeception DB Module

4 |
5 |

6 | 7 | [![Latest Stable Version](https://poser.pugx.org/vjik/codeception-db-populator/v)](https://packagist.org/packages/vjik/codeception-db-populator) 8 | [![Total Downloads](https://poser.pugx.org/vjik/codeception-db-populator/downloads)](https://packagist.org/packages/vjik/codeception-db-populator) 9 | [![MySQL build](https://github.com/vjik/codeception-db-populator/actions/workflows/build-mysql.yml/badge.svg)](https://github.com/vjik/codeception-db-populator/actions/workflows/build-mysql.yml) 10 | [![PgSQL build](https://github.com/vjik/codeception-db-populator/actions/workflows/build-pgsql.yml/badge.svg)](https://github.com/vjik/codeception-db-populator/actions/workflows/build-pgsql.yml) 11 | [![static analysis](https://github.com/vjik/codeception-db-populator/actions/workflows/static.yml/badge.svg)](https://github.com/vjik/codeception-db-populator/actions/workflows/static.yml) 12 | [![License](https://poser.pugx.org/vjik/codeception-db-populator/license)](/LICENSE) 13 | 14 | [Codeception](https://codeception.com/) DB module addon that helps you to tune database populations. 15 | So for a test you could load only needed tables or rows. As a result it dramatically reduces the total execution time. 16 | 17 | ## Requirements 18 | 19 | - PHP 8.0 or higher. 20 | - Codeception 5.0 or higher. 21 | - Codeception Module DB 3.0 or higher. 22 | 23 | ## Installation 24 | 25 | The package could be installed with [composer](https://getcomposer.org/download/): 26 | 27 | ```shell 28 | composer require vjik/codeception-db-populator --dev 29 | ``` 30 | ## General usage 31 | 32 | Enable module `Db` and `DatabasePopulator` addon in the suite: 33 | 34 | ```yml 35 | modules: 36 | enabled: 37 | - Db: 38 | dsn: 'mysql:host=%DB_HOST%;dbname=%DB_NAME%' 39 | user: '%DB_USERNAME%' 40 | password: '%DB_PASSWORD%' 41 | - Vjik\Codeception\DatabasePopulator\Module: 42 | dumpsPath: 'tests/_data/dumps' 43 | rowsPath: 'tests/_data/rows' 44 | ``` 45 | 46 | Create SQL dumps that contains a record of the table structure and/or the data for use in tests. 47 | Put dumps into path, specified in options (for example, `tests/_data/dumps`). 48 | 49 | Create row sets for populate database tables. Row sets is PHP file that return array in format `table => rows`. 50 | For example: 51 | 52 | ```php 53 | [ 56 | [ 57 | 'id' => 1, 58 | 'name' => 'Ivan', 59 | ], 60 | [ 61 | 'id' => 2, 62 | 'name' => 'Petr', 63 | ], 64 | ], 65 | 'post' => [ 66 | [ 67 | 'id' => 1, 68 | 'author_id' => 2, 69 | 'name' => 'First post', 70 | ], 71 | [ 72 | 'id' => 2, 73 | 'author_id' => 2, 74 | 'name' => 'My history', 75 | ], 76 | ], 77 | ]; 78 | ``` 79 | 80 | You can get structure, similar to this: 81 | 82 | ``` 83 | tests/ 84 | _data/ 85 | dumps/ 86 | user-management.sql 87 | blog.sql 88 | catalog.sql 89 | rows/ 90 | users.php 91 | authors.php 92 | blog-categories.php 93 | posts-with-categories.php 94 | ``` 95 | 96 | Load dumps and row sets in your tests: 97 | 98 | ```php 99 | final class BlogTest extends Unit 100 | { 101 | public function testCreatePost(): void 102 | { 103 | $this->tester->loadDump('blog'); 104 | $this->tester->loadRows('authors'); 105 | ... 106 | } 107 | } 108 | ``` 109 | 110 | ## Actions 111 | 112 | ### `loadDump()` 113 | 114 | Load the specified dump(s) to database. Before loading the dump, the database is cleaned. 115 | 116 | ```php 117 | $I->loadDump('blog'); // load one dump 118 | $I->loadDump('blog', 'catalog'); // load several dumps 119 | ``` 120 | 121 | ### `loadRows()` 122 | 123 | Load the specified row set(s) to database. 124 | 125 | ```php 126 | $I->loadRows('posts'); // load one set 127 | $I->loadRows('users', 'comments'); // load several sets 128 | ``` 129 | 130 | ## Configuration 131 | 132 | 133 | - `dumpsPath` (required) — relative path to directory with dumps (for example, `tests/_dump`). 134 | - `rowsPath` (required) — relative path to directory with row sets (for example, `tests/_rows`). 135 | - `preloadDump` — dump(s) for preload before run suite. 136 | - `preloadRows` — row set(s) for preload before run suite. 137 | 138 | ## Testing 139 | 140 | ### Unit and integration testing 141 | 142 | The package is tested with [Codeception](https://codeception.com/). For tests need MySQL database with configuration: 143 | 144 | - host: `127.0.0.1` 145 | - name: `db_test` 146 | - user: `root` 147 | - password: `root` 148 | 149 | To run tests: 150 | 151 | ```shell 152 | ./vendor/bin/codecept run 153 | ``` 154 | 155 | ### Static analysis 156 | 157 | The code is statically analyzed with [Psalm](https://psalm.dev/). To run static analysis: 158 | 159 | ```shell 160 | ./vendor/bin/psalm 161 | ``` 162 | 163 | ## License 164 | 165 | The Database Populator for Codeception DB Module is free software. It is released under the terms of the BSD License. Please see [`LICENSE`](./LICENSE.md) for more information. 166 | --------------------------------------------------------------------------------