├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .scrutinizer.yml ├── .styleci.yml ├── README.md ├── composer.json ├── doc └── images │ └── screencast.gif ├── phpunit.xml.dist ├── src ├── DefaultNamingStrategy.php ├── FluidColumn.php ├── FluidColumnOptions.php ├── FluidSchema.php ├── FluidSchemaException.php ├── FluidTable.php └── NamingStrategyInterface.php └── tests ├── DefaultNamingStrategyTest.php ├── FluidColumnOptionsTest.php ├── FluidColumnTest.php ├── FluidSchemaTest.php └── FluidTableTest.php /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: "Continuous Integration" 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: 7 | - master 8 | jobs: 9 | test: 10 | name: "Test" 11 | runs-on: "ubuntu-latest" 12 | 13 | strategy: 14 | matrix: 15 | php-version: 16 | # PHPUnit v10 needs PHP 8.1, hence no way to test on older php versions 17 | - "8.1" 18 | - "8.2" 19 | - "8.3" 20 | dependencies: 21 | - "lowest" 22 | - "highest" 23 | 24 | steps: 25 | - name: "Checkout" 26 | uses: "actions/checkout@v4" 27 | 28 | - name: "Install PHP" 29 | uses: "shivammathur/setup-php@v2" 30 | with: 31 | coverage: "xdebug" 32 | php-version: "${{ matrix.php-version }}" 33 | ini-values: "zend.assertions=1" 34 | 35 | - uses: "ramsey/composer-install@v2" 36 | with: 37 | dependency-versions: "${{ matrix.dependencies }}" 38 | 39 | - name: "Run PHPUnit" 40 | run: "vendor/bin/phpunit -c phpunit.xml.dist" 41 | 42 | - name: Upload coverage results to Coveralls 43 | # skip php-coversalls for lowest deps 44 | # it fails on lowest depedencies because old versions of guzzle doesn't work well with newer php versions 45 | if: "${{ 'highest' == matrix.dependencies }}" 46 | env: 47 | COVERALLS_REPO_TOKEN: ${{ secrets.GITHUB_TOKEN }} 48 | run: | 49 | vendor/bin/php-coveralls --coverage_clover=build/logs/clover.xml -v 50 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor/ 2 | /composer.lock 3 | /build/ 4 | /phpunit.xml 5 | .phpunit.result.cache -------------------------------------------------------------------------------- /.scrutinizer.yml: -------------------------------------------------------------------------------- 1 | build: 2 | environment: 3 | php: 4 | version: 8.1 5 | ini: 6 | xdebug.mode: coverage 7 | 8 | checks: 9 | php: 10 | code_rating: true 11 | duplication: true -------------------------------------------------------------------------------- /.styleci.yml: -------------------------------------------------------------------------------- 1 | preset: psr2 -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Latest Stable Version](https://poser.pugx.org/thecodingmachine/dbal-fluid-schema-builder/v/stable)](https://packagist.org/packages/thecodingmachine/dbal-fluid-schema-builder) 2 | [![Total Downloads](https://poser.pugx.org/thecodingmachine/dbal-fluid-schema-builder/downloads)](https://packagist.org/packages/thecodingmachine/dbal-fluid-schema-builder) 3 | [![Latest Unstable Version](https://poser.pugx.org/thecodingmachine/dbal-fluid-schema-builder/v/unstable)](https://packagist.org/packages/thecodingmachine/dbal-fluid-schema-builder) 4 | [![License](https://poser.pugx.org/thecodingmachine/dbal-fluid-schema-builder/license)](https://packagist.org/packages/thecodingmachine/dbal-fluid-schema-builder) 5 | [![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/thecodingmachine/dbal-fluid-schema-builder/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/thecodingmachine/dbal-fluid-schema-builder/?branch=master) 6 | [![Build Status](https://travis-ci.org/thecodingmachine/dbal-fluid-schema-builder.svg?branch=master)](https://travis-ci.org/thecodingmachine/dbal-fluid-schema-builder) 7 | [![Coverage Status](https://coveralls.io/repos/thecodingmachine/dbal-fluid-schema-builder/badge.svg?branch=master&service=github)](https://coveralls.io/github/thecodingmachine/dbal-fluid-schema-builder?branch=master) 8 | 9 | # Fluid schema builder for Doctrine DBAL 10 | 11 | Build and modify your database schema using [DBAL](http://docs.doctrine-project.org/projects/doctrine-dbal/en/latest/reference/schema-representation.html) and a fluid syntax. 12 | 13 | ![Screencast](doc/images/screencast.gif) 14 | 15 | ## Why? 16 | 17 | Doctrine DBAL provides a powerful API to alter your database schema. 18 | This library is a wrapper around the DBAL standard API to provide a shorter, quicker syntax for day-to-day use. It provides shortcuts and syntactic sugars to make you efficient. 19 | 20 | ### IDE friendly 21 | 22 | You can use the autocomplete of your preferred IDE (PHPStorm, Eclipse PDT, Netbeans...) to build your schema easily. No need to look at the docs anymore! 23 | 24 | ### Static code analysis 25 | 26 | Your favorite static PHP code analyzer (Scrutinizer, PHPStan...) can catch errors for you! 27 | For instance, each database type is a PHP method, so no typos anymore in the column type - ... was it 'INT' or 'INTEGER' already? :) 28 | 29 | ## Why not? 30 | 31 | The fluid schema builders aims at solving the cases you encounter in 99% of your schemas in a concise way. 32 | It does not cover the whole possible use cases and there is no aim to target that goal. 33 | 34 | For instance, if you have foreign keys on several columns, you cannot use `FluidSchema`. You should fallback to classic DBAL. 35 | 36 | 37 | ## Comparison with DBAL "native" API 38 | 39 | Instead of: 40 | 41 | ```php 42 | $table = $schema->createTable('posts'); 43 | $table->addColumn('id', 'integer'); 44 | $table->addColumn('description', 'string', [ 45 | 'length' => 50, 46 | 'notnull' => false, 47 | ]); 48 | $table->addColumn('user_id', 'integer'); 49 | $table->setPrimaryKey(['id']); 50 | $table->addForeignKeyConstraint('users', ['user_id'], ['id']); 51 | ``` 52 | 53 | you write: 54 | 55 | ```php 56 | $db = new FluidSchema($schema); 57 | 58 | $posts = $db->table('posts'); 59 | 60 | $posts->id() // Let's create a default autoincremented ID column 61 | ->column('description')->string(50)->null() // Let's create a 'description' column 62 | ->column('user_id')->references('users'); // Let's create a foreign key. 63 | // We only specify the table name. 64 | // FluidSchema infers the column type and the "remote" column. 65 | ``` 66 | 67 | ## Features 68 | 69 | FluidSchema does its best to make your life easier. 70 | 71 | **Tables and column types** 72 | 73 | ```php 74 | $table = $db->table('foo'); 75 | 76 | // Supported types 77 | $table->column('xxxx')->string(50) // VARCHAR(50) 78 | ->column('xxxx')->integer() 79 | ->column('xxxx')->float() 80 | ->column('xxxx')->text() // Long string 81 | ->column('xxxx')->boolean() 82 | ->column('xxxx')->smallInt() 83 | ->column('xxxx')->bigInt() 84 | ->column('xxxx')->decimal(10, 2) // DECIMAL(10, 2) 85 | ->column('xxxx')->guid() 86 | ->column('xxxx')->binary(255) 87 | ->column('xxxx')->blob() // Long binary 88 | ->column('xxxx')->date() 89 | ->column('xxxx')->datetime() 90 | ->column('xxxx')->datetimeTz() 91 | ->column('xxxx')->time() 92 | ->column('xxxx')->dateImmutable() // From Doctrine DBAL 2.6+ 93 | ->column('xxxx')->datetimeImmutable() // From Doctrine DBAL 2.6+ 94 | ->column('xxxx')->datetimeTzImmutable() // From Doctrine DBAL 2.6+ 95 | ->column('xxxx')->timeImmutable() // From Doctrine DBAL 2.6+ 96 | ->column('xxxx')->dateInterval() // From Doctrine DBAL 2.6+ 97 | ->column('xxxx')->array() 98 | ->column('xxxx')->simpleArray() 99 | ->column('xxxx')->json() // From Doctrine DBAL 2.6+ 100 | ->column('xxxx')->jsonArray() // Deprecated in Doctrine DBAL 2.6+ 101 | ->column('xxxx')->object(); // Serialized PHP object 102 | ``` 103 | 104 | **Shortcut methods:** 105 | 106 | ```php 107 | // Create an 'id' primary key that is an autoincremented integer 108 | $table->id(); 109 | 110 | // Don't like autincrements? No problem! 111 | // Create an 'uuid' primary key that is of the DBAL 'guid' type 112 | $table->uuid(); 113 | 114 | // Create "created_at" and "updated_at" columns 115 | $table->timestamps(); 116 | ``` 117 | 118 | **Creating indexes:** 119 | 120 | ```php 121 | // Directly on a column: 122 | $table->column('login')->string(50)->index(); 123 | 124 | // Or on the table object (if there are several columns to add to an index): 125 | $table->index(['category1', 'category2']); 126 | ``` 127 | 128 | **Creating unique indexes:** 129 | 130 | ```php 131 | // Directly on a column: 132 | $table->column('login')->string(50)->unique(); 133 | 134 | // Or on the table object (if there are several columns to add to the constraint): 135 | $table->unique(['login', 'status']); 136 | ``` 137 | 138 | **Make a column nullable:** 139 | 140 | ```php 141 | $table->column('description')->string(50)->null(); 142 | ``` 143 | 144 | **Set the default value of a column:** 145 | 146 | ```php 147 | $table->column('enabled')->bool()->default(true); 148 | ``` 149 | 150 | **Create a foreign key** 151 | 152 | ```php 153 | $table->column('country_id')->references('countries'); 154 | ``` 155 | 156 | **Note:** The foreign key will be automatically created on the primary table of the table "countries". 157 | The type of the "country_id" column will be exactly the same as the type of the primary key of the "countries" table. 158 | 159 | **Create a jointure table (aka associative table) between 2 tables:** 160 | 161 | ```php 162 | $db->junctionTable('users', 'roles'); 163 | 164 | // This will create a 'users_roles' table with 2 foreign keys: 165 | // - 'user_id' pointing on the PK of 'users' 166 | // - 'role_id' pointing on the PK of 'roles' 167 | ``` 168 | 169 | **Add a comment to a column:** 170 | 171 | ```php 172 | $table->column('description')->string(50)->comment('Lorem ipsum'); 173 | ``` 174 | 175 | **Declare a primary key:** 176 | 177 | ```php 178 | $table->column('uuid')->string(36)->primaryKey(); 179 | 180 | // or 181 | 182 | $table->column('uuid')->then() 183 | ->primaryKey(['uuid']); 184 | ``` 185 | 186 | **Declare an inheritance relationship between 2 tables:** 187 | 188 | In SQL, there is no notion of "inheritance" like with PHP objects. 189 | However, a common way to model inheritance is to write one table for the base class (containing the base columns/properties) and then one table per extended class containing the additional columns/properties. 190 | Each extended table has **a primary key that is also a foreign key pointing to the base table**. 191 | 192 | ```php 193 | $db->table('contacts') 194 | ->id() 195 | ->column('email')->string(50); 196 | 197 | $db->table('users') 198 | ->extends('contacts') 199 | ->column('password')->string(50); 200 | ``` 201 | 202 | The `extends` method will automatically create a primary key with the same name and same type as the extended table. It will also make sure this primary key is a foreign key pointing to the extended table. 203 | 204 | ## Automatic 'quoting' of table and column names 205 | 206 | By default, the fluid-schema-builder will **not** quote your identifiers (because it does not know what database you use). 207 | 208 | This means that you cannot create an item with a reserved keyword. 209 | 210 | ```php 211 | $db->table('contacts') 212 | ->id() 213 | ->column('date')->datetime(); // Will most likely fail, because "date" is a reserved keyword! 214 | ``` 215 | 216 | However, if you give to *fluid-schema-builder* your database platform at build time, then it **will quote all identifiers by default**. No more nasty surprises! 217 | 218 | ```php 219 | use TheCodingMachine\FluidSchema\DefaultNamingStrategy; 220 | 221 | // Assuming $connection is your DBAL connection 222 | $db = new FluidSchema($schema, new DefaultNamingStrategy($connection->getDatabasePlatform())); 223 | ``` 224 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "thecodingmachine/dbal-fluid-schema-builder", 3 | "description": "Build and modify your database schema using Doctrine DBAL and a fluid syntax.", 4 | "license": "MIT", 5 | "authors": [ 6 | { 7 | "name": "David Négrier", 8 | "email": "d.negrier@thecodingmachine.com" 9 | } 10 | ], 11 | "require": { 12 | "php": "^7.4 || ^8.0", 13 | "doctrine/dbal": "^3.0", 14 | "doctrine/inflector": "^1.4 || ^2.0" 15 | }, 16 | "require-dev": { 17 | "phpunit/phpunit": "^10.2", 18 | "php-coveralls/php-coveralls": "^2.7.0" 19 | }, 20 | "autoload": { 21 | "psr-4": { 22 | "TheCodingMachine\\FluidSchema\\": "src/" 23 | } 24 | }, 25 | "autoload-dev": { 26 | "psr-4": { 27 | "TheCodingMachine\\FluidSchema\\": "tests/" 28 | } 29 | }, 30 | "minimum-stability": "dev", 31 | "prefer-stable": true 32 | } 33 | -------------------------------------------------------------------------------- /doc/images/screencast.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thecodingmachine/dbal-fluid-schema-builder/e0b57b14327ae484959e391f838af303a619c887/doc/images/screencast.gif -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 | 10 | 11 | ./tests/ 12 | 13 | 14 | 15 | 16 | 17 | src/ 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /src/DefaultNamingStrategy.php: -------------------------------------------------------------------------------- 1 | platform = $platform; 24 | $this->inflector = InflectorFactory::create()->build(); 25 | } 26 | 27 | /** 28 | * Returns the name of the jointure table from the name of the joined tables. 29 | * 30 | * @param string $table1 31 | * @param string $table2 32 | * @return string 33 | */ 34 | public function getJointureTableName(string $table1, string $table2): string 35 | { 36 | return $table1.'_'.$table2; 37 | } 38 | 39 | /** 40 | * Returns the name of a foreign key column based on the name of the targeted table. 41 | * 42 | * @param string $targetTable 43 | * @return string 44 | */ 45 | public function getForeignKeyColumnName(string $targetTable): string 46 | { 47 | return $this->toSingular($targetTable).'_id'; 48 | } 49 | 50 | /** 51 | * Put all the words of a string separated by underscores on singular. 52 | * Assumes the words are in English and in their plural form. 53 | * 54 | * @param $plural 55 | * @return string 56 | */ 57 | private function toSingular($plural): string 58 | { 59 | $tokens = preg_split("/[_ ]+/", $plural); 60 | 61 | $strs = []; 62 | foreach ($tokens as $token) { 63 | $strs[] = $this->inflector->singularize($token); 64 | } 65 | 66 | return implode('_', $strs); 67 | } 68 | 69 | /** 70 | * Let's quote if a database platform has been provided to us! 71 | * 72 | * @param string $identifier 73 | * @return string 74 | */ 75 | public function quoteIdentifier(string $identifier): string 76 | { 77 | return ($this->platform !== null) ? $this->platform->quoteIdentifier($identifier) : $identifier; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/FluidColumn.php: -------------------------------------------------------------------------------- 1 | fluidSchema = $fluidSchema; 44 | $this->fluidTable = $fluidTable; 45 | $this->column = $column; 46 | $this->table = $table; 47 | $this->namingStrategy = $namingStrategy; 48 | } 49 | 50 | public function integer(): FluidColumnOptions 51 | { 52 | $this->column->setType(Type::getType(Types::INTEGER)); 53 | return $this->getOptions(); 54 | } 55 | 56 | public function smallInt(): FluidColumnOptions 57 | { 58 | $this->column->setType(Type::getType(Types::SMALLINT)); 59 | return $this->getOptions(); 60 | } 61 | 62 | public function bigInt(): FluidColumnOptions 63 | { 64 | $this->column->setType(Type::getType(Types::BIGINT)); 65 | return $this->getOptions(); 66 | } 67 | 68 | public function decimal(int $precision = 10, int $scale = 0): FluidColumnOptions 69 | { 70 | $this->column->setType(Type::getType(Types::DECIMAL)); 71 | $this->column->setPrecision($precision); 72 | $this->column->setScale($scale); 73 | return $this->getOptions(); 74 | } 75 | 76 | public function float(int $precision = 10, int $scale = 0): FluidColumnOptions 77 | { 78 | $this->column->setType(Type::getType(Types::FLOAT)); 79 | $this->column->setPrecision($precision); 80 | $this->column->setScale($scale); 81 | return $this->getOptions(); 82 | } 83 | 84 | public function string(?int $length = null, bool $fixed = false): FluidColumnOptions 85 | { 86 | $this->column->setType(Type::getType(Types::STRING)); 87 | $this->column->setLength($length); 88 | $this->column->setFixed($fixed); 89 | return $this->getOptions(); 90 | } 91 | 92 | public function text(?int $length = null): FluidColumnOptions 93 | { 94 | $this->column->setType(Type::getType(Types::TEXT)); 95 | $this->column->setLength($length); 96 | return $this->getOptions(); 97 | } 98 | 99 | public function guid(): FluidColumnOptions 100 | { 101 | $this->column->setType(Type::getType(Types::GUID)); 102 | return $this->getOptions(); 103 | } 104 | 105 | /** 106 | * From Doctrine DBAL 2.4+. 107 | */ 108 | public function binary(?int $length = null, bool $fixed = false): FluidColumnOptions 109 | { 110 | $this->column->setType(Type::getType(Types::BINARY)); 111 | $this->column->setLength($length); 112 | $this->column->setFixed($fixed); 113 | return $this->getOptions(); 114 | } 115 | 116 | public function blob(): FluidColumnOptions 117 | { 118 | $this->column->setType(Type::getType(Types::BLOB)); 119 | return $this->getOptions(); 120 | } 121 | 122 | public function boolean(): FluidColumnOptions 123 | { 124 | $this->column->setType(Type::getType(Types::BOOLEAN)); 125 | return $this->getOptions(); 126 | } 127 | 128 | public function date(): FluidColumnOptions 129 | { 130 | $this->column->setType(Type::getType(Types::DATE_MUTABLE)); 131 | return $this->getOptions(); 132 | } 133 | 134 | public function dateImmutable(): FluidColumnOptions 135 | { 136 | $this->column->setType(Type::getType(Types::DATE_IMMUTABLE)); 137 | return $this->getOptions(); 138 | } 139 | 140 | public function datetime(): FluidColumnOptions 141 | { 142 | $this->column->setType(Type::getType(Types::DATETIME_MUTABLE)); 143 | return $this->getOptions(); 144 | } 145 | 146 | public function datetimeImmutable(): FluidColumnOptions 147 | { 148 | $this->column->setType(Type::getType(Types::DATETIME_IMMUTABLE)); 149 | return $this->getOptions(); 150 | } 151 | 152 | public function datetimeTz(): FluidColumnOptions 153 | { 154 | $this->column->setType(Type::getType(Types::DATETIMETZ_MUTABLE)); 155 | return $this->getOptions(); 156 | } 157 | 158 | public function datetimeTzImmutable(): FluidColumnOptions 159 | { 160 | $this->column->setType(Type::getType(Types::DATETIMETZ_IMMUTABLE)); 161 | return $this->getOptions(); 162 | } 163 | 164 | public function time(): FluidColumnOptions 165 | { 166 | $this->column->setType(Type::getType(Types::TIME_MUTABLE)); 167 | return $this->getOptions(); 168 | } 169 | 170 | public function timeImmutable(): FluidColumnOptions 171 | { 172 | $this->column->setType(Type::getType(Types::TIME_IMMUTABLE)); 173 | return $this->getOptions(); 174 | } 175 | 176 | public function dateInterval(): FluidColumnOptions 177 | { 178 | $this->column->setType(Type::getType(Types::DATEINTERVAL)); 179 | return $this->getOptions(); 180 | } 181 | 182 | /** 183 | * @deprecated Use json() instead 184 | */ 185 | public function array(): FluidColumnOptions 186 | { 187 | $this->column->setType(Type::getType(Types::ARRAY)); 188 | return $this->getOptions(); 189 | } 190 | 191 | public function simpleArray(): FluidColumnOptions 192 | { 193 | $this->column->setType(Type::getType(Types::SIMPLE_ARRAY)); 194 | return $this->getOptions(); 195 | } 196 | 197 | public function json(): FluidColumnOptions 198 | { 199 | $this->column->setType(Type::getType(Types::JSON)); 200 | return $this->getOptions(); 201 | } 202 | 203 | /** 204 | * @deprecated From DBAL 2.6, use json() instead. 205 | * @return FluidColumnOptions 206 | */ 207 | public function jsonArray(): FluidColumnOptions 208 | { 209 | $this->column->setType(Type::getType(Types::JSON)); 210 | return $this->getOptions(); 211 | } 212 | 213 | /** 214 | * @deprecated Use json() instead 215 | */ 216 | public function object(): FluidColumnOptions 217 | { 218 | $this->column->setType(Type::getType(Types::OBJECT)); 219 | return $this->getOptions(); 220 | } 221 | 222 | public function references(string $tableName, ?string $constraintName = null, string $onUpdate = 'RESTRICT', string $onDelete = 'RESTRICT'): FluidColumnOptions 223 | { 224 | $tableName = $this->namingStrategy->quoteIdentifier($tableName); 225 | 226 | $table = $this->fluidSchema->getDbalSchema()->getTable($tableName); 227 | 228 | $referencedColumns = $table->getPrimaryKey()->getColumns(); 229 | 230 | if (count($referencedColumns) > 1) { 231 | throw new FluidSchemaException('You cannot reference a table with a primary key on several columns using FluidSchema. Use DBAL Schema methods instead.'); 232 | } 233 | 234 | $referencedColumnName = $this->namingStrategy->quoteIdentifier($referencedColumns[0]); 235 | $referencedColumn = $table->getColumn($referencedColumnName); 236 | 237 | $this->column->setType($referencedColumn->getType()); 238 | $this->column->setLength($referencedColumn->getLength()); 239 | $this->column->setFixed($referencedColumn->getFixed()); 240 | $this->column->setScale($referencedColumn->getScale()); 241 | $this->column->setPrecision($referencedColumn->getPrecision()); 242 | $this->column->setUnsigned($referencedColumn->getUnsigned()); 243 | 244 | $this->table->addForeignKeyConstraint($table, [$this->namingStrategy->quoteIdentifier($this->column->getName())], $referencedColumns, [ 245 | 'onUpdate' => $onUpdate, 246 | 'onDelete' => $onDelete 247 | ], $constraintName); 248 | return $this->getOptions(); 249 | } 250 | 251 | private function getOptions(): FluidColumnOptions 252 | { 253 | return new FluidColumnOptions($this->fluidTable, $this->column, $this->namingStrategy); 254 | } 255 | 256 | /** 257 | * Returns the underlying DBAL column. 258 | * @return Column 259 | */ 260 | public function getDbalColumn(): Column 261 | { 262 | return $this->column; 263 | } 264 | } 265 | -------------------------------------------------------------------------------- /src/FluidColumnOptions.php: -------------------------------------------------------------------------------- 1 | fluidTable = $fluidTable; 32 | $this->column = $column; 33 | $this->namingStrategy = $namingStrategy; 34 | } 35 | 36 | /** 37 | * Makes the column not nullable. 38 | * @return FluidColumnOptions 39 | */ 40 | public function notNull(): FluidColumnOptions 41 | { 42 | $this->column->setNotnull(true); 43 | return $this; 44 | } 45 | 46 | /** 47 | * Makes the column nullable. 48 | * @return FluidColumnOptions 49 | */ 50 | public function null(): FluidColumnOptions 51 | { 52 | $this->column->setNotnull(false); 53 | return $this; 54 | } 55 | 56 | /** 57 | * Automatically add a unique constraint for the column. 58 | * 59 | * @return FluidColumnOptions 60 | */ 61 | public function unique(): FluidColumnOptions 62 | { 63 | $this->column->setCustomSchemaOption('unique', true); 64 | return $this; 65 | } 66 | 67 | /** 68 | * Automatically add an index for the column. 69 | * 70 | * @return FluidColumnOptions 71 | */ 72 | public function index(?string $indexName = null): FluidColumnOptions 73 | { 74 | $this->fluidTable->index([$this->namingStrategy->quoteIdentifier($this->column->getName())], $indexName); 75 | return $this; 76 | } 77 | public function comment(string $comment): FluidColumnOptions 78 | { 79 | $this->column->setComment($comment); 80 | return $this; 81 | } 82 | 83 | public function autoIncrement(): FluidColumnOptions 84 | { 85 | $this->column->setAutoincrement(true); 86 | return $this; 87 | } 88 | 89 | public function primaryKey(?string $indexName = null): FluidColumnOptions 90 | { 91 | $newIndexName = $indexName ?: false; 92 | 93 | $this->fluidTable->primaryKey([$this->namingStrategy->quoteIdentifier($this->column->getName())], $newIndexName); 94 | return $this; 95 | } 96 | 97 | public function default($defaultValue): FluidColumnOptions 98 | { 99 | $this->column->setDefault($defaultValue); 100 | return $this; 101 | } 102 | 103 | public function then(): FluidTable 104 | { 105 | return $this->fluidTable; 106 | } 107 | 108 | public function column($name): FluidColumn 109 | { 110 | return $this->fluidTable->column($name); 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/FluidSchema.php: -------------------------------------------------------------------------------- 1 | schema = $schema; 32 | $this->namingStrategy = $namingStrategy ?: new DefaultNamingStrategy(); 33 | } 34 | 35 | public function table(string $name): FluidTable 36 | { 37 | $name = $this->namingStrategy->quoteIdentifier($name); 38 | 39 | if (isset($this->fluidTables[$name])) { 40 | return $this->fluidTables[$name]; 41 | } 42 | 43 | if ($this->schema->hasTable($name)) { 44 | $table = $this->schema->getTable($name); 45 | } else { 46 | $table = $this->schema->createTable($name); 47 | } 48 | 49 | $this->fluidTables[$name] = new FluidTable($this, $table, $this->namingStrategy); 50 | return $this->fluidTables[$name]; 51 | } 52 | 53 | /** 54 | * Creates a table joining 2 other tables through a foreign key. 55 | * 56 | * @param string $table1 57 | * @param string $table2 58 | * @return FluidSchema 59 | */ 60 | public function junctionTable(string $table1, string $table2): FluidSchema 61 | { 62 | $tableName = $this->namingStrategy->getJointureTableName($table1, $table2); 63 | $column1 = $this->namingStrategy->getForeignKeyColumnName($table1); 64 | $column2 = $this->namingStrategy->getForeignKeyColumnName($table2); 65 | 66 | $this->table($tableName) 67 | ->column($column1)->references($table1)->then() 68 | ->column($column2)->references($table2)->then() 69 | ->primaryKey([$column1, $column2]); 70 | 71 | return $this; 72 | } 73 | 74 | /** 75 | * Returns the underlying schema. 76 | * @return Schema 77 | */ 78 | public function getDbalSchema(): Schema 79 | { 80 | return $this->schema; 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/FluidSchemaException.php: -------------------------------------------------------------------------------- 1 | schema = $schema; 34 | $this->table = $table; 35 | $this->namingStrategy = $namingStrategy; 36 | } 37 | 38 | public function column(string $name): FluidColumn 39 | { 40 | $name = $this->namingStrategy->quoteIdentifier($name); 41 | 42 | if (isset($this->fluidColumns[$name])) { 43 | return $this->fluidColumns[$name]; 44 | } 45 | 46 | if ($this->table->hasColumn($name)) { 47 | $column = $this->table->getColumn($name); 48 | } else { 49 | $column = $this->table->addColumn($name, 'string'); 50 | } 51 | 52 | $this->fluidColumns[$name] = new FluidColumn($this->schema, $this, $this->table, $column, $this->namingStrategy); 53 | return $this->fluidColumns[$name]; 54 | } 55 | 56 | public function index(array $columnNames, ?string $indexName = null): FluidTable 57 | { 58 | $this->table->addIndex($this->quoteArray($columnNames), $indexName); 59 | return $this; 60 | } 61 | 62 | public function unique(array $columnNames, ?string $indexName = null): FluidTable 63 | { 64 | $this->table->addUniqueIndex($this->quoteArray($columnNames), $indexName); 65 | return $this; 66 | } 67 | 68 | public function primaryKey(array $columnNames, ?string $indexName = null): FluidTable 69 | { 70 | $newIndexName = $indexName ?: false; 71 | 72 | $this->table->setPrimaryKey($this->quoteArray($columnNames), $newIndexName); 73 | return $this; 74 | } 75 | 76 | private function quoteArray(array $columnNames): array 77 | { 78 | return array_map([$this->namingStrategy, 'quoteIdentifier'], $columnNames); 79 | } 80 | 81 | /** 82 | * Creates a "id" autoincremented primary key column. 83 | * 84 | * @return FluidTable 85 | */ 86 | public function id(): FluidTable 87 | { 88 | $this->column('id')->integer()->primaryKey()->autoIncrement(); 89 | return $this; 90 | } 91 | 92 | /** 93 | * Creates a "uuid" primary key column. 94 | * 95 | * @return FluidTable 96 | */ 97 | public function uuid(): FluidTable 98 | { 99 | $this->column('uuid')->guid()->primaryKey(); 100 | return $this; 101 | } 102 | 103 | /** 104 | * Creates "created_at" and "updated_at" columns. 105 | * 106 | * @return FluidTable 107 | */ 108 | public function timestamps(): FluidTable 109 | { 110 | $this->column('created_at')->datetimeImmutable(); 111 | $this->column('updated_at')->datetimeImmutable(); 112 | return $this; 113 | } 114 | 115 | public function extends(string $tableName): FluidTable 116 | { 117 | $tableName = $this->namingStrategy->quoteIdentifier($tableName); 118 | 119 | $inheritedTable = $this->schema->getDbalSchema()->getTable($tableName); 120 | 121 | $pks = $inheritedTable->getPrimaryKey()->getColumns(); 122 | 123 | if (count($pks) > 1) { 124 | throw new FluidSchemaException('You cannot inherit from a table with a primary key on several columns using FluidSchema. Use DBAL Schema methods instead.'); 125 | } 126 | 127 | $pkName = $pks[0]; 128 | $pk = $inheritedTable->getColumn($pkName); 129 | 130 | $this->column($pk->getName())->references($tableName)->primaryKey(); 131 | return $this; 132 | } 133 | 134 | /** 135 | * Returns the underlying DBAL table. 136 | * @return Table 137 | */ 138 | public function getDbalTable(): Table 139 | { 140 | return $this->table; 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /src/NamingStrategyInterface.php: -------------------------------------------------------------------------------- 1 | assertSame('users_roles', $strategy->getJointureTableName('users', 'roles')); 14 | } 15 | 16 | public function testGetForeignKeyColumnName() 17 | { 18 | $strategy = new DefaultNamingStrategy(); 19 | 20 | $this->assertSame('user_id', $strategy->getForeignKeyColumnName('user')); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /tests/FluidColumnOptionsTest.php: -------------------------------------------------------------------------------- 1 | table('posts'); 17 | 18 | $column = $posts->column('foo'); 19 | $columnOptions = $column->integer(); 20 | 21 | $dbalColumn = $schema->getTable('posts')->getColumn('foo'); 22 | 23 | $columnOptions->null(); 24 | $this->assertSame(false, $dbalColumn->getNotnull()); 25 | 26 | $columnOptions->notNull(); 27 | $this->assertSame(true, $dbalColumn->getNotnull()); 28 | 29 | $columnOptions->unique(); 30 | $this->assertSame(true, $dbalColumn->getCustomSchemaOption('unique')); 31 | 32 | $columnOptions->comment('foo'); 33 | $this->assertSame('foo', $dbalColumn->getComment()); 34 | 35 | $columnOptions->autoIncrement(); 36 | $this->assertSame(true, $dbalColumn->getAutoincrement()); 37 | 38 | $columnOptions->default(42); 39 | $this->assertSame(42, $dbalColumn->getDefault()); 40 | 41 | $this->assertSame($posts, $columnOptions->then()); 42 | 43 | $columnOptions->column('bar'); 44 | $this->assertTrue($schema->getTable('posts')->hasColumn('bar')); 45 | } 46 | 47 | public function testIndex() 48 | { 49 | $schema = new Schema(); 50 | $fluid = new FluidSchema($schema); 51 | 52 | $posts = $fluid->table('posts'); 53 | 54 | $posts->column('foo')->integer()->index(); 55 | 56 | $this->assertCount(1, $schema->getTable('posts')->getIndexes()); 57 | } 58 | 59 | public function testPrimaryKey() 60 | { 61 | $schema = new Schema(); 62 | $fluid = new FluidSchema($schema); 63 | 64 | $posts = $fluid->table('posts'); 65 | 66 | $posts->column('id')->integer()->primaryKey('pkname'); 67 | 68 | $this->assertTrue($schema->getTable('posts')->hasPrimaryKey()); 69 | $this->assertTrue($schema->getTable('posts')->hasIndex('pkname')); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /tests/FluidColumnTest.php: -------------------------------------------------------------------------------- 1 | table('posts'); 18 | 19 | $column = $posts->column('foo'); 20 | 21 | $dbalColumn = $schema->getTable('posts')->getColumn('foo'); 22 | 23 | $column->integer(); 24 | $this->assertSame(Type::getType(Types::INTEGER), $dbalColumn->getType()); 25 | 26 | $column->smallInt(); 27 | $this->assertSame(Type::getType(Types::SMALLINT), $dbalColumn->getType()); 28 | 29 | $column->bigInt(); 30 | $this->assertSame(Type::getType(Types::BIGINT), $dbalColumn->getType()); 31 | 32 | $column->decimal(12, 32); 33 | $this->assertSame(Type::getType(Types::DECIMAL), $dbalColumn->getType()); 34 | $this->assertSame(12, $dbalColumn->getPrecision()); 35 | $this->assertSame(32, $dbalColumn->getScale()); 36 | 37 | $column->float(32, 12); 38 | $this->assertSame(Type::getType(Types::FLOAT), $dbalColumn->getType()); 39 | $this->assertSame(32, $dbalColumn->getPrecision()); 40 | $this->assertSame(12, $dbalColumn->getScale()); 41 | 42 | $column->string(42, true); 43 | $this->assertSame(Type::getType(Types::STRING), $dbalColumn->getType()); 44 | $this->assertSame(42, $dbalColumn->getLength()); 45 | $this->assertSame(true, $dbalColumn->getFixed()); 46 | 47 | $column->text(); 48 | $this->assertSame(Type::getType(Types::TEXT), $dbalColumn->getType()); 49 | 50 | $column->guid(); 51 | $this->assertSame(Type::getType(Types::GUID), $dbalColumn->getType()); 52 | 53 | $column->blob(); 54 | $this->assertSame(Type::getType(Types::BLOB), $dbalColumn->getType()); 55 | 56 | $column->boolean(); 57 | $this->assertSame(Type::getType(Types::BOOLEAN), $dbalColumn->getType()); 58 | 59 | $column->date(); 60 | $this->assertSame(Type::getType(Types::DATE_MUTABLE), $dbalColumn->getType()); 61 | 62 | $column->datetime(); 63 | $this->assertSame(Type::getType(Types::DATETIME_MUTABLE), $dbalColumn->getType()); 64 | 65 | $column->datetimeTz(); 66 | $this->assertSame(Type::getType(Types::DATETIMETZ_MUTABLE), $dbalColumn->getType()); 67 | 68 | $column->time(); 69 | $this->assertSame(Type::getType(Types::TIME_MUTABLE), $dbalColumn->getType()); 70 | 71 | $column->array(); 72 | $this->assertSame(Type::getType(Types::ARRAY), $dbalColumn->getType()); 73 | 74 | $column->simpleArray(); 75 | $this->assertSame(Type::getType(Types::SIMPLE_ARRAY), $dbalColumn->getType()); 76 | 77 | $column->jsonArray(); 78 | $this->assertSame(Type::getType(Types::JSON), $dbalColumn->getType()); 79 | 80 | $column->object(); 81 | $this->assertSame(Type::getType(Types::OBJECT), $dbalColumn->getType()); 82 | 83 | $column->binary(43); 84 | $this->assertSame(Type::getType(Types::BINARY), $dbalColumn->getType()); 85 | $this->assertSame(43, $dbalColumn->getLength()); 86 | $this->assertSame(false, $dbalColumn->getFixed()); 87 | 88 | $column->dateImmutable(); 89 | $this->assertSame(Type::getType('date_immutable'), $dbalColumn->getType()); 90 | 91 | $column->datetimeImmutable(); 92 | $this->assertSame(Type::getType(Types::DATETIME_IMMUTABLE), $dbalColumn->getType()); 93 | 94 | $column->datetimeTzImmutable(); 95 | $this->assertSame(Type::getType(Types::DATETIMETZ_IMMUTABLE), $dbalColumn->getType()); 96 | 97 | $column->time(); 98 | $this->assertSame(Type::getType(Types::TIME_MUTABLE), $dbalColumn->getType()); 99 | 100 | $column->timeImmutable(); 101 | $this->assertSame(Type::getType(Types::TIME_IMMUTABLE), $dbalColumn->getType()); 102 | 103 | $column->dateInterval(); 104 | $this->assertSame(Type::getType(Types::DATEINTERVAL), $dbalColumn->getType()); 105 | 106 | $column->json(); 107 | $this->assertSame(Type::getType(Types::JSON), $dbalColumn->getType()); 108 | 109 | $this->assertSame('foo', $column->getDbalColumn()->getName()); 110 | } 111 | 112 | public function testReference() 113 | { 114 | $schema = new Schema(); 115 | $fluid = new FluidSchema($schema); 116 | 117 | $countries = $fluid->table('countries'); 118 | $countries->id(); 119 | 120 | $users = $fluid->table('users'); 121 | $users->column('country_id')->references('countries', 'myfk'); 122 | 123 | $dbalColumn = $schema->getTable('users')->getColumn('country_id'); 124 | 125 | $this->assertSame(Type::getType(Types::INTEGER), $dbalColumn->getType()); 126 | $fk = $schema->getTable('users')->getForeignKey('myfk'); 127 | $this->assertSame('users', $fk->getLocalTableName()); 128 | $this->assertSame('countries', $fk->getForeignTableName()); 129 | $this->assertSame(['country_id'], $fk->getLocalColumns()); 130 | } 131 | 132 | public function testReferenceException() 133 | { 134 | $schema = new Schema(); 135 | $fluid = new FluidSchema($schema); 136 | 137 | $countries = $fluid->table('countries'); 138 | $countries->column('id1')->integer(); 139 | $countries->column('id2')->integer(); 140 | $countries->primaryKey(['id1','id2']); 141 | 142 | $users = $fluid->table('users'); 143 | $this->expectException(FluidSchemaException::class); 144 | $users->column('country_id')->references('countries', 'myfk'); 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /tests/FluidSchemaTest.php: -------------------------------------------------------------------------------- 1 | table('posts'); 16 | 17 | $this->assertTrue($schema->hasTable('posts')); 18 | 19 | $this->assertSame($posts, $fluid->table('posts'), 'Failed asserting that the same instance is returned.'); 20 | } 21 | 22 | public function testExistingTable() 23 | { 24 | $schema = new Schema(); 25 | $postsSchemaTable = $schema->createTable('posts'); 26 | $fluid = new FluidSchema($schema); 27 | 28 | $posts = $fluid->table('posts'); 29 | $posts->column('foo'); 30 | 31 | $this->assertTrue($postsSchemaTable->hasColumn('foo')); 32 | } 33 | 34 | public function testJunctionTable() 35 | { 36 | $schema = new Schema(); 37 | $db = new FluidSchema($schema); 38 | $db->table('users')->id(); 39 | $db->table('roles')->id(); 40 | $db->junctionTable('users', 'roles'); 41 | 42 | $this->assertTrue($schema->hasTable('users_roles')); 43 | $this->assertCount(2, $schema->getTable('users_roles')->getColumns()); 44 | $this->assertNotNull($schema->getTable('users_roles')->getColumn('user_id')); 45 | $this->assertNotNull($schema->getTable('users_roles')->getColumn('role_id')); 46 | } 47 | 48 | public function testGetDbalSchema() 49 | { 50 | $schema = new Schema(); 51 | $fluid = new FluidSchema($schema); 52 | 53 | $this->assertSame($schema, $fluid->getDbalSchema()); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /tests/FluidTableTest.php: -------------------------------------------------------------------------------- 1 | table('posts'); 18 | 19 | $column = $posts->column('foo'); 20 | 21 | $this->assertTrue($schema->getTable('posts')->hasColumn('foo')); 22 | 23 | $this->assertSame($column, $posts->column('foo'), 'Failed asserting that the same instance is returned.'); 24 | } 25 | 26 | public function testExistingColumn() 27 | { 28 | $schema = new Schema(); 29 | $postsSchemaTable = $schema->createTable('posts'); 30 | $postsSchemaTable->addColumn('foo', 'string'); 31 | $fluid = new FluidSchema($schema); 32 | 33 | $posts = $fluid->table('posts'); 34 | 35 | $posts->column('foo')->integer(); 36 | 37 | $this->assertSame(Type::getType(Types::INTEGER), $schema->getTable('posts')->getColumn('foo')->getType()); 38 | } 39 | 40 | public function testIndex() 41 | { 42 | $schema = new Schema(); 43 | $fluid = new FluidSchema($schema); 44 | 45 | $posts = $fluid->table('posts'); 46 | 47 | $posts->column('foo')->integer()->then()->index(['foo']); 48 | 49 | $this->assertCount(1, $schema->getTable('posts')->getIndexes()); 50 | } 51 | 52 | public function testUnique() 53 | { 54 | $schema = new Schema(); 55 | $fluid = new FluidSchema($schema); 56 | 57 | $posts = $fluid->table('posts'); 58 | 59 | $posts->column('foo')->integer()->then()->unique(['foo']); 60 | 61 | $this->assertCount(1, $schema->getTable('posts')->getIndexes()); 62 | } 63 | 64 | public function testPrimaryKey() 65 | { 66 | $schema = new Schema(); 67 | $fluid = new FluidSchema($schema); 68 | 69 | $posts = $fluid->table('posts'); 70 | 71 | $posts->column('id')->integer()->then()->primaryKey(['id'], 'pkname'); 72 | 73 | $this->assertTrue($schema->getTable('posts')->hasPrimaryKey()); 74 | $this->assertTrue($schema->getTable('posts')->hasIndex('pkname')); 75 | } 76 | 77 | public function testId() 78 | { 79 | $schema = new Schema(); 80 | $fluid = new FluidSchema($schema); 81 | 82 | $posts = $fluid->table('posts'); 83 | 84 | $posts->id(); 85 | 86 | $this->assertNotNull($schema->getTable('posts')->getPrimaryKey()); 87 | $this->assertTrue($schema->getTable('posts')->hasColumn('id')); 88 | } 89 | 90 | public function testUuid() 91 | { 92 | $schema = new Schema(); 93 | $fluid = new FluidSchema($schema); 94 | 95 | $posts = $fluid->table('posts'); 96 | 97 | $posts->uuid(); 98 | 99 | $this->assertNotNull($schema->getTable('posts')->getPrimaryKey()); 100 | $this->assertTrue($schema->getTable('posts')->hasColumn('uuid')); 101 | } 102 | 103 | public function testTimestamps() 104 | { 105 | $schema = new Schema(); 106 | $fluid = new FluidSchema($schema); 107 | 108 | $posts = $fluid->table('posts'); 109 | 110 | $posts->timestamps(); 111 | 112 | $this->assertTrue($schema->getTable('posts')->hasColumn('created_at')); 113 | $this->assertTrue($schema->getTable('posts')->hasColumn('updated_at')); 114 | } 115 | 116 | public function testInherits() 117 | { 118 | $schema = new Schema(); 119 | $fluid = new FluidSchema($schema); 120 | 121 | $contacts = $fluid->table('contacts'); 122 | $contacts->id(); 123 | 124 | $fluid->table('users')->extends('contacts'); 125 | 126 | $dbalColumn = $schema->getTable('users')->getColumn('id'); 127 | 128 | $this->assertSame(Type::getType(Types::INTEGER), $dbalColumn->getType()); 129 | $fks = $schema->getTable('users')->getForeignKeys(); 130 | $this->assertCount(1, $fks); 131 | $fk = array_pop($fks); 132 | $this->assertSame('users', $fk->getLocalTableName()); 133 | $this->assertSame('contacts', $fk->getForeignTableName()); 134 | $this->assertSame(['id'], $fk->getLocalColumns()); 135 | } 136 | 137 | public function testCannotInheritFromATableWithMultiplePrimaryKeys() 138 | { 139 | $schema = new Schema(); 140 | $fluid = new FluidSchema($schema); 141 | 142 | $contacts = $fluid->table('contacts'); 143 | $contacts->column('foo')->string(); 144 | $contacts->column('bar')->string(); 145 | $contacts->primaryKey(['foo', 'bar']); 146 | 147 | $this->expectException(FluidSchemaException::class); 148 | 149 | $fluid->table('users')->extends('contacts'); 150 | } 151 | 152 | public function testGetDbalTable() 153 | { 154 | $schema = new Schema(); 155 | $fluid = new FluidSchema($schema); 156 | 157 | $contacts = $fluid->table('contacts'); 158 | $this->assertSame('contacts', $contacts->getDbalTable()->getName()); 159 | } 160 | } 161 | --------------------------------------------------------------------------------