├── .styleci.yml ├── config └── database-synchronizer.php ├── src ├── Exceptions │ └── DatabaseConnectionException.php ├── DatabaseSynchronizerServiceProvider.php ├── Commands │ └── Synchronise.php └── DatabaseSynchronizer.php ├── .travis.yml ├── changelog.md ├── .scrutinizer.yml ├── phpunit.xml ├── license.md ├── contributing.md ├── composer.json └── readme.md /.styleci.yml: -------------------------------------------------------------------------------- 1 | preset: laravel -------------------------------------------------------------------------------- /config/database-synchronizer.php: -------------------------------------------------------------------------------- 1 | 'production', 5 | 'to' => 'staging', 6 | 'tables' => [], 7 | 'skip_tables' => [], 8 | 'limit' => 5000, 9 | ]; 10 | -------------------------------------------------------------------------------- /src/Exceptions/DatabaseConnectionException.php: -------------------------------------------------------------------------------- 1 | 2 | 12 | 13 | 14 | ./tests/ 15 | 16 | 17 | 18 | 19 | src/ 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /license.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) Maarten Tolhuijs mtolhuys@hotmail.com 4 | 5 | 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: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | 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. 10 | -------------------------------------------------------------------------------- /contributing.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Contributions are welcome and will be fully credited. 4 | 5 | Contributions are accepted via Pull Requests on [Github](https://github.com/mtolhuys/laravel-database-synchronizer). 6 | 7 | # Things you could do 8 | If you want to contribute but do not know where to start, this list provides some starting points. 9 | - Add tests 10 | 11 | ## Pull Requests 12 | 13 | - **Document any change in behaviour** - Make sure the `readme.md` and any other relevant documentation are kept up-to-date. 14 | 15 | - **Consider our release cycle** - We try to follow [SemVer v2.0.0](http://semver.org/). Randomly breaking public APIs is not an option. 16 | 17 | - **One pull request per feature** - If you want to do more than one thing, send multiple pull requests. 18 | 19 | - **Send coherent history** - Make sure each individual commit in your pull request is meaningful. If you had to make multiple intermediate commits while developing, please [squash them](http://www.git-scm.com/book/en/v2/Git-Tools-Rewriting-History#Changing-Multiple-Commit-Messages) before submitting. 20 | 21 | 22 | **Happy coding**! 23 | -------------------------------------------------------------------------------- /src/DatabaseSynchronizerServiceProvider.php: -------------------------------------------------------------------------------- 1 | app->runningInConsole()) { 17 | $this->bootForConsole(); 18 | } 19 | 20 | $this->publishes([ 21 | __DIR__.'/../config/database-synchronizer.php' => config_path('database-synchronizer.php'), 22 | ]); 23 | } 24 | 25 | /** 26 | * Register any package services. 27 | * 28 | * @return void 29 | */ 30 | public function register() 31 | { 32 | $this->mergeConfigFrom(__DIR__.'/../config/database-synchronizer.php', 'database-synchronizer'); 33 | } 34 | 35 | /** 36 | * Console-specific booting. 37 | * 38 | * @return void 39 | */ 40 | protected function bootForConsole() 41 | { 42 | $this->commands([ 43 | Commands\Synchronise::class, 44 | ]); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mtolhuijs/laravel-database-synchronizer", 3 | "description": "Synchronize your production and development databases with a simple command", 4 | "license": "MIT", 5 | "authors": [ 6 | { 7 | "name": "Maarten Tolhuijs", 8 | "email": "mtolhuys@hotmail.com", 9 | "homepage": "https://github.com/mtolhuys/laravel-database-synchronize" 10 | } 11 | ], 12 | "homepage": "https://github.com/mtolhuys/laravel-database-synchronizer", 13 | "keywords": ["Laravel", "laravel-database-synchronizer"], 14 | "require": { 15 | "illuminate/support": "~5|~6|~7|~8", 16 | "doctrine/dbal":"~2.3", 17 | "ext-pdo": "*" 18 | }, 19 | "require-dev": { 20 | "phpunit/phpunit": "~7.0|~8.0|~9.0|~10.0", 21 | "mockery/mockery": "^1.1", 22 | "orchestra/testbench": "~3.0|~4.0|~5.0|~6.0", 23 | "sempro/phpunit-pretty-print": "^1.0" 24 | }, 25 | "autoload": { 26 | "psr-4": { 27 | "mtolhuijs\\LDS\\": "src/" 28 | } 29 | }, 30 | "autoload-dev": { 31 | "psr-4": { 32 | "mtolhuijs\\LDS\\Tests\\": "tests" 33 | } 34 | }, 35 | "extra": { 36 | "laravel": { 37 | "providers": [ 38 | "mtolhuijs\\LDS\\DatabaseSynchronizerServiceProvider" 39 | ] 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Commands/Synchronise.php: -------------------------------------------------------------------------------- 1 | option('from') ?? config('database-synchronizer.from'), 44 | $this->option('to') ?? config('database-synchronizer.to'), 45 | $this 46 | )) 47 | ->setTables($this->getTables()) 48 | ->setSkipTables($this->getSkipTables()) 49 | ->setLimit((int) $this->getLimit()) 50 | ->setOptions($this->options()) 51 | ->run(); 52 | } catch (DatabaseConnectionException $e) { 53 | $this->error($e->getMessage()); 54 | 55 | return; 56 | } 57 | } 58 | 59 | private function getTables() 60 | { 61 | return empty($this->option('tables')) ? 62 | config('database-synchronizer.tables') : $this->option('tables'); 63 | } 64 | 65 | private function getSkipTables() 66 | { 67 | return empty($this->option('skip-tables')) ? 68 | config('database-synchronizer.skip_tables') : $this->option('skip-tables'); 69 | } 70 | 71 | private function getLimit() 72 | { 73 | return $this->option('limit') ?? config('database-synchronizer.limit', DatabaseSynchronizer::DEFAULT_LIMIT); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Laravel database synchronizer 2 | [![Latest Version on Packagist][ico-version]][link-packagist] 3 | [![Total Downloads][ico-downloads]][link-downloads] 4 | [![Build Status][ico-travis]][link-travis] 5 | [![StyleCI][ico-styleci]][link-styleci] 6 | [![Scrutinizer Code Quality][ico-scrutinizer]][link-scrutinizer] 7 | 8 | # Keep your production and development databases in sync 9 | 10 | This package will completely synchronize the database specified as "from" and "to" in the config or through the command options. 11 | 12 | Want to collaborate? Nice! Take a look at [contributing.md](contributing.md) to see a to do list. 13 | 14 | ## Installation 15 | 16 | Via Composer 17 | 18 | ``` bash 19 | $ composer require mtolhuijs/laravel-database-synchronizer 20 | ``` 21 | 22 | *Optionally* you can run `php artisan vendor:publish --provider mtolhuijs\LDS\DatabaseSynchronizerServiceProvider` 23 | which will create a config file in the root config folder to adjust the behavior of the package. 24 | 25 | ## Usage 26 | 27 | This package comes with 1 command: 28 | 29 | - `php artisan db:sync` Synchronizes your "from" database with your "to" database 30 | ``` 31 | db:sync 32 | { --from= : Synchronize data from this database instead of the one specified in config } 33 | { --to= : Synchronize data to this database instead of the one specified in config } 34 | { --t|tables=* : Only run for given table(s) } 35 | { --st|skip-tables=* : Skip given table(s) } 36 | { --l|limit= : Limit query rows (defaults to 5000) } 37 | { --truncate : Truncate before inserting data } 38 | ``` 39 | 40 | ## Change log 41 | 42 | Please see the [changelog](changelog.md) for more information on what has changed recently. 43 | 44 | ## Contributing 45 | 46 | Please see [contributing.md](contributing.md) for details and a todolist. 47 | 48 | ## Security 49 | 50 | If you discover any security related issues, please email author email instead of using the issue tracker. 51 | 52 | ## Credits 53 | 54 | - [Maarten Tolhuijs][link-author] 55 | - [All Contributors][link-contributors] 56 | 57 | ## License 58 | 59 | license. Please see the [license file](license.md) for more information. 60 | 61 | [ico-version]: https://img.shields.io/packagist/v/Mtolhuijs/laravel-database-synchronizer.svg?style=flat-square 62 | [ico-downloads]: https://img.shields.io/packagist/dt/mtolhuijs/laravel-database-synchronizer.svg?style=flat-square 63 | [ico-travis]: https://api.travis-ci.com/mtolhuys/laravel-database-synchronizer.svg?branch=master 64 | [ico-styleci]: https://styleci.io/repos/177603107/shield 65 | [ico-scrutinizer]: https://scrutinizer-ci.com/g/mtolhuys/laravel-database-synchronizer/badges/quality-score.png?b=master 66 | 67 | [link-packagist]: https://packagist.org/packages/mtolhuijs/laravel-database-synchronizer 68 | [link-downloads]: https://packagist.org/packages/mtolhuijs/laravel-database-synchronizer 69 | [link-travis]: https://travis-ci.org/mtolhuys/laravel-database-synchronizer 70 | [link-styleci]: https://styleci.io/repos/177603107 71 | [link-scrutinizer]: https://scrutinizer-ci.com/g/mtolhuys/laravel-database-synchronizer/?branch=master 72 | 73 | [link-author]: https://github.com/mtolhuys 74 | [link-contributors]: ../../contributors 75 | -------------------------------------------------------------------------------- /src/DatabaseSynchronizer.php: -------------------------------------------------------------------------------- 1 | from = $from; 32 | $this->to = $to; 33 | $this->cli = $cli; 34 | 35 | try { 36 | $this->fromDB = DB::connection($this->from); 37 | $this->toDB = DB::connection($this->to); 38 | } catch (\Exception $e) { 39 | throw new DatabaseConnectionException($e->getMessage()); 40 | } 41 | } 42 | 43 | public function run(): void 44 | { 45 | $originHost = $this->fromDB->getConfig()['host']; 46 | $targetHost = $this->toDB->getConfig()['host']; 47 | 48 | if ($this->cli->confirm("Target host is set to $targetHost, continue?")) { 49 | $this->feedback("origin($originHost) => target($targetHost)", 'line'); 50 | } else { 51 | $this->feedback('Canceled!', 'warn'); 52 | 53 | return; 54 | } 55 | 56 | if ($this->migrate) { 57 | Artisan::call('migrate'.($this->truncate ? ':refresh' : ''), [ 58 | '--database' => $this->to, 59 | ]); 60 | } 61 | 62 | foreach ($this->getTables() as $table) { 63 | $this->feedback(PHP_EOL.PHP_EOL."Table: $table", 'line'); 64 | 65 | if (! Schema::connection($this->from)->hasTable($table)) { 66 | $this->feedback("Table '$table' does not exist in $this->from", 'error'); 67 | 68 | continue; 69 | } 70 | 71 | $this->syncTable($table); 72 | $this->syncRows($table); 73 | } 74 | 75 | $this->feedback('Synchronization done!', 'info'); 76 | } 77 | 78 | private function createTable(string $table, array $columns): void 79 | { 80 | $this->feedback("Creating '$this->to.$table' table", 'warn'); 81 | 82 | Schema::connection($this->to)->create($table, function (Blueprint $table_bp) use ($table, $columns) { 83 | foreach ($columns as $column) { 84 | $type = Schema::connection($this->from)->getColumnType($table, $column); 85 | 86 | $table_bp->{$type}($column)->nullable(); 87 | 88 | $this->feedback("Added {$type}('$column')->nullable()"); 89 | } 90 | }); 91 | } 92 | 93 | private function updateTable(string $table, string $column): void 94 | { 95 | Schema::connection($this->to)->table($table, function (Blueprint $table_bp) use ($table, $column) { 96 | $type = Schema::connection($this->from)->getColumnType($table, $column); 97 | 98 | $table_bp->{$type}($column)->nullable(); 99 | 100 | $this->feedback("Added {$type}('$column')->nullable()"); 101 | }); 102 | } 103 | 104 | public function setSkipTables(array $skipTables) 105 | { 106 | $this->skipTables = $skipTables; 107 | 108 | return $this; 109 | } 110 | 111 | public function setTables(array $tables) 112 | { 113 | $this->tables = $tables; 114 | 115 | return $this; 116 | } 117 | 118 | public function setLimit(int $limit) 119 | { 120 | $this->limit = $limit; 121 | 122 | return $this; 123 | } 124 | 125 | public function setOptions(array $options) 126 | { 127 | foreach ($options as $option => $value) { 128 | if (! isset($this->{$option})) { 129 | $this->{$option} = $value; 130 | } 131 | } 132 | 133 | return $this; 134 | } 135 | 136 | protected function getFromDb(): ConnectionInterface 137 | { 138 | if ($this->fromDB === null) { 139 | $this->fromDB = DB::connection($this->from); 140 | } 141 | 142 | return $this->fromDB; 143 | } 144 | 145 | protected function getToDb(): ConnectionInterface 146 | { 147 | if ($this->toDB === null) { 148 | $this->toDB = DB::connection($this->to); 149 | } 150 | 151 | return $this->toDB; 152 | } 153 | 154 | public function getTables(): array 155 | { 156 | if (empty($this->tables)) { 157 | $this->tables = $this->getFromDb()->getDoctrineSchemaManager()->listTableNames(); 158 | } 159 | 160 | return array_filter($this->tables, function ($table) { 161 | return ! in_array($table, $this->skipTables, true); 162 | }); 163 | } 164 | 165 | /** 166 | * Check if tables and columns are present 167 | * Create or update them if not. 168 | * 169 | * @param string $table 170 | */ 171 | public function syncTable(string $table): void 172 | { 173 | $schema = Schema::connection($this->to); 174 | $columns = Schema::connection($this->from)->getColumnListing($table); 175 | 176 | if ($schema->hasTable($table)) { 177 | foreach ($columns as $column) { 178 | if ($schema->hasColumn($table, $column)) { 179 | continue; 180 | } 181 | 182 | $this->updateTable($table, $column); 183 | } 184 | 185 | return; 186 | } 187 | 188 | $this->createTable($table, $columns); 189 | } 190 | 191 | /** 192 | * Fetch all rows in $this->from and insert or update $this->to. 193 | * @todo need to get the real primary key 194 | * @todo add limit offset setup 195 | * @todo investigate: insert into on duplicate key update 196 | * 197 | * @param string $table 198 | */ 199 | public function syncRows(string $table): void 200 | { 201 | $queryColumn = Schema::connection($this->from)->getColumnListing($table)[0]; 202 | $statement = $this->prepareForInserts($table); 203 | 204 | while ($row = $statement->fetch(\PDO::FETCH_OBJ)) { 205 | $exists = $this->getToDb()->table($table)->where($queryColumn, $row->{$queryColumn})->first(); 206 | 207 | if (! $exists) { 208 | $this->getToDb()->table($table)->insert((array) $row); 209 | } else { 210 | $this->getToDb()->table($table)->where($queryColumn, $row->{$queryColumn})->update((array) $row); 211 | } 212 | 213 | if ($this->cli) { 214 | $this->cli->progressBar->advance(); 215 | } 216 | } 217 | 218 | if ($this->cli) { 219 | $this->cli->progressBar->finish(); 220 | } 221 | } 222 | 223 | /** 224 | * @param string $table 225 | * @return \PDOStatement 226 | */ 227 | private function prepareForInserts(string $table): \PDOStatement 228 | { 229 | $pdo = $this->getFromDb()->getPdo(); 230 | $builder = $this->fromDB->table($table); 231 | $statement = $pdo->prepare($builder->toSql()); 232 | 233 | if (! $statement instanceof \PDOStatement) { 234 | throw new PDOException("Could not prepare PDOStatement for $table"); 235 | } 236 | 237 | $statement->execute($builder->getBindings()); 238 | $amount = $statement->rowCount(); 239 | 240 | if ($this->cli) { 241 | if ($amount > 0) { 242 | $this->feedback("Synchronizing '$this->to.$table' rows", 'comment'); 243 | $this->cli->progressBar = $this->cli->getOutput()->createProgressBar($amount); 244 | } else { 245 | $this->feedback('No rows...', 'comment'); 246 | } 247 | } 248 | 249 | if ($this->truncate) { 250 | $this->getToDb()->table($table)->truncate(); 251 | } 252 | 253 | return $statement; 254 | } 255 | 256 | private function feedback(string $msg, $type = 'info'): void 257 | { 258 | if ($this->cli) { 259 | $this->cli->{$type}($msg); 260 | } else { 261 | echo PHP_EOL.$msg.PHP_EOL; 262 | } 263 | } 264 | } 265 | --------------------------------------------------------------------------------