diff --git a/.dockerignore b/.dockerignore
new file mode 100644
index 00000000..5e4af88c
--- /dev/null
+++ b/.dockerignore
@@ -0,0 +1,13 @@
+/.idea
+/.git
+/tests/tmp/*
+/vendor
+/.dockerignore
+/.editorconfig
+/.env
+/.env.dist
+/.gitattributes
+/.gitignore
+/.php_cs.cache
+/composer.lock
+/Makefile
\ No newline at end of file
diff --git a/.travis.yml b/.travis.yml
index d319d22b..680b5701 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -4,9 +4,11 @@ php:
- '7.1'
- '7.2'
- '7.3'
- - nightly
+# - '7.4'
-install: make install
+install:
+ - travis_retry composer self-update && composer --version
+ - travis_retry composer install --prefer-dist --no-interaction
script:
- make test
- - if [[ $TRAVIS_PHP_VERSION = "7.3" || $TRAVIS_PHP_VERSION = "nightly" ]]; then true; else make check-style; fi
+ - if [[ $TRAVIS_PHP_VERSION = "7.3" || $TRAVIS_PHP_VERSION = "7.4" ]]; then true; else make check-style; fi
diff --git a/Makefile b/Makefile
index d7281b87..dad3cba8 100644
--- a/Makefile
+++ b/Makefile
@@ -18,5 +18,30 @@ install:
test:
php $(PHPARGS) vendor/bin/phpunit
-.PHONY: all check-style fix-style install test
+clean_all:
+ docker-compose down
+ sudo rm -rf tests/tmp/*
+
+clean:
+ sudo rm -rf tests/tmp/app/*
+ sudo rm -rf tests/tmp/docker_app/*
+
+up:
+ docker-compose up -d
+
+cli:
+ docker-compose exec php bash
+
+migrate:
+ mkdir -p "tests/tmp/app"
+ mkdir -p "tests/tmp/docker_app"
+ docker-compose run --rm php sh -c 'cd /app/tests && ./yii migrate --interactive=0'
+
+installdocker:
+ docker-compose run --rm php composer install && chmod +x tests/yii
+
+testdocker:
+ docker-compose run --rm php sh -c 'vendor/bin/phpunit tests/unit'
+
+.PHONY: all check-style fix-style install test clean clean_all up cli installdocker migrate testdocker
diff --git a/composer.json b/composer.json
index 73d4c05f..b0696706 100644
--- a/composer.json
+++ b/composer.json
@@ -19,9 +19,9 @@
},
"require": {
"php": ">=7.1.0",
- "cebe/php-openapi": "^1.5",
+ "cebe/php-openapi": "dev-wip-reference-cache as 1.5",
"yiisoft/yii2": "~2.0.15",
- "yiisoft/yii2-gii": "~2.0.0 | ~2.1.0",
+ "yiisoft/yii2-gii": "~2.0.0 | ~2.1.0| ~2.2.0",
"fzaninotto/faker": "^1.8",
"laminas/laminas-code": "^3.4"
},
@@ -36,6 +36,11 @@
"cebe\\yii2openapi\\": "src/"
}
},
+ "autoload-dev": {
+ "psr-4": {
+ "tests\\": "tests/"
+ }
+ },
"config": {
"platform": {
"php": "7.1.3"
diff --git a/docker-compose.yml b/docker-compose.yml
new file mode 100644
index 00000000..1c85402e
--- /dev/null
+++ b/docker-compose.yml
@@ -0,0 +1,68 @@
+version: "3.5"
+services:
+ php:
+ build:
+ dockerfile: tests/docker/Dockerfile
+ context: .
+ volumes:
+ - ./tests/tmp/.composer:/root/.composer:rw
+ - .:/app
+ environment:
+ - TZ=UTC
+ - TIMEZONE=UTC
+ - DB_USER=dbuser
+ - DB_PASSWORD=dbpass
+ - IN_DOCKER=docker
+ depends_on:
+ - mysql
+ - postgres
+ - maria
+ tty: true
+ networks:
+ net: {}
+ mysql:
+ image: mysql:5.7
+ ports:
+ - '13306:3306'
+ volumes:
+ - ./tests/tmp/mysql:/var/lib/mysql:rw
+ environment:
+ TZ: UTC
+ MYSQL_ALLOW_EMPTY_PASSWORD: 1
+ MYSQL_USER: dbuser
+ MYSQL_PASSWORD: dbpass
+ MYSQL_DATABASE: testdb
+ networks:
+ net: {}
+ maria:
+ image: mariadb
+ ports:
+ - '23306:3306'
+ volumes:
+ - ./tests/tmp/maria:/var/lib/mysql:rw
+ environment:
+ TZ: UTC
+ MYSQL_ALLOW_EMPTY_PASSWORD: 1
+ MYSQL_USER: dbuser
+ MYSQL_PASSWORD: dbpass
+ MYSQL_DATABASE: testdb
+ MYSQL_INITDB_SKIP_TZINFO: 1
+ networks:
+ net: {}
+ postgres:
+ image: postgres:12
+ ports:
+ - '15432:5432'
+ volumes:
+ - ./tests/tmp/postgres:/var/lib/postgresql/data:rw
+ environment:
+ TZ: UTC
+ PGTZ: UTC
+ POSTGRES_USER: dbuser
+ POSTGRES_PASSWORD: dbpass
+ POSTGRES_DB: testdb
+ networks:
+ net: {}
+
+networks:
+ net: {}
diff --git a/phpunit.xml.dist b/phpunit.xml.dist
index 667ac5f3..fb71ca51 100644
--- a/phpunit.xml.dist
+++ b/phpunit.xml.dist
@@ -7,7 +7,7 @@
stopOnFailure="false">
- ./tests
+ ./tests/unit
diff --git a/src/generator/ApiGenerator.php b/src/generator/ApiGenerator.php
index 3e497d84..ed76e763 100644
--- a/src/generator/ApiGenerator.php
+++ b/src/generator/ApiGenerator.php
@@ -15,9 +15,9 @@
use cebe\openapi\spec\PathItem;
use cebe\openapi\spec\Reference;
use cebe\openapi\spec\Schema;
-use cebe\yii2openapi\generator\helpers\DatabaseDiff;
-use cebe\yii2openapi\generator\helpers\DbModel;
-use cebe\yii2openapi\generator\helpers\SchemaToDatabase;
+use cebe\yii2openapi\lib\items\DbModel;
+use cebe\yii2openapi\lib\MigrationsGenerator;
+use cebe\yii2openapi\lib\SchemaToDatabase;
use Exception;
use Laminas\Code\Generator\ClassGenerator;
use Laminas\Code\Generator\FileGenerator;
@@ -29,11 +29,12 @@
use yii\di\Instance;
use yii\gii\CodeFile;
use yii\gii\Generator;
-use yii\helpers\ArrayHelper;
use yii\helpers\FileHelper;
use yii\helpers\Html;
use yii\helpers\Inflector;
use yii\helpers\StringHelper;
+use function array_filter;
+use const YII_ENV_TEST;
/**
*
@@ -113,7 +114,7 @@ class ApiGenerator extends Generator
public $migrationNamespace;
- public $dbDiff = DatabaseDiff::class;
+ public $migrationGenerator = MigrationsGenerator::class;
/**
@@ -454,7 +455,7 @@ private function guessModelClass(Operation $operation, $actionName)
$requestBody = $requestBody->resolve();
}
foreach ($requestBody->content as $contentType => $content) {
- list($modelClass, ) = $this->guessModelClassFromContent($content);
+ [$modelClass, ] = $this->guessModelClassFromContent($content);
if ($modelClass !== null) {
return $modelClass;
}
@@ -478,7 +479,7 @@ private function guessModelClass(Operation $operation, $actionName)
$successResponse = $successResponse->resolve();
}
foreach ($successResponse->content as $contentType => $content) {
- list($modelClass, ) = $this->guessModelClassFromContent($content);
+ [$modelClass, ] = $this->guessModelClassFromContent($content);
if ($modelClass !== null) {
return $modelClass;
}
@@ -569,7 +570,7 @@ private function findResponseWrapper(Operation $operation, $actionName, $modelCl
$successResponse = $successResponse->resolve();
}
foreach ($successResponse->content as $contentType => $content) {
- list($detectedModelClass, $itemWrapper, $itemsWrapper) = $this->guessModelClassFromContent($content);
+ [$detectedModelClass, $itemWrapper, $itemsWrapper] = $this->guessModelClassFromContent($content);
if (($itemWrapper !== null || $itemsWrapper !== null) && $detectedModelClass === $modelClass) {
return [$itemWrapper, $itemsWrapper];
}
@@ -606,12 +607,17 @@ protected function generateControllers()
return $c;
}
+ /**
+ * @return DbModel[]|array
+ */
protected function generateModels()
{
- return (new SchemaToDatabase([
+ $converter = Yii::createObject([
+ 'class'=>SchemaToDatabase::class,
'excludeModels' => $this->excludeModels,
'generateModelsOnlyXTable' => $this->generateModelsOnlyXTable,
- ]))->generateModels($this->getOpenApi());
+ ]);
+ return $converter->generateModels($this->getOpenApi());
}
/**
@@ -673,7 +679,6 @@ public function generate()
$classFileGenerator->generate()
);
}
-
}
}
@@ -686,12 +691,8 @@ public function generate()
$files[] = new CodeFile(
Yii::getAlias("$modelPath/base/$className.php"),
$this->render('dbmodel.php', [
- 'className' => $className,
- 'tableName' => $model->tableName,
+ 'model' => $model,
'namespace' => $this->modelNamespace . '\\base',
- 'description' => $model->description,
- 'attributes' => $model->attributes,
- 'relations' => $model->relations,
'relationNamespace' => $this->modelNamespace,
])
);
@@ -700,16 +701,14 @@ public function generate()
$files[] = new CodeFile(
Yii::getAlias("$fakerPath/{$className}Faker.php"),
$this->render('faker.php', [
- 'className' => "{$className}Faker",
- 'modelClass' => $className,
+ 'model' => $model,
'modelNamespace' => $this->modelNamespace,
- 'namespace' => $this->fakerNamespace,
- 'attributes' => $model->attributes,
-// 'relations' => $model['relations'],
+ 'namespace' => $this->fakerNamespace
])
);
}
} else {
+ /** This case not implemented yet, just keep it **/
$files[] = new CodeFile(
Yii::getAlias("$modelPath/base/$className.php"),
$this->render('model.php', [
@@ -736,7 +735,6 @@ public function generate()
$classFileGenerator->generate()
);
}
-
}
}
@@ -744,54 +742,25 @@ public function generate()
if (!isset($models)) {
$models = $this->generateModels();
}
- /** @var $dbDiff DatabaseDiff */
- $dbDiff = Instance::ensure($this->dbDiff, DatabaseDiff::class);
- $migrationFiles = [];
- foreach ($models as $modelName => $model) {
-
- if (!($model instanceof DbModel)) {
- continue;
- }
-
- $tableName = $model->tableName;
- list($upCode, $downCode, $dependencies, $migrationName) = $dbDiff->diffTable($tableName, $this->attributesToColumnSchemas($model->attributes), $model->relations);
- if (empty($upCode) && empty($downCode)) {
- continue;
- }
-
- $migrationFiles[$tableName] = [
- 'dependencies' => $dependencies,
- 'upCode' => $upCode,
- 'downCode' => $downCode,
- 'model' => $model,
- 'modelName' => $modelName,
- 'migrationName' => $migrationName,
- ];
- }
-
- // sort files by dependecies of foreign keys
- // TODO circular references could be solved by adding a separate migration which contains only foreign key changes
- $migrationFiles = $this->sortByDependency($migrationFiles);
+ /** @var $migrationGenerator MigrationsGenerator */
+ $migrationGenerator = Instance::ensure($this->migrationGenerator, MigrationsGenerator::class);
+ $migrationModels = $migrationGenerator->generate(array_filter($models, function ($model) {
+ return $model instanceof DbModel;
+ }));
$migrationPath = Yii::getAlias($this->migrationPath);
$migrationNamespace = $this->migrationNamespace;
+ $isTransactional = Yii::$app->db->getDriverName() === 'pgsql';//Probably some another yet
// TODO start $i by looking at all files, otherwise only one generation per hours causes correct order!!!
$i = 0;
- foreach($migrationFiles as $tableName => $migrationFile) {
-
- // migration files get invalidated directly after generating
- // if they contain a timestamp
- // use fixed time here instead
+ foreach ($migrationModels as $tableName => $migration) {
+ // migration files get invalidated directly after generating,
+ // if they contain a timestamp use fixed time here instead
do {
- if ($migrationNamespace) {
- $m = sprintf('%s%04d', date('ymdH'), $i);
- $className = "M{$m}" . Inflector::id2camel($migrationFile['migrationName'], '_');
- } else {
- $m = sprintf('%s%04d', date('ymd_H'), $i);
- $className = "m{$m}_" . $migrationFile['migrationName'];
- }
+ $date = YII_ENV_TEST ? '200000_00': '';
+ $className = $migration->makeClassNameByTime($i, $migrationNamespace, $date);
$i++;
} while (file_exists(Yii::getAlias("$migrationPath/$className.php")));
@@ -799,14 +768,9 @@ public function generate()
$files[] = new CodeFile(
Yii::getAlias("$migrationPath/$className.php"),
$this->render('migration.php', [
- 'className' => $className,
+ 'isTransactional' => $isTransactional,
'namespace' => $migrationNamespace,
- 'tableName' => $tableName,
- 'attributes' => $migrationFile['model']->attributes,
- 'relations' => $migrationFile['model']->relations,
- 'description' => 'Table for ' . $migrationFile['modelName'],
- 'upCode' => $migrationFile['upCode'],
- 'downCode' => $migrationFile['downCode'],
+ 'migration' => $migration
])
);
}
@@ -815,88 +779,8 @@ public function generate()
return $files;
}
- private $_sortResult = [];
-
- private function sortByDependency($migrationFiles)
- {
- $this->_sortResult = [];
-
- // sort alpabetically by name first
- // to have consistent result in case no dependencies exist
- ksort($migrationFiles);
-
- foreach ($migrationFiles as $name => $migrationFile) {
- echo "adding $name\n";
- $this->sortByDependencyRecurse($name, $migrationFile, $migrationFiles);
- }
- return $this->_sortResult;
- }
-
- private function sortByDependencyRecurse($name, $migrationFile, $migrationFiles)
- {
- if (!isset($this->_sortResult[$name])) {
- $this->_sortResult[$name] = false;
- foreach($migrationFile['dependencies'] as $dependency) {
- if (!isset($migrationFiles[$dependency])) {
- echo "skipping dep $dependency\n";
- continue;
- }
- echo "adding dep $dependency\n";
- $this->sortByDependencyRecurse($dependency, $migrationFiles[$dependency], $migrationFiles);
- }
- unset($this->_sortResult[$name]);
- $this->_sortResult[$name] = $migrationFile;
- } elseif ($this->_sortResult[$name] === false) {
- throw new \Exception("A circular dependency is detected for table '$name'.");
- }
- }
-
private function getPathFromNamespace($namespace)
{
return Yii::getAlias('@' . str_replace('\\', '/', $namespace));
}
-
- private function attributesToColumnSchemas($attributes)
- {
- $columns = [];
- foreach($attributes as $name => $attribute) {
- $size = null;
- if (isset($attribute['dbType']) && preg_match('~^string\((\d+)\)$~', $attribute['dbType'], $matches)) {
- $size = $matches[1];
- $attribute['dbType'] = 'string';
- }
-
- $columns[$name] = new ColumnSchema([
- 'dbType' => $attribute['dbType'],
- 'type' => $this->dbTypeToAbstractType($attribute['dbType']),
- 'allowNull' => !$attribute['required'],
- 'size' => $size,
- // TODO add more fields
- ]);
- if ($columns[$name]->type === 'json') {
- $columns[$name]->allowNull = false;
- }
- }
- return $columns;
- }
-
- private function dbTypeToAbstractType($type)
- {
- if (stripos($type, 'int') === 0) {
- return 'integer';
- }
- if (stripos($type, 'string') === 0) {
- return 'string';
- }
- if (stripos($type, 'varchar') === 0) {
- return 'string';
- }
- if (stripos($type, 'json') === 0) {
- return 'json';
- }
- if (stripos($type, 'datetime') === 0) {
- return 'timestamp';
- }
- return $type;
- }
}
diff --git a/src/generator/default/dbmodel.php b/src/generator/default/dbmodel.php
index 9e2adaf6..7e17c83b 100644
--- a/src/generator/default/dbmodel.php
+++ b/src/generator/default/dbmodel.php
@@ -1,98 +1,57 @@
+
= '
namespace = $namespace ?>;
/**
- * = str_replace("\n", "\n * ", trim($description)) ?>
+ * = str_replace("\n", "\n * ", trim($model->description)) ?>
*
-
- * @property = $attribute['type'] ?? 'mixed' ?> $= str_replace("\n", "\n * ", rtrim($attribute['name'] . ' ' . $attribute['description'])) ?>
+attributes as $attribute): ?>
+ * @property = $attribute->getFormattedDescription() ?>
*
- $relation): ?>
- * @property \= trim($relationNamespace, '\\') ?>\= $relation['class'] ?> $= $relationName ?>
+relations as $relationName => $relation): ?>
+isHasOne()):?>
+ * @property \= trim($relationNamespace, '\\') ?>\= $relation->getClassName() ?> $= $relationName ?>
+
+ * @property array|\= trim($relationNamespace, '\\') ?>\= $relation->getClassName() ?>[] $= $relationName ?>
+
*/
-abstract class = $className ?> extends \yii\db\ActiveRecord
+abstract class = $model->name ?> extends \yii\db\ActiveRecord
{
public static function tableName()
{
- return = var_export($tableName) ?>;
+ return = var_export($model->getTableAlias()) ?>;
}
public function rules()
{
return [
-
+=implode(",\n", $model->getValidationRules()).",\n"?>
];
}
- $relation): ?>
- public function get= ucfirst($relationName) ?>()
+relations as $relationName => $relation): ?>
+ public function get= $relation->getCamelName() ?>()
{
- return $this->= $relation['method'] ?>(\= trim($relationNamespace, '\\') ?>\= $relation['class'] ?>::class, = $relation->getMethod() ?>(\= trim($relationNamespace, '\\') ?>\= $relation->getClassName() ?>::class,', ', ]'],
[', ', ' => ', ']'],
- preg_replace('~\s+~', '', \yii\helpers\VarDumper::export($relation['link']))
+ preg_replace('~\s+~', '', \yii\helpers\VarDumper::export($relation->getLink()))
)
?>);
}
-
}
diff --git a/src/generator/default/faker.php b/src/generator/default/faker.php
index f40947dd..0ac7a5f5 100644
--- a/src/generator/default/faker.php
+++ b/src/generator/default/faker.php
@@ -1,3 +1,10 @@
+
= '
@@ -6,27 +13,33 @@
use Faker\Factory as FakerFactory;
use Faker\UniqueGenerator;
-use = $modelNamespace ?>\= $modelClass ?>;
+use = $modelNamespace ?>\= $model->name ?>;
/**
- * Fake data generator for = $modelClass ?>
+ * Fake data generator for = $model->name ?>
*/
-class = $className ?>
-
+class = $model->name ?>Faker
{
public function generateModel()
{
$faker = FakerFactory::create(\Yii::$app->language);
$uniqueFaker = new UniqueGenerator($faker);
- $model = new = $modelClass ?>;
-name ?>();
+attributes as $attribute):
+ if (!$attribute->fakerStub || $attribute->isReference()) {
continue;
} ?>
- $model->= $attribute['name'] ?> = = $attribute['faker'] ?>;
+ $model->= $attribute->columnName ?> = = $attribute->fakerStub ?>;
+attributes as $attribute):
+ if (!$attribute->fakerStub || !$attribute->isReference()) {
+ continue;
+ } ?>
+ $model->= $attribute->columnName ?> = = $attribute->fakerStub ?>;
+ **/?>
return $model;
}
}
diff --git a/src/generator/default/migration.php b/src/generator/default/migration.php
index 27ae9fc0..835e8b76 100644
--- a/src/generator/default/migration.php
+++ b/src/generator/default/migration.php
@@ -1,3 +1,10 @@
+
= '
/**
- * = $description ?>
+ * = $migration->getDescription() ?>
*/
-class = $className ?> extends \yii\db\Migration
+class = $migration->fileClassName ?> extends \yii\db\Migration
{
- public function up()
+ public function =$isTransactional? 'safeUp':'up'?>()
{
-= $upCode ?>
+= str_replace(["'\$this", ")',"], ['$this', '),'], $migration->upCodeString) ?>
}
- public function down()
+ public function =$isTransactional? 'safeDown':'down'?>()
{
-= $downCode ?>
+= str_replace(["'\$this", ")',"], ['$this', '),'], $migration->downCodeString) ?>
}
}
diff --git a/src/generator/helpers/DatabaseDiff.php b/src/generator/helpers/DatabaseDiff.php
deleted file mode 100644
index 27c86a66..00000000
--- a/src/generator/helpers/DatabaseDiff.php
+++ /dev/null
@@ -1,163 +0,0 @@
-db = Instance::ensure($this->db, Connection::class);
- }
-
- /**
- * Calculate the difference between a database table and the desired data schema.
- * @param string $tableName name of the database table.
- * @param ColumnSchema[] $columns
- * @param array $relations
- * @return array
- */
- public function diffTable($tableName, $columns, $relations)
- {
- $tableSchema = $this->db->getTableSchema($tableName, true);
- if ($tableSchema === null) {
- // create table
- $codeColumns = VarDumper::export(array_map(function ($c) {
- return $this->columnToDbType($c);
- }, $columns));
- $upCode = str_replace("\n", "\n ", " \$this->createTable('$tableName', $codeColumns);");
- $downCode = " \$this->dropTable('$tableName');";
-
- $dependencies = [];
- foreach($relations as $relation) {
- if ($relation['method'] !== 'hasOne') {
- continue;
- }
- $fkCol = reset($relation['link']);
- $fkRefCol = key($relation['link']);
- $fkRefTable = $relation['tableName'];
- $fkName = $this->foreignKeyName($tableName, $fkCol, $fkRefTable, $fkRefCol);
- $upCode .= "\n \$this->addForeignKey('$fkName', '$tableName', '$fkCol', '$fkRefTable', '$fkRefCol');";
- $downCode = " \$this->dropForeignKey('$fkName', '$tableName');\n$downCode";
- $dependencies[] = $fkRefTable;
- }
-
- return [$upCode, $downCode, $dependencies, 'create_table_' . $this->normalizeTableName($tableName)];
- }
-
- $upCode = [];
- $downCode = [];
-
- // compare existing columns with expected columns
- $wantNames = array_keys($columns);
- $haveNames = $tableSchema->columnNames;
- sort($wantNames);
- sort($haveNames);
- $missingDiff = array_diff($wantNames, $haveNames);
- $unknownDiff = array_diff($haveNames, $wantNames);
- foreach ($missingDiff as $missingColumn) {
- $upCode[] = "\$this->addColumn('$tableName', '$missingColumn', '{$this->escapeQuote($this->columnToDbType($columns[$missingColumn]))}');";
- $downCode[] = "\$this->dropColumn('$tableName', '$missingColumn');";
- }
- foreach ($unknownDiff as $unknownColumn) {
- $upCode[] = "\$this->dropColumn('$tableName', '$unknownColumn');";
- $oldDbType = $this->columnToDbType($tableSchema->columns[$unknownColumn]);
- $downCode[] = "\$this->addColumn('$tableName', '$unknownColumn', '$oldDbType');";
- }
-
- // compare desired type with existing type
- foreach ($tableSchema->columns as $columnName => $currentColumnSchema) {
- if (!isset($columns[$columnName])) {
- continue;
- }
- $desiredColumnSchema = $columns[$columnName];
- switch (true) {
- case $desiredColumnSchema->dbType === 'pk':
- case $desiredColumnSchema->dbType === 'bigpk':
- // do not adjust existing primary keys
- break;
- case $desiredColumnSchema->type !== $currentColumnSchema->type:
- case $desiredColumnSchema->allowNull != $currentColumnSchema->allowNull:
- case $desiredColumnSchema->type === 'string' && $desiredColumnSchema->size != $currentColumnSchema->size:
- $upCode[] = "\$this->alterColumn('$tableName', '$columnName', '{$this->escapeQuote($this->columnToDbType($desiredColumnSchema))}');";
- $downCode[] = "\$this->alterColumn('$tableName', '$columnName', '{$this->escapeQuote($this->columnToDbType($currentColumnSchema))}');";
- }
- }
-
- // compare existing foreign keys with relations
- $dependencies = [];
- foreach($relations as $relation) {
- if ($relation['method'] !== 'hasOne') {
- continue;
- }
- $fkCol = reset($relation['link']);
- $fkRefCol = key($relation['link']);
- $fkRefTable = $relation['tableName'];
- $fkName = $this->foreignKeyName($tableName, $fkCol, $fkRefTable, $fkRefCol);
-
- if (isset($tableSchema->foreignKeys[$fkName])) {
- continue;
- }
- $upCode[] = "\$this->addForeignKey('$fkName', '$tableName', '$fkCol', '$fkRefTable', '$fkRefCol');";
- array_unshift($downCode, "\$this->dropForeignKey('$fkName', '$tableName');");
- $dependencies[] = $fkRefTable;
- }
-
- if (empty($upCode) && empty($downCode)) {
- return ['', '', [], ''];
- }
-
- return [
- " " . implode("\n ", $upCode),
- " " . implode("\n ", $downCode),
- [],
- 'change_table_' . $this->normalizeTableName($tableName),
- ];
- }
-
- private function foreignKeyName($table, $column, $foreignTable, $foreignColumn)
- {
- $table = $this->normalizeTableName($table);
- $foreignTable = $this->normalizeTableName($foreignTable);
- return "fk_{$table}_{$column}_{$foreignTable}_{$foreignColumn}";
- }
-
- private function normalizeTableName($tableName)
- {
- if (preg_match('~^{{%?(.*)}}$~', $tableName, $m)) {
- return $m[1];
- }
- return $tableName;
- }
-
- private function escapeQuote($str)
- {
- return str_replace("'", "\\'", $str);
- }
-
- private function columnToDbType(ColumnSchema $column)
- {
- if ($column->dbType === 'pk') {
- return $column->dbType;
- }
- return $column->dbType . ($column->size ? "({$column->size})" : '') . ($column->allowNull ? '' : ' NOT NULL');
- }
-}
diff --git a/src/generator/helpers/DbModel.php b/src/generator/helpers/DbModel.php
deleted file mode 100644
index 78e19c5c..00000000
--- a/src/generator/helpers/DbModel.php
+++ /dev/null
@@ -1,32 +0,0 @@
-components->schemas as $schemaName => $schema) {
- if ($schema instanceof Reference) {
- $schema->getContext()->mode = ReferenceContext::RESOLVE_MODE_INLINE;
- $schema = $schema->resolve();
- }
-
- // only generate tables for schemas of type object and those who have defined properties
- if ((empty($schema->type) || $schema->type === 'object') && empty($schema->properties)) {
- continue;
- }
- if (!empty($schema->type) && $schema->type !== 'object') {
- continue;
- }
- // do not generate tables for composite schemas
- if ($schema->allOf || $schema->anyOf || $schema->multipleOf || $schema->oneOf) {
- continue;
- }
- // skip excluded model names
- if (in_array($schemaName, $this->excludeModels)) {
- continue;
- }
-
- if ($this->generateModelsOnlyXTable && empty($schema->{'x-table'})) {
- continue;
- }
-
- list($attributes, $relations) = $this->generateAttributesAndRelations($schemaName, $schema);
-
- $models[$schemaName] = new DbModel([
- 'name' => $schemaName,
- 'tableName' => '{{%' . ($schema->{'x-table'} ?? $this->generateTableName($schemaName)) . '}}',
- 'description' => $schema->description,
- 'attributes' => $attributes,
- 'relations' => $relations,
- ]);
-
- }
-
- // TODO generate hasMany relations and inverse relations
-
- return $models;
- }
-
- /**
- * Auto generate table name from model name.
- * @param string $modelName
- * @return string
- */
- protected function generateTableName($schemaName)
- {
- return Inflector::camel2id(StringHelper::basename(Inflector::pluralize($schemaName)), '_');
- }
-
- protected function generateAttributesAndRelations($schemaName, Schema $schema)
- {
- $attributes = [];
- $relations = [];
- foreach ($schema->properties as $name => $property) {
-
- if ($property instanceof Reference) {
- $refPointer = $property->getJsonReference()->getJsonPointer()->getPointer();
- $property->getContext()->mode = ReferenceContext::RESOLVE_MODE_ALL;
- $resolvedProperty = $property->resolve();
- $dbName = "{$name}_id";
- $dbType = 'integer'; // for a foreign key
- if (strpos($refPointer, '/components/schemas/') === 0) {
- // relation
- $type = substr($refPointer, 20);
- $relations[$name] = [
- 'class' => $type,
- 'method' => 'hasOne',
- 'link' => ['id' => $dbName], // TODO pk may not be 'id'
- 'tableName' => '{{%' . ($resolvedProperty->{'x-table'} ?? $this->generateTableName($type)) . '}}',
- ];
- } else {
- $type = $this->getSchemaType($resolvedProperty);
- }
- } else {
- $resolvedProperty = $property;
- $type = $this->getSchemaType($property);
- $dbName = $name;
- $dbType = $this->getDbType($name, $property);
- }
-
- // relation
- if (is_array($type)) {
- $relations[$name] = [
- 'class' => $type[1],
- 'method' => 'hasMany',
- 'link' => [Inflector::camel2id($schemaName, '_') . '_id' => 'id'], // TODO pk may not be 'id'
- ];
- $type = $type[0];
- continue;
- }
-
- $attributes[$dbName] = [
- 'name' => $dbName,
- 'type' => $type,
- 'dbType' => $dbType,
- 'dbName' => $dbName,
- 'required' => false,
- 'readOnly' => $resolvedProperty->readOnly ?? false,
- 'description' => $resolvedProperty->description,
- 'faker' => $this->guessModelFaker($name, $type, $resolvedProperty),
- ];
- }
- if (!empty($schema->required)) {
- foreach ($schema->required as $property) {
- if (!isset($attributes[$property])) {
- continue;
- }
- $attributes[$property]['required'] = true;
- }
- }
- return [$attributes, $relations];
- }
-
-
- /**
- * @param Schema $schema
- * @return string|array
- */
- protected function getSchemaType($schema)
- {
- switch ($schema->type) {
- case 'integer':
- return 'int';
- case 'boolean':
- return 'bool';
- case 'number': // can be double and float
- return 'float';
- case 'array':
- if (isset($schema->items) && $schema->items instanceof Reference) {
- $ref = $schema->items->getJsonReference()->getJsonPointer()->getPointer();
- if (strpos($ref, '/components/schemas/') === 0) {
- return [substr($ref, 20) . '[]', substr($ref, 20)];
- }
- }
- // no break here
- default:
- return $schema->type;
- }
- }
-
- /**
- * @param string $name
- * @param Schema $schema
- * @return string
- * @link http://spec.openapis.org/oas/v3.0.3#data-types
- */
- protected function getDbType($name, $schema)
- {
- if (isset($schema->{'x-db-type'})) {
- return $schema->{'x-db-type'};
- }
-
- if ($name === 'id' && $schema->type === 'integer') {
- return $schema->format === 'int64' ? 'bigpk' : 'pk';
- }
-
- switch ($schema->type) {
- case 'string':
- if (isset($schema->maxLength)) {
- return 'string(' . ((int) $schema->maxLength) . ')';
- }
- if ($schema->format === 'date') {
- return 'date';
- }
- if ($schema->format === 'date-time') {
- return 'datetime';
- }
- if ($schema->format === 'binary') {
- return 'blob';
- }
-
- return 'text';
- case 'integer':
- if ($schema->format === 'int64') {
- return 'bigint';
- }
- // no break
- case 'boolean':
- return $schema->type;
- case 'number': // can be double and float
- return $schema->format ?? 'float';
-// case 'array':
-// // TODO array might refer to has_many relation
-// if (isset($schema->items) && $schema->items instanceof Reference) {
-// $ref = $schema->items->getJsonReference()->getJsonPointer()->getPointer();
-// if (strpos($ref, '/components/schemas/') === 0) {
-// return substr($ref, 20) . '[]';
-// }
-// }
-// // no break here
- default:
- return 'text';
- }
- }
-
-
- /**
- * Guess faker for attribute.
- * @param string $name
- * @param string $type
- * @return string|null the faker PHP code or null.
- * @link https://github.com/fzaninotto/Faker#formatters
- */
- private function guessModelFaker($name, $type, Schema $property)
- {
- if (isset($property->{'x-faker'})) {
- return $property->{'x-faker'};
- }
-
- $min = $max = null;
- if (isset($property->minimum)) {
- $min = $property->minimum;
- if ($property->exclusiveMinimum) {
- $min++;
- }
- }
- if (isset($property->maximum)) {
- $max = $property->maximum;
- if ($property->exclusiveMaximum) {
- $max++;
- }
- }
-
- switch ($type) {
- case 'string':
- if ($property->format === 'date') {
- return '$faker->iso8601';
- }
- if (!empty($property->enum) && is_array($property->enum)) {
- return '$faker->randomElement('.var_export($property->enum, true).')';
- }
- if ($name === 'title' && isset($property->maxLength) && $property->maxLength < 10) {
- return '$faker->title';
- }
-
- $patterns = [
- '~_id$~' => '$uniqueFaker->numberBetween(0, 1000000)',
- '~uuid$~' => '$uniqueFaker->uuid',
- '~firstname~i' => '$faker->firstName',
- '~(last|sur)name~i' => '$faker->lastName',
- '~(company|employer)~i' => '$faker->company',
- '~(city|town)~i' => '$faker->city',
- '~(post|zip)code~i' => '$faker->postcode',
- '~streetaddress~i' => '$faker->streetAddress',
- '~address~i' => '$faker->address',
- '~street~i' => '$faker->streetName',
- '~state~i' => '$faker->state',
- '~county~i' => 'sprintf("%s County", $faker->city)',
- '~country~i' => '$faker->countryCode',
- '~lang~i' => '$faker->languageCode',
- '~locale~i' => '$faker->locale',
- '~currency~i' => '$faker->currencyCode',
- '~hash~i' => '$faker->sha256',
- '~e?mail~i' => '$faker->safeEmail',
- '~timestamp~i' => '$faker->unixTime',
- '~.*At$~' => '$faker->dateTimeThisCentury->format(\'Y-m-d H:i:s\')', // createdAt, updatedAt, ...
- '~.*ed_at$~i' => '$faker->dateTimeThisCentury->format(\'Y-m-d H:i:s\')', // created_at, updated_at, ...
- '~(phone|fax|mobile|telnumber)~i' => '$faker->e164PhoneNumber',
- '~(^lat|coord)~i' => '$faker->latitude',
- '~^lon~i' => '$faker->longitude',
- '~title~i' => '$faker->sentence',
- '~(body|summary|article|content|descr|comment|detail)~i' => '$faker->paragraphs(6, true)',
- '~(url|site|website)~i' => '$faker->url',
- '~(username|login)~i' => '$faker->userName',
- ];
- foreach ($patterns as $pattern => $faker) {
- if (preg_match($pattern, $name)) {
- if (isset($property->maxLength)) {
- return 'substr(' . $faker . ', 0, ' . $property->maxLength . ')';
- } else {
- return $faker;
- }
- }
- }
-
- // TODO maybe also consider OpenAPI examples here
-
- if (isset($property->maxLength)) {
- return 'substr($faker->text(' . $property->maxLength . '), 0, ' . $property->maxLength . ')';
- }
- return '$faker->sentence';
- case 'int':
- $fakerVariable = preg_match('~_?id$~', $name) ? 'uniqueFaker' : 'faker';
-
- $maxInt = 2147483647; // 2^31-1 PHP_MAX_INT may be 64bit and too big in some cases
- if ($min !== null && $max !== null) {
- return "\${$fakerVariable}->numberBetween($min, $max)";
- } elseif ($min !== null) {
- return "\${$fakerVariable}->numberBetween($min, $maxInt)";
- } elseif ($max !== null) {
- return "\${$fakerVariable}->numberBetween(0, $max)";
- }
-
- $patterns = [
- '~timestamp~i' => '$faker->unixTime',
- '~.*At$~' => '$faker->unixTime', // createdAt, updatedAt, ...
- '~.*ed_at$~i' => '$faker->unixTime', // created_at, updated_at, ...
- ];
- foreach ($patterns as $pattern => $faker) {
- if (preg_match($pattern, $name)) {
- return $faker;
- }
- }
-
- return "\${$fakerVariable}->numberBetween(0, $maxInt)";
- case 'bool':
- return '$faker->boolean';
- case 'float':
- if ($min !== null && $max !== null) {
- return "\$faker->randomFloat(null, $min, $max)";
- } elseif ($min !== null) {
- return "\$faker->randomFloat(null, $min)";
- } elseif ($max !== null) {
- return "\$faker->randomFloat(null, 0, $max)";
- }
- return '$faker->randomFloat()';
- }
-
-
- return null;
- }
-
-}
diff --git a/src/lib/AttributeResolver.php b/src/lib/AttributeResolver.php
new file mode 100644
index 00000000..7ddd20ad
--- /dev/null
+++ b/src/lib/AttributeResolver.php
@@ -0,0 +1,225 @@
+ and contributors
+ * @license https://github.com/cebe/yii2-openapi/blob/master/LICENSE
+ */
+
+namespace cebe\yii2openapi\lib;
+
+use cebe\openapi\ReferenceContext;
+use cebe\openapi\spec\Reference;
+use cebe\openapi\spec\Schema;
+use cebe\openapi\SpecObjectInterface;
+use cebe\yii2openapi\lib\items\Attribute;
+use cebe\yii2openapi\lib\items\AttributeRelation;
+use cebe\yii2openapi\lib\items\DbModel;
+use Yii;
+use yii\helpers\Inflector;
+use yii\helpers\StringHelper;
+use function in_array;
+use function str_replace;
+use function strpos;
+use function substr;
+
+class AttributeResolver
+{
+ private const REFERENCE_PATH = '/components/schemas/';
+ private const REFERENCE_PATH_LEN = 20;
+
+ /**
+ * @var Attribute[]|array
+ */
+ private $attributes = [];
+
+ /**
+ * @var AttributeRelation[]|array
+ */
+ private $relations = [];
+
+ /**
+ * @var string
+ */
+ private $schemaName;
+
+ /**
+ * @var \cebe\openapi\spec\Schema
+ */
+ private $componentSchema;
+
+ private $primaryKey;
+
+ /**
+ * @var array|false|mixed|string
+ */
+ private $tableName;
+
+ public function __construct(string $schemaName, Schema $componentSchema)
+ {
+ $this->schemaName = $schemaName;
+ $this->componentSchema = $componentSchema;
+ $this->primaryKey = $componentSchema->{CustomSpecAttr::PRIMARY_KEY} ?? 'id';
+ $this->tableName = $componentSchema->{CustomSpecAttr::TABLE} ?? self::tableNameBySchema($this->schemaName);
+ }
+
+ /**
+ * @return \cebe\yii2openapi\lib\items\DbModel
+ */
+ public function resolve():DbModel
+ {
+ $requiredProps = $this->componentSchema->required ?? [];
+ foreach ($this->componentSchema->properties as $propertyName => $property) {
+ $isRequired = in_array($propertyName, $requiredProps);
+ $this->resolveProperty($propertyName, $property, $isRequired);
+ }
+ return new DbModel([
+ 'name' => $this->schemaName,
+ 'tableName' => $this->tableName,
+ 'description' => $this->componentSchema->description,
+ 'attributes' => $this->attributes,
+ 'relations' => $this->relations,
+ ]);
+ }
+
+ protected static function tableNameBySchema(string $schemaName):string
+ {
+ return Inflector::camel2id(StringHelper::basename(Inflector::pluralize($schemaName)), '_');
+ }
+
+ protected function resolveProperty($propertyName, SpecObjectInterface $property, bool $isRequired)
+ {
+ $attribute = new Attribute($propertyName);
+ $attribute->setRequired($isRequired)
+ ->setDescription($property->description ?? '')
+ ->setReadOnly($property->readOnly ?? false)
+ ->setIsPrimary($propertyName === $this->primaryKey);
+ if ($property instanceof Reference) {
+ $refPointer = $property->getJsonReference()->getJsonPointer()->getPointer();
+ $property->getContext()->mode = ReferenceContext::RESOLVE_MODE_ALL;
+ $relatedSchema = $property->resolve();
+ if (strpos($refPointer, self::REFERENCE_PATH) === 0) {
+ if (strpos($refPointer, '/properties/')!==false) {
+ $attribute->asReference($this->schemaName);
+ $foreignPk = $this->componentSchema->{CustomSpecAttr::PRIMARY_KEY} ?? 'id';
+ $foreignPkProperty = $this->componentSchema->properties[$foreignPk];
+ $relatedTableName = $this->tableName;
+ $phpType = TypeResolver::schemaToPhpType($foreignPkProperty);
+ $attribute->setPhpType($phpType)
+ ->setDbType($this->guessDbType($foreignPkProperty, true, true));
+
+ $relation = (new AttributeRelation($propertyName, $relatedTableName, $this->schemaName))
+ ->asHasOne([$foreignPk => $attribute->columnName])->asSelfReference();
+ $this->relations[$propertyName] = $relation;
+ } else {
+ $relatedClassName = substr($refPointer, self::REFERENCE_PATH_LEN);
+ $relatedTableName =
+ $relatedSchema->{CustomSpecAttr::TABLE} ?? self::tableNameBySchema($relatedClassName);
+ $attribute->asReference($relatedClassName)->setDescription($relatedSchema->description ?? '');
+ /**
+ * TODO: We need to detect primary key name of related column if it is not "id"
+ * So we should declare custom pk name in schema if it is not id
+ **/
+ $foreignPk = $relatedSchema->{CustomSpecAttr::PRIMARY_KEY} ?? 'id';
+ $foreignPkProperty = $relatedSchema->properties[$foreignPk];
+
+ $phpType = TypeResolver::schemaToPhpType($foreignPkProperty);
+ $attribute->setPhpType($phpType)
+ ->setDbType($this->guessDbType($foreignPkProperty, true, true));
+
+ $relation = (new AttributeRelation($propertyName, $relatedTableName, $relatedClassName))
+ ->asHasOne([$foreignPk => $attribute->columnName]);
+ $this->relations[$propertyName] = $relation;
+ }
+ } else {
+ //TODO: This case for self-table relations, should be covered later
+// $attribute->setPhpType(
+// TypeResolver::schemaToPhpType($relatedSchema->type, $relatedSchema->format)
+// );
+ }
+ }
+
+ if (!$attribute->isReference()) {
+ /**@var Schema $property */
+ $phpType = TypeResolver::schemaToPhpType($property);
+ $attribute->setPhpType($phpType)
+ ->setDbType($this->guessDbType($property, ($propertyName === $this->primaryKey)))
+ ->setUnique($property->{CustomSpecAttr::UNIQUE} ?? false)
+ ->setSize($property->maxLength ?? null)
+ ->setDefault($property->default ?? null);
+ [$min, $max] = $this->guessMinMax($property);
+ $attribute->setLimits($min, $max, $property->minLength ?? null);
+ if (isset($property->enum) && is_array($property->enum)) {
+ $attribute->setEnumValues($property->enum);
+ }
+ }
+
+ // has Many relation
+ $refPointer = $this->getHasManyReference($property);
+ if ($refPointer !== null) {
+ if (strpos($refPointer, '/properties/')!==false) {
+ $relatedClassName = $this->schemaName;
+ $relatedTableName = $this->tableName;
+ $foreignAttr = str_replace(self::REFERENCE_PATH.$relatedClassName.'/properties/', '', $refPointer);
+ $foreignPk = Inflector::camel2id($foreignAttr, '_') . '_id';
+ $attribute->setPhpType($relatedClassName . '[]');
+ $this->relations[$propertyName] =
+ (new AttributeRelation($propertyName, $relatedTableName, $relatedClassName))
+ ->asHasMany([$foreignPk => $this->primaryKey]);
+ return;
+ }
+ $relatedClassName = substr($refPointer, self::REFERENCE_PATH_LEN);
+ $property->items->getContext()->mode = ReferenceContext::RESOLVE_MODE_ALL;
+ $relatedSchema = $property->items->resolve();
+ $relatedTableName =
+ $relatedSchema->{CustomSpecAttr::TABLE} ?? self::tableNameBySchema($relatedClassName);
+// $foreignPk = $relatedSchema->{CustomSpecAttr::PRIMARY_KEY} ?? 'id';
+ $attribute->setPhpType($relatedClassName . '[]');
+ $this->relations[$propertyName] =
+ (new AttributeRelation($propertyName, $relatedTableName, $relatedClassName))
+ ->asHasMany([Inflector::camel2id($this->schemaName, '_') . '_id' => $this->primaryKey]);
+ return;
+ }
+ $this->attributes[$propertyName] = $attribute->setFakerStub($this->guessFakerStub($attribute, $property));
+ }
+
+ protected function getHasManyReference(SpecObjectInterface $property):?string
+ {
+ if ($property instanceof Reference) {
+ return null;
+ }
+ if ($property->type === 'array' && isset($property->items) && $property->items instanceof Reference) {
+ $ref = $property->items->getJsonReference()->getJsonPointer()->getPointer();
+ if (strpos($ref, self::REFERENCE_PATH) === 0) {
+ return $ref;
+ }
+ }
+ return null;
+ }
+
+ protected function guessMinMax(SpecObjectInterface $property):array
+ {
+ $min = $property->minimum ?? null;
+ $max = $property->maximum ?? null;
+ if ($min !== null && $property->exclusiveMinimum) {
+ $min++; //Need for ensure
+ }
+ if ($max !== null && $property->exclusiveMaximum) {
+ $max++;
+ }
+ return [$min, $max];
+ }
+
+ protected function guessFakerStub(Attribute $attribute, SpecObjectInterface $property):?string
+ {
+ $resolver = Yii::createObject(['class' => FakerStubResolver::class], [$attribute, $property]);
+ return $resolver->resolve();
+ }
+
+ protected function guessDbType(Schema $property, bool $isPk, bool $isReference = false):string
+ {
+ if ($isReference === true) {
+ return TypeResolver::referenceToDbType($property);
+ }
+ return TypeResolver::schemaToDbType($property, $isPk);
+ }
+}
diff --git a/src/lib/ColumnToCode.php b/src/lib/ColumnToCode.php
new file mode 100644
index 00000000..27ff0061
--- /dev/null
+++ b/src/lib/ColumnToCode.php
@@ -0,0 +1,371 @@
+ and contributors
+ * @license https://github.com/cebe/yii2-openapi/blob/master/LICENSE
+ */
+
+namespace cebe\yii2openapi\lib;
+
+use yii\db\ArrayExpression;
+use yii\db\ColumnSchema;
+use yii\db\ColumnSchemaBuilder;
+use yii\db\JsonExpression;
+use yii\db\mysql\Schema as MySqlSchema;
+use yii\db\pgsql\Schema as PgSqlSchema;
+use yii\db\Schema;
+use yii\helpers\StringHelper;
+use function array_key_exists;
+use function in_array;
+use function is_array;
+use function method_exists;
+use function strpos;
+use function strtolower;
+use function substr;
+use function trim;
+use function ucfirst;
+use const PHP_EOL;
+
+class ColumnToCode
+{
+ public const PK_TYPE_MAP = [
+ Schema::TYPE_PK => 'primaryKey()',
+ Schema::TYPE_UPK => 'primaryKey()->unsigned()',
+ Schema::TYPE_BIGPK => 'bigPrimaryKey()',
+ Schema::TYPE_UBIGPK => 'bigPrimaryKey()->unsigned()',
+ ];
+
+ public const INT_TYPE_MAP = [
+ Schema::TYPE_TINYINT => 'tinyInteger',
+ Schema::TYPE_SMALLINT => 'smallInteger',
+ Schema::TYPE_BIGINT => 'bigInteger',
+ ];
+
+ /**
+ * @var \yii\db\ColumnSchema
+ */
+ private $column;
+
+ /**
+ * @var \yii\db\Schema
+ */
+ private $dbSchema;
+
+ /**
+ * @var bool
+ */
+ private $columnUnique;
+
+ /**
+ * @var bool
+ */
+ private $fromDb;
+
+ /**
+ * @var bool
+ */
+ private $typeOnly = false;
+ private $defaultOnly = false;
+
+ /**
+ * ColumnToCode constructor.
+ * @param \yii\db\Schema $dbSchema
+ * @param \yii\db\ColumnSchema $column
+ * @param bool $columnUnique (Pass unique marker from schema, because ColumnSchema not contain it)
+ * @param bool $fromDb (if from database we prefer column type for usage, from schema - dbType)
+ */
+ public function __construct(Schema $dbSchema, ColumnSchema $column, bool $columnUnique, bool $fromDb = false)
+ {
+ $this->dbSchema = $dbSchema;
+ $this->column = $column;
+ $this->columnUnique = $columnUnique;
+ $this->fromDb = $fromDb;
+ }
+
+ public function resolveTypeOnly():string
+ {
+ $this->typeOnly = true;
+ return $this->resolve();
+ }
+
+ public function resolveDefaultOnly():string
+ {
+ $this->defaultOnly = true;
+ return $this->resolve();
+ }
+
+ public function resolve():string
+ {
+ $dbType = $this->column->dbType;
+ $type = $this->column->type;
+ //Primary Keys
+ if (array_key_exists($type, self::PK_TYPE_MAP)) {
+ return '$this->' . self::PK_TYPE_MAP[$type];
+ }
+ if (array_key_exists($dbType, self::PK_TYPE_MAP)) {
+ return '$this->' . self::PK_TYPE_MAP[$dbType];
+ }
+ if ($this->fromDb === true) {
+ $categoryType = (new ColumnSchemaBuilder(''))->categoryMap[$type] ?? '';
+ } else {
+ $categoryType = (new ColumnSchemaBuilder(''))->categoryMap[$dbType] ?? '';
+ }
+
+ $columnTypeMethod = 'resolve' . ucfirst($categoryType) . 'Type';
+
+ if (StringHelper::startsWith($dbType, 'enum')) {
+ $columnTypeMethod = 'resolveEnumType';
+ }
+ if (StringHelper::startsWith($dbType, 'set')) {
+ $columnTypeMethod = 'resolveSetType';
+ }
+ if (StringHelper::startsWith($dbType, 'tsvector')) {
+ $columnTypeMethod = 'resolveTsvectorType';
+ }
+ if (isset($column->dimension) && $column->dimension > 0) {
+ $columnTypeMethod = 'resolveArrayType';
+ }
+
+ if (method_exists($this, $columnTypeMethod)) {
+ return $this->$columnTypeMethod();
+ }
+
+ return $categoryType && !$this->defaultOnly? $this->resolveCommon() : $this->resolveRaw();
+ }
+
+ private function buildRawDefaultValue():string
+ {
+ $value = $this->column->defaultValue;
+ $nullable = $this->column->allowNull;
+ $isJson = in_array($this->column->dbType, ['json', 'jsonb']);
+ if ($value === null) {
+ return $nullable === true ? 'DEFAULT NULL' : '';
+ }
+ switch (gettype($value)) {
+ case 'integer':
+ return 'DEFAULT ' . $value;
+ case 'object':
+ if ($value instanceof JsonExpression) {
+ return 'DEFAULT '.self::defaultValueJson($value->getValue());
+ }
+ if ($value instanceof ArrayExpression) {
+ return 'DEFAULT '.self::defaultValueArray($value->getValue());
+ }
+ return 'DEFAULT ' .(string) $value;
+ case 'double':
+ // ensure type cast always has . as decimal separator in all locales
+ return 'DEFAULT ' . str_replace(',', '.', (string)$value);
+ case 'boolean':
+ return 'DEFAULT ' . ($value ? 'TRUE' : 'FALSE');
+ case 'array':
+ return $isJson? 'DEFAULT '.self::defaultValueJson($value)
+ :'DEFAULT '.self::defaultValueArray($value);
+ default:
+ if (stripos($value, 'NULL::') !== false) {
+ return 'DEFAULT NULL';
+ }
+ return 'DEFAULT '.self::wrapQuotes($value);
+ }
+ }
+
+ private static function defaultValueJson(array $value):string
+ {
+ return "'".\json_encode($value)."'";
+ }
+ private static function defaultValueArray(array $value):string
+ {
+ return "'{".trim(\json_encode($value), '[]')."}'";
+ }
+ public static function escapeQuotes(string $str):string
+ {
+ return str_replace(["'", '"', '$'], ["\\'", "\\'", '\$'], $str);
+ }
+
+ public static function wrapQuotesOnlyRaw(string $code, bool $escapeQuotes = false):string
+ {
+ if (strpos($code, '$this->') === false) {
+ return $escapeQuotes ? '"' . self::escapeQuotes($code) . '"' : '"' . $code . '"';
+ }
+ return $code;
+ }
+
+ public static function wrapQuotes(string $str, string $quotes = "'", bool $escape = true):string
+ {
+ if ($escape && strpos($str, $quotes) !== false) {
+ return $quotes . self::escapeQuotes($str) . $quotes;
+ }
+ return $quotes . $str . $quotes;
+ }
+
+ public static function enumToString(array $enum): string
+ {
+ $items = implode(",", array_map(function ($v) {
+ return self::wrapQuotes($v);
+ }, $enum));
+ return self::escapeQuotes($items);
+ }
+
+ private function resolveCommon():string
+ {
+ $size = $this->column->size ? '(' . $this->column->size . ')' : '()';
+ $default = $this->buildDefaultValue();
+ $nullable = $this->column->allowNull === true ? 'null()' : 'notNull()';
+ if (array_key_exists($this->column->dbType, self::INT_TYPE_MAP)) {
+ $type = self::INT_TYPE_MAP[$this->column->dbType] . $size;
+ } elseif (array_key_exists($this->column->type, self::INT_TYPE_MAP)) {
+ $type = self::INT_TYPE_MAP[$this->column->type] . $size;
+ } else {
+ $type = $this->column->type . $size;
+ }
+ return $this->buildString($type, $default, $nullable);
+ }
+
+ private function resolveRaw():string
+ {
+ $nullable = $this->column->allowNull ? 'NULL' : 'NOT NULL';
+ $type = $this->column->dbType;
+ $default = $this->isDefaultAllowed() ? $this->buildRawDefaultValue(): '';
+ if ($this->defaultOnly) {
+ return $default;
+ }
+
+ $size = $this->column->size ? '(' . $this->column->size . ')' : '';
+ $type = strpos($type, '(') === false ? $type . $size : $type;
+ if ($this->typeOnly === true) {
+ return $type;
+ }
+ $columns = $nullable . ($default ? ' ' . trim($default) : '');
+ return $type . ' ' . $columns;
+ }
+
+ private function resolveEnumType():string
+ {
+ if (!$this->column->enumValues || !is_array($this->column->enumValues)) {
+ return '';
+ }
+ $default = $this->buildRawDefaultValue();
+ if ($this->defaultOnly) {
+ return $default;
+ }
+ $nullable = $this->column->allowNull ? 'NULL' : 'NOT NULL';
+ if ($this->isPostgres()) {
+ $type = $this->typeOnly
+ ? \sprintf('enum_%1$s USING %1$s::enum_%1$s', $this->column->name)
+ : 'enum_'.$this->column->name;
+ } else {
+ $values = array_map(
+ function ($v) {
+ return self::wrapQuotes($v);
+ },
+ $this->column->enumValues
+ );
+ $type = "enum(" . implode(', ', $values) . ")";
+ }
+ if ($this->typeOnly === true) {
+ return $type;
+ }
+ $columns = $nullable . ($default ? ' ' . trim($default) : '');
+ return $type . ' ' . $columns;
+ }
+
+ private function resolveSetType():string
+ {
+ $default = $this->buildRawDefaultValue();
+ if ($this->defaultOnly) {
+ return $default;
+ }
+ $type = $this->column->dbType;
+ $nullable = $this->column->allowNull ? 'NULL' : 'NOT NULL';
+ $columns = $nullable . ($default ? ' ' . trim($default) : '');
+ return $type . ' ' . $columns;
+ }
+
+ private function resolveArrayType():string
+ {
+ $default = $this->buildDefaultValue();
+ if ($this->defaultOnly) {
+ return $default;
+ }
+ $nullable = $this->column->allowNull === true ? 'null()' : 'notNull()';
+ $type = $this->column->dbType;
+ return $this->buildString($type, $default, $nullable);
+ }
+
+ private function resolveTsvectorType():string
+ {
+ //\var_dump($this->column);
+ return $this->resolveRaw();
+ }
+
+ private function buildDefaultValue():string
+ {
+ $value = $this->column->defaultValue;
+ if (!$this->isDefaultAllowed()) {
+ return '';
+ }
+ if ($value === null) {
+ return ($this->column->allowNull === true)? 'defaultValue(null)' : '';
+ }
+
+ switch (gettype($value)) {
+ case 'integer':
+ return 'defaultValue(' . (int)$value . ')';
+ case 'double':
+ case 'float':
+ // ensure type cast always has . as decimal separator in all locales
+ return 'defaultValue("' . str_replace(',', '.', (string)$value) . '")';
+ case 'boolean':
+ return $value === true ? 'defaultValue(true)' : 'defaultValue(false)';
+ case 'object':
+ if ($value instanceof JsonExpression) {
+ return 'defaultValue(' . json_encode($value->getValue()) . ')';
+ }
+ return 'defaultExpression("' . self::escapeQuotes((string)$value) . '")';
+ case 'array':
+ return (string)'defaultValue(' . json_encode($value) . ')';
+ default:
+ {
+ if (stripos($value, 'NULL::') !== false) {
+ return '';
+ }
+ if (
+ StringHelper::startsWith($value, 'CURRENT')
+ || StringHelper::startsWith($value, 'LOCAL')
+ || substr($value, -1, 1) === ')') {
+ //TIMESTAMP MARKER OR DATABASE FUNCTION
+ return 'defaultExpression("' . self::escapeQuotes((string)$value) . '")';
+ }
+ return 'defaultValue("' . self::escapeQuotes((string)$value) . '")';
+ }
+ }
+ }
+
+ private function buildString(string $type, $default = '', $nullable = ''):string
+ {
+ if ($this->typeOnly === true) {
+ $columnParts = [$type];
+ } else {
+ $columnParts = [$type, $nullable, $default];
+ if ($this->columnUnique) {
+ $columnParts[] = 'unique()';
+ }
+ }
+ array_unshift($columnParts, '$this');
+ return implode('->', array_filter(array_map('trim', $columnParts), 'trim'));
+ }
+
+ private function isDefaultAllowed():bool
+ {
+ $type = strtolower($this->column->dbType);
+ if ($this->dbSchema instanceof MySqlSchema && in_array($type, ['blob', 'geometry', 'text', 'json'])) {
+ //Only mysql specific restriction, mariadb can it
+ return strpos($this->dbSchema->getServerVersion(), 'MariaDB') !== false;
+ }
+ return true;
+ }
+
+ private function isPostgres():bool
+ {
+ return $this->dbSchema instanceof PgSqlSchema;
+ }
+}
diff --git a/src/lib/CustomSpecAttr.php b/src/lib/CustomSpecAttr.php
new file mode 100644
index 00000000..8035864d
--- /dev/null
+++ b/src/lib/CustomSpecAttr.php
@@ -0,0 +1,25 @@
+ and contributors
+ * @license https://github.com/cebe/yii2-openapi/blob/master/LICENSE
+ */
+
+namespace cebe\yii2openapi\lib;
+
+class CustomSpecAttr
+{
+ // --- For component schema ---
+ //Custom table name
+ public const TABLE = 'x-table';
+ //Primary key property name, if it different from "id" (Only one value, compound keys not supported yet)
+ public const PRIMARY_KEY = 'x-pk';
+
+ // --- For each property schema ---
+ //Custom fake data for property
+ public const FAKER = 'x-faker';
+ // Custom db type (MUST CONTAINS ONLY DB TYPE! (json, jsonb, uuid, varchar etc))
+ public const DB_TYPE = 'x-db-type';
+ // Flag for database unique constraint
+ public const UNIQUE = 'x-db-unique';
+}
diff --git a/src/lib/FakerStubResolver.php b/src/lib/FakerStubResolver.php
new file mode 100644
index 00000000..2a12dc51
--- /dev/null
+++ b/src/lib/FakerStubResolver.php
@@ -0,0 +1,178 @@
+ and contributors
+ * @license https://github.com/cebe/yii2-openapi/blob/master/LICENSE
+ */
+
+namespace cebe\yii2openapi\lib;
+
+use cebe\openapi\SpecObjectInterface;
+use cebe\yii2openapi\lib\items\Attribute;
+
+/**
+ * Guess faker for attribute
+ * @link https://github.com/fzaninotto/Faker#formatters
+ **/
+class FakerStubResolver
+{
+ // 2^31-1 PHP_MAX_INT may be 64bit and too big in some cases
+ public const MAX_INT = 2147483647;
+ /**
+ * @var \cebe\yii2openapi\lib\items\Attribute
+ */
+ private $attribute;
+
+ /**
+ * @var \cebe\openapi\spec\Schema
+ */
+ private $property;
+
+ public function __construct(Attribute $attribute, SpecObjectInterface $property)
+ {
+ $this->attribute = $attribute;
+ $this->property = $property;
+ }
+
+ public function resolve():?string
+ {
+ if (isset($this->property->{CustomSpecAttr::FAKER})) {
+ return $this->property->{CustomSpecAttr::FAKER};
+ }
+ $limits = $this->attribute->limits;
+ switch ($this->attribute->phpType) {
+ case 'bool':
+ return '$faker->boolean';
+ case 'int':
+ return $this->fakeForInt($limits['min'], $limits['max']);
+ case 'string':
+ return $this->fakeForString();
+ case 'float':
+ case 'double':
+ return $this->fakeForFloat($limits['min'], $limits['max']);
+ case 'array':
+ return $this->fakeForArray($this->attribute);
+ default:
+ return null;
+ }
+ }
+
+ private function fakeForString():?string
+ {
+ $formats = [
+ 'date' => '$faker->iso8601',
+ 'date-time' => '$faker->dateTimeThisCentury->format(\'Y-m-d H:i:s\')',
+ 'email' => '$faker->safeEmail',
+ ];
+ if ($this->property->format && isset($formats[$this->property->format])) {
+ return $formats[$this->property->format];
+ }
+ if (!empty($this->property->enum) && is_array($this->property->enum)) {
+ return '$faker->randomElement(' . var_export($this->property->enum, true) . ')';
+ }
+ if ($this->attribute->columnName === 'title'
+ && $this->attribute->size
+ && $this->attribute->size < 10) {
+ return '$faker->title';
+ }
+
+ $patterns = [
+ '~_id$~' => '$uniqueFaker->numberBetween(0, 1000000)',
+ '~uuid$~' => '$uniqueFaker->uuid',
+ '~slug$~' => '$uniqueFaker->slug',
+ '~firstname~i' => '$faker->firstName',
+ '~password~i' => '$faker->password',
+ '~(last|sur)name~i' => '$faker->lastName',
+ '~(company|employer)~i' => '$faker->company',
+ '~(city|town)~i' => '$faker->city',
+ '~(post|zip)code~i' => '$faker->postcode',
+ '~streetaddress~i' => '$faker->streetAddress',
+ '~address~i' => '$faker->address',
+ '~street~i' => '$faker->streetName',
+ '~state~i' => '$faker->state',
+ '~county~i' => 'sprintf("%s County", $faker->city)',
+ '~country~i' => '$faker->countryCode',
+ '~lang~i' => '$faker->languageCode',
+ '~locale~i' => '$faker->locale',
+ '~currency~i' => '$faker->currencyCode',
+ '~(hash|token)~i' => '$faker->sha256',
+ '~e?mail~i' => '$faker->safeEmail',
+ '~timestamp~i' => '$faker->unixTime',
+ '~.*At$~' => '$faker->dateTimeThisCentury->format(\'Y-m-d H:i:s\')', // createdAt, updatedAt, ...
+ '~.*ed_at$~i' => '$faker->dateTimeThisCentury->format(\'Y-m-d H:i:s\')', // created_at, updated_at, ...
+ '~(phone|fax|mobile|telnumber)~i' => '$faker->e164PhoneNumber',
+ '~(^lat|coord)~i' => '$faker->latitude',
+ '~^lon~i' => '$faker->longitude',
+ '~title~i' => '$faker->sentence',
+ '~(body|summary|article|content|descr|comment|detail)~i' => '$faker->paragraphs(6, true)',
+ '~(url|site|website|href)~i' => '$faker->url',
+ '~(username|login)~i' => '$faker->userName',
+ ];
+ $size = $this->attribute->size > 0 ? $this->attribute->size: null;
+ foreach ($patterns as $pattern => $fake) {
+ if (preg_match($pattern, $this->attribute->columnName)) {
+ if ($size) {
+ return 'substr(' . $fake . ', 0, ' . $size . ')';
+ }
+ return $fake;
+ }
+ }
+
+ // TODO maybe also consider OpenAPI examples here
+
+ if ($size) {
+ return 'substr($faker->text(' . $size . '), 0, ' . $size . ')';
+ }
+ return '$faker->sentence';
+ }
+
+ private function fakeForInt(?int $min, ?int $max):?string
+ {
+ $fakerVariable = 'faker';
+ if (preg_match('~_?id$~', $this->attribute->columnName)) {
+ $fakerVariable = 'uniqueFaker';
+ }
+ if ($min !== null && $max !== null) {
+ return "\${$fakerVariable}->numberBetween($min, $max)";
+ }
+
+ if ($min !== null) {
+ return "\${$fakerVariable}->numberBetween($min, ".self::MAX_INT.")";
+ }
+
+ if ($max !== null) {
+ return "\${$fakerVariable}->numberBetween(0, $max)";
+ }
+
+ $patterns = [
+ '~timestamp~i' => '$faker->unixTime',
+ '~.*At$~' => '$faker->unixTime', // createdAt, updatedAt, ...
+ '~.*ed_at$~i' => '$faker->unixTime', // created_at, updated_at, ...
+ ];
+ foreach ($patterns as $pattern => $fake) {
+ if (preg_match($pattern, $this->attribute->columnName)) {
+ return $fake;
+ }
+ }
+ return "\${$fakerVariable}->numberBetween(0, ".self::MAX_INT.")";
+ }
+
+ private function fakeForFloat(?int $min, ?int $max):?string
+ {
+ if ($min !== null && $max !== null) {
+ return "\$faker->randomFloat(null, $min, $max)";
+ }
+ if ($min !== null) {
+ return "\$faker->randomFloat(null, $min)";
+ }
+ if ($max !== null) {
+ return "\$faker->randomFloat(null, 0, $max)";
+ }
+ return '$faker->randomFloat()';
+ }
+
+ private function fakeForArray(Attribute $attribute)
+ {
+ return '[]';
+ }
+}
diff --git a/src/lib/MigrationBuilder.php b/src/lib/MigrationBuilder.php
new file mode 100644
index 00000000..788a52f1
--- /dev/null
+++ b/src/lib/MigrationBuilder.php
@@ -0,0 +1,365 @@
+ and contributors
+ * @license https://github.com/cebe/yii2-openapi/blob/master/LICENSE
+ */
+
+namespace cebe\yii2openapi\lib;
+
+use cebe\yii2openapi\lib\items\DbModel;
+use cebe\yii2openapi\lib\items\MigrationModel;
+use yii\base\NotSupportedException;
+use yii\db\ColumnSchema;
+use yii\db\Connection;
+use yii\helpers\VarDumper;
+use function array_intersect;
+use function array_map;
+use function in_array;
+use function sprintf;
+
+class MigrationBuilder
+{
+ public const INDENT = ' ';
+ private const ADD_TABLE = self::INDENT . "\$this->createTable('%s', %s);";
+ private const DROP_TABLE = self::INDENT . "\$this->dropTable('%s');";
+ private const ADD_COLUMN = self::INDENT . "\$this->addColumn('%s', '%s', %s);";
+ private const DROP_COLUMN = self::INDENT . "\$this->dropColumn('%s', '%s');";
+ private const ALTER_COLUMN = self::INDENT . "\$this->alterColumn('%s', '%s', %s);";
+ private const ADD_FK = self::INDENT . "\$this->addForeignKey('%s', '%s', '%s', '%s', '%s');";
+ private const DROP_FK = self::INDENT . "\$this->dropForeignKey('%s', '%s');";
+ private const ADD_PK = self::INDENT . "\$this->addPrimaryKey('%s', '%s', '%s');";
+ private const ADD_ENUM = self::INDENT . "\$this->execute('CREATE TYPE enum_%s AS ENUM(%s)');";
+ private const DROP_ENUM = self::INDENT . "\$this->execute('DROP TYPE enum_%s');";
+ private const ADD_UNIQUE = self::INDENT . "\$this->createIndex('%s', '%s', '%s', true);";
+ private const DROP_INDEX = self::INDENT . "\$this->dropIndex('%s', '%s');";
+
+ /**
+ * @var \yii\db\Connection
+ */
+ private $db;
+
+ /**
+ * @var \cebe\yii2openapi\lib\items\DbModel
+ */
+ private $model;
+
+ /**
+ * @var \yii\db\TableSchema|null
+ */
+ private $tableSchema;
+
+ /**@var bool */
+ private $isPostgres;
+
+ /**
+ * @var MigrationModel $migration
+ **/
+ private $migration;
+
+ /**
+ * @var array
+ */
+ private $uniqueColumns;
+
+ /**
+ * @var array
+ */
+ private $currentUniqueColumns;
+
+ /**
+ * @var \yii\db\ColumnSchema[]
+ */
+ private $newColumns;
+
+ /**
+ * @var \yii\db\Schema
+ */
+ private $dbSchema;
+
+ public function __construct(Connection $db, DbModel $model)
+ {
+ $this->db = $db;
+ $this->model = $model;
+ $this->tableSchema = $db->getTableSchema($model->getTableAlias(), true);
+ $this->dbSchema = $db->getSchema();
+ $this->isPostgres = $this->db->getDriverName() === 'pgsql';
+ }
+
+ public function build():MigrationModel
+ {
+ return $this->tableSchema === null ? $this->buildFresh() : $this->buildSecondary();
+ }
+
+ public function buildFresh():MigrationModel
+ {
+ $this->migration = new MigrationModel($this->model, true);
+ $this->uniqueColumns = $this->model->getUniqueColumnsList();
+ $this->newColumns = $this->model->attributesToColumnSchema();
+ $tableName = $this->model->getTableAlias();
+ $codeColumns = VarDumper::export(array_map(
+ function (ColumnSchema $column) {
+ $isUnique = in_array($column->name, $this->uniqueColumns, true);
+ return $this->columnToCode($column, $isUnique, false);
+ },
+ $this->model->attributesToColumnSchema()
+ ));
+ $codeColumns = str_replace(PHP_EOL, PHP_EOL . self::INDENT, $codeColumns);
+ $this->migration->addUpCode(sprintf(self::ADD_TABLE, $this->model->getTableAlias(), $codeColumns))
+ ->addDownCode(sprintf(self::DROP_TABLE, $this->model->getTableAlias()));
+ if ($this->isPostgres) {
+ $enums = $this->model->getEnumAttributes();
+ foreach ($enums as $attr) {
+ $items = ColumnToCode::enumToString($attr->enumValues);
+ $this->migration->addUpCode(sprintf(self::ADD_ENUM, $attr->columnName, $items), true)
+ ->addDownCode(sprintf(self::DROP_ENUM, $attr->columnName));
+ }
+ }
+ foreach ($this->model->getHasOneRelations() as $relation) {
+ $fkCol = $relation->getColumnName();
+ $refCol = $relation->getForeignName();
+ $refTable = $relation->getTableAlias();
+ $fkName = $this->foreignKeyName($this->model->tableName, $fkCol, $relation->getTableName(), $refCol);
+ $this->migration->addUpCode(sprintf(self::ADD_FK, $fkName, $tableName, $fkCol, $refTable, $refCol))
+ ->addDownCode(sprintf(self::DROP_FK, $fkName, $tableName));
+ if ($relation->getTableName() !== $this->model->tableName) {
+ $this->migration->dependencies[] = $refTable;
+ }
+ }
+
+ return $this->migration;
+ }
+
+ public function buildSecondary():MigrationModel
+ {
+ $this->migration = new MigrationModel($this->model, false);
+ $this->uniqueColumns = $this->model->getUniqueColumnsList();
+ $this->currentUniqueColumns = array_values($this->findUniqueIndexes());
+ $this->newColumns = $this->model->attributesToColumnSchema();
+ $wantNames = array_keys($this->newColumns);
+ $haveNames = $this->tableSchema->columnNames;
+ sort($wantNames);
+ sort($haveNames);
+ $columnsForCreate = array_map(
+ function (string $missingColumn) {
+ return $this->newColumns[$missingColumn];
+ },
+ array_diff($wantNames, $haveNames)
+ );
+
+ $columnsForDrop = array_map(
+ function (string $unknownColumn) {
+ return $this->tableSchema->columns[$unknownColumn];
+ },
+ array_diff($haveNames, $wantNames)
+ );
+
+ $columnsForChange = array_intersect($wantNames, $haveNames);
+
+ $this->buildColumnsCreation($columnsForCreate);
+ $this->buildColumnsDrop($columnsForDrop);
+ foreach ($columnsForChange as $commonColumn) {
+ $this->buildColumnChanges($this->tableSchema->columns[$commonColumn], $this->newColumns[$commonColumn]);
+ }
+
+ $this->buildRelations();
+ return $this->migration;
+ }
+
+ /**
+ * @param array|\yii\db\ColumnSchema[] $columns
+ */
+ private function buildColumnsCreation(array $columns):void
+ {
+ foreach ($columns as $column) {
+ if ($column->isPrimaryKey) {
+ // TODO: Avoid pk changes, or previous pk should be dropped before
+ }
+ $isUnique = in_array($column->name, $this->uniqueColumns, true);
+ $columnCode = ColumnToCode::wrapQuotesOnlyRaw($this->columnToCode($column, $isUnique, false));
+ $tableName = $this->model->getTableAlias();
+ $this->migration->addUpCode(sprintf(self::ADD_COLUMN, $tableName, $column->name, $columnCode))
+ ->addDownCode(sprintf(self::DROP_COLUMN, $tableName, $column->name));
+ if ($this->isPostgres && $column->dbType === 'enum') {
+ $items = ColumnToCode::enumToString($column->enumValues);
+ $this->migration->addUpCode(sprintf(self::ADD_ENUM, $column->name, $items), true)
+ ->addDownCode(sprintf(self::DROP_ENUM, $column->name));
+ }
+ }
+ }
+
+ /**
+ * @param array|\yii\db\ColumnSchema[] $columns
+ */
+ private function buildColumnsDrop(array $columns):void
+ {
+ foreach ($columns as $column) {
+ if ($column->isPrimaryKey) {
+ // TODO: drop pk index and foreign keys before or avoid drop
+ }
+ $isUnique = in_array($column->name, $this->currentUniqueColumns, true);
+ $columnCode = $this->columnToCode($column, $isUnique, true);
+ $tableName = $this->model->getTableAlias();
+ $this->migration->addDownCode(sprintf(self::ADD_COLUMN, $tableName, $column->name, $columnCode))
+ ->addUpCode(sprintf(self::DROP_COLUMN, $tableName, $column->name));
+ if ($this->isPostgres && $column->dbType === 'enum') {
+ $items = ColumnToCode::enumToString($column->enumValues);
+ $this->migration->addDownCode(sprintf(self::ADD_ENUM, $column->name, $items, true))
+ ->addUpCode(sprintf(self::DROP_ENUM, $column->name), true);
+ }
+ }
+ }
+
+ private function buildColumnChanges(ColumnSchema $current, ColumnSchema $desired):void
+ {
+ $isUniqueCurrent = in_array($current->name, $this->currentUniqueColumns, true);
+ $isUniqueDesired = in_array($desired->name, $this->uniqueColumns, true);
+ if ($current->isPrimaryKey || in_array($desired->dbType, ['pk', 'upk', 'bigpk', 'ubigpk'])) {
+ // do not adjust existing primary keys
+ return;
+ }
+ $columnName = $current->name;
+ $tableName = $this->model->getTableAlias();
+ if ($isUniqueCurrent !== $isUniqueDesired) {
+ $addUnique = sprintf(self::ADD_UNIQUE, 'unique_' . $columnName, $tableName, $columnName);
+ $dropUnique = sprintf(self::DROP_INDEX, 'unique_' . $columnName, $tableName);
+ $this->migration->addUpCode($isUniqueDesired === true ? $addUnique : $dropUnique)
+ ->addDownCode($isUniqueDesired === true ? $dropUnique : $addUnique);
+ }
+ $changedAttributes = [];
+ foreach (['type', 'size', 'allowNull', 'defaultValue', 'enumValues'] as $attr) {
+ if ($current->$attr !== $desired->$attr) {
+ $changedAttributes[] = $attr;
+ }
+ }
+ if (empty($changedAttributes)) {
+ return;
+ }
+ if ($this->isPostgres) {
+ $this->buildColumnsChangePostgres($current, $desired, $changedAttributes);
+ return;
+ }
+ $newColumn = clone $current;
+ foreach ($changedAttributes as $attr) {
+ $newColumn->$attr = $desired->$attr;
+ }
+ if (!empty($newColumn->enumValues)) {
+ $newColumn->dbType = 'enum';
+ }
+ $upCode = $this->columnToCode($newColumn, false, true); //unique marks solved in separated queries
+ $downCode = $this->columnToCode($current, false, true);
+ $upCode = ColumnToCode::wrapQuotesOnlyRaw($upCode);
+ $downCode = ColumnToCode::wrapQuotesOnlyRaw($downCode);
+ $this->migration->addUpCode(sprintf(self::ALTER_COLUMN, $tableName, $columnName, $upCode))
+ ->addDownCode(sprintf(self::ALTER_COLUMN, $tableName, $columnName, $downCode));
+ }
+
+ private function buildColumnsChangePostgres(ColumnSchema $current, ColumnSchema $desired, array $changes):void
+ {
+ $tableName = $this->model->getTableAlias();
+ $columnName = $desired->name;
+ $upCodes = $downCodes = [];
+ $isChangeToEnum = $current->type !== $desired->type && !empty($desired->enumValues);
+ $isChangeFromEnum = $current->type !== $desired->type && !empty($current->enumValues);
+ if ($isChangeToEnum) {
+ $items = ColumnToCode::enumToString($desired->enumValues);
+ $this->migration->addUpCode(sprintf(self::ADD_ENUM, $desired->name, $items), true);
+ }
+ if ($isChangeFromEnum) {
+ $this->migration->addUpCode(sprintf(self::DROP_ENUM, $current->name));
+ }
+ if (!empty(array_intersect(['type', 'size'], $changes))) {
+ $upCodes[] = (new ColumnToCode($this->dbSchema, $desired, false))->resolveTypeOnly();
+ $downCodes[] = (new ColumnToCode($this->dbSchema, $current, false, true))->resolveTypeOnly();
+ }
+ if (in_array('allowNull', $changes, true)) {
+ $upCodes[] = $desired->allowNull === true ? 'DROP NOT NULL' : 'SET NOT NULL';
+ $downCodes[] = $desired->allowNull === true ? 'SET NOT NULL' : 'DROP NOT NULL';
+ }
+ if (in_array('defaultValue', $changes, true)) {
+ $upCodes[] = $desired->defaultValue !== null
+ ? 'SET ' . (new ColumnToCode($this->dbSchema, $desired, false))->resolveDefaultOnly()
+ : 'DROP DEFAULT';
+ $downCodes[] = $current->defaultValue !== null
+ ? 'SET ' . (new ColumnToCode($this->dbSchema, $current, false))->resolveDefaultOnly()
+ : 'DROP DEFAULT';
+ }
+ foreach ($upCodes as $upCode) {
+ $upCode = ColumnToCode::wrapQuotesOnlyRaw($upCode);
+ $this->migration->addUpCode(sprintf(self::ALTER_COLUMN, $tableName, $columnName, $upCode));
+ }
+ foreach ($downCodes as $downCode) {
+ $downCode = ColumnToCode::wrapQuotesOnlyRaw($downCode);
+ $this->migration->addDownCode(sprintf(self::ALTER_COLUMN, $tableName, $columnName, $downCode), true);
+ }
+ if ($isChangeFromEnum) {
+ $items = ColumnToCode::enumToString($current->enumValues);
+ $this->migration->addDownCode(sprintf(self::ADD_ENUM, $current->name, $items), true);
+ }
+ if ($isChangeToEnum) {
+ $this->migration->addDownCode(sprintf(self::DROP_ENUM, $desired->name), true);
+ }
+ }
+
+ private function buildRelations():void
+ {
+ $tableName = $this->model->getTableAlias();
+ if (empty($this->model->relations)) {
+ //? Revert existed relations
+ foreach ($this->tableSchema->foreignKeys as $relation) {
+ $refTable = array_shift($relation);
+ $refCol = array_keys($relation)[0];
+ $fkCol = $relation[$refCol];
+ $fkName = $this->foreignKeyName($this->model->tableName, $fkCol, $refTable, $refCol);
+ $this->migration->addUpCode(sprintf(self::DROP_FK, $fkName, $tableName), true)
+ ->addDownCode(sprintf(self::ADD_FK, $fkName, $tableName, $fkCol, $refTable, $refCol));
+ if ($refTable !== $this->model->tableName) {
+ $this->migration->dependencies[$refTable];
+ }
+ }
+ }
+ foreach ($this->model->getHasOneRelations() as $relation) {
+ $fkCol = $relation->getColumnName();
+ $refCol = $relation->getForeignName();
+ $refTable = $relation->getTableAlias();
+ $fkName = $this->foreignKeyName($this->model->tableName, $fkCol, $relation->getTableName(), $refCol);
+ if (isset($tableSchema->foreignKeys[$fkName])) {
+ continue;
+ }
+ $this->migration->addUpCode(sprintf(self::ADD_FK, $fkName, $tableName, $fkCol, $refTable, $refCol))
+ ->addDownCode(sprintf(self::DROP_FK, $fkName, $tableName));
+ if ($relation->getTableName() !== $this->model->tableName) {
+ $this->migration->dependencies[] = $refTable;
+ }
+ }
+ }
+
+ private function findUniqueIndexes():array
+ {
+ try {
+ return $this->db->getSchema()->findUniqueIndexes($this->tableSchema);
+ } catch (NotSupportedException $e) {
+ return [];
+ }
+ }
+
+ private function columnToCode(ColumnSchema $column, bool $isUnique, bool $fromDb = false):string
+ {
+ return (new ColumnToCode($this->dbSchema, $column, $isUnique, $fromDb))->resolve();
+ }
+
+ private function foreignKeyName(string $table, string $column, string $foreignTable, string $foreignColumn):string
+ {
+ $table = $this->normalizeTableName($table);
+ $foreignTable = $this->normalizeTableName($foreignTable);
+ return "fk_{$table}_{$column}_{$foreignTable}_{$foreignColumn}";
+ }
+
+ private function normalizeTableName($tableName)
+ {
+ if (preg_match('~^{{%?(.*)}}$~', $tableName, $m)) {
+ return $m[1];
+ }
+ return $tableName;
+ }
+}
diff --git a/src/lib/MigrationsGenerator.php b/src/lib/MigrationsGenerator.php
new file mode 100644
index 00000000..d08c67f3
--- /dev/null
+++ b/src/lib/MigrationsGenerator.php
@@ -0,0 +1,92 @@
+ and contributors
+ * @license https://github.com/cebe/yii2-openapi/blob/master/LICENSE
+ */
+
+namespace cebe\yii2openapi\lib;
+
+use cebe\yii2openapi\lib\items\MigrationModel;
+use Exception;
+use yii\base\Component;
+use yii\db\Connection;
+use yii\di\Instance;
+use function ksort;
+
+class MigrationsGenerator extends Component
+{
+ /**
+ * @var string|array|Connection the Yii database connection component for connecting to the database.
+ */
+ public $db = 'db';
+
+ /**
+ * @var MigrationModel[]
+ **/
+ private $migrations;
+ /**
+ * @var MigrationModel[]
+ **/
+ private $sorted;
+
+ public function init()
+ {
+ parent::init();
+ $this->db = Instance::ensure($this->db, Connection::class);
+ }
+
+ /**
+ * @param array|\cebe\yii2openapi\lib\items\DbModel[] $models
+ * @return array|\cebe\yii2openapi\lib\items\MigrationModel[]
+ * @throws \Exception
+ */
+ public function generate(array $models):array
+ {
+ foreach ($models as $model) {
+ $migration = (new MigrationBuilder($this->db, $model))->build();
+ if ($migration->notEmpty()) {
+ $this->migrations[$model->tableAlias] = $migration;
+ }
+ }
+ return $this->sortMigrationsByDeps();
+ }
+
+ /**
+ * @return array|MigrationModel[]
+ * @throws \Exception
+ */
+ private function sortMigrationsByDeps():array
+ {
+ $this->sorted = [];
+ ksort($this->migrations);
+ foreach ($this->migrations as $migration) {
+ //echo "adding {$migration->tableAlias}\n";
+ $this->sortByDependencyRecurse($migration);
+ }
+ return $this->sorted;
+ }
+
+ /**
+ * @param \cebe\yii2openapi\lib\items\MigrationModel $migration
+ * @throws \Exception
+ */
+ private function sortByDependencyRecurse(MigrationModel $migration):void
+ {
+ if (!isset($this->sorted[$migration->tableAlias])) {
+ $this->sorted[$migration->tableAlias] = false;
+ foreach ($migration->dependencies as $dependency) {
+ if (!isset($this->migrations[$dependency])) {
+ //echo "skipping dep $dependency\n";
+ continue;
+ }
+ //echo "adding dep $dependency\n";
+ $this->sortByDependencyRecurse($this->migrations[$dependency]);
+ }
+ unset($this->sorted[$migration->tableAlias]);//necessary for provide valid order
+ $this->sorted[$migration->tableAlias] = $migration;
+ } elseif ($this->sorted[$migration->tableAlias] === false) {
+ throw new Exception("A circular dependency is detected for table '{$migration->tableAlias}'.");
+ }
+ }
+}
diff --git a/src/lib/SchemaToDatabase.php b/src/lib/SchemaToDatabase.php
new file mode 100644
index 00000000..99c67d1e
--- /dev/null
+++ b/src/lib/SchemaToDatabase.php
@@ -0,0 +1,119 @@
+ and contributors
+ * @license https://github.com/cebe/yii2-openapi/blob/master/LICENSE
+ */
+
+namespace cebe\yii2openapi\lib;
+
+use cebe\openapi\ReferenceContext;
+use cebe\openapi\spec\OpenApi;
+use cebe\openapi\spec\Reference;
+use cebe\openapi\spec\Schema;
+use Yii;
+use yii\base\Component;
+
+/**
+ * Convert OpenAPI description into a database schema.
+ * There are two options:
+ * 1. let the generator guess which schemas need a database table
+ * for storing their data and which do not.
+ * 2. Explicitly define schemas which represent a database table by adding the
+ * `x-table` property to the schema.
+ * The [[]]
+ *
+ * OpenApi Schema definition rules for database conversion:
+ * https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.3.md#schema-object
+ *
+ * components:
+ * schemas:
+ * ModelName: #(table name becomes model_names)
+ * description: #(optional, become as model class comment)
+ * required: #(list of required property names that can't be nullable)
+ * - id
+ * - some
+ * x-table: custom_table #(explicit database table name)
+ * x-pk: pid #(optional, primary key name if it called not "id") (composite keys not supported yet)
+ * properties: #(table columns and relations)
+ * prop_name:
+ * type: #(one of common types string|integer|number|boolean|array)
+ * format: #(see https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.3.md#dataTypes)
+ * readOnly: true/false #(If true, should be skipped from validation rules)
+ * minimum: #(numeric value, applied for validation rules and faker generation)
+ * maximum: #(numeric value, applied for integer|number validation rules and faker generation)
+ * maxLength: #(numeric value, applied for database column size limit!, also can be applied for validation)
+ * minLength: #(numeric value, can be applied for validation rules)
+ * default: #(int|string, default value, used for database migration and model rules)
+ * x-db-type: #(Custom database type like JSON, JSONB, CHAR, VARCHAR, UUID, etc )
+ * x-db-unique: true #(mark unique attribute for database and validation constraining)
+ * x-faker: #(custom faker generator, for ex '$faker->gender')
+ * description: #(optional, used for comment)
+ *
+ */
+class SchemaToDatabase extends Component
+{
+
+ /**
+ * @var array List of model names to exclude.
+ */
+ public $excludeModels = [];
+
+ /**
+ * @var array Generate database models only for Schemas that have the `x-table` annotation.
+ */
+ public $generateModelsOnlyXTable = false;
+
+ public $attributeResolverClass = AttributeResolver::class;
+
+ /**
+ * @param \cebe\openapi\spec\OpenApi $openApi
+ * @return array|\cebe\yii2openapi\lib\items\DbModel[]
+ * @throws \cebe\openapi\exceptions\UnresolvableReferenceException
+ * @throws \yii\base\InvalidConfigException
+ */
+ public function generateModels(OpenApi $openApi):array
+ {
+ $models = [];
+ foreach ($openApi->components->schemas as $schemaName => $schema) {
+ if ($schema instanceof Reference) {
+ $schema->getContext()->mode = ReferenceContext::RESOLVE_MODE_INLINE;
+ $schema = $schema->resolve();
+ }
+
+ if (!$this->canGenerateModel($schemaName, $schema)) {
+ continue;
+ }
+ $resolver = Yii::createObject($this->attributeResolverClass, [$schemaName, $schema]);
+ $models[$schemaName] = $resolver->resolve();
+ }
+
+ // TODO generate inverse relations
+
+ return $models;
+ }
+
+ private function canGenerateModel(string $schemaName, Schema $schema):bool
+ {
+ // only generate tables for schemas of type object and those who have defined properties
+ if ((empty($schema->type) || $schema->type === 'object') && empty($schema->properties)) {
+ return false;
+ }
+ if (!empty($schema->type) && $schema->type !== 'object') {
+ return false;
+ }
+ // do not generate tables for composite schemas
+ if ($schema->allOf || $schema->anyOf || $schema->multipleOf || $schema->oneOf) {
+ return false;
+ }
+ // skip excluded model names
+ if (in_array($schemaName, $this->excludeModels, true)) {
+ return false;
+ }
+
+ if ($this->generateModelsOnlyXTable && empty($schema->{CustomSpecAttr::TABLE})) {
+ return false;
+ }
+ return true;
+ }
+}
diff --git a/src/lib/TypeResolver.php b/src/lib/TypeResolver.php
new file mode 100644
index 00000000..64de5fba
--- /dev/null
+++ b/src/lib/TypeResolver.php
@@ -0,0 +1,102 @@
+ and contributors
+ * @license https://github.com/cebe/yii2-openapi/blob/master/LICENSE
+ */
+
+namespace cebe\yii2openapi\lib;
+
+use cebe\openapi\spec\Schema;
+use yii\db\Schema as YiiDbSchema;
+use yii\helpers\StringHelper;
+use function in_array;
+use function strtolower;
+
+class TypeResolver
+{
+ public static function schemaToPhpType(Schema $property):string
+ {
+ $customDbType = isset($property->{CustomSpecAttr::DB_TYPE})
+ ? strtolower($property->{CustomSpecAttr::DB_TYPE}) : null;
+ if ($customDbType !== null
+ && (in_array($customDbType, ['json', 'jsonb'], true) || StringHelper::endsWith($customDbType, '[]'))
+ ) {
+ return 'array';
+ }
+ switch ($property->type) {
+ case 'integer':
+ return 'int';
+ case 'boolean':
+ return 'bool';
+ case 'number': // can be double and float
+ return $property->format && $property->format === 'double' ? 'double' : 'float';
+// case 'array':
+// return $property->type;
+ default:
+ return $property->type;
+ }
+ }
+
+ public static function referenceToDbType(Schema $property):string
+ {
+ if ($property->type === 'integer') {
+ return $property->format === 'int64' ? YiiDbSchema::TYPE_BIGINT : YiiDbSchema::TYPE_INTEGER;
+ }
+ return $property->type;
+ }
+
+ public static function schemaToDbType(Schema $property, bool $isPrimary = false):string
+ {
+ if (isset($property->{CustomSpecAttr::DB_TYPE})) {
+ $customDbType = strtolower($property->{CustomSpecAttr::DB_TYPE});
+ if ($customDbType === 'varchar') {
+ return YiiDbSchema::TYPE_STRING;
+ }
+ if ($customDbType !== null) {
+ return $customDbType;
+ }
+ }
+ if ($isPrimary && $property->type === 'integer') {
+ return $property->format === 'int64' ? YiiDbSchema::TYPE_BIGPK : YiiDbSchema::TYPE_PK;
+ }
+
+ switch ($property->type) {
+ case 'boolean':
+ return $property->type;
+ case 'number': // can be double and float
+ return $property->format ?? 'float';
+ case 'integer':
+ if ($property->format === 'int64') {
+ return YiiDbSchema::TYPE_BIGINT;
+ }
+ if ($property->format === 'int32') {
+ return YiiDbSchema::TYPE_INTEGER;
+ }
+ return YiiDbSchema::TYPE_INTEGER;
+ case 'string':
+ if (in_array($property->format, ['date', 'time', 'binary'])) {
+ return $property->format;
+ }
+ if ($property->maxLength && $property->maxLength < 2049) {
+ //What if we want to restrict length of text column?
+ return YiiDbSchema::TYPE_STRING;
+ }
+ if ($property->format === 'date-time' || $property->format === 'datetime') {
+ return YiiDbSchema::TYPE_DATETIME;
+ }
+ if (in_array($property->format, ['email', 'url', 'phone', 'password'])) {
+ return YiiDbSchema::TYPE_STRING;
+ }
+ if (isset($property->enum) && !empty($property->enum)) {
+ return YiiDbSchema::TYPE_STRING;
+ }
+ return YiiDbSchema::TYPE_TEXT;
+// case 'array':
+// Need schema example for this case if it possible
+// return $this->typeForArray();
+ default:
+ return YiiDbSchema::TYPE_TEXT;
+ }
+ }
+}
diff --git a/src/lib/ValidationRulesBuilder.php b/src/lib/ValidationRulesBuilder.php
new file mode 100644
index 00000000..2c6ef038
--- /dev/null
+++ b/src/lib/ValidationRulesBuilder.php
@@ -0,0 +1,107 @@
+ and contributors
+ * @license https://github.com/cebe/yii2-openapi/blob/master/LICENSE
+ */
+
+namespace cebe\yii2openapi\lib;
+
+use cebe\yii2openapi\lib\items\DbModel;
+use cebe\yii2openapi\lib\items\ValidationRule;
+
+class ValidationRulesBuilder
+{
+ /**
+ * @var \cebe\yii2openapi\lib\items\DbModel
+ */
+ private $model;
+
+ /**
+ * @var array|ValidationRule[]
+ **/
+ private $rules = [];
+
+ private $typeScope = [
+ 'safe' => [], 'required' => [], 'int' => [], 'bool' => [], 'float' => [], 'string' => [], 'ref' => []
+ ];
+
+ public function __construct(DbModel $model)
+ {
+ $this->model = $model;
+ }
+
+ /**
+ * @return array|\cebe\yii2openapi\lib\items\ValidationRule[]
+ */
+ public function build(): array
+ {
+ $this->prepareTypeScope();
+ $this->rulesByType();
+ return $this->rules;
+ }
+
+ private function prepareTypeScope():void
+ {
+ foreach ($this->model->attributes as $attribute) {
+ if ($attribute->isReadOnly()) {
+ continue;
+ }
+ if ($attribute->isRequired()) {
+ $this->typeScope['required'][$attribute->columnName] = $attribute->columnName;
+ }
+
+ if ($attribute->isReference()) {
+ if (in_array($attribute->phpType, ['int', 'string'])) {
+ $this->typeScope[$attribute->phpType][$attribute->columnName] = $attribute->columnName;
+ }
+ $this->typeScope['ref'][] = ['attr' => $attribute->columnName, 'rel' => $attribute->camelName()];
+ continue;
+ }
+
+ if (in_array($attribute->phpType, ['int', 'string', 'bool', 'float'])) {
+ $this->typeScope[$attribute->phpType][$attribute->columnName] = $attribute->columnName;
+ continue;
+ }
+
+ if ($attribute->phpType === 'double') {
+ $this->typeScope['float'][$attribute->columnName] = $attribute->columnName;
+ continue;
+ }
+
+ $this->typeScope['safe'][$attribute->columnName] = $attribute->columnName;
+ }
+ }
+
+ private function rulesByType():void
+ {
+ if (!empty($this->typeScope['string'])) {
+ $this->rules[] = new ValidationRule($this->typeScope['string'], 'trim');
+ }
+ if (!empty($this->typeScope['required'])) {
+ $this->rules[] = new ValidationRule($this->typeScope['required'], 'required');
+ }
+
+ if (!empty($this->typeScope['int'])) {
+ $this->rules[] = new ValidationRule($this->typeScope['int'], 'integer');
+ }
+
+ foreach ($this->typeScope['ref'] as $relation) {
+ $this->rules[] = new ValidationRule([$relation['attr']], 'exist', ['targetRelation'=>$relation['rel']]);
+ }
+
+ if (!empty($this->typeScope['string'])) {
+ $this->rules[] = new ValidationRule($this->typeScope['string'], 'string');
+ }
+
+ if (!empty($this->typeScope['float'])) {
+ $this->rules[] = new ValidationRule($this->typeScope['float'], 'double');
+ }
+ if (!empty($this->typeScope['bool'])) {
+ $this->rules[] = new ValidationRule($this->typeScope['bool'], 'boolean');
+ }
+ if (!empty($this->typeScope['safe'])) {
+ $this->rules[] = new ValidationRule($this->typeScope['safe'], 'safe');
+ }
+ }
+}
diff --git a/src/lib/items/Attribute.php b/src/lib/items/Attribute.php
new file mode 100644
index 00000000..6d50b14e
--- /dev/null
+++ b/src/lib/items/Attribute.php
@@ -0,0 +1,268 @@
+ and contributors
+ * @license https://github.com/cebe/yii2-openapi/blob/master/LICENSE
+ */
+
+namespace cebe\yii2openapi\lib\items;
+
+use yii\base\BaseObject;
+use yii\db\ColumnSchema;
+use yii\helpers\Inflector;
+use function is_array;
+use function strtolower;
+
+/**
+ * @property-write mixed $default
+ * @property-write bool $isPrimary
+ * @property-read string $formattedDescription
+ */
+class Attribute extends BaseObject
+{
+ /**
+ * openApi schema property name
+ * @var string
+ */
+ public $propertyName;
+
+ /**
+ * should be string/integer/boolean/float/double
+ * @var string
+ */
+ public $phpType = 'string';
+
+ /**
+ * model/database column name
+ * @var string
+ */
+ public $columnName;
+
+ /**
+ * should be one of \yii\db\Schema types or complete db column definition
+ * @var string
+ */
+ public $dbType = 'string';
+
+ /**
+ * @var string
+ */
+ public $description = '';
+
+ /**
+ * @var bool
+ */
+ public $readOnly = false;
+
+ /**
+ * @var bool
+ */
+ public $required = false;
+
+ /**
+ * related object name, if it exists
+ * @var string
+ */
+ public $reference;
+ /**
+ * @var int|null (db field length)
+ **/
+ public $size;
+
+ public $limits = ['min' => null, 'max' => null, 'minLength' => null];
+
+ /**
+ * @var bool
+ */
+ public $unique = false;
+
+ /**
+ * @var bool
+ */
+ public $primary = false;
+
+ /**
+ * @var mixed
+ */
+ public $defaultValue;
+
+ /**
+ * @var array|null
+ */
+ public $enumValues;
+
+ /**
+ * @var string|null
+ **/
+ public $fakerStub;
+
+ public function __construct(string $propertyName, array $config = [])
+ {
+ $this->propertyName = $propertyName;
+ $this->columnName = $propertyName; // force camel2id ?
+ parent::__construct($config);
+ }
+
+ public function setPhpType(string $phpType):Attribute
+ {
+ $this->phpType = $phpType;
+ return $this;
+ }
+
+ public function setDbType(string $dbType):Attribute
+ {
+ $this->dbType = $dbType;
+ return $this;
+ }
+
+ public function setDescription(string $description):Attribute
+ {
+ $this->description = $description;
+ return $this;
+ }
+
+ public function setReadOnly(bool $readOnly = true):Attribute
+ {
+ $this->readOnly = $readOnly;
+ return $this;
+ }
+
+ public function setRequired(bool $required = true):Attribute
+ {
+ $this->required = $required;
+ return $this;
+ }
+
+ public function setUnique(bool $unique = true):Attribute
+ {
+ $this->unique = $unique;
+ return $this;
+ }
+
+ public function setSize(?int $size):Attribute
+ {
+ $this->size = $size;
+ return $this;
+ }
+
+ public function setDefault($value):Attribute
+ {
+ $this->defaultValue = $value;
+ return $this;
+ }
+
+ public function setEnumValues(array $values):Attribute
+ {
+ $this->enumValues = $values;
+ return $this;
+ }
+
+ /**
+ * @param int|float|null $min
+ * @param int|float|null $max
+ * @param int|null $minLength
+ * @return $this
+ */
+ public function setLimits($min, $max, ?int $minLength):Attribute
+ {
+ $this->limits = ['min' => $min, 'max' => $max, 'minLength' => $minLength];
+ return $this;
+ }
+
+ public function setFakerStub(?string $fakerStub):Attribute
+ {
+ $this->fakerStub = $fakerStub;
+ return $this;
+ }
+
+ public function setIsPrimary(bool $isPrimary = true):Attribute
+ {
+ $this->primary = $isPrimary;
+ return $this;
+ }
+
+
+ public function asReference(string $relatedClass):Attribute
+ {
+ $this->reference = $relatedClass;
+ $this->columnName = $this->propertyName . '_id';
+ return $this;
+ }
+ public function isReadOnly():bool
+ {
+ return $this->readOnly;
+ }
+
+ public function isReference():bool
+ {
+ return $this->reference !== null;
+ }
+
+ public function isUnique():bool
+ {
+ return $this->unique;
+ }
+
+ public function isRequired():bool
+ {
+ return $this->required;
+ }
+
+ public function camelName():string
+ {
+ return Inflector::camelize($this->propertyName);
+ }
+
+ public function getFormattedDescription():string
+ {
+ $comment = $this->columnName.' '.$this->description;
+ $type = $this->phpType;
+ return $type.' $'.str_replace("\n", "\n * ", rtrim($comment));
+ }
+
+ public function toColumnSchema():ColumnSchema
+ {
+ $column = new ColumnSchema([
+ 'name' => $this->columnName,
+ 'phpType'=>$this->phpType,
+ 'dbType' => strtolower($this->dbType),
+ 'type' => $this->dbTypeAbstract($this->dbType),
+ 'allowNull' => !$this->isRequired(),
+ 'size' => $this->size > 0 ? $this->size : null,
+ ]);
+ if ($column->type === 'json') {
+ $column->allowNull = false;
+ }
+ if ($this->defaultValue !== null) {
+ $column->defaultValue = $this->defaultValue;
+ } elseif ($column->allowNull) {
+ //@TODO: Need to discuss
+ $column->defaultValue = null;
+ }
+ if (is_array($this->enumValues)) {
+ $column->enumValues = $this->enumValues;
+ }
+
+ return $column;
+ }
+
+ private function dbTypeAbstract(string $type):string
+ {
+ if (stripos($type, 'int') === 0) {
+ return 'integer';
+ }
+ if (stripos($type, 'string') === 0) {
+ return 'string';
+ }
+ if (stripos($type, 'varchar') === 0) {
+ return 'string';
+ }
+ if (stripos($type, 'json') === 0) {
+ return 'json';
+ }
+ if (stripos($type, 'datetime') === 0) {
+ return 'timestamp';
+ }
+ return $type;
+ }
+}
diff --git a/src/lib/items/AttributeRelation.php b/src/lib/items/AttributeRelation.php
new file mode 100644
index 00000000..83c5a9b3
--- /dev/null
+++ b/src/lib/items/AttributeRelation.php
@@ -0,0 +1,178 @@
+ and contributors
+ * @license https://github.com/cebe/yii2-openapi/blob/master/LICENSE
+ */
+
+namespace cebe\yii2openapi\lib\items;
+
+use yii\helpers\Inflector;
+use function reset;
+
+class AttributeRelation
+{
+ public const HAS_ONE = 'hasOne';
+ public const HAS_MANY = 'hasMany';
+
+ /**
+ * @var string $name
+ **/
+ private $name;
+
+ /**
+ * @var string $tableName
+ **/
+ private $tableName;
+
+ /**
+ * @var string $className
+ **/
+ private $className;
+
+ /**
+ * @var string $method (hasOne/hasMany)
+ **/
+ private $method;
+
+ /**
+ * @var array
+ **/
+ private $link = [];
+
+ /**@var bool */
+ private $selfReference = false;
+
+ public function __construct(
+ string $name,
+ ?string $tableName = null,
+ ?string $className = null,
+ ?string $method = null,
+ array $link = []
+ ) {
+ $this->name = $name;
+ $this->tableName = $tableName;
+ $this->className = $className;
+ $this->method = $method;
+ $this->link = $link;
+ }
+
+ /**
+ * @param string $name
+ * @return AttributeRelation
+ */
+ public function setName(string $name):AttributeRelation
+ {
+ $this->name = $name;
+ return $this;
+ }
+
+ /**
+ * @param string $tableName
+ * @return AttributeRelation
+ */
+ public function setTableName(string $tableName):AttributeRelation
+ {
+ $this->tableName = $tableName;
+ return $this;
+ }
+
+ /**
+ * @param string $className
+ * @return AttributeRelation
+ */
+ public function setClassName(string $className):AttributeRelation
+ {
+ $this->className = $className;
+ return $this;
+ }
+
+ public function asSelfReference():AttributeRelation
+ {
+ $this->selfReference = true;
+ return $this;
+ }
+
+ public function asHasOne(array $link):AttributeRelation
+ {
+ $this->method = self::HAS_ONE;
+ $this->link = $link;
+ return $this;
+ }
+
+ public function asHasMany(array $link):AttributeRelation
+ {
+ $this->method = self::HAS_MANY;
+ $this->link = $link;
+ return $this;
+ }
+
+ public function isHasOne():bool
+ {
+ return $this->method === self::HAS_ONE;
+ }
+
+ public function isSelfReferenced():bool
+ {
+ return $this->selfReference;
+ }
+ /**
+ * @return string
+ */
+ public function getName():string
+ {
+ return $this->name;
+ }
+
+ /**
+ * @return string
+ */
+ public function getTableName():string
+ {
+ return $this->tableName;
+ }
+
+ public function getTableAlias():string
+ {
+ return "{{%$this->tableName}}";
+ }
+
+ /**
+ * @return string
+ */
+ public function getClassName():string
+ {
+ return $this->className;
+ }
+
+ /**
+ * @return string
+ */
+ public function getMethod():string
+ {
+ return $this->method;
+ }
+
+ /**
+ * @return array
+ */
+ public function getLink():array
+ {
+ return $this->link;
+ }
+
+ public function getCamelName():string
+ {
+ return Inflector::camelize($this->name);
+ }
+
+ public function getColumnName():string
+ {
+ return reset($this->link);
+ }
+
+ public function getForeignName():string
+ {
+ return key($this->link);
+ }
+}
diff --git a/src/lib/items/DbModel.php b/src/lib/items/DbModel.php
new file mode 100644
index 00000000..d2d844e4
--- /dev/null
+++ b/src/lib/items/DbModel.php
@@ -0,0 +1,110 @@
+ and contributors
+ * @license https://github.com/cebe/yii2-openapi/blob/master/LICENSE
+ */
+
+namespace cebe\yii2openapi\lib\items;
+
+use cebe\yii2openapi\lib\ValidationRulesBuilder;
+use yii\base\BaseObject;
+use yii\db\ColumnSchema;
+use yii\helpers\StringHelper;
+use function array_filter;
+
+/**
+ * @property-read string $tableAlias
+ * @property-read array $uniqueColumnsList
+ * @property-read array[]|array $attributesByType
+ * @property-read array|\cebe\yii2openapi\lib\items\AttributeRelation[] $hasOneRelations
+ */
+class DbModel extends BaseObject
+{
+ /**
+ * @var string model name.
+ */
+ public $name;
+
+ /**
+ * @var string table name. (without brackets and db prefix)
+ */
+ public $tableName;
+
+ /**
+ * @var string description from the schema.
+ */
+ public $description;
+
+ /**
+ * @var array|\cebe\yii2openapi\lib\items\Attribute[] model attributes.
+ */
+ public $attributes = [];
+
+ /**
+ * @var array|\cebe\yii2openapi\lib\items\AttributeRelation[] database relations.
+ */
+ public $relations = [];
+
+ public function getTableAlias():string
+ {
+ return '{{%' . $this->tableName . '}}';
+ }
+
+ public function getValidationRules():array
+ {
+ return (new ValidationRulesBuilder($this))->build();
+ }
+
+ public function getUniqueColumnsList():array
+ {
+ $uniques = [];
+ foreach ($this->attributes as $attribute) {
+ if ($attribute->isUnique()) {
+ $uniques[] = $attribute->columnName;
+ }
+ }
+ return $uniques;
+ }
+
+ /**
+ * @return \cebe\yii2openapi\lib\items\AttributeRelation[]|array
+ */
+ public function getHasOneRelations():array
+ {
+ return array_filter(
+ $this->relations,
+ static function (AttributeRelation $relation) {
+ return $relation->isHasOne();
+ }
+ );
+ }
+
+ /**
+ * @return ColumnSchema[]
+ */
+ public function attributesToColumnSchema():array
+ {
+ return array_reduce(
+ $this->attributes,
+ static function ($acc, Attribute $attribute) {
+ $acc[$attribute->columnName] = $attribute->toColumnSchema();
+ return $acc;
+ },
+ []
+ );
+ }
+
+ /**
+ * @return array|\cebe\yii2openapi\lib\items\Attribute[]
+ */
+ public function getEnumAttributes():array
+ {
+ return array_filter(
+ $this->attributes,
+ function (Attribute $attribute) {
+ return StringHelper::startsWith($attribute->dbType, 'enum') && !empty($attribute->enumValues);
+ }
+ );
+ }
+}
diff --git a/src/lib/items/MigrationModel.php b/src/lib/items/MigrationModel.php
new file mode 100644
index 00000000..d981c2a3
--- /dev/null
+++ b/src/lib/items/MigrationModel.php
@@ -0,0 +1,138 @@
+ and contributors
+ * @license https://github.com/cebe/yii2-openapi/blob/master/LICENSE
+ */
+
+namespace cebe\yii2openapi\lib\items;
+
+use yii\base\BaseObject;
+use yii\helpers\Inflector;
+use function array_push;
+use function array_unshift;
+use function implode;
+use function is_string;
+use const PHP_EOL;
+
+/**
+ * @property-read string $tableAlias
+ * @property-read string $upCodeString
+ * @property-read string $downCodeString
+ * @property-read string $fileClassName
+ */
+class MigrationModel extends BaseObject
+{
+ /**
+ * @var string
+ **/
+ public $fileName;
+
+ /**
+ * @var array
+ **/
+ public $upCodes = [];
+
+ /**
+ * @var array
+ **/
+ public $downCodes = [];
+
+ /**
+ * @var array
+ **/
+ public $dependencies = [];
+
+ /**
+ * @var string
+ **/
+ private $fileClassName = '';
+
+ /**
+ * @var \cebe\yii2openapi\lib\items\DbModel
+ */
+ private $model;
+
+ public function __construct(DbModel $model, bool $isFresh = true, $config = [])
+ {
+ parent::__construct($config);
+ $this->model = $model;
+ $this->fileName = $isFresh
+ ? 'create_table_' . $model->tableName
+ : 'change_table_' . $model->tableName;
+ }
+
+ public function getUpCodeString():string
+ {
+ return !empty($this->upCodes) ? implode(PHP_EOL, $this->upCodes) : '';
+ }
+
+ public function getDownCodeString():string
+ {
+ return !empty($this->downCodes) ? implode(PHP_EOL, $this->downCodes) : '';
+ }
+
+ public function notEmpty():bool
+ {
+ return !empty($this->upCodes) && !empty($this->downCodes);
+ }
+
+ public function getTableAlias():string
+ {
+ return $this->model->tableAlias;
+ }
+
+ public function getFileClassName():string
+ {
+ return $this->fileClassName;
+ }
+
+ public function getDescription():string
+ {
+ return 'Table for '.$this->model->name;
+ }
+
+ public function makeClassNameByTime(int $index, ?string $nameSpace = null, ?string $date = null):string
+ {
+ if ($nameSpace) {
+ $m = sprintf('%s%04d', ($date ?: date('ymdH')), $index);
+ $this->fileClassName = "M{$m}" . Inflector::id2camel($this->fileName, '_');
+ } else {
+ $m = sprintf('%s%04d', ($date ?: date('ymd_H')), $index);
+ $this->fileClassName = "m{$m}_" . $this->fileName;
+ }
+ return $this->fileClassName;
+ }
+
+ /**Add up code, by default at bottom
+ * @param array|string $code
+ * @param bool $toTop
+ * @return $this
+ */
+ public function addUpCode($code, bool $toTop = false):MigrationModel
+ {
+ $code = is_string($code) ? [$code] : $code;
+ if ($toTop === true) {
+ array_unshift($this->upCodes, ...$code);
+ } else {
+ array_push($this->upCodes, ...$code);
+ }
+ return $this;
+ }
+
+ /**add down code, by default to top
+ * @param array|string $code
+ * @param bool $toBottom
+ * @return $this
+ */
+ public function addDownCode($code, bool $toBottom = false):MigrationModel
+ {
+ $code = is_string($code) ? [$code] : $code;
+ if ($toBottom === true) {
+ array_push($this->downCodes, ...$code);
+ } else {
+ array_unshift($this->downCodes, ...$code);
+ }
+ return $this;
+ }
+}
diff --git a/src/lib/items/ValidationRule.php b/src/lib/items/ValidationRule.php
new file mode 100644
index 00000000..f9e2d9af
--- /dev/null
+++ b/src/lib/items/ValidationRule.php
@@ -0,0 +1,97 @@
+ and contributors
+ * @license https://github.com/cebe/yii2-openapi/blob/master/LICENSE
+ */
+
+namespace cebe\yii2openapi\lib\items;
+
+use yii\base\BaseObject;
+use yii\helpers\ArrayHelper;
+use yii\helpers\VarDumper;
+use function gettype;
+use function implode;
+use function is_string;
+use function sprintf;
+
+class ValidationRule extends BaseObject
+{
+ /**@var array * */
+ public $attributes = [];
+
+ /**@var string * */
+ public $validator;
+
+ /**@var array * */
+ public $params = [];
+
+ public function __construct(array $attributes, string $validator, array $params = [], $config = [])
+ {
+ $this->attributes = array_values($attributes);
+ $this->validator = $validator;
+ $this->params = $params;
+ parent::__construct($config);
+ }
+
+ /**
+ * @param string $key
+ * @param int|string|array $value
+ * @return $this
+ */
+ public function addParam(string $key, $value):ValidationRule
+ {
+ $this->params[$key] = $value;
+ return $this;
+ }
+
+ public function withParams(array $params):ValidationRule
+ {
+ $this->params = $params;
+ return $this;
+ }
+
+ public function __toString():string
+ {
+ $attrs = implode("', '", $this->attributes);
+ $params = empty($this->params) ? '' : ', ' . $this->arrayToString($this->params);
+ return sprintf(" [['%s'], '%s'%s]", $attrs, $this->validator, $params);
+ }
+
+ private function arrayToString(array $data):string
+ {
+ $params = [];
+ foreach ($data as $key => $val) {
+ $type = gettype($val);
+ switch ($type) {
+ case 'NULL':
+ case 'boolean':
+ $value = VarDumper::export($val);
+ break;
+ case 'integer':
+ case 'float':
+ case 'double':
+ $value = $val;
+ break;
+ case 'array':
+ if (empty($val)) {
+ $value = '[]';
+ } elseif (ArrayHelper::isIndexed($val)) {
+ $value = "['" . implode("', '", $val) . "']";
+ } else {
+ $value = '[' . $this->arrayToString($val) . ']';
+ }
+ break;
+ case 'resource':
+ case 'object':
+ //probably will be resolved later
+ $value = "''";
+ break;
+ default:
+ $value = "'$val'";
+ }
+ $params[] = is_string($key) ? "'$key' => $value" : $value;
+ }
+ return implode(', ', $params);
+ }
+}
diff --git a/tests/DbTestCase.php b/tests/DbTestCase.php
new file mode 100644
index 00000000..4d6ec518
--- /dev/null
+++ b/tests/DbTestCase.php
@@ -0,0 +1,34 @@
+ 'yii2-openapi-test',
+ 'basePath' => __DIR__ . '/tmp/app',
+ 'components'=>[]
+ ], $extendConfig);
+ if($dbMock !== null){
+ $config['components']['db'] = $dbMock;
+ }
+ return new Application($config);
+ }
+
+ protected function mockRealApplication($config = [], $appClass = '\yii\console\Application')
+ {
+ $fileConfig = require __DIR__ . '/config/console.php';
+ new $appClass(ArrayHelper::merge($fileConfig, $config));
+ }
+
+ protected function mockDbSchemaAsEmpty($driver = 'mysql')
+ {
+ $schema = $this->createMock(Schema::class);
+ $schema->method('getTableSchema')->willReturn(null);
+ $schema->method('findUniqueIndexes')->willReturn([]);
+ $schema->method('quoteValue')->willReturnCallback(function($v){ return "'$v'";});
+ $db = $this->createMock(Connection::class);
+ $db->method('getSchema')->willReturn($schema);
+ $db->method('getTableSchema')->willReturn(null);
+ $db->method('getDriverName')->willReturn($driver);
+ return $db;
+ }
+}
\ No newline at end of file
diff --git a/tests/bootstrap.php b/tests/bootstrap.php
index 99935ed2..4ba19919 100644
--- a/tests/bootstrap.php
+++ b/tests/bootstrap.php
@@ -1,6 +1,9 @@
'cebe/yii2-openapi',
+ 'timeZone' => 'UTC',
+ 'basePath' => dirname(__DIR__) . '/tmp/docker_app',
+ 'runtimePath' => dirname(__DIR__) . '/tmp',
+ 'vendorPath' => dirname(__DIR__, 2) . '/vendor',
+ 'aliases' => [
+ '@bower' => '@vendor/bower-asset',
+ '@npm' => '@vendor/npm-asset',
+ ],
+ //'bootstrap'=>['log'],
+ 'controllerMap' => [
+ 'migrate' => [
+ 'class' => \yii\console\controllers\MigrateController::class,
+ 'migrationPath' => dirname(__DIR__).'/migrations',
+ ],
+ ],
+ 'components' => [
+ 'pgsql' => [
+ 'class' => \yii\db\Connection::class,
+ 'dsn' => 'pgsql:host=postgres;dbname=testdb',
+ 'username' => 'dbuser',
+ 'password' => 'dbpass',
+ 'charset' => 'utf8',
+ 'tablePrefix'=>'itt_',
+ ],
+ 'mysql' => [
+ 'class' => \yii\db\Connection::class,
+ 'dsn' => 'mysql:host=mysql;dbname=testdb',
+ 'username' => 'dbuser',
+ 'password' => 'dbpass',
+ 'charset' => 'utf8',
+ 'tablePrefix'=>'itt_',
+ ],
+ 'maria' => [
+ 'class' => \yii\db\Connection::class,
+ 'dsn' => 'mysql:host=maria;dbname=testdb',
+ 'username' => 'dbuser',
+ 'password' => 'dbpass',
+ 'charset' => 'utf8',
+ 'tablePrefix'=>'itt_',
+ ],
+ 'db'=>[
+ 'class' => \yii\db\Connection::class,
+ 'dsn' => 'mysql:host=mysql;dbname=testdb',
+ 'username' => 'dbuser',
+ 'password' => 'dbpass',
+ 'charset' => 'utf8',
+ 'tablePrefix'=>'itt_',
+ ],
+ ],
+];
diff --git a/tests/docker/Dockerfile b/tests/docker/Dockerfile
new file mode 100644
index 00000000..52955801
--- /dev/null
+++ b/tests/docker/Dockerfile
@@ -0,0 +1,59 @@
+FROM php:7.1-cli
+
+ENV DEBIAN_FRONTEND=noninteractive
+
+RUN echo "force-unsafe-io" > /etc/dpkg/dpkg.cfg.d/02apt-speedup && \
+ echo "Acquire::http {No-Cache=True;};" > /etc/apt/apt.conf.d/no-cache
+RUN apt-get update && \
+ apt-get -y install \
+ gnupg2 && \
+ apt-key update && \
+ apt-get update && \
+ apt-get install -y --no-install-recommends \
+ imagemagick \
+ libmagickwand-dev libmagickcore-dev \
+ libfreetype6-dev \
+ libjpeg62-turbo-dev \
+ libpng-dev \
+ libwebp-dev \
+ libicu-dev \
+ libzip-dev \
+ libpq-dev \
+ nano \
+ git \
+ unzip\
+ libxml2-dev \
+ curl \
+ libcurl4-openssl-dev \
+ libssl-dev \
+ --no-install-recommends && \
+ apt-get clean && \
+ rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* \
+ && docker-php-ext-install \
+ zip \
+ curl \
+ bcmath \
+ exif \
+ gd \
+ iconv \
+ intl \
+ opcache \
+ pdo_mysql \
+ pdo_pgsql \
+ mbstring
+
+# Install composer
+ENV COMPOSER_ALLOW_SUPERUSER=1 \
+ PHP_USER_ID=33 \
+ PHP_ENABLE_XDEBUG=0 \
+ COMPOSER_HOME=/root/.composer/ \
+ PATH=/app:/app/vendor/bin:/root/.composer/vendor/bin:$PATH
+
+RUN curl -o /tmp/composer-setup.php https://getcomposer.org/installer \
+&& curl -o /tmp/composer-setup.sig https://composer.github.io/installer.sig \
+# Make sure we're installing what we think we're installing!
+&& php -r "if (hash('SHA384', file_get_contents('/tmp/composer-setup.php')) !== trim(file_get_contents('/tmp/composer-setup.sig'))) { unlink('/tmp/composer-setup.php'); echo 'Invalid installer' . PHP_EOL; exit(1); }" \
+&& php /tmp/composer-setup.php --no-ansi --install-dir=/usr/local/bin --filename=composer \
+&& rm -f /tmp/composer-setup.*
+
+WORKDIR /app
diff --git a/tests/fixtures/blog.php b/tests/fixtures/blog.php
new file mode 100644
index 00000000..3de13344
--- /dev/null
+++ b/tests/fixtures/blog.php
@@ -0,0 +1,111 @@
+ new DbModel([
+ 'name' => 'User',
+ 'tableName' => 'users',
+ 'description' => 'The User',
+ 'attributes' => [
+ 'id' => (new Attribute('id', ['phpType' => 'int', 'dbType' => 'pk']))
+ ->setReadOnly()->setRequired()->setIsPrimary()->setFakerStub('$uniqueFaker->numberBetween(0, 2147483647)'),
+ 'username' => (new Attribute('username', ['phpType' => 'string', 'dbType' => 'string']))
+ ->setSize(200)->setRequired()->setUnique()->setFakerStub('substr($faker->userName, 0, 200)'),
+ 'email' => (new Attribute('email', ['phpType' => 'string', 'dbType' => 'string']))
+ ->setSize(200)->setUnique()->setRequired()->setFakerStub('substr($faker->safeEmail, 0, 200)'),
+ 'password' => (new Attribute('password', ['phpType' => 'string', 'dbType' => 'string']))
+ ->setRequired()->setFakerStub('$faker->password'),
+ 'role' => (new Attribute('role', ['phpType' => 'string', 'dbType' => 'string']))
+ ->setSize(20)
+ ->setDefault('reader')
+ ->setFakerStub('$faker->randomElement([\'admin\', \'editor\', \'reader\'])'),
+ 'created_at' => (new Attribute('created_at', ['phpType' => 'string', 'dbType' => 'datetime']))
+ ->setDefault('CURRENT_TIMESTAMP')->setFakerStub('$faker->dateTimeThisCentury->format(\'Y-m-d H:i:s\')'),
+ ],
+ 'relations' => [],
+ ]),
+ 'category' => new DbModel([
+ 'name' => 'Category',
+ 'tableName' => 'categories',
+ 'description' => 'Category of posts',
+ 'attributes' => [
+ 'id' => (new Attribute('id', ['phpType' => 'int', 'dbType' => 'pk']))
+ ->setReadOnly()->setRequired()->setIsPrimary()->setFakerStub('$uniqueFaker->numberBetween(0, 2147483647)'),
+ 'title' => (new Attribute('title', ['phpType' => 'string', 'dbType' => 'string']))
+ ->setRequired()->setUnique()->setSize(255)->setFakerStub('substr($faker->sentence, 0, 255)'),
+ 'active' => (new Attribute('active', ['phpType' => 'bool', 'dbType' => 'boolean']))
+ ->setRequired()->setDefault(false)->setFakerStub('$faker->boolean'),
+ ],
+ 'relations' => [
+ 'posts' => new AttributeRelation('posts', 'blog_posts', 'Post', 'hasMany', ['category_id' => 'id']),
+ ],
+ ]),
+ 'post' => new DbModel([
+ 'name' => 'Post',
+ 'tableName' => 'blog_posts',
+ 'description' => 'A blog post (uid used as pk for test purposes)',
+ 'attributes' => [
+ 'uid' => (new Attribute('uid', ['phpType' => 'int', 'dbType' => 'bigpk']))
+ ->setReadOnly()->setRequired()->setIsPrimary()->setFakerStub('$uniqueFaker->numberBetween(0, 2147483647)'),
+ 'title' => (new Attribute('title', ['phpType' => 'string', 'dbType' => 'string']))
+ ->setRequired()->setUnique()->setSize(255)->setFakerStub('substr($faker->sentence, 0, 255)'),
+ 'slug' => (new Attribute('slug', ['phpType' => 'string', 'dbType' => 'string']))
+ ->setUnique()->setSize(200)->setLimits(null, null, 1)->setFakerStub('substr($uniqueFaker->slug, 0, 200)'),
+ 'active' => (new Attribute('active', ['phpType' => 'bool', 'dbType' => 'boolean']))
+ ->setRequired()->setDefault(false)->setFakerStub('$faker->boolean'),
+ 'category' => (new Attribute('category', ['phpType' => 'int', 'dbType' => 'integer']))
+ ->asReference('Category')
+ ->setRequired()
+ ->setDescription('Category of posts')
+ ->setFakerStub('$uniqueFaker->numberBetween(0, 2147483647)'),
+ 'created_at' => (new Attribute('created_at', ['phpType' => 'string', 'dbType' => 'date']))
+ ->setFakerStub('$faker->iso8601'),
+ 'created_by' => (new Attribute('created_by', ['phpType' => 'int', 'dbType' => 'integer']))
+ ->asReference('User')
+ ->setDescription('The User')
+ ->setFakerStub('$uniqueFaker->numberBetween(0, 2147483647)'),
+ ],
+ 'relations' => [
+ 'category' => new AttributeRelation('category',
+ 'categories',
+ 'Category',
+ 'hasOne',
+ ['id' => 'category_id']),
+ 'created_by' => new AttributeRelation('created_by', 'users', 'User', 'hasOne', ['id' => 'created_by_id']),
+ 'comments' => new AttributeRelation('comments', 'post_comments', 'Comment', 'hasMany', ['post_id' => 'uid']),
+ ],
+ ]),
+ 'comment' => new DbModel([
+ 'name' => 'Comment',
+ 'tableName' => 'post_comments',
+ 'description' => '',
+ 'attributes' => [
+ 'id' => (new Attribute('id', ['phpType' => 'int', 'dbType' => 'bigpk']))
+ ->setReadOnly(true)
+ ->setRequired(true)
+ ->setIsPrimary(true)
+ ->setFakerStub('$uniqueFaker->numberBetween(0, 2147483647)'),
+ 'post' => (new Attribute('post', ['phpType' => 'int', 'dbType' => 'bigint']))
+ ->setRequired()
+ ->asReference('Post')
+ ->setDescription('A blog post (uid used as pk for test purposes)')
+ ->setFakerStub('$uniqueFaker->numberBetween(0, 2147483647)'),
+ 'author' => (new Attribute('author', ['phpType' => 'int', 'dbType' => 'integer']))
+ ->setRequired()
+ ->asReference('User')
+ ->setDescription('The User')
+ ->setFakerStub('$uniqueFaker->numberBetween(0, 2147483647)'),
+ 'message' => (new Attribute('message', ['phpType' => 'array', 'dbType' => 'json']))
+ ->setRequired()->setDefault('{}')->setFakerStub('[]'),
+ 'created_at' => (new Attribute('created_at',['phpType' => 'int', 'dbType' => 'integer']))
+ ->setRequired()->setFakerStub('$faker->unixTime'),
+ ],
+ 'relations' => [
+ 'post' => new AttributeRelation('post', 'blog_posts', 'Post', 'hasOne', ['uid' => 'post_id']),
+ 'author' => new AttributeRelation('author', 'users', 'User', 'hasOne', ['id' => 'author_id']),
+ ],
+ ]),
+];
\ No newline at end of file
diff --git a/tests/migrations/m100000_000000_maria.php b/tests/migrations/m100000_000000_maria.php
new file mode 100644
index 00000000..1f0f890d
--- /dev/null
+++ b/tests/migrations/m100000_000000_maria.php
@@ -0,0 +1,113 @@
+db = 'maria';
+ parent::init();
+ }
+
+ public function up()
+ {
+ $this->dropTableIfExists('{{%v2_fakerable}}');
+ $this->dropTableIfExists('{{%v2_comments}}');
+ $this->dropTableIfExists('{{%v2_posts}}');
+ $this->dropTableIfExists('{{%v2_users}}');
+ $this->dropTableIfExists('{{%v2_categories}}');
+
+ $this->createTable('{{%v2_categories}}',
+ [
+ 'id' => $this->primaryKey(),
+ 'title' => $this->string(255)->notNull()->unique(),
+ 'active' => $this->boolean()->notNull()->defaultValue(false),
+ ]);
+ $this->createTable('{{%v2_users}}',
+ [
+ 'id' => $this->primaryKey(),
+ 'username' => $this->string(200)->notNull()->unique(),
+ 'email' => $this->string(200)->notNull()->unique(),
+ 'password' => $this->string()->notNull(),
+ 'role' => $this->string(20)->null()->defaultValue('reader'),
+ 'created_at' => $this->timestamp()->null()->defaultExpression("CURRENT_TIMESTAMP"),
+ ]);
+ $this->createTable('{{%v2_posts}}',
+ [
+ 'uid' => $this->bigPrimaryKey(),
+ 'title' => $this->string(255)->notNull()->unique(),
+ 'slug' => $this->string(200)->null()->defaultValue(null)->unique(),
+ 'category_id' => $this->integer()->notNull(),
+ 'active' => $this->boolean()->notNull()->defaultValue(false),
+ 'created_at' => $this->date()->null()->defaultValue(null),
+ 'created_by_id' => $this->integer()->null()->defaultValue(null),
+ ]);
+ $this->addForeignKey('fk_v2_posts_category_id_categories_id',
+ '{{%v2_posts}}',
+ 'category_id',
+ '{{%v2_categories}}',
+ 'id');
+ $this->addForeignKey('fk_v2_posts_created_by_id_users_id',
+ '{{%v2_posts}}',
+ 'created_by_id',
+ '{{%v2_users}}',
+ 'id');
+ $this->createTable('{{%v2_comments}}',
+ [
+ 'id' => $this->bigPrimaryKey(),
+ 'post_id' => $this->bigInteger()->notNull(),
+ 'author_id' => $this->integer()->notNull(),
+ 'message' => $this->json()->notNull()->defaultValue('{}'),
+ 'created_at' => $this->integer()->notNull(),
+ ]);
+ $this->addForeignKey('fk_v2_comments_post_id_v2_posts_uid',
+ '{{%v2_comments}}',
+ 'post_id',
+ '{{%v2_posts}}',
+ 'uid');
+ $this->addForeignKey('fk_v2_comments_author_id_v2_users_id',
+ '{{%v2_comments}}',
+ 'author_id',
+ '{{%v2_users}}',
+ 'id');
+ $this->createTable('{{%v2_fakerable}}', [
+ 'id' => $this->bigPrimaryKey(),
+ 'active' => $this->boolean()->null()->defaultValue(null),
+ 'floatval' => $this->float()->null()->defaultValue(null),
+ 'floatval_lim' => $this->float()->null()->defaultValue(null),
+ 'doubleval' => $this->double()->null()->defaultValue(null),
+ 'int_min' => $this->integer()->null()->defaultValue(3),
+ 'int_max' => $this->integer()->null()->defaultValue(null),
+ 'int_minmax' => $this->integer()->null()->defaultValue(null),
+ 'int_created_at' => $this->integer()->null()->defaultValue(null),
+ 'int_simple' => $this->integer()->null()->defaultValue(null),
+ 'str_text' => $this->text()->null()->defaultValue(null),
+ 'str_varchar' => $this->string(100)->null()->defaultValue(null),
+ 'str_date' => $this->date()->null()->defaultValue(null),
+ 'str_datetime' => $this->timestamp()->null()->defaultValue(null),
+ 'str_country' => $this->text()->null()->defaultValue(null),
+ ]);
+ }
+
+ public function down()
+ {
+ $this->dropTable('{{%v2_fakerable}}');
+ $this->dropForeignKey('fk_v2_comments_author_id_v2_users_id', '{{%v2_comments}}');
+ $this->dropForeignKey('fk_v2_comments_post_id_v2_posts_uid', '{{%v2_comments}}');
+ $this->dropTable('{{%v2_comments}}');
+ $this->dropForeignKey('fk_v2_posts_created_by_id_users_id', '{{%v2_posts}}');
+ $this->dropForeignKey('fk_v2_posts_category_id_categories_id', '{{%v2_posts}}');
+ $this->dropTable('{{%v2_posts}}');
+ $this->dropTable('{{%v2_users}}');
+ $this->dropTable('{{%v2_categories}}');
+ }
+
+ private function dropTableIfExists(string $table)
+ {
+ $this->db->createCommand('DROP TABLE IF EXISTS ' . $this->db->quoteTableName($table))
+ ->execute();
+ }
+}
diff --git a/tests/migrations/m100000_000000_mysql.php b/tests/migrations/m100000_000000_mysql.php
new file mode 100644
index 00000000..4b312036
--- /dev/null
+++ b/tests/migrations/m100000_000000_mysql.php
@@ -0,0 +1,112 @@
+db = 'mysql';
+ parent::init();
+ }
+
+ public function up()
+ {
+ $this->dropTableIfExists('{{%v2_fakerable}}');
+ $this->dropTableIfExists('{{%v2_comments}}');
+ $this->dropTableIfExists('{{%v2_posts}}');
+ $this->dropTableIfExists('{{%v2_users}}');
+ $this->dropTableIfExists('{{%v2_categories}}');
+ $this->createTable('{{%v2_categories}}',
+ [
+ 'id' => $this->primaryKey(),
+ 'title' => $this->string(255)->notNull()->unique(),
+ 'active' => $this->boolean()->notNull()->defaultValue(false),
+ ]);
+ $this->createTable('{{%v2_users}}',
+ [
+ 'id' => $this->primaryKey(),
+ 'username' => $this->string(200)->notNull()->unique(),
+ 'email' => $this->string(200)->notNull()->unique(),
+ 'password' => $this->string()->notNull(),
+ 'role' => $this->string(20)->null()->defaultValue('reader'),
+ 'created_at' => $this->timestamp()->null()->defaultExpression("CURRENT_TIMESTAMP"),
+ ]);
+ $this->createTable('{{%v2_posts}}',
+ [
+ 'uid' => $this->bigPrimaryKey(),
+ 'title' => $this->string(255)->notNull()->unique(),
+ 'slug' => $this->string(200)->null()->defaultValue(null)->unique(),
+ 'category_id' => $this->integer()->notNull(),
+ 'active' => $this->boolean()->notNull()->defaultValue(false),
+ 'created_at' => $this->date()->null()->defaultValue(null),
+ 'created_by_id' => $this->integer()->null()->defaultValue(null),
+ ]);
+ $this->addForeignKey('fk_v2_posts_category_id_categories_id',
+ '{{%v2_posts}}',
+ 'category_id',
+ '{{%v2_categories}}',
+ 'id');
+ $this->addForeignKey('fk_v2_posts_created_by_id_users_id',
+ '{{%v2_posts}}',
+ 'created_by_id',
+ '{{%v2_users}}',
+ 'id');
+ $this->createTable('{{%v2_comments}}',
+ [
+ 'id' => $this->bigPrimaryKey(),
+ 'post_id' => $this->bigInteger()->notNull(),
+ 'author_id' => $this->integer()->notNull(),
+ 'message' => $this->json()->notNull(),
+ 'created_at' => $this->integer()->notNull(),
+ ]);
+ $this->addForeignKey('fk_v2_comments_post_id_v2_posts_uid',
+ '{{%v2_comments}}',
+ 'post_id',
+ '{{%v2_posts}}',
+ 'uid');
+ $this->addForeignKey('fk_v2_comments_author_id_v2_users_id',
+ '{{%v2_comments}}',
+ 'author_id',
+ '{{%v2_users}}',
+ 'id');
+ $this->createTable('{{%v2_fakerable}}', [
+ 'id' => $this->bigPrimaryKey(),
+ 'active' => $this->boolean()->null()->defaultValue(null),
+ 'floatval' => $this->float()->null()->defaultValue(null),
+ 'floatval_lim' => $this->float()->null()->defaultValue(null),
+ 'doubleval' => $this->double()->null()->defaultValue(null),
+ 'int_min' => $this->integer()->null()->defaultValue(3),
+ 'int_max' => $this->integer()->null()->defaultValue(null),
+ 'int_minmax' => $this->integer()->null()->defaultValue(null),
+ 'int_created_at' => $this->integer()->null()->defaultValue(null),
+ 'int_simple' => $this->integer()->null()->defaultValue(null),
+ 'str_text' => $this->text()->null(),
+ 'str_varchar' => $this->string(100)->null()->defaultValue(null),
+ 'str_date' => $this->date()->null()->defaultValue(null),
+ 'str_datetime' => $this->timestamp()->null()->defaultValue(null),
+ 'str_country' => $this->text()->null(),
+ ]);
+ }
+
+ public function down()
+ {
+ $this->dropTable('{{%v2_fakerable}}');
+ $this->dropForeignKey('fk_v2_comments_author_id_v2_users_id', '{{%v2_comments}}');
+ $this->dropForeignKey('fk_v2_comments_post_id_v2_posts_uid', '{{%v2_comments}}');
+ $this->dropTable('{{%v2_comments}}');
+ $this->dropForeignKey('fk_v2_posts_created_by_id_users_id', '{{%v2_posts}}');
+ $this->dropForeignKey('fk_v2_posts_category_id_categories_id', '{{%v2_posts}}');
+ $this->dropTable('{{%v2_posts}}');
+ $this->dropTable('{{%v2_users}}');
+ $this->dropTable('{{%v2_categories}}');
+ }
+
+ private function dropTableIfExists(string $table)
+ {
+ $this->db->createCommand('DROP TABLE IF EXISTS ' . $this->db->quoteTableName($table))
+ ->execute();
+ }
+}
diff --git a/tests/migrations/m100000_000000_pgsql.php b/tests/migrations/m100000_000000_pgsql.php
new file mode 100644
index 00000000..f3d0c75c
--- /dev/null
+++ b/tests/migrations/m100000_000000_pgsql.php
@@ -0,0 +1,102 @@
+db = 'pgsql';
+ parent::init();
+ }
+
+ public function safeUp()
+ {
+ $this->createTable('{{%v2_categories}}',
+ [
+ 'id' => $this->primaryKey(),
+ 'title' => $this->string(255)->notNull()->unique(),
+ 'active' => $this->boolean()->notNull()->defaultValue(false),
+ ]);
+ $this->createTable('{{%v2_users}}',
+ [
+ 'id' => $this->primaryKey(),
+ 'username' => $this->string(200)->notNull()->unique(),
+ 'email' => $this->string(200)->notNull()->unique(),
+ 'password' => $this->string()->notNull(),
+ 'role' => $this->string(20)->null()->defaultValue('reader'),
+ 'created_at' => $this->timestamp()->null()->defaultExpression("CURRENT_TIMESTAMP"),
+ ]);
+ $this->createTable('{{%v2_posts}}',
+ [
+ 'uid' => $this->bigPrimaryKey(),
+ 'title' => $this->string(255)->notNull()->unique(),
+ 'slug' => $this->string(200)->null()->defaultValue(null)->unique(),
+ 'category_id' => $this->integer()->notNull(),
+ 'active' => $this->boolean()->notNull()->defaultValue(false),
+ 'created_at' => $this->date()->null()->defaultValue(null),
+ 'created_by_id' => $this->integer()->null()->defaultValue(null),
+ ]);
+ $this->addForeignKey('fk_v2_posts_category_id_categories_id',
+ '{{%v2_posts}}',
+ 'category_id',
+ '{{%v2_categories}}',
+ 'id');
+ $this->addForeignKey('fk_v2_posts_created_by_id_users_id',
+ '{{%v2_posts}}',
+ 'created_by_id',
+ '{{%v2_users}}',
+ 'id');
+ $this->createTable('{{%v2_comments}}',
+ [
+ 'id' => $this->bigPrimaryKey(),
+ 'post_id' => $this->bigInteger()->notNull(),
+ 'author_id' => $this->integer()->notNull(),
+ 'message' => $this->json()->notNull()->defaultValue('{}'),
+ 'created_at' => $this->integer()->notNull(),
+ ]);
+ $this->addForeignKey('fk_v2_comments_post_id_v2_posts_uid',
+ '{{%v2_comments}}',
+ 'post_id',
+ '{{%v2_posts}}',
+ 'uid');
+ $this->addForeignKey('fk_v2_comments_author_id_v2_users_id',
+ '{{%v2_comments}}',
+ 'author_id',
+ '{{%v2_users}}',
+ 'id');
+ $this->createTable('{{%v2_fakerable}}', [
+ 'id' => $this->bigPrimaryKey(),
+ 'active' => $this->boolean()->null()->defaultValue(null),
+ 'floatval' => $this->float()->null()->defaultValue(null),
+ 'floatval_lim' => $this->float()->null()->defaultValue(null),
+ 'doubleval' => $this->double()->null()->defaultValue(null),
+ 'int_min' => $this->integer()->null()->defaultValue(3),
+ 'int_max' => $this->integer()->null()->defaultValue(null),
+ 'int_minmax' => $this->integer()->null()->defaultValue(null),
+ 'int_created_at' => $this->integer()->null()->defaultValue(null),
+ 'int_simple' => $this->integer()->null()->defaultValue(null),
+ 'uuid' => 'uuid NULL DEFAULT NULL',
+ 'str_text' => $this->text()->null()->defaultValue(null),
+ 'str_varchar' => $this->string(100)->null()->defaultValue(null),
+ 'str_date' => $this->date()->null()->defaultValue(null),
+ 'str_datetime' => $this->timestamp()->null()->defaultValue(null),
+ 'str_country' => $this->text()->null()->defaultValue(null),
+ ]);
+ }
+
+ public function safeDown()
+ {
+ $this->dropTable('{{%v2_fakerable}}');
+ $this->dropForeignKey('fk_v2_comments_author_id_v2_users_id', '{{%v2_comments}}');
+ $this->dropForeignKey('fk_v2_comments_post_id_v2_posts_uid', '{{%v2_comments}}');
+ $this->dropTable('{{%v2_comments}}');
+ $this->dropForeignKey('fk_v2_posts_created_by_id_users_id', '{{%v2_posts}}');
+ $this->dropForeignKey('fk_v2_posts_category_id_categories_id', '{{%v2_posts}}');
+ $this->dropTable('{{%v2_posts}}');
+ $this->dropTable('{{%v2_users}}');
+ $this->dropTable('{{%v2_categories}}');
+ }
+}
diff --git a/tests/specs/blog.php b/tests/specs/blog.php
new file mode 100644
index 00000000..b767cf1e
--- /dev/null
+++ b/tests/specs/blog.php
@@ -0,0 +1,11 @@
+ '@specs/blog.yaml',
+ 'generateUrls' => false,
+ 'generateControllers' => false,
+ 'generateModels' => true,
+ 'generateMigrations' => true,
+ 'excludeModels' => [
+ 'Error',
+ ],
+];
\ No newline at end of file
diff --git a/tests/specs/blog.yaml b/tests/specs/blog.yaml
new file mode 100644
index 00000000..74de26fd
--- /dev/null
+++ b/tests/specs/blog.yaml
@@ -0,0 +1,238 @@
+openapi: "3.0.0"
+info:
+ version: 1.0.0
+ title: Blog prototype for test migrations
+ license:
+ name: MIT
+servers:
+ - url: http://blog.dummy.io/v1
+paths:
+ /posts:
+ get:
+ summary: List all posts
+ operationId: listPosts
+ tags:
+ - posts
+ parameters:
+ - name: limit
+ in: query
+ description: How many items to return at one time (max 100)
+ required: false
+ schema:
+ type: integer
+ format: int32
+ responses:
+ '200':
+ description: A paged array of posts
+ headers:
+ x-next:
+ description: A link to the next page of responses
+ schema:
+ type: string
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/Posts"
+ default:
+ description: unexpected error
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/Error"
+components:
+ schemas:
+ User:
+ description: The User
+ required:
+ - id
+ - username
+ - email
+ - password
+ properties:
+ id:
+ type: integer
+ format: int32
+ readOnly: True
+ username:
+ type: string
+ maxLength: 200
+ x-db-unique: 1
+ email:
+ type: string
+ maxLength: 200
+ x-db-unique: 1
+ password:
+ type: string
+ format: password
+ x-db-type: string
+ role:
+ type: string
+ maxLength: 20
+ x-faker: "$faker->randomElement(['admin', 'editor', 'reader'])"
+ default: reader
+ created_at:
+ type: string
+ format: date-time
+ default: CURRENT_TIMESTAMP
+ Users:
+ type: array
+ items:
+ $ref: "#/components/schemas/User"
+ Category:
+ description: Category of posts
+ required:
+ - id
+ - title
+ - active
+ properties:
+ id:
+ type: integer
+ format: int32
+ readOnly: True
+ title:
+ type: string
+ maxLength: 255
+ x-db-unique: true
+ active:
+ type: boolean
+ default: false
+ posts:
+ type: array
+ items:
+ $ref: "#/components/schemas/Post"
+ Categories:
+ type: array
+ items:
+ $ref: "#/components/schemas/Category"
+ Post:
+ x-table: blog_posts
+ x-pk: uid
+ description: A blog post (uid used as pk for test purposes)
+ required:
+ - uid
+ - title
+ - category
+ - author
+ - active
+ properties:
+ uid:
+ type: integer
+ format: int64
+ readOnly: True
+ title:
+ type: string
+ x-db-unique: true
+ maxLength: 255
+ slug:
+ type: string
+ minLength: 1
+ maxLength: 200
+ x-db-unique: true
+ category:
+ $ref: "#/components/schemas/Category"
+ active:
+ type: boolean
+ default: false
+ created_at:
+ type: string
+ format: date
+ created_by:
+ $ref: "#/components/schemas/User"
+ comments:
+ type: array
+ items:
+ $ref: "#/components/schemas/Comment"
+ Posts:
+ type: array
+ items:
+ $ref: "#/components/schemas/Post"
+ Comment:
+ x-table: post_comments
+ required:
+ - id
+ - post
+ - author
+ - message
+ - created_at
+ properties:
+ id:
+ type: integer
+ format: int64
+ readOnly: True
+ post:
+ $ref: "#/components/schemas/Post"
+ author:
+ $ref: "#/components/schemas/User"
+ message:
+ type: string
+ x-db-type: json
+ default: '{}'
+ created_at:
+ type: integer
+ format: int32
+ Comments:
+ type: array
+ items:
+ $ref: "#/components/schemas/Comment"
+ Fakerable:
+ x-table: fakerable
+ properties:
+ id:
+ type: integer
+ format: int64
+ readOnly: True
+ active:
+ type: boolean
+ floatval:
+ type: number
+ format: float
+ floatval_lim:
+ type: number
+ format: float
+ minimum: 0
+ maximum: 1
+ doubleval:
+ type: number
+ format: double
+ int_min:
+ type: integer
+ minimum: 5
+ default: 3
+ int_max:
+ type: integer
+ maximum: 5
+ int_minmax:
+ type: integer
+ minimum: 5
+ maximum: 25
+ int_created_at:
+ type: integer
+ int_simple:
+ type: integer
+ uuid:
+ type: string
+ x-db-type: UUID
+ x-faker: '$faker->uuid'
+ str_text:
+ type: string
+ str_varchar:
+ type: string
+ maxLength: 100
+ str_date:
+ type: string
+ format: date
+ str_datetime:
+ type: string
+ format: date-time
+ str_country:
+ type: string
+ Error:
+ required:
+ - code
+ - message
+ properties:
+ code:
+ type: integer
+ format: int32
+ message:
+ type: string
diff --git a/tests/specs/blog/migrations/m200000_000000_create_table_categories.php b/tests/specs/blog/migrations/m200000_000000_create_table_categories.php
new file mode 100644
index 00000000..2d9ac79f
--- /dev/null
+++ b/tests/specs/blog/migrations/m200000_000000_create_table_categories.php
@@ -0,0 +1,21 @@
+createTable('{{%categories}}', [
+ 'id' => $this->primaryKey(),
+ 'title' => $this->string(255)->notNull()->unique(),
+ 'active' => $this->boolean()->notNull()->defaultValue(false),
+ ]);
+ }
+
+ public function down()
+ {
+ $this->dropTable('{{%categories}}');
+ }
+}
diff --git a/tests/specs/blog/migrations/m200000_000001_create_table_users.php b/tests/specs/blog/migrations/m200000_000001_create_table_users.php
new file mode 100644
index 00000000..f395e8c4
--- /dev/null
+++ b/tests/specs/blog/migrations/m200000_000001_create_table_users.php
@@ -0,0 +1,24 @@
+createTable('{{%users}}', [
+ 'id' => $this->primaryKey(),
+ 'username' => $this->string(200)->notNull()->unique(),
+ 'email' => $this->string(200)->notNull()->unique(),
+ 'password' => $this->string()->notNull(),
+ 'role' => $this->string(20)->null()->defaultValue("reader"),
+ 'created_at' => $this->timestamp()->null()->defaultExpression("CURRENT_TIMESTAMP"),
+ ]);
+ }
+
+ public function down()
+ {
+ $this->dropTable('{{%users}}');
+ }
+}
diff --git a/tests/specs/blog/migrations/m200000_000002_create_table_blog_posts.php b/tests/specs/blog/migrations/m200000_000002_create_table_blog_posts.php
new file mode 100644
index 00000000..0e1284d0
--- /dev/null
+++ b/tests/specs/blog/migrations/m200000_000002_create_table_blog_posts.php
@@ -0,0 +1,29 @@
+createTable('{{%blog_posts}}', [
+ 'uid' => $this->bigPrimaryKey(),
+ 'title' => $this->string(255)->notNull()->unique(),
+ 'slug' => $this->string(200)->null()->defaultValue(null)->unique(),
+ 'category_id' => $this->integer()->notNull(),
+ 'active' => $this->boolean()->notNull()->defaultValue(false),
+ 'created_at' => $this->date()->null()->defaultValue(null),
+ 'created_by_id' => $this->integer()->null()->defaultValue(null),
+ ]);
+ $this->addForeignKey('fk_blog_posts_category_id_categories_id', '{{%blog_posts}}', 'category_id', '{{%categories}}', 'id');
+ $this->addForeignKey('fk_blog_posts_created_by_id_users_id', '{{%blog_posts}}', 'created_by_id', '{{%users}}', 'id');
+ }
+
+ public function down()
+ {
+ $this->dropForeignKey('fk_blog_posts_created_by_id_users_id', '{{%blog_posts}}');
+ $this->dropForeignKey('fk_blog_posts_category_id_categories_id', '{{%blog_posts}}');
+ $this->dropTable('{{%blog_posts}}');
+ }
+}
diff --git a/tests/specs/blog/migrations/m200000_000003_create_table_fakerable.php b/tests/specs/blog/migrations/m200000_000003_create_table_fakerable.php
new file mode 100644
index 00000000..b86b5181
--- /dev/null
+++ b/tests/specs/blog/migrations/m200000_000003_create_table_fakerable.php
@@ -0,0 +1,34 @@
+createTable('{{%fakerable}}', [
+ 'id' => $this->bigPrimaryKey(),
+ 'active' => $this->boolean()->null()->defaultValue(null),
+ 'floatval' => $this->float()->null()->defaultValue(null),
+ 'floatval_lim' => $this->float()->null()->defaultValue(null),
+ 'doubleval' => $this->double()->null()->defaultValue(null),
+ 'int_min' => $this->integer()->null()->defaultValue(3),
+ 'int_max' => $this->integer()->null()->defaultValue(null),
+ 'int_minmax' => $this->integer()->null()->defaultValue(null),
+ 'int_created_at' => $this->integer()->null()->defaultValue(null),
+ 'int_simple' => $this->integer()->null()->defaultValue(null),
+ 'uuid' => 'uuid NULL DEFAULT NULL',
+ 'str_text' => $this->text()->null()->defaultValue(null),
+ 'str_varchar' => $this->string(100)->null()->defaultValue(null),
+ 'str_date' => $this->date()->null()->defaultValue(null),
+ 'str_datetime' => $this->timestamp()->null()->defaultValue(null),
+ 'str_country' => $this->text()->null()->defaultValue(null),
+ ]);
+ }
+
+ public function down()
+ {
+ $this->dropTable('{{%fakerable}}');
+ }
+}
diff --git a/tests/specs/blog/migrations/m200000_000004_create_table_post_comments.php b/tests/specs/blog/migrations/m200000_000004_create_table_post_comments.php
new file mode 100644
index 00000000..7ed94036
--- /dev/null
+++ b/tests/specs/blog/migrations/m200000_000004_create_table_post_comments.php
@@ -0,0 +1,27 @@
+createTable('{{%post_comments}}', [
+ 'id' => $this->bigPrimaryKey(),
+ 'post_id' => $this->bigInteger()->notNull(),
+ 'author_id' => $this->integer()->notNull(),
+ 'message' => 'json NOT NULL DEFAULT \'{}\'',
+ 'created_at' => $this->integer()->notNull(),
+ ]);
+ $this->addForeignKey('fk_post_comments_post_id_blog_posts_uid', '{{%post_comments}}', 'post_id', '{{%blog_posts}}', 'uid');
+ $this->addForeignKey('fk_post_comments_author_id_users_id', '{{%post_comments}}', 'author_id', '{{%users}}', 'id');
+ }
+
+ public function down()
+ {
+ $this->dropForeignKey('fk_post_comments_author_id_users_id', '{{%post_comments}}');
+ $this->dropForeignKey('fk_post_comments_post_id_blog_posts_uid', '{{%post_comments}}');
+ $this->dropTable('{{%post_comments}}');
+ }
+}
diff --git a/tests/specs/blog/migrations_maria_db/m200000_000000_create_table_categories.php b/tests/specs/blog/migrations_maria_db/m200000_000000_create_table_categories.php
new file mode 100644
index 00000000..2d9ac79f
--- /dev/null
+++ b/tests/specs/blog/migrations_maria_db/m200000_000000_create_table_categories.php
@@ -0,0 +1,21 @@
+createTable('{{%categories}}', [
+ 'id' => $this->primaryKey(),
+ 'title' => $this->string(255)->notNull()->unique(),
+ 'active' => $this->boolean()->notNull()->defaultValue(false),
+ ]);
+ }
+
+ public function down()
+ {
+ $this->dropTable('{{%categories}}');
+ }
+}
diff --git a/tests/specs/blog/migrations_maria_db/m200000_000001_create_table_users.php b/tests/specs/blog/migrations_maria_db/m200000_000001_create_table_users.php
new file mode 100644
index 00000000..f395e8c4
--- /dev/null
+++ b/tests/specs/blog/migrations_maria_db/m200000_000001_create_table_users.php
@@ -0,0 +1,24 @@
+createTable('{{%users}}', [
+ 'id' => $this->primaryKey(),
+ 'username' => $this->string(200)->notNull()->unique(),
+ 'email' => $this->string(200)->notNull()->unique(),
+ 'password' => $this->string()->notNull(),
+ 'role' => $this->string(20)->null()->defaultValue("reader"),
+ 'created_at' => $this->timestamp()->null()->defaultExpression("CURRENT_TIMESTAMP"),
+ ]);
+ }
+
+ public function down()
+ {
+ $this->dropTable('{{%users}}');
+ }
+}
diff --git a/tests/specs/blog/migrations_maria_db/m200000_000002_create_table_blog_posts.php b/tests/specs/blog/migrations_maria_db/m200000_000002_create_table_blog_posts.php
new file mode 100644
index 00000000..0e1284d0
--- /dev/null
+++ b/tests/specs/blog/migrations_maria_db/m200000_000002_create_table_blog_posts.php
@@ -0,0 +1,29 @@
+createTable('{{%blog_posts}}', [
+ 'uid' => $this->bigPrimaryKey(),
+ 'title' => $this->string(255)->notNull()->unique(),
+ 'slug' => $this->string(200)->null()->defaultValue(null)->unique(),
+ 'category_id' => $this->integer()->notNull(),
+ 'active' => $this->boolean()->notNull()->defaultValue(false),
+ 'created_at' => $this->date()->null()->defaultValue(null),
+ 'created_by_id' => $this->integer()->null()->defaultValue(null),
+ ]);
+ $this->addForeignKey('fk_blog_posts_category_id_categories_id', '{{%blog_posts}}', 'category_id', '{{%categories}}', 'id');
+ $this->addForeignKey('fk_blog_posts_created_by_id_users_id', '{{%blog_posts}}', 'created_by_id', '{{%users}}', 'id');
+ }
+
+ public function down()
+ {
+ $this->dropForeignKey('fk_blog_posts_created_by_id_users_id', '{{%blog_posts}}');
+ $this->dropForeignKey('fk_blog_posts_category_id_categories_id', '{{%blog_posts}}');
+ $this->dropTable('{{%blog_posts}}');
+ }
+}
diff --git a/tests/specs/blog/migrations_maria_db/m200000_000003_create_table_fakerable.php b/tests/specs/blog/migrations_maria_db/m200000_000003_create_table_fakerable.php
new file mode 100644
index 00000000..b86b5181
--- /dev/null
+++ b/tests/specs/blog/migrations_maria_db/m200000_000003_create_table_fakerable.php
@@ -0,0 +1,34 @@
+createTable('{{%fakerable}}', [
+ 'id' => $this->bigPrimaryKey(),
+ 'active' => $this->boolean()->null()->defaultValue(null),
+ 'floatval' => $this->float()->null()->defaultValue(null),
+ 'floatval_lim' => $this->float()->null()->defaultValue(null),
+ 'doubleval' => $this->double()->null()->defaultValue(null),
+ 'int_min' => $this->integer()->null()->defaultValue(3),
+ 'int_max' => $this->integer()->null()->defaultValue(null),
+ 'int_minmax' => $this->integer()->null()->defaultValue(null),
+ 'int_created_at' => $this->integer()->null()->defaultValue(null),
+ 'int_simple' => $this->integer()->null()->defaultValue(null),
+ 'uuid' => 'uuid NULL DEFAULT NULL',
+ 'str_text' => $this->text()->null()->defaultValue(null),
+ 'str_varchar' => $this->string(100)->null()->defaultValue(null),
+ 'str_date' => $this->date()->null()->defaultValue(null),
+ 'str_datetime' => $this->timestamp()->null()->defaultValue(null),
+ 'str_country' => $this->text()->null()->defaultValue(null),
+ ]);
+ }
+
+ public function down()
+ {
+ $this->dropTable('{{%fakerable}}');
+ }
+}
diff --git a/tests/specs/blog/migrations_maria_db/m200000_000004_create_table_post_comments.php b/tests/specs/blog/migrations_maria_db/m200000_000004_create_table_post_comments.php
new file mode 100644
index 00000000..7ed94036
--- /dev/null
+++ b/tests/specs/blog/migrations_maria_db/m200000_000004_create_table_post_comments.php
@@ -0,0 +1,27 @@
+createTable('{{%post_comments}}', [
+ 'id' => $this->bigPrimaryKey(),
+ 'post_id' => $this->bigInteger()->notNull(),
+ 'author_id' => $this->integer()->notNull(),
+ 'message' => 'json NOT NULL DEFAULT \'{}\'',
+ 'created_at' => $this->integer()->notNull(),
+ ]);
+ $this->addForeignKey('fk_post_comments_post_id_blog_posts_uid', '{{%post_comments}}', 'post_id', '{{%blog_posts}}', 'uid');
+ $this->addForeignKey('fk_post_comments_author_id_users_id', '{{%post_comments}}', 'author_id', '{{%users}}', 'id');
+ }
+
+ public function down()
+ {
+ $this->dropForeignKey('fk_post_comments_author_id_users_id', '{{%post_comments}}');
+ $this->dropForeignKey('fk_post_comments_post_id_blog_posts_uid', '{{%post_comments}}');
+ $this->dropTable('{{%post_comments}}');
+ }
+}
diff --git a/tests/specs/blog/migrations_mysql_db/m200000_000000_create_table_categories.php b/tests/specs/blog/migrations_mysql_db/m200000_000000_create_table_categories.php
new file mode 100644
index 00000000..2d9ac79f
--- /dev/null
+++ b/tests/specs/blog/migrations_mysql_db/m200000_000000_create_table_categories.php
@@ -0,0 +1,21 @@
+createTable('{{%categories}}', [
+ 'id' => $this->primaryKey(),
+ 'title' => $this->string(255)->notNull()->unique(),
+ 'active' => $this->boolean()->notNull()->defaultValue(false),
+ ]);
+ }
+
+ public function down()
+ {
+ $this->dropTable('{{%categories}}');
+ }
+}
diff --git a/tests/specs/blog/migrations_mysql_db/m200000_000001_create_table_users.php b/tests/specs/blog/migrations_mysql_db/m200000_000001_create_table_users.php
new file mode 100644
index 00000000..f395e8c4
--- /dev/null
+++ b/tests/specs/blog/migrations_mysql_db/m200000_000001_create_table_users.php
@@ -0,0 +1,24 @@
+createTable('{{%users}}', [
+ 'id' => $this->primaryKey(),
+ 'username' => $this->string(200)->notNull()->unique(),
+ 'email' => $this->string(200)->notNull()->unique(),
+ 'password' => $this->string()->notNull(),
+ 'role' => $this->string(20)->null()->defaultValue("reader"),
+ 'created_at' => $this->timestamp()->null()->defaultExpression("CURRENT_TIMESTAMP"),
+ ]);
+ }
+
+ public function down()
+ {
+ $this->dropTable('{{%users}}');
+ }
+}
diff --git a/tests/specs/blog/migrations_mysql_db/m200000_000002_create_table_blog_posts.php b/tests/specs/blog/migrations_mysql_db/m200000_000002_create_table_blog_posts.php
new file mode 100644
index 00000000..0e1284d0
--- /dev/null
+++ b/tests/specs/blog/migrations_mysql_db/m200000_000002_create_table_blog_posts.php
@@ -0,0 +1,29 @@
+createTable('{{%blog_posts}}', [
+ 'uid' => $this->bigPrimaryKey(),
+ 'title' => $this->string(255)->notNull()->unique(),
+ 'slug' => $this->string(200)->null()->defaultValue(null)->unique(),
+ 'category_id' => $this->integer()->notNull(),
+ 'active' => $this->boolean()->notNull()->defaultValue(false),
+ 'created_at' => $this->date()->null()->defaultValue(null),
+ 'created_by_id' => $this->integer()->null()->defaultValue(null),
+ ]);
+ $this->addForeignKey('fk_blog_posts_category_id_categories_id', '{{%blog_posts}}', 'category_id', '{{%categories}}', 'id');
+ $this->addForeignKey('fk_blog_posts_created_by_id_users_id', '{{%blog_posts}}', 'created_by_id', '{{%users}}', 'id');
+ }
+
+ public function down()
+ {
+ $this->dropForeignKey('fk_blog_posts_created_by_id_users_id', '{{%blog_posts}}');
+ $this->dropForeignKey('fk_blog_posts_category_id_categories_id', '{{%blog_posts}}');
+ $this->dropTable('{{%blog_posts}}');
+ }
+}
diff --git a/tests/specs/blog/migrations_mysql_db/m200000_000003_create_table_fakerable.php b/tests/specs/blog/migrations_mysql_db/m200000_000003_create_table_fakerable.php
new file mode 100644
index 00000000..31485c92
--- /dev/null
+++ b/tests/specs/blog/migrations_mysql_db/m200000_000003_create_table_fakerable.php
@@ -0,0 +1,34 @@
+createTable('{{%fakerable}}', [
+ 'id' => $this->bigPrimaryKey(),
+ 'active' => $this->boolean()->null()->defaultValue(null),
+ 'floatval' => $this->float()->null()->defaultValue(null),
+ 'floatval_lim' => $this->float()->null()->defaultValue(null),
+ 'doubleval' => $this->double()->null()->defaultValue(null),
+ 'int_min' => $this->integer()->null()->defaultValue(3),
+ 'int_max' => $this->integer()->null()->defaultValue(null),
+ 'int_minmax' => $this->integer()->null()->defaultValue(null),
+ 'int_created_at' => $this->integer()->null()->defaultValue(null),
+ 'int_simple' => $this->integer()->null()->defaultValue(null),
+ 'uuid' => 'uuid NULL DEFAULT NULL',
+ 'str_text' => $this->text()->null(),
+ 'str_varchar' => $this->string(100)->null()->defaultValue(null),
+ 'str_date' => $this->date()->null()->defaultValue(null),
+ 'str_datetime' => $this->timestamp()->null()->defaultValue(null),
+ 'str_country' => $this->text()->null(),
+ ]);
+ }
+
+ public function down()
+ {
+ $this->dropTable('{{%fakerable}}');
+ }
+}
diff --git a/tests/specs/blog/migrations_mysql_db/m200000_000004_create_table_post_comments.php b/tests/specs/blog/migrations_mysql_db/m200000_000004_create_table_post_comments.php
new file mode 100644
index 00000000..635ad043
--- /dev/null
+++ b/tests/specs/blog/migrations_mysql_db/m200000_000004_create_table_post_comments.php
@@ -0,0 +1,27 @@
+createTable('{{%post_comments}}', [
+ 'id' => $this->bigPrimaryKey(),
+ 'post_id' => $this->bigInteger()->notNull(),
+ 'author_id' => $this->integer()->notNull(),
+ 'message' => 'json NOT NULL',
+ 'created_at' => $this->integer()->notNull(),
+ ]);
+ $this->addForeignKey('fk_post_comments_post_id_blog_posts_uid', '{{%post_comments}}', 'post_id', '{{%blog_posts}}', 'uid');
+ $this->addForeignKey('fk_post_comments_author_id_users_id', '{{%post_comments}}', 'author_id', '{{%users}}', 'id');
+ }
+
+ public function down()
+ {
+ $this->dropForeignKey('fk_post_comments_author_id_users_id', '{{%post_comments}}');
+ $this->dropForeignKey('fk_post_comments_post_id_blog_posts_uid', '{{%post_comments}}');
+ $this->dropTable('{{%post_comments}}');
+ }
+}
diff --git a/tests/specs/blog/migrations_pgsql_db/m200000_000000_create_table_categories.php b/tests/specs/blog/migrations_pgsql_db/m200000_000000_create_table_categories.php
new file mode 100644
index 00000000..76d3c5fe
--- /dev/null
+++ b/tests/specs/blog/migrations_pgsql_db/m200000_000000_create_table_categories.php
@@ -0,0 +1,21 @@
+createTable('{{%categories}}', [
+ 'id' => $this->primaryKey(),
+ 'title' => $this->string(255)->notNull()->unique(),
+ 'active' => $this->boolean()->notNull()->defaultValue(false),
+ ]);
+ }
+
+ public function safeDown()
+ {
+ $this->dropTable('{{%categories}}');
+ }
+}
diff --git a/tests/specs/blog/migrations_pgsql_db/m200000_000001_create_table_users.php b/tests/specs/blog/migrations_pgsql_db/m200000_000001_create_table_users.php
new file mode 100644
index 00000000..499ad78c
--- /dev/null
+++ b/tests/specs/blog/migrations_pgsql_db/m200000_000001_create_table_users.php
@@ -0,0 +1,24 @@
+createTable('{{%users}}', [
+ 'id' => $this->primaryKey(),
+ 'username' => $this->string(200)->notNull()->unique(),
+ 'email' => $this->string(200)->notNull()->unique(),
+ 'password' => $this->string()->notNull(),
+ 'role' => $this->string(20)->null()->defaultValue("reader"),
+ 'created_at' => $this->timestamp()->null()->defaultExpression("CURRENT_TIMESTAMP"),
+ ]);
+ }
+
+ public function safeDown()
+ {
+ $this->dropTable('{{%users}}');
+ }
+}
diff --git a/tests/specs/blog/migrations_pgsql_db/m200000_000002_create_table_blog_posts.php b/tests/specs/blog/migrations_pgsql_db/m200000_000002_create_table_blog_posts.php
new file mode 100644
index 00000000..eac7bd6f
--- /dev/null
+++ b/tests/specs/blog/migrations_pgsql_db/m200000_000002_create_table_blog_posts.php
@@ -0,0 +1,29 @@
+createTable('{{%blog_posts}}', [
+ 'uid' => $this->bigPrimaryKey(),
+ 'title' => $this->string(255)->notNull()->unique(),
+ 'slug' => $this->string(200)->null()->defaultValue(null)->unique(),
+ 'category_id' => $this->integer()->notNull(),
+ 'active' => $this->boolean()->notNull()->defaultValue(false),
+ 'created_at' => $this->date()->null()->defaultValue(null),
+ 'created_by_id' => $this->integer()->null()->defaultValue(null),
+ ]);
+ $this->addForeignKey('fk_blog_posts_category_id_categories_id', '{{%blog_posts}}', 'category_id', '{{%categories}}', 'id');
+ $this->addForeignKey('fk_blog_posts_created_by_id_users_id', '{{%blog_posts}}', 'created_by_id', '{{%users}}', 'id');
+ }
+
+ public function safeDown()
+ {
+ $this->dropForeignKey('fk_blog_posts_created_by_id_users_id', '{{%blog_posts}}');
+ $this->dropForeignKey('fk_blog_posts_category_id_categories_id', '{{%blog_posts}}');
+ $this->dropTable('{{%blog_posts}}');
+ }
+}
diff --git a/tests/specs/blog/migrations_pgsql_db/m200000_000003_create_table_fakerable.php b/tests/specs/blog/migrations_pgsql_db/m200000_000003_create_table_fakerable.php
new file mode 100644
index 00000000..650fd2c5
--- /dev/null
+++ b/tests/specs/blog/migrations_pgsql_db/m200000_000003_create_table_fakerable.php
@@ -0,0 +1,34 @@
+createTable('{{%fakerable}}', [
+ 'id' => $this->bigPrimaryKey(),
+ 'active' => $this->boolean()->null()->defaultValue(null),
+ 'floatval' => $this->float()->null()->defaultValue(null),
+ 'floatval_lim' => $this->float()->null()->defaultValue(null),
+ 'doubleval' => $this->double()->null()->defaultValue(null),
+ 'int_min' => $this->integer()->null()->defaultValue(3),
+ 'int_max' => $this->integer()->null()->defaultValue(null),
+ 'int_minmax' => $this->integer()->null()->defaultValue(null),
+ 'int_created_at' => $this->integer()->null()->defaultValue(null),
+ 'int_simple' => $this->integer()->null()->defaultValue(null),
+ 'uuid' => 'uuid NULL DEFAULT NULL',
+ 'str_text' => $this->text()->null()->defaultValue(null),
+ 'str_varchar' => $this->string(100)->null()->defaultValue(null),
+ 'str_date' => $this->date()->null()->defaultValue(null),
+ 'str_datetime' => $this->timestamp()->null()->defaultValue(null),
+ 'str_country' => $this->text()->null()->defaultValue(null),
+ ]);
+ }
+
+ public function safeDown()
+ {
+ $this->dropTable('{{%fakerable}}');
+ }
+}
diff --git a/tests/specs/blog/migrations_pgsql_db/m200000_000004_create_table_post_comments.php b/tests/specs/blog/migrations_pgsql_db/m200000_000004_create_table_post_comments.php
new file mode 100644
index 00000000..380a57e6
--- /dev/null
+++ b/tests/specs/blog/migrations_pgsql_db/m200000_000004_create_table_post_comments.php
@@ -0,0 +1,27 @@
+createTable('{{%post_comments}}', [
+ 'id' => $this->bigPrimaryKey(),
+ 'post_id' => $this->bigInteger()->notNull(),
+ 'author_id' => $this->integer()->notNull(),
+ 'message' => 'json NOT NULL DEFAULT \'{}\'',
+ 'created_at' => $this->integer()->notNull(),
+ ]);
+ $this->addForeignKey('fk_post_comments_post_id_blog_posts_uid', '{{%post_comments}}', 'post_id', '{{%blog_posts}}', 'uid');
+ $this->addForeignKey('fk_post_comments_author_id_users_id', '{{%post_comments}}', 'author_id', '{{%users}}', 'id');
+ }
+
+ public function safeDown()
+ {
+ $this->dropForeignKey('fk_post_comments_author_id_users_id', '{{%post_comments}}');
+ $this->dropForeignKey('fk_post_comments_post_id_blog_posts_uid', '{{%post_comments}}');
+ $this->dropTable('{{%post_comments}}');
+ }
+}
diff --git a/tests/specs/blog/models/Category.php b/tests/specs/blog/models/Category.php
new file mode 100644
index 00000000..6e9d01c6
--- /dev/null
+++ b/tests/specs/blog/models/Category.php
@@ -0,0 +1,10 @@
+language);
+ $uniqueFaker = new UniqueGenerator($faker);
+ $model = new Category();
+ $model->id = $uniqueFaker->numberBetween(0, 2147483647);
+ $model->title = substr($faker->sentence, 0, 255);
+ $model->active = $faker->boolean;
+ return $model;
+ }
+}
diff --git a/tests/specs/blog/models/Comment.php b/tests/specs/blog/models/Comment.php
new file mode 100644
index 00000000..7a4886f7
--- /dev/null
+++ b/tests/specs/blog/models/Comment.php
@@ -0,0 +1,10 @@
+language);
+ $uniqueFaker = new UniqueGenerator($faker);
+ $model = new Comment();
+ $model->id = $uniqueFaker->numberBetween(0, 2147483647);
+ $model->message = [];
+ $model->created_at = $faker->unixTime;
+ return $model;
+ }
+}
diff --git a/tests/specs/blog/models/Fakerable.php b/tests/specs/blog/models/Fakerable.php
new file mode 100644
index 00000000..5083a385
--- /dev/null
+++ b/tests/specs/blog/models/Fakerable.php
@@ -0,0 +1,10 @@
+language);
+ $uniqueFaker = new UniqueGenerator($faker);
+ $model = new Fakerable();
+ $model->id = $uniqueFaker->numberBetween(0, 2147483647);
+ $model->active = $faker->boolean;
+ $model->floatval = $faker->randomFloat();
+ $model->floatval_lim = $faker->randomFloat(null, 0, 1);
+ $model->doubleval = $faker->randomFloat();
+ $model->int_min = $faker->numberBetween(5, 2147483647);
+ $model->int_max = $faker->numberBetween(0, 5);
+ $model->int_minmax = $faker->numberBetween(5, 25);
+ $model->int_created_at = $faker->unixTime;
+ $model->int_simple = $faker->numberBetween(0, 2147483647);
+ $model->uuid = $faker->uuid;
+ $model->str_text = $faker->sentence;
+ $model->str_varchar = substr($faker->text(100), 0, 100);
+ $model->str_date = $faker->iso8601;
+ $model->str_datetime = $faker->dateTimeThisCentury->format('Y-m-d H:i:s');
+ $model->str_country = $faker->countryCode;
+ return $model;
+ }
+}
diff --git a/tests/specs/blog/models/Post.php b/tests/specs/blog/models/Post.php
new file mode 100644
index 00000000..2825fe31
--- /dev/null
+++ b/tests/specs/blog/models/Post.php
@@ -0,0 +1,10 @@
+language);
+ $uniqueFaker = new UniqueGenerator($faker);
+ $model = new Post();
+ $model->uid = $uniqueFaker->numberBetween(0, 2147483647);
+ $model->title = substr($faker->sentence, 0, 255);
+ $model->slug = substr($uniqueFaker->slug, 0, 200);
+ $model->active = $faker->boolean;
+ $model->created_at = $faker->iso8601;
+ return $model;
+ }
+}
diff --git a/tests/specs/blog/models/User.php b/tests/specs/blog/models/User.php
new file mode 100644
index 00000000..9b837d6e
--- /dev/null
+++ b/tests/specs/blog/models/User.php
@@ -0,0 +1,10 @@
+language);
+ $uniqueFaker = new UniqueGenerator($faker);
+ $model = new User();
+ $model->id = $uniqueFaker->numberBetween(0, 2147483647);
+ $model->username = substr($faker->userName, 0, 200);
+ $model->email = substr($faker->safeEmail, 0, 200);
+ $model->password = $faker->password;
+ $model->role = $faker->randomElement(['admin', 'editor', 'reader']);
+ $model->created_at = $faker->dateTimeThisCentury->format('Y-m-d H:i:s');
+ return $model;
+ }
+}
diff --git a/tests/specs/blog/models/base/Category.php b/tests/specs/blog/models/base/Category.php
new file mode 100644
index 00000000..aa670e13
--- /dev/null
+++ b/tests/specs/blog/models/base/Category.php
@@ -0,0 +1,35 @@
+hasMany(\app\models\Post::class,['category_id' => 'id']);
+ }
+}
diff --git a/tests/specs/blog/models/base/Comment.php b/tests/specs/blog/models/base/Comment.php
new file mode 100644
index 00000000..77d79f0a
--- /dev/null
+++ b/tests/specs/blog/models/base/Comment.php
@@ -0,0 +1,43 @@
+ 'Post'],
+ [['author_id'], 'exist', 'targetRelation' => 'Author'],
+ [['message'], 'safe'],
+ ];
+ }
+
+ public function getPost()
+ {
+ return $this->hasOne(\app\models\Post::class,['uid' => 'post_id']);
+ }
+ public function getAuthor()
+ {
+ return $this->hasOne(\app\models\User::class,['id' => 'author_id']);
+ }
+}
diff --git a/tests/specs/blog/models/base/Fakerable.php b/tests/specs/blog/models/base/Fakerable.php
new file mode 100644
index 00000000..36171d90
--- /dev/null
+++ b/tests/specs/blog/models/base/Fakerable.php
@@ -0,0 +1,44 @@
+ 'Category'],
+ [['created_by_id'], 'exist', 'targetRelation' => 'CreatedBy'],
+ [['title', 'slug', 'created_at'], 'string'],
+ [['active'], 'boolean'],
+ ];
+ }
+
+ public function getCategory()
+ {
+ return $this->hasOne(\app\models\Category::class,['id' => 'category_id']);
+ }
+ public function getCreatedBy()
+ {
+ return $this->hasOne(\app\models\User::class,['id' => 'created_by_id']);
+ }
+ public function getComments()
+ {
+ return $this->hasMany(\app\models\Comment::class,['post_id' => 'uid']);
+ }
+}
diff --git a/tests/specs/blog/models/base/User.php b/tests/specs/blog/models/base/User.php
new file mode 100644
index 00000000..ae3da1f0
--- /dev/null
+++ b/tests/specs/blog/models/base/User.php
@@ -0,0 +1,32 @@
+ '@specs/blog_v2.yaml',
+ 'generateUrls' => false,
+ 'generateControllers' => false,
+ 'generateModels' => true,
+ 'generateMigrations' => true,
+ 'excludeModels' => [
+ 'Error',
+ ],
+];
\ No newline at end of file
diff --git a/tests/specs/blog_v2.yaml b/tests/specs/blog_v2.yaml
new file mode 100644
index 00000000..b5dfbf4c
--- /dev/null
+++ b/tests/specs/blog_v2.yaml
@@ -0,0 +1,244 @@
+openapi: "3.0.0"
+info:
+ version: 2.0.0
+ title: Blog prototype for test migrations. Modify columns, remove log_records table, added tags, post_tag tables
+ license:
+ name: MIT
+servers:
+ - url: http://blog.dummy.io/v2
+paths:
+ /posts:
+ get:
+ summary: List all posts
+ operationId: listPosts
+ tags:
+ - posts
+ parameters:
+ - name: limit
+ in: query
+ description: How many items to return at one time (max 100)
+ required: false
+ schema:
+ type: integer
+ format: int32
+ responses:
+ '200':
+ description: A paged array of posts
+ headers:
+ x-next:
+ description: A link to the next page of responses
+ schema:
+ type: string
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/Posts"
+ default:
+ description: unexpected error
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/Error"
+components:
+ schemas:
+ User:
+ description: The User
+ x-table: v2_users
+ required:
+ - id
+ - login
+ - email
+ - password
+ properties:
+ id:
+ type: integer
+ format: int64
+ readOnly: True
+ login:
+ type: string
+ x-db-unique: 1
+ email:
+ type: string
+ x-db-unique: 1
+ password:
+ type: string
+ format: password
+ role:
+ type: string
+ x-db-type: enum
+ enum:
+ - admin
+ - editor
+ - reader
+ x-faker: "$faker->randomElement(['admin', 'editor', 'reader'])"
+ created_at:
+ type: string
+ format: date-time
+ Users:
+ type: array
+ items:
+ $ref: "#/components/schemas/User"
+ Category:
+ x-table: v2_categories
+ description: Category of posts
+ required:
+ - id
+ - title
+ - cover
+ - active
+ properties:
+ id:
+ type: integer
+ format: int64
+ readOnly: True
+ title:
+ type: string
+ x-db-unique: true
+ maxLength: 100
+ cover:
+ type: string
+ active:
+ type: boolean
+ posts:
+ type: array
+ items:
+ $ref: "#/components/schemas/Post"
+ Categories:
+ type: array
+ items:
+ $ref: "#/components/schemas/Category"
+ Post:
+ x-table: v2_posts
+ description: A blog post (uid used as pk for test purposes)
+ required:
+ - id
+ - title
+ - category
+ - author
+ - active
+ properties:
+ id:
+ type: integer
+ format: int64
+ readOnly: True
+ title:
+ type: string
+ x-db-type: VARCHAR
+ x-db-unique: true
+ maxLength: 255
+ slug:
+ type: string
+ minLength: 1
+ maxLength: 200
+ lang:
+ type: string
+ x-db-type: enum
+ enum:
+ - ru
+ - eng
+ default: ru
+ category:
+ $ref: "#/components/schemas/Category"
+ active:
+ type: boolean
+ created_at:
+ type: string
+ format: date
+ created_by:
+ $ref: "#/components/schemas/User"
+ comments:
+ type: array
+ items:
+ $ref: "#/components/schemas/Comment"
+ post_tags:
+ type: array
+ items:
+ $ref: "#/components/schemas/PostTag"
+ Posts:
+ type: array
+ items:
+ $ref: "#/components/schemas/Post"
+ Comment:
+ x-table: v2_comments
+ required:
+ - id
+ - post
+ - message
+ - created_at
+ properties:
+ id:
+ type: integer
+ format: int64
+ readOnly: True
+ post:
+ $ref: "#/components/schemas/Post"
+ user:
+ $ref: "#/components/schemas/User"
+ message:
+ type: string
+ created_at:
+ type: string
+ format: date-time
+ Comments:
+ type: array
+ items:
+ $ref: "#/components/schemas/Comment"
+ Tag:
+ x-table: v2_tags
+ required:
+ - id
+ - name
+ - lang
+ properties:
+ id:
+ type: integer
+ format: int64
+ readOnly: True
+ name:
+ type: string
+ x-db-type: VARCHAR
+ x-db-unique: true
+ maxLength: 100
+ lang:
+ type: string
+ x-db-type: enum
+ enum:
+ - ru
+ - eng
+ post_tags:
+ type: array
+ items:
+ $ref: "#/components/schemas/PostTag"
+ Tags:
+ type: array
+ items:
+ $ref: "#/components/schemas/Tag"
+ PostTag:
+ x-table: v2_post_tag
+ required:
+ - id
+ - post
+ - tag
+ properties:
+ id:
+ type: integer
+ format: int64
+ readOnly: True
+ post:
+ $ref: "#/components/schemas/Post"
+ tag:
+ $ref: "#/components/schemas/Tag"
+ PostTags:
+ type: array
+ items:
+ $ref: "#/components/schemas/PostTag"
+ Error:
+ required:
+ - code
+ - message
+ properties:
+ code:
+ type: integer
+ format: int32
+ message:
+ type: string
diff --git a/tests/specs/blog_v2/migrations/m200000_000000_create_table_v2_categories.php b/tests/specs/blog_v2/migrations/m200000_000000_create_table_v2_categories.php
new file mode 100644
index 00000000..6ed093bb
--- /dev/null
+++ b/tests/specs/blog_v2/migrations/m200000_000000_create_table_v2_categories.php
@@ -0,0 +1,22 @@
+createTable('{{%v2_categories}}', [
+ 'id' => $this->bigPrimaryKey(),
+ 'title' => $this->string(100)->notNull()->unique(),
+ 'cover' => $this->text()->notNull(),
+ 'active' => $this->boolean()->notNull(),
+ ]);
+ }
+
+ public function down()
+ {
+ $this->dropTable('{{%v2_categories}}');
+ }
+}
diff --git a/tests/specs/blog_v2/migrations/m200000_000001_create_table_v2_users.php b/tests/specs/blog_v2/migrations/m200000_000001_create_table_v2_users.php
new file mode 100644
index 00000000..2c033da0
--- /dev/null
+++ b/tests/specs/blog_v2/migrations/m200000_000001_create_table_v2_users.php
@@ -0,0 +1,24 @@
+createTable('{{%v2_users}}', [
+ 'id' => $this->bigPrimaryKey(),
+ 'login' => $this->text()->notNull()->unique(),
+ 'email' => $this->text()->notNull()->unique(),
+ 'password' => $this->string()->notNull(),
+ 'role' => 'enum(\'admin\', \'editor\', \'reader\') NULL DEFAULT NULL',
+ 'created_at' => $this->timestamp()->null()->defaultValue(null),
+ ]);
+ }
+
+ public function down()
+ {
+ $this->dropTable('{{%v2_users}}');
+ }
+}
diff --git a/tests/specs/blog_v2/migrations/m200000_000002_create_table_v2_posts.php b/tests/specs/blog_v2/migrations/m200000_000002_create_table_v2_posts.php
new file mode 100644
index 00000000..bc7c00a8
--- /dev/null
+++ b/tests/specs/blog_v2/migrations/m200000_000002_create_table_v2_posts.php
@@ -0,0 +1,30 @@
+createTable('{{%v2_posts}}', [
+ 'id' => $this->bigPrimaryKey(),
+ 'title' => $this->string(255)->notNull()->unique(),
+ 'slug' => $this->string(200)->null()->defaultValue(null),
+ 'lang' => 'enum(\'ru\', \'eng\') NULL DEFAULT \'ru\'',
+ 'category_id' => $this->bigInteger()->notNull(),
+ 'active' => $this->boolean()->notNull(),
+ 'created_at' => $this->date()->null()->defaultValue(null),
+ 'created_by_id' => $this->bigInteger()->null()->defaultValue(null),
+ ]);
+ $this->addForeignKey('fk_v2_posts_category_id_v2_categories_id', '{{%v2_posts}}', 'category_id', '{{%v2_categories}}', 'id');
+ $this->addForeignKey('fk_v2_posts_created_by_id_v2_users_id', '{{%v2_posts}}', 'created_by_id', '{{%v2_users}}', 'id');
+ }
+
+ public function down()
+ {
+ $this->dropForeignKey('fk_v2_posts_created_by_id_v2_users_id', '{{%v2_posts}}');
+ $this->dropForeignKey('fk_v2_posts_category_id_v2_categories_id', '{{%v2_posts}}');
+ $this->dropTable('{{%v2_posts}}');
+ }
+}
diff --git a/tests/specs/blog_v2/migrations/m200000_000003_create_table_v2_comments.php b/tests/specs/blog_v2/migrations/m200000_000003_create_table_v2_comments.php
new file mode 100644
index 00000000..53ab3ea5
--- /dev/null
+++ b/tests/specs/blog_v2/migrations/m200000_000003_create_table_v2_comments.php
@@ -0,0 +1,27 @@
+createTable('{{%v2_comments}}', [
+ 'id' => $this->bigPrimaryKey(),
+ 'post_id' => $this->bigInteger()->notNull(),
+ 'user_id' => $this->bigInteger()->null()->defaultValue(null),
+ 'message' => $this->text()->notNull(),
+ 'created_at' => $this->timestamp()->notNull(),
+ ]);
+ $this->addForeignKey('fk_v2_comments_post_id_v2_posts_id', '{{%v2_comments}}', 'post_id', '{{%v2_posts}}', 'id');
+ $this->addForeignKey('fk_v2_comments_user_id_v2_users_id', '{{%v2_comments}}', 'user_id', '{{%v2_users}}', 'id');
+ }
+
+ public function down()
+ {
+ $this->dropForeignKey('fk_v2_comments_user_id_v2_users_id', '{{%v2_comments}}');
+ $this->dropForeignKey('fk_v2_comments_post_id_v2_posts_id', '{{%v2_comments}}');
+ $this->dropTable('{{%v2_comments}}');
+ }
+}
diff --git a/tests/specs/blog_v2/migrations/m200000_000004_create_table_v2_tags.php b/tests/specs/blog_v2/migrations/m200000_000004_create_table_v2_tags.php
new file mode 100644
index 00000000..e7553664
--- /dev/null
+++ b/tests/specs/blog_v2/migrations/m200000_000004_create_table_v2_tags.php
@@ -0,0 +1,21 @@
+createTable('{{%v2_tags}}', [
+ 'id' => $this->bigPrimaryKey(),
+ 'name' => $this->string(100)->notNull()->unique(),
+ 'lang' => 'enum(\'ru\', \'eng\') NOT NULL',
+ ]);
+ }
+
+ public function down()
+ {
+ $this->dropTable('{{%v2_tags}}');
+ }
+}
diff --git a/tests/specs/blog_v2/migrations/m200000_000005_create_table_v2_post_tag.php b/tests/specs/blog_v2/migrations/m200000_000005_create_table_v2_post_tag.php
new file mode 100644
index 00000000..c3232acb
--- /dev/null
+++ b/tests/specs/blog_v2/migrations/m200000_000005_create_table_v2_post_tag.php
@@ -0,0 +1,25 @@
+createTable('{{%v2_post_tag}}', [
+ 'id' => $this->bigPrimaryKey(),
+ 'post_id' => $this->bigInteger()->notNull(),
+ 'tag_id' => $this->bigInteger()->notNull(),
+ ]);
+ $this->addForeignKey('fk_v2_post_tag_post_id_v2_posts_id', '{{%v2_post_tag}}', 'post_id', '{{%v2_posts}}', 'id');
+ $this->addForeignKey('fk_v2_post_tag_tag_id_v2_tags_id', '{{%v2_post_tag}}', 'tag_id', '{{%v2_tags}}', 'id');
+ }
+
+ public function down()
+ {
+ $this->dropForeignKey('fk_v2_post_tag_tag_id_v2_tags_id', '{{%v2_post_tag}}');
+ $this->dropForeignKey('fk_v2_post_tag_post_id_v2_posts_id', '{{%v2_post_tag}}');
+ $this->dropTable('{{%v2_post_tag}}');
+ }
+}
diff --git a/tests/specs/blog_v2/migrations_maria_db/m200000_000000_change_table_v2_categories.php b/tests/specs/blog_v2/migrations_maria_db/m200000_000000_change_table_v2_categories.php
new file mode 100644
index 00000000..503eddd0
--- /dev/null
+++ b/tests/specs/blog_v2/migrations_maria_db/m200000_000000_change_table_v2_categories.php
@@ -0,0 +1,23 @@
+addColumn('{{%v2_categories}}', 'cover', $this->text()->notNull());
+ $this->alterColumn('{{%v2_categories}}', 'active', $this->boolean()->notNull());
+ $this->createIndex('unique_title', '{{%v2_categories}}', 'title', true);
+ $this->alterColumn('{{%v2_categories}}', 'title', $this->string(100)->notNull());
+ }
+
+ public function down()
+ {
+ $this->alterColumn('{{%v2_categories}}', 'title', $this->string(255)->notNull());
+ $this->dropIndex('unique_title', '{{%v2_categories}}');
+ $this->alterColumn('{{%v2_categories}}', 'active', $this->tinyInteger(1)->notNull()->defaultValue(0));
+ $this->dropColumn('{{%v2_categories}}', 'cover');
+ }
+}
diff --git a/tests/specs/blog_v2/migrations_maria_db/m200000_000001_change_table_v2_users.php b/tests/specs/blog_v2/migrations_maria_db/m200000_000001_change_table_v2_users.php
new file mode 100644
index 00000000..56e3a98b
--- /dev/null
+++ b/tests/specs/blog_v2/migrations_maria_db/m200000_000001_change_table_v2_users.php
@@ -0,0 +1,29 @@
+addColumn('{{%v2_users}}', 'login', $this->text()->notNull()->unique());
+ $this->dropColumn('{{%v2_users}}', 'username');
+ $this->alterColumn('{{%v2_users}}', 'created_at', $this->timestamp()->null()->defaultValue(null));
+ $this->createIndex('unique_email', '{{%v2_users}}', 'email', true);
+ $this->alterColumn('{{%v2_users}}', 'email', $this->text()->notNull());
+ $this->alterColumn('{{%v2_users}}', 'password', $this->string()->notNull());
+ $this->alterColumn('{{%v2_users}}', 'role', "enum('admin', 'editor', 'reader') NULL DEFAULT NULL");
+ }
+
+ public function down()
+ {
+ $this->alterColumn('{{%v2_users}}', 'role', $this->string(20)->null()->defaultValue("reader"));
+ $this->alterColumn('{{%v2_users}}', 'password', $this->string(255)->notNull());
+ $this->alterColumn('{{%v2_users}}', 'email', $this->string(200)->notNull());
+ $this->dropIndex('unique_email', '{{%v2_users}}');
+ $this->alterColumn('{{%v2_users}}', 'created_at', $this->timestamp()->null()->defaultExpression("CURRENT_TIMESTAMP"));
+ $this->addColumn('{{%v2_users}}', 'username', $this->string(200)->notNull());
+ $this->dropColumn('{{%v2_users}}', 'login');
+ }
+}
diff --git a/tests/specs/blog_v2/migrations_maria_db/m200000_000002_change_table_v2_posts.php b/tests/specs/blog_v2/migrations_maria_db/m200000_000002_change_table_v2_posts.php
new file mode 100644
index 00000000..e43d2e85
--- /dev/null
+++ b/tests/specs/blog_v2/migrations_maria_db/m200000_000002_change_table_v2_posts.php
@@ -0,0 +1,33 @@
+addColumn('{{%v2_posts}}', 'id', $this->bigPrimaryKey());
+ $this->addColumn('{{%v2_posts}}', 'lang', "enum('ru', 'eng') NULL DEFAULT 'ru'");
+ $this->dropColumn('{{%v2_posts}}', 'uid');
+ $this->alterColumn('{{%v2_posts}}', 'active', $this->boolean()->notNull());
+ $this->alterColumn('{{%v2_posts}}', 'category_id', $this->bigInteger()->notNull());
+ $this->alterColumn('{{%v2_posts}}', 'created_by_id', $this->bigInteger()->null()->defaultValue(null));
+ $this->createIndex('unique_title', '{{%v2_posts}}', 'title', true);
+ $this->addForeignKey('fk_v2_posts_category_id_v2_categories_id', '{{%v2_posts}}', 'category_id', '{{%v2_categories}}', 'id');
+ $this->addForeignKey('fk_v2_posts_created_by_id_v2_users_id', '{{%v2_posts}}', 'created_by_id', '{{%v2_users}}', 'id');
+ }
+
+ public function down()
+ {
+ $this->dropForeignKey('fk_v2_posts_created_by_id_v2_users_id', '{{%v2_posts}}');
+ $this->dropForeignKey('fk_v2_posts_category_id_v2_categories_id', '{{%v2_posts}}');
+ $this->dropIndex('unique_title', '{{%v2_posts}}');
+ $this->alterColumn('{{%v2_posts}}', 'created_by_id', $this->integer(11)->null()->defaultValue(null));
+ $this->alterColumn('{{%v2_posts}}', 'category_id', $this->integer(11)->notNull());
+ $this->alterColumn('{{%v2_posts}}', 'active', $this->tinyInteger(1)->notNull()->defaultValue(0));
+ $this->addColumn('{{%v2_posts}}', 'uid', $this->bigInteger(20)->notNull());
+ $this->dropColumn('{{%v2_posts}}', 'lang');
+ $this->dropColumn('{{%v2_posts}}', 'id');
+ }
+}
diff --git a/tests/specs/blog_v2/migrations_maria_db/m200000_000003_change_table_v2_comments.php b/tests/specs/blog_v2/migrations_maria_db/m200000_000003_change_table_v2_comments.php
new file mode 100644
index 00000000..665208af
--- /dev/null
+++ b/tests/specs/blog_v2/migrations_maria_db/m200000_000003_change_table_v2_comments.php
@@ -0,0 +1,29 @@
+addColumn('{{%v2_comments}}', 'user_id', $this->bigInteger()->null()->defaultValue(null));
+ $this->dropColumn('{{%v2_comments}}', 'author_id');
+ $this->alterColumn('{{%v2_comments}}', 'created_at', $this->timestamp()->notNull());
+ $this->alterColumn('{{%v2_comments}}', 'message', $this->text()->notNull());
+ $this->alterColumn('{{%v2_comments}}', 'post_id', $this->bigInteger()->notNull());
+ $this->addForeignKey('fk_v2_comments_post_id_v2_posts_id', '{{%v2_comments}}', 'post_id', '{{%v2_posts}}', 'id');
+ $this->addForeignKey('fk_v2_comments_user_id_v2_users_id', '{{%v2_comments}}', 'user_id', '{{%v2_users}}', 'id');
+ }
+
+ public function down()
+ {
+ $this->dropForeignKey('fk_v2_comments_user_id_v2_users_id', '{{%v2_comments}}');
+ $this->dropForeignKey('fk_v2_comments_post_id_v2_posts_id', '{{%v2_comments}}');
+ $this->alterColumn('{{%v2_comments}}', 'post_id', $this->bigInteger(20)->notNull());
+ $this->alterColumn('{{%v2_comments}}', 'message', $this->text()->notNull()->defaultValue("\'{}\'"));
+ $this->alterColumn('{{%v2_comments}}', 'created_at', $this->integer(11)->notNull());
+ $this->addColumn('{{%v2_comments}}', 'author_id', $this->integer(11)->notNull());
+ $this->dropColumn('{{%v2_comments}}', 'user_id');
+ }
+}
diff --git a/tests/specs/blog_v2/migrations_maria_db/m200000_000004_create_table_v2_tags.php b/tests/specs/blog_v2/migrations_maria_db/m200000_000004_create_table_v2_tags.php
new file mode 100644
index 00000000..e7553664
--- /dev/null
+++ b/tests/specs/blog_v2/migrations_maria_db/m200000_000004_create_table_v2_tags.php
@@ -0,0 +1,21 @@
+createTable('{{%v2_tags}}', [
+ 'id' => $this->bigPrimaryKey(),
+ 'name' => $this->string(100)->notNull()->unique(),
+ 'lang' => 'enum(\'ru\', \'eng\') NOT NULL',
+ ]);
+ }
+
+ public function down()
+ {
+ $this->dropTable('{{%v2_tags}}');
+ }
+}
diff --git a/tests/specs/blog_v2/migrations_maria_db/m200000_000005_create_table_v2_post_tag.php b/tests/specs/blog_v2/migrations_maria_db/m200000_000005_create_table_v2_post_tag.php
new file mode 100644
index 00000000..c3232acb
--- /dev/null
+++ b/tests/specs/blog_v2/migrations_maria_db/m200000_000005_create_table_v2_post_tag.php
@@ -0,0 +1,25 @@
+createTable('{{%v2_post_tag}}', [
+ 'id' => $this->bigPrimaryKey(),
+ 'post_id' => $this->bigInteger()->notNull(),
+ 'tag_id' => $this->bigInteger()->notNull(),
+ ]);
+ $this->addForeignKey('fk_v2_post_tag_post_id_v2_posts_id', '{{%v2_post_tag}}', 'post_id', '{{%v2_posts}}', 'id');
+ $this->addForeignKey('fk_v2_post_tag_tag_id_v2_tags_id', '{{%v2_post_tag}}', 'tag_id', '{{%v2_tags}}', 'id');
+ }
+
+ public function down()
+ {
+ $this->dropForeignKey('fk_v2_post_tag_tag_id_v2_tags_id', '{{%v2_post_tag}}');
+ $this->dropForeignKey('fk_v2_post_tag_post_id_v2_posts_id', '{{%v2_post_tag}}');
+ $this->dropTable('{{%v2_post_tag}}');
+ }
+}
diff --git a/tests/specs/blog_v2/migrations_mysql_db/m200000_000000_change_table_v2_categories.php b/tests/specs/blog_v2/migrations_mysql_db/m200000_000000_change_table_v2_categories.php
new file mode 100644
index 00000000..503eddd0
--- /dev/null
+++ b/tests/specs/blog_v2/migrations_mysql_db/m200000_000000_change_table_v2_categories.php
@@ -0,0 +1,23 @@
+addColumn('{{%v2_categories}}', 'cover', $this->text()->notNull());
+ $this->alterColumn('{{%v2_categories}}', 'active', $this->boolean()->notNull());
+ $this->createIndex('unique_title', '{{%v2_categories}}', 'title', true);
+ $this->alterColumn('{{%v2_categories}}', 'title', $this->string(100)->notNull());
+ }
+
+ public function down()
+ {
+ $this->alterColumn('{{%v2_categories}}', 'title', $this->string(255)->notNull());
+ $this->dropIndex('unique_title', '{{%v2_categories}}');
+ $this->alterColumn('{{%v2_categories}}', 'active', $this->tinyInteger(1)->notNull()->defaultValue(0));
+ $this->dropColumn('{{%v2_categories}}', 'cover');
+ }
+}
diff --git a/tests/specs/blog_v2/migrations_mysql_db/m200000_000001_change_table_v2_users.php b/tests/specs/blog_v2/migrations_mysql_db/m200000_000001_change_table_v2_users.php
new file mode 100644
index 00000000..56e3a98b
--- /dev/null
+++ b/tests/specs/blog_v2/migrations_mysql_db/m200000_000001_change_table_v2_users.php
@@ -0,0 +1,29 @@
+addColumn('{{%v2_users}}', 'login', $this->text()->notNull()->unique());
+ $this->dropColumn('{{%v2_users}}', 'username');
+ $this->alterColumn('{{%v2_users}}', 'created_at', $this->timestamp()->null()->defaultValue(null));
+ $this->createIndex('unique_email', '{{%v2_users}}', 'email', true);
+ $this->alterColumn('{{%v2_users}}', 'email', $this->text()->notNull());
+ $this->alterColumn('{{%v2_users}}', 'password', $this->string()->notNull());
+ $this->alterColumn('{{%v2_users}}', 'role', "enum('admin', 'editor', 'reader') NULL DEFAULT NULL");
+ }
+
+ public function down()
+ {
+ $this->alterColumn('{{%v2_users}}', 'role', $this->string(20)->null()->defaultValue("reader"));
+ $this->alterColumn('{{%v2_users}}', 'password', $this->string(255)->notNull());
+ $this->alterColumn('{{%v2_users}}', 'email', $this->string(200)->notNull());
+ $this->dropIndex('unique_email', '{{%v2_users}}');
+ $this->alterColumn('{{%v2_users}}', 'created_at', $this->timestamp()->null()->defaultExpression("CURRENT_TIMESTAMP"));
+ $this->addColumn('{{%v2_users}}', 'username', $this->string(200)->notNull());
+ $this->dropColumn('{{%v2_users}}', 'login');
+ }
+}
diff --git a/tests/specs/blog_v2/migrations_mysql_db/m200000_000002_change_table_v2_posts.php b/tests/specs/blog_v2/migrations_mysql_db/m200000_000002_change_table_v2_posts.php
new file mode 100644
index 00000000..e43d2e85
--- /dev/null
+++ b/tests/specs/blog_v2/migrations_mysql_db/m200000_000002_change_table_v2_posts.php
@@ -0,0 +1,33 @@
+addColumn('{{%v2_posts}}', 'id', $this->bigPrimaryKey());
+ $this->addColumn('{{%v2_posts}}', 'lang', "enum('ru', 'eng') NULL DEFAULT 'ru'");
+ $this->dropColumn('{{%v2_posts}}', 'uid');
+ $this->alterColumn('{{%v2_posts}}', 'active', $this->boolean()->notNull());
+ $this->alterColumn('{{%v2_posts}}', 'category_id', $this->bigInteger()->notNull());
+ $this->alterColumn('{{%v2_posts}}', 'created_by_id', $this->bigInteger()->null()->defaultValue(null));
+ $this->createIndex('unique_title', '{{%v2_posts}}', 'title', true);
+ $this->addForeignKey('fk_v2_posts_category_id_v2_categories_id', '{{%v2_posts}}', 'category_id', '{{%v2_categories}}', 'id');
+ $this->addForeignKey('fk_v2_posts_created_by_id_v2_users_id', '{{%v2_posts}}', 'created_by_id', '{{%v2_users}}', 'id');
+ }
+
+ public function down()
+ {
+ $this->dropForeignKey('fk_v2_posts_created_by_id_v2_users_id', '{{%v2_posts}}');
+ $this->dropForeignKey('fk_v2_posts_category_id_v2_categories_id', '{{%v2_posts}}');
+ $this->dropIndex('unique_title', '{{%v2_posts}}');
+ $this->alterColumn('{{%v2_posts}}', 'created_by_id', $this->integer(11)->null()->defaultValue(null));
+ $this->alterColumn('{{%v2_posts}}', 'category_id', $this->integer(11)->notNull());
+ $this->alterColumn('{{%v2_posts}}', 'active', $this->tinyInteger(1)->notNull()->defaultValue(0));
+ $this->addColumn('{{%v2_posts}}', 'uid', $this->bigInteger(20)->notNull());
+ $this->dropColumn('{{%v2_posts}}', 'lang');
+ $this->dropColumn('{{%v2_posts}}', 'id');
+ }
+}
diff --git a/tests/specs/blog_v2/migrations_mysql_db/m200000_000003_change_table_v2_comments.php b/tests/specs/blog_v2/migrations_mysql_db/m200000_000003_change_table_v2_comments.php
new file mode 100644
index 00000000..d7ea9cf9
--- /dev/null
+++ b/tests/specs/blog_v2/migrations_mysql_db/m200000_000003_change_table_v2_comments.php
@@ -0,0 +1,29 @@
+addColumn('{{%v2_comments}}', 'user_id', $this->bigInteger()->null()->defaultValue(null));
+ $this->dropColumn('{{%v2_comments}}', 'author_id');
+ $this->alterColumn('{{%v2_comments}}', 'created_at', $this->timestamp()->notNull());
+ $this->alterColumn('{{%v2_comments}}', 'message', $this->text()->notNull());
+ $this->alterColumn('{{%v2_comments}}', 'post_id', $this->bigInteger()->notNull());
+ $this->addForeignKey('fk_v2_comments_post_id_v2_posts_id', '{{%v2_comments}}', 'post_id', '{{%v2_posts}}', 'id');
+ $this->addForeignKey('fk_v2_comments_user_id_v2_users_id', '{{%v2_comments}}', 'user_id', '{{%v2_users}}', 'id');
+ }
+
+ public function down()
+ {
+ $this->dropForeignKey('fk_v2_comments_user_id_v2_users_id', '{{%v2_comments}}');
+ $this->dropForeignKey('fk_v2_comments_post_id_v2_posts_id', '{{%v2_comments}}');
+ $this->alterColumn('{{%v2_comments}}', 'post_id', $this->bigInteger(20)->notNull());
+ $this->alterColumn('{{%v2_comments}}', 'message', "json NOT NULL");
+ $this->alterColumn('{{%v2_comments}}', 'created_at', $this->integer(11)->notNull());
+ $this->addColumn('{{%v2_comments}}', 'author_id', $this->integer(11)->notNull());
+ $this->dropColumn('{{%v2_comments}}', 'user_id');
+ }
+}
diff --git a/tests/specs/blog_v2/migrations_mysql_db/m200000_000004_create_table_v2_tags.php b/tests/specs/blog_v2/migrations_mysql_db/m200000_000004_create_table_v2_tags.php
new file mode 100644
index 00000000..e7553664
--- /dev/null
+++ b/tests/specs/blog_v2/migrations_mysql_db/m200000_000004_create_table_v2_tags.php
@@ -0,0 +1,21 @@
+createTable('{{%v2_tags}}', [
+ 'id' => $this->bigPrimaryKey(),
+ 'name' => $this->string(100)->notNull()->unique(),
+ 'lang' => 'enum(\'ru\', \'eng\') NOT NULL',
+ ]);
+ }
+
+ public function down()
+ {
+ $this->dropTable('{{%v2_tags}}');
+ }
+}
diff --git a/tests/specs/blog_v2/migrations_mysql_db/m200000_000005_create_table_v2_post_tag.php b/tests/specs/blog_v2/migrations_mysql_db/m200000_000005_create_table_v2_post_tag.php
new file mode 100644
index 00000000..c3232acb
--- /dev/null
+++ b/tests/specs/blog_v2/migrations_mysql_db/m200000_000005_create_table_v2_post_tag.php
@@ -0,0 +1,25 @@
+createTable('{{%v2_post_tag}}', [
+ 'id' => $this->bigPrimaryKey(),
+ 'post_id' => $this->bigInteger()->notNull(),
+ 'tag_id' => $this->bigInteger()->notNull(),
+ ]);
+ $this->addForeignKey('fk_v2_post_tag_post_id_v2_posts_id', '{{%v2_post_tag}}', 'post_id', '{{%v2_posts}}', 'id');
+ $this->addForeignKey('fk_v2_post_tag_tag_id_v2_tags_id', '{{%v2_post_tag}}', 'tag_id', '{{%v2_tags}}', 'id');
+ }
+
+ public function down()
+ {
+ $this->dropForeignKey('fk_v2_post_tag_tag_id_v2_tags_id', '{{%v2_post_tag}}');
+ $this->dropForeignKey('fk_v2_post_tag_post_id_v2_posts_id', '{{%v2_post_tag}}');
+ $this->dropTable('{{%v2_post_tag}}');
+ }
+}
diff --git a/tests/specs/blog_v2/migrations_pgsql_db/m200000_000000_change_table_v2_categories.php b/tests/specs/blog_v2/migrations_pgsql_db/m200000_000000_change_table_v2_categories.php
new file mode 100644
index 00000000..c3c7add0
--- /dev/null
+++ b/tests/specs/blog_v2/migrations_pgsql_db/m200000_000000_change_table_v2_categories.php
@@ -0,0 +1,23 @@
+addColumn('{{%v2_categories}}', 'cover', $this->text()->notNull());
+ $this->alterColumn('{{%v2_categories}}', 'active', "DROP DEFAULT");
+ $this->createIndex('unique_title', '{{%v2_categories}}', 'title', true);
+ $this->alterColumn('{{%v2_categories}}', 'title', $this->string(100));
+ }
+
+ public function safeDown()
+ {
+ $this->dropIndex('unique_title', '{{%v2_categories}}');
+ $this->dropColumn('{{%v2_categories}}', 'cover');
+ $this->alterColumn('{{%v2_categories}}', 'active', "SET DEFAULT FALSE");
+ $this->alterColumn('{{%v2_categories}}', 'title', $this->string(255));
+ }
+}
diff --git a/tests/specs/blog_v2/migrations_pgsql_db/m200000_000001_change_table_v2_users.php b/tests/specs/blog_v2/migrations_pgsql_db/m200000_000001_change_table_v2_users.php
new file mode 100644
index 00000000..8e4dd383
--- /dev/null
+++ b/tests/specs/blog_v2/migrations_pgsql_db/m200000_000001_change_table_v2_users.php
@@ -0,0 +1,33 @@
+execute('CREATE TYPE enum_role AS ENUM(\'admin\',\'editor\',\'reader\')');
+ $this->addColumn('{{%v2_users}}', 'login', $this->text()->notNull()->unique());
+ $this->dropColumn('{{%v2_users}}', 'username');
+ $this->alterColumn('{{%v2_users}}', 'created_at', "DROP DEFAULT");
+ $this->createIndex('unique_email', '{{%v2_users}}', 'email', true);
+ $this->alterColumn('{{%v2_users}}', 'email', $this->text());
+ $this->alterColumn('{{%v2_users}}', 'password', $this->string());
+ $this->alterColumn('{{%v2_users}}', 'role', "enum_role USING role::enum_role");
+ $this->alterColumn('{{%v2_users}}', 'role', "DROP DEFAULT");
+ }
+
+ public function safeDown()
+ {
+ $this->dropIndex('unique_email', '{{%v2_users}}');
+ $this->addColumn('{{%v2_users}}', 'username', $this->string(200)->notNull());
+ $this->dropColumn('{{%v2_users}}', 'login');
+ $this->alterColumn('{{%v2_users}}', 'created_at', "SET DEFAULT 'CURRENT_TIMESTAMP'");
+ $this->alterColumn('{{%v2_users}}', 'email', $this->string(200));
+ $this->alterColumn('{{%v2_users}}', 'password', $this->string(255));
+ $this->alterColumn('{{%v2_users}}', 'role', $this->string(20));
+ $this->alterColumn('{{%v2_users}}', 'role', "SET DEFAULT 'reader'");
+ $this->execute('DROP TYPE enum_role');
+ }
+}
diff --git a/tests/specs/blog_v2/migrations_pgsql_db/m200000_000002_change_table_v2_posts.php b/tests/specs/blog_v2/migrations_pgsql_db/m200000_000002_change_table_v2_posts.php
new file mode 100644
index 00000000..26289c06
--- /dev/null
+++ b/tests/specs/blog_v2/migrations_pgsql_db/m200000_000002_change_table_v2_posts.php
@@ -0,0 +1,35 @@
+execute('CREATE TYPE enum_lang AS ENUM(\'ru\',\'eng\')');
+ $this->addColumn('{{%v2_posts}}', 'id', $this->bigPrimaryKey());
+ $this->addColumn('{{%v2_posts}}', 'lang', "enum_lang NULL DEFAULT 'ru'");
+ $this->dropColumn('{{%v2_posts}}', 'uid');
+ $this->alterColumn('{{%v2_posts}}', 'active', "DROP DEFAULT");
+ $this->alterColumn('{{%v2_posts}}', 'category_id', $this->bigInteger());
+ $this->alterColumn('{{%v2_posts}}', 'created_by_id', $this->bigInteger());
+ $this->createIndex('unique_title', '{{%v2_posts}}', 'title', true);
+ $this->addForeignKey('fk_v2_posts_category_id_v2_categories_id', '{{%v2_posts}}', 'category_id', '{{%v2_categories}}', 'id');
+ $this->addForeignKey('fk_v2_posts_created_by_id_v2_users_id', '{{%v2_posts}}', 'created_by_id', '{{%v2_users}}', 'id');
+ }
+
+ public function safeDown()
+ {
+ $this->dropForeignKey('fk_v2_posts_created_by_id_v2_users_id', '{{%v2_posts}}');
+ $this->dropForeignKey('fk_v2_posts_category_id_v2_categories_id', '{{%v2_posts}}');
+ $this->dropIndex('unique_title', '{{%v2_posts}}');
+ $this->addColumn('{{%v2_posts}}', 'uid', $this->bigInteger()->notNull());
+ $this->execute('DROP TYPE enum_lang');
+ $this->dropColumn('{{%v2_posts}}', 'lang');
+ $this->dropColumn('{{%v2_posts}}', 'id');
+ $this->alterColumn('{{%v2_posts}}', 'active', "SET DEFAULT FALSE");
+ $this->alterColumn('{{%v2_posts}}', 'category_id', $this->integer());
+ $this->alterColumn('{{%v2_posts}}', 'created_by_id', $this->integer());
+ }
+}
diff --git a/tests/specs/blog_v2/migrations_pgsql_db/m200000_000003_change_table_v2_comments.php b/tests/specs/blog_v2/migrations_pgsql_db/m200000_000003_change_table_v2_comments.php
new file mode 100644
index 00000000..2af6194e
--- /dev/null
+++ b/tests/specs/blog_v2/migrations_pgsql_db/m200000_000003_change_table_v2_comments.php
@@ -0,0 +1,29 @@
+addColumn('{{%v2_comments}}', 'user_id', $this->bigInteger()->null()->defaultValue(null));
+ $this->dropColumn('{{%v2_comments}}', 'author_id');
+ $this->alterColumn('{{%v2_comments}}', 'created_at', $this->timestamp());
+ $this->alterColumn('{{%v2_comments}}', 'message', $this->text());
+ $this->alterColumn('{{%v2_comments}}', 'message', "DROP DEFAULT");
+ $this->addForeignKey('fk_v2_comments_post_id_v2_posts_id', '{{%v2_comments}}', 'post_id', '{{%v2_posts}}', 'id');
+ $this->addForeignKey('fk_v2_comments_user_id_v2_users_id', '{{%v2_comments}}', 'user_id', '{{%v2_users}}', 'id');
+ }
+
+ public function safeDown()
+ {
+ $this->dropForeignKey('fk_v2_comments_user_id_v2_users_id', '{{%v2_comments}}');
+ $this->dropForeignKey('fk_v2_comments_post_id_v2_posts_id', '{{%v2_comments}}');
+ $this->addColumn('{{%v2_comments}}', 'author_id', $this->integer()->notNull());
+ $this->dropColumn('{{%v2_comments}}', 'user_id');
+ $this->alterColumn('{{%v2_comments}}', 'created_at', $this->integer());
+ $this->alterColumn('{{%v2_comments}}', 'message', "jsonb");
+ $this->alterColumn('{{%v2_comments}}', 'message', "SET DEFAULT '[]'");
+ }
+}
diff --git a/tests/specs/blog_v2/migrations_pgsql_db/m200000_000004_create_table_v2_tags.php b/tests/specs/blog_v2/migrations_pgsql_db/m200000_000004_create_table_v2_tags.php
new file mode 100644
index 00000000..1da20213
--- /dev/null
+++ b/tests/specs/blog_v2/migrations_pgsql_db/m200000_000004_create_table_v2_tags.php
@@ -0,0 +1,23 @@
+execute('CREATE TYPE enum_lang AS ENUM(\'ru\',\'eng\')');
+ $this->createTable('{{%v2_tags}}', [
+ 'id' => $this->bigPrimaryKey(),
+ 'name' => $this->string(100)->notNull()->unique(),
+ 'lang' => 'enum_lang NOT NULL',
+ ]);
+ }
+
+ public function safeDown()
+ {
+ $this->execute('DROP TYPE enum_lang');
+ $this->dropTable('{{%v2_tags}}');
+ }
+}
diff --git a/tests/specs/blog_v2/migrations_pgsql_db/m200000_000005_create_table_v2_post_tag.php b/tests/specs/blog_v2/migrations_pgsql_db/m200000_000005_create_table_v2_post_tag.php
new file mode 100644
index 00000000..816bba17
--- /dev/null
+++ b/tests/specs/blog_v2/migrations_pgsql_db/m200000_000005_create_table_v2_post_tag.php
@@ -0,0 +1,25 @@
+createTable('{{%v2_post_tag}}', [
+ 'id' => $this->bigPrimaryKey(),
+ 'post_id' => $this->bigInteger()->notNull(),
+ 'tag_id' => $this->bigInteger()->notNull(),
+ ]);
+ $this->addForeignKey('fk_v2_post_tag_post_id_v2_posts_id', '{{%v2_post_tag}}', 'post_id', '{{%v2_posts}}', 'id');
+ $this->addForeignKey('fk_v2_post_tag_tag_id_v2_tags_id', '{{%v2_post_tag}}', 'tag_id', '{{%v2_tags}}', 'id');
+ }
+
+ public function safeDown()
+ {
+ $this->dropForeignKey('fk_v2_post_tag_tag_id_v2_tags_id', '{{%v2_post_tag}}');
+ $this->dropForeignKey('fk_v2_post_tag_post_id_v2_posts_id', '{{%v2_post_tag}}');
+ $this->dropTable('{{%v2_post_tag}}');
+ }
+}
diff --git a/tests/specs/blog_v2/models/Category.php b/tests/specs/blog_v2/models/Category.php
new file mode 100644
index 00000000..6e9d01c6
--- /dev/null
+++ b/tests/specs/blog_v2/models/Category.php
@@ -0,0 +1,10 @@
+language);
+ $uniqueFaker = new UniqueGenerator($faker);
+ $model = new Category();
+ $model->id = $uniqueFaker->numberBetween(0, 2147483647);
+ $model->title = substr($faker->sentence, 0, 100);
+ $model->cover = $faker->sentence;
+ $model->active = $faker->boolean;
+ return $model;
+ }
+}
diff --git a/tests/specs/blog_v2/models/Comment.php b/tests/specs/blog_v2/models/Comment.php
new file mode 100644
index 00000000..7a4886f7
--- /dev/null
+++ b/tests/specs/blog_v2/models/Comment.php
@@ -0,0 +1,10 @@
+language);
+ $uniqueFaker = new UniqueGenerator($faker);
+ $model = new Comment();
+ $model->id = $uniqueFaker->numberBetween(0, 2147483647);
+ $model->message = $faker->sentence;
+ $model->created_at = $faker->dateTimeThisCentury->format('Y-m-d H:i:s');
+ return $model;
+ }
+}
diff --git a/tests/specs/blog_v2/models/Post.php b/tests/specs/blog_v2/models/Post.php
new file mode 100644
index 00000000..2825fe31
--- /dev/null
+++ b/tests/specs/blog_v2/models/Post.php
@@ -0,0 +1,10 @@
+language);
+ $uniqueFaker = new UniqueGenerator($faker);
+ $model = new Post();
+ $model->id = $uniqueFaker->numberBetween(0, 2147483647);
+ $model->title = substr($faker->sentence, 0, 255);
+ $model->slug = substr($uniqueFaker->slug, 0, 200);
+ $model->lang = $faker->randomElement(array (
+ 0 => 'ru',
+ 1 => 'eng',
+));
+ $model->active = $faker->boolean;
+ $model->created_at = $faker->iso8601;
+ return $model;
+ }
+}
diff --git a/tests/specs/blog_v2/models/PostTag.php b/tests/specs/blog_v2/models/PostTag.php
new file mode 100644
index 00000000..3538dcae
--- /dev/null
+++ b/tests/specs/blog_v2/models/PostTag.php
@@ -0,0 +1,10 @@
+language);
+ $uniqueFaker = new UniqueGenerator($faker);
+ $model = new PostTag();
+ $model->id = $uniqueFaker->numberBetween(0, 2147483647);
+ return $model;
+ }
+}
diff --git a/tests/specs/blog_v2/models/Tag.php b/tests/specs/blog_v2/models/Tag.php
new file mode 100644
index 00000000..43327f05
--- /dev/null
+++ b/tests/specs/blog_v2/models/Tag.php
@@ -0,0 +1,10 @@
+language);
+ $uniqueFaker = new UniqueGenerator($faker);
+ $model = new Tag();
+ $model->id = $uniqueFaker->numberBetween(0, 2147483647);
+ $model->name = substr($faker->text(100), 0, 100);
+ $model->lang = $faker->randomElement(array (
+ 0 => 'ru',
+ 1 => 'eng',
+));
+ return $model;
+ }
+}
diff --git a/tests/specs/blog_v2/models/User.php b/tests/specs/blog_v2/models/User.php
new file mode 100644
index 00000000..9b837d6e
--- /dev/null
+++ b/tests/specs/blog_v2/models/User.php
@@ -0,0 +1,10 @@
+language);
+ $uniqueFaker = new UniqueGenerator($faker);
+ $model = new User();
+ $model->id = $uniqueFaker->numberBetween(0, 2147483647);
+ $model->login = $faker->userName;
+ $model->email = $faker->safeEmail;
+ $model->password = $faker->password;
+ $model->role = $faker->randomElement(['admin', 'editor', 'reader']);
+ $model->created_at = $faker->dateTimeThisCentury->format('Y-m-d H:i:s');
+ return $model;
+ }
+}
diff --git a/tests/specs/blog_v2/models/base/Category.php b/tests/specs/blog_v2/models/base/Category.php
new file mode 100644
index 00000000..9612e7ff
--- /dev/null
+++ b/tests/specs/blog_v2/models/base/Category.php
@@ -0,0 +1,36 @@
+hasMany(\app\models\Post::class,['category_id' => 'id']);
+ }
+}
diff --git a/tests/specs/blog_v2/models/base/Comment.php b/tests/specs/blog_v2/models/base/Comment.php
new file mode 100644
index 00000000..355f9df8
--- /dev/null
+++ b/tests/specs/blog_v2/models/base/Comment.php
@@ -0,0 +1,44 @@
+ 'Post'],
+ [['user_id'], 'exist', 'targetRelation' => 'User'],
+ [['message', 'created_at'], 'string'],
+ ];
+ }
+
+ public function getPost()
+ {
+ return $this->hasOne(\app\models\Post::class,['id' => 'post_id']);
+ }
+ public function getUser()
+ {
+ return $this->hasOne(\app\models\User::class,['id' => 'user_id']);
+ }
+}
diff --git a/tests/specs/blog_v2/models/base/Post.php b/tests/specs/blog_v2/models/base/Post.php
new file mode 100644
index 00000000..2e9ea950
--- /dev/null
+++ b/tests/specs/blog_v2/models/base/Post.php
@@ -0,0 +1,58 @@
+ 'Category'],
+ [['created_by_id'], 'exist', 'targetRelation' => 'CreatedBy'],
+ [['title', 'slug', 'lang', 'created_at'], 'string'],
+ [['active'], 'boolean'],
+ ];
+ }
+
+ public function getCategory()
+ {
+ return $this->hasOne(\app\models\Category::class,['id' => 'category_id']);
+ }
+ public function getCreatedBy()
+ {
+ return $this->hasOne(\app\models\User::class,['id' => 'created_by_id']);
+ }
+ public function getComments()
+ {
+ return $this->hasMany(\app\models\Comment::class,['post_id' => 'id']);
+ }
+ public function getPostTags()
+ {
+ return $this->hasMany(\app\models\PostTag::class,['post_id' => 'id']);
+ }
+}
diff --git a/tests/specs/blog_v2/models/base/PostTag.php b/tests/specs/blog_v2/models/base/PostTag.php
new file mode 100644
index 00000000..1b47cc14
--- /dev/null
+++ b/tests/specs/blog_v2/models/base/PostTag.php
@@ -0,0 +1,40 @@
+ 'Post'],
+ [['tag_id'], 'exist', 'targetRelation' => 'Tag'],
+ ];
+ }
+
+ public function getPost()
+ {
+ return $this->hasOne(\app\models\Post::class,['id' => 'post_id']);
+ }
+ public function getTag()
+ {
+ return $this->hasOne(\app\models\Tag::class,['id' => 'tag_id']);
+ }
+}
diff --git a/tests/specs/blog_v2/models/base/Tag.php b/tests/specs/blog_v2/models/base/Tag.php
new file mode 100644
index 00000000..411b725a
--- /dev/null
+++ b/tests/specs/blog_v2/models/base/Tag.php
@@ -0,0 +1,34 @@
+hasMany(\app\models\PostTag::class,['tag_id' => 'id']);
+ }
+}
diff --git a/tests/specs/blog_v2/models/base/User.php b/tests/specs/blog_v2/models/base/User.php
new file mode 100644
index 00000000..e35e7328
--- /dev/null
+++ b/tests/specs/blog_v2/models/base/User.php
@@ -0,0 +1,32 @@
+ '@specs/menu.yaml',
+ 'generateUrls' => false,
+ 'generateControllers' => false,
+ 'generateModels' => true,
+ 'generateMigrations' => true,
+ 'excludeModels' => [
+ 'Error',
+ ],
+];
\ No newline at end of file
diff --git a/tests/specs/menu.yaml b/tests/specs/menu.yaml
new file mode 100644
index 00000000..f209a591
--- /dev/null
+++ b/tests/specs/menu.yaml
@@ -0,0 +1,77 @@
+openapi: "3.0.0"
+info:
+ version: 1.0.0
+ title: Menu prototype for test migrations
+ license:
+ name: MIT
+servers:
+ - url: http://menu.dummy.io/v1
+paths:
+ /:
+ get:
+ summary: List all
+ operationId: listAll
+ tags:
+ - all
+ responses:
+ '200':
+ description: A paged array of menu items
+ headers:
+ x-next:
+ description: A link to the next page of responses
+ schema:
+ type: string
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/Menu"
+ default:
+ description: unexpected error
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/Error"
+components:
+ schemas:
+ Menu:
+ required:
+ - id
+ - name
+ properties:
+ id:
+ type: integer
+ format: int64
+ readOnly: True
+ name:
+ type: string
+ maxLength: 100
+ minLength: 3
+ parent:
+ $ref: '#/components/schemas/Menu/properties/id'
+ childes:
+ type: array
+ items:
+ $ref: '#/components/schemas/Menu/properties/parent'
+ args:
+ type: array
+ x-db-type: text[]
+ default:
+ - foo
+ - bar
+ - baz
+ kwargs:
+ type: string
+ x-db-type: json
+ default:
+ - foo: bar
+ - buzz: fizz
+ Error:
+ required:
+ - code
+ - message
+ properties:
+ code:
+ type: integer
+ format: int32
+ message:
+ type: string
diff --git a/tests/specs/menu/migrations/m200000_000000_create_table_menus.php b/tests/specs/menu/migrations/m200000_000000_create_table_menus.php
new file mode 100644
index 00000000..66d43585
--- /dev/null
+++ b/tests/specs/menu/migrations/m200000_000000_create_table_menus.php
@@ -0,0 +1,25 @@
+createTable('{{%menus}}', [
+ 'id' => $this->bigPrimaryKey(),
+ 'name' => $this->string(100)->notNull(),
+ 'parent_id' => $this->bigInteger()->null()->defaultValue(null),
+ 'args' => 'text[] NULL DEFAULT \'{"foo","bar","baz"}\'',
+ 'kwargs' => 'json NOT NULL DEFAULT \'[{"foo":"bar"},{"buzz":"fizz"}]\'',
+ ]);
+ $this->addForeignKey('fk_menus_parent_id_menus_id', '{{%menus}}', 'parent_id', '{{%menus}}', 'id');
+ }
+
+ public function down()
+ {
+ $this->dropForeignKey('fk_menus_parent_id_menus_id', '{{%menus}}');
+ $this->dropTable('{{%menus}}');
+ }
+}
diff --git a/tests/specs/menu/models/Menu.php b/tests/specs/menu/models/Menu.php
new file mode 100644
index 00000000..2b5867b4
--- /dev/null
+++ b/tests/specs/menu/models/Menu.php
@@ -0,0 +1,10 @@
+language);
+ $uniqueFaker = new UniqueGenerator($faker);
+ $model = new Menu();
+ $model->id = $uniqueFaker->numberBetween(0, 2147483647);
+ $model->name = substr($faker->text(100), 0, 100);
+ $model->args = [];
+ $model->kwargs = [];
+ return $model;
+ }
+}
diff --git a/tests/specs/menu/models/base/Menu.php b/tests/specs/menu/models/base/Menu.php
new file mode 100644
index 00000000..4f103bc1
--- /dev/null
+++ b/tests/specs/menu/models/base/Menu.php
@@ -0,0 +1,44 @@
+ 'Parent'],
+ [['name'], 'string'],
+ [['args', 'kwargs'], 'safe'],
+ ];
+ }
+
+ public function getParent()
+ {
+ return $this->hasOne(\app\models\Menu::class,['id' => 'parent_id']);
+ }
+ public function getChildes()
+ {
+ return $this->hasMany(\app\models\Menu::class,['parent_id' => 'id']);
+ }
+}
diff --git a/tests/specs/petstore/models/PetFaker.php b/tests/specs/petstore/models/PetFaker.php
index 4d589cca..428aa401 100644
--- a/tests/specs/petstore/models/PetFaker.php
+++ b/tests/specs/petstore/models/PetFaker.php
@@ -14,7 +14,7 @@ public function generateModel()
{
$faker = FakerFactory::create(\Yii::$app->language);
$uniqueFaker = new UniqueGenerator($faker);
- $model = new Pet;
+ $model = new Pet();
$model->id = $uniqueFaker->numberBetween(0, 2147483647);
$model->name = $faker->sentence;
$model->tag = $faker->randomElement(['one', 'two', 'three', 'four']);
diff --git a/tests/specs/petstore/models/StoreFaker.php b/tests/specs/petstore/models/StoreFaker.php
index 848e9c7b..e921def8 100644
--- a/tests/specs/petstore/models/StoreFaker.php
+++ b/tests/specs/petstore/models/StoreFaker.php
@@ -14,7 +14,7 @@ public function generateModel()
{
$faker = FakerFactory::create(\Yii::$app->language);
$uniqueFaker = new UniqueGenerator($faker);
- $model = new Store;
+ $model = new Store();
$model->id = $uniqueFaker->numberBetween(0, 2147483647);
$model->name = $faker->sentence;
return $model;
diff --git a/tests/specs/petstore/models/base/Pet.php b/tests/specs/petstore/models/base/Pet.php
index a8da2c71..243c6bf5 100644
--- a/tests/specs/petstore/models/base/Pet.php
+++ b/tests/specs/petstore/models/base/Pet.php
@@ -24,13 +24,14 @@ public function rules()
return [
[['name', 'tag'], 'trim'],
[['name'], 'required'],
+ [['store_id'], 'integer'],
+ [['store_id'], 'exist', 'targetRelation' => 'Store'],
[['name', 'tag'], 'string'],
];
}
public function getStore()
{
- return $this->hasOne(\app\models\Store::class, ['id' => 'store_id']);
+ return $this->hasOne(\app\models\Store::class,['id' => 'store_id']);
}
-
}
diff --git a/tests/specs/petstore_arrayref/models/PetFaker.php b/tests/specs/petstore_arrayref/models/PetFaker.php
index 4d589cca..428aa401 100644
--- a/tests/specs/petstore_arrayref/models/PetFaker.php
+++ b/tests/specs/petstore_arrayref/models/PetFaker.php
@@ -14,7 +14,7 @@ public function generateModel()
{
$faker = FakerFactory::create(\Yii::$app->language);
$uniqueFaker = new UniqueGenerator($faker);
- $model = new Pet;
+ $model = new Pet();
$model->id = $uniqueFaker->numberBetween(0, 2147483647);
$model->name = $faker->sentence;
$model->tag = $faker->randomElement(['one', 'two', 'three', 'four']);
diff --git a/tests/specs/petstore_namespace/mymodels/base/Pet.php b/tests/specs/petstore_namespace/mymodels/base/Pet.php
index db6b20eb..c3331c98 100644
--- a/tests/specs/petstore_namespace/mymodels/base/Pet.php
+++ b/tests/specs/petstore_namespace/mymodels/base/Pet.php
@@ -24,13 +24,14 @@ public function rules()
return [
[['name', 'tag'], 'trim'],
[['name'], 'required'],
+ [['store_id'], 'integer'],
+ [['store_id'], 'exist', 'targetRelation' => 'Store'],
[['name', 'tag'], 'string'],
];
}
public function getStore()
{
- return $this->hasOne(\app\mymodels\Store::class, ['id' => 'store_id']);
+ return $this->hasOne(\app\mymodels\Store::class,['id' => 'store_id']);
}
-
}
diff --git a/tests/specs/petstore_namespace/mymodels/faker/PetFaker.php b/tests/specs/petstore_namespace/mymodels/faker/PetFaker.php
index ded8f5f3..2caf6c6f 100644
--- a/tests/specs/petstore_namespace/mymodels/faker/PetFaker.php
+++ b/tests/specs/petstore_namespace/mymodels/faker/PetFaker.php
@@ -15,7 +15,7 @@ public function generateModel()
{
$faker = FakerFactory::create(\Yii::$app->language);
$uniqueFaker = new UniqueGenerator($faker);
- $model = new Pet;
+ $model = new Pet();
$model->id = $uniqueFaker->numberBetween(0, 2147483647);
$model->name = $faker->sentence;
$model->tag = $faker->randomElement(['one', 'two', 'three', 'four']);
diff --git a/tests/specs/petstore_namespace/mymodels/faker/StoreFaker.php b/tests/specs/petstore_namespace/mymodels/faker/StoreFaker.php
index 6f18f9ef..be14e6bb 100644
--- a/tests/specs/petstore_namespace/mymodels/faker/StoreFaker.php
+++ b/tests/specs/petstore_namespace/mymodels/faker/StoreFaker.php
@@ -15,7 +15,7 @@ public function generateModel()
{
$faker = FakerFactory::create(\Yii::$app->language);
$uniqueFaker = new UniqueGenerator($faker);
- $model = new Store;
+ $model = new Store();
$model->id = $uniqueFaker->numberBetween(0, 2147483647);
$model->name = $faker->sentence;
return $model;
diff --git a/tests/specs/petstore_xtable.php b/tests/specs/petstore_xtable.php
index db56afbc..e0349384 100644
--- a/tests/specs/petstore_xtable.php
+++ b/tests/specs/petstore_xtable.php
@@ -6,5 +6,5 @@
'generateModels' => true,
'generateModelsOnlyXTable' => true,
'generateControllers' => false,
- 'generateMigrations' => false, // TODO add tests for migrations
+ 'generateMigrations' => true,
];
diff --git a/tests/specs/petstore_xtable/migrations/m200000_000000_create_table_pets.php b/tests/specs/petstore_xtable/migrations/m200000_000000_create_table_pets.php
new file mode 100644
index 00000000..f712c5ba
--- /dev/null
+++ b/tests/specs/petstore_xtable/migrations/m200000_000000_create_table_pets.php
@@ -0,0 +1,21 @@
+createTable('{{%pets}}', [
+ 'id' => $this->bigPrimaryKey(),
+ 'name' => $this->text()->notNull(),
+ 'tag' => $this->text()->null()->defaultValue(null),
+ ]);
+ }
+
+ public function down()
+ {
+ $this->dropTable('{{%pets}}');
+ }
+}
diff --git a/tests/specs/petstore_xtable/models/PetFaker.php b/tests/specs/petstore_xtable/models/PetFaker.php
index 4d589cca..428aa401 100644
--- a/tests/specs/petstore_xtable/models/PetFaker.php
+++ b/tests/specs/petstore_xtable/models/PetFaker.php
@@ -14,7 +14,7 @@ public function generateModel()
{
$faker = FakerFactory::create(\Yii::$app->language);
$uniqueFaker = new UniqueGenerator($faker);
- $model = new Pet;
+ $model = new Pet();
$model->id = $uniqueFaker->numberBetween(0, 2147483647);
$model->name = $faker->sentence;
$model->tag = $faker->randomElement(['one', 'two', 'three', 'four']);
diff --git a/tests/unit/AttributeResolverTest.php b/tests/unit/AttributeResolverTest.php
new file mode 100644
index 00000000..35694e95
--- /dev/null
+++ b/tests/unit/AttributeResolverTest.php
@@ -0,0 +1,69 @@
+resolve();
+ echo $schemaName.PHP_EOL;
+ self::assertEquals($expected->name, $model->name);
+ self::assertEquals($expected->tableName, $model->tableName);
+ self::assertEquals($expected->description, $model->description);
+ self::assertEquals($expected->tableAlias, $model->tableAlias);
+ foreach ($model->relations as $name => $relation){
+ self::assertTrue(isset($expected->relations[$name]));
+ self::assertEquals($expected->relations[$name], $relation);
+ }
+ foreach ($model->attributes as $name => $attribute){
+ self::assertTrue(isset($expected->attributes[$name]));
+ self::assertEquals($expected->attributes[$name], $attribute);
+ }
+ }
+
+ public function dataProvider():array
+ {
+ $schemaFile = Yii::getAlias("@specs/blog.yaml");
+ $fixture = require Yii::getAlias('@fixtures/blog.php');
+ $openApi = Reader::readFromYamlFile($schemaFile, OpenApi::class, false);
+ return [
+ [
+ 'User',
+ $openApi->components->schemas['User'],
+ $fixture['user']
+ ],
+ [
+ 'Category',
+ $openApi->components->schemas['Category'],
+ $fixture['category']
+ ],
+ [
+ 'Post',
+ $openApi->components->schemas['Post'],
+ $fixture['post']
+ ],
+ [
+ 'Comment',
+ $openApi->components->schemas['Comment'],
+ $fixture['comment']
+ ],
+ ];
+ }
+}
\ No newline at end of file
diff --git a/tests/unit/FakerStubResolverTest.php b/tests/unit/FakerStubResolverTest.php
new file mode 100644
index 00000000..62b21cfa
--- /dev/null
+++ b/tests/unit/FakerStubResolverTest.php
@@ -0,0 +1,131 @@
+ FakerStubResolver::class], [$column, $property]);
+ self::assertEquals($expected, $resolver->resolve());
+ }
+
+ public function dataProvider()
+ {
+ $schemaFile = Yii::getAlias("@specs/blog.yaml");
+ $openApi = Reader::readFromYamlFile($schemaFile, OpenApi::class, false);
+ $schema = $openApi->components->schemas['Fakerable'];
+ return [
+ [
+ (new Attribute('id'))->setPhpType('int')->setDbType(YiiDbSchema::TYPE_BIGPK),
+ $schema->properties['id'],
+ '$uniqueFaker->numberBetween(0, 2147483647)',
+ ],
+ [
+ (new Attribute('someint'))->setPhpType('int')->setDbType(YiiDbSchema::TYPE_BIGPK),
+ $schema->properties['id'],
+ '$faker->numberBetween(0, 2147483647)',
+ ],
+ [
+ (new Attribute('active'))->setPhpType('bool')->setDbType(YiiDbSchema::TYPE_BOOLEAN),
+ $schema->properties['active'],
+ '$faker->boolean',
+ ],
+ [
+ (new Attribute('floatval'))->setPhpType('float')->setDbType(YiiDbSchema::TYPE_FLOAT),
+ $schema->properties['floatval'],
+ '$faker->randomFloat()',
+ ],
+ [
+ (new Attribute('doubleval'))
+ ->setPhpType(TypeResolver::schemaToPhpType($schema->properties['doubleval']))
+ ->setDbType(TypeResolver::schemaToDbType($schema->properties['doubleval'])),
+ $schema->properties['doubleval'],
+ '$faker->randomFloat()',
+ ],
+ [
+ (new Attribute('floatval_lim'))
+ ->setPhpType('float')->setDbType(YiiDbSchema::TYPE_FLOAT)
+ ->setLimits(0, 1, null),
+ $schema->properties['floatval_lim'],
+ '$faker->randomFloat(null, 0, 1)',
+ ],
+ [
+ (new Attribute('int_simple'))
+ ->setPhpType('int')->setDbType(YiiDbSchema::TYPE_INTEGER),
+ $schema->properties['int_simple'],
+ '$faker->numberBetween(0, 2147483647)',
+ ],
+ [
+ (new Attribute('int_created_at'))
+ ->setPhpType('int')->setDbType(YiiDbSchema::TYPE_INTEGER),
+ $schema->properties['int_created_at'],
+ '$faker->unixTime',
+ ],
+ [
+ (new Attribute('int_min'))
+ ->setPhpType('int')->setDbType(YiiDbSchema::TYPE_INTEGER)
+ ->setLimits(5, null, null),
+ $schema->properties['int_min'],
+ '$faker->numberBetween(5, 2147483647)',
+ ],
+ [
+ (new Attribute('int_max'))
+ ->setPhpType('int')->setDbType(YiiDbSchema::TYPE_INTEGER)
+ ->setLimits(null, 5, null),
+ $schema->properties['int_max'],
+ '$faker->numberBetween(0, 5)',
+ ],
+ [
+ (new Attribute('int_minmax'))
+ ->setPhpType('int')->setDbType(YiiDbSchema::TYPE_INTEGER)
+ ->setLimits(5, 25, null),
+ $schema->properties['int_minmax'],
+ '$faker->numberBetween(5, 25)',
+ ],
+ [
+ (new Attribute('uuid'))->setPhpType('string')->setDbType('uuid'),
+ $schema->properties['uuid'],
+ '$faker->uuid',
+ ],
+ [
+ (new Attribute('str_text'))->setPhpType('string')->setDbType(YiiDbSchema::TYPE_TEXT),
+ $schema->properties['str_text'],
+ '$faker->sentence',
+ ],
+ [
+ (new Attribute('str_varchar'))->setPhpType('string')->setDbType(YiiDbSchema::TYPE_STRING),
+ $schema->properties['str_varchar'],
+ '$faker->sentence',
+ ],
+ [
+ (new Attribute('str_varchar'))->setPhpType('string')->setDbType(YiiDbSchema::TYPE_STRING)->setSize(100),
+ $schema->properties['str_varchar'],
+ 'substr($faker->text(100), 0, 100)',
+ ],
+ [
+ (new Attribute('str_date'))->setPhpType('string')->setDbType(YiiDbSchema::TYPE_DATE),
+ $schema->properties['str_date'],
+ '$faker->iso8601',
+ ],
+ [
+ (new Attribute('str_datetime'))->setPhpType('string')->setDbType(YiiDbSchema::TYPE_DATETIME),
+ $schema->properties['str_datetime'],
+ '$faker->dateTimeThisCentury->format(\'Y-m-d H:i:s\')',
+ ],
+ ];
+ }
+}
\ No newline at end of file
diff --git a/tests/GeneratorTest.php b/tests/unit/GeneratorTest.php
similarity index 60%
rename from tests/GeneratorTest.php
rename to tests/unit/GeneratorTest.php
index 44cc6fd2..189cca49 100644
--- a/tests/GeneratorTest.php
+++ b/tests/unit/GeneratorTest.php
@@ -1,24 +1,20 @@
false, 'only' => ['*.php']]);
$ret = [];
- foreach($tests as $testFile) {
+ foreach ($tests as $testFile) {
$ret[] = [substr($testFile, strlen(Yii::getAlias('@specs')) + 1)];
}
return $ret;
@@ -30,35 +26,42 @@ public function provideTestcases()
public function testGenerate($testFile)
{
$testFile = Yii::getAlias("@specs/$testFile");
- FileHelper::removeDirectory(__DIR__ . '/tmp');
- FileHelper::createDirectory(__DIR__ . '/tmp');
- Yii::setAlias('@app', __DIR__ . '/tmp');
+ $this->prepareTempDir();
- $app = new \yii\web\Application([
- 'id' => 'yii2-openapi-test',
- 'basePath' => __DIR__ . '/tmp',
- ]);
+ $this->mockApplication($this->mockDbSchemaAsEmpty());
$generator = $this->createGenerator($testFile);
$this->assertTrue($generator->validate(), print_r($generator->getErrors(), true));
$codeFiles = $generator->generate();
- foreach($codeFiles as $file) {
+ foreach ($codeFiles as $file) {
$file->save();
}
$expectedFiles = array_map(function($file) use ($testFile) {
return '@app' . substr($file, strlen($testFile) - 4);
- }, FileHelper::findFiles(substr($testFile, 0, -4), ['recursive' => true]));
- $actualFiles = array_map(function($file) use ($testFile) {
+ },
+ FileHelper::findFiles(substr($testFile, 0, -4), ['recursive' => true]));
+ $actualFiles = array_map(function($file) {
return '@app' . substr($file, strlen(Yii::getAlias('@app')));
- }, FileHelper::findFiles(Yii::getAlias('@app'), ['recursive' => true]));
+ },
+ FileHelper::findFiles(Yii::getAlias('@app'), ['recursive' => true]));
+
+ //Skip database-specific migrations
+ $expectedFiles = array_filter($expectedFiles,
+ function($file) {
+ return strpos($file, 'migrations_') === false;
+ });
+ $actualFiles = array_filter($actualFiles,
+ function($file) {
+ return strpos($file, 'migrations_') === false;
+ });
sort($expectedFiles);
sort($actualFiles);
$this->assertEquals($expectedFiles, $actualFiles);
- foreach($expectedFiles as $file) {
+ foreach ($expectedFiles as $file) {
$expectedFile = str_replace('@app', substr($testFile, 0, -4), $file);
$actualFile = str_replace('@app', Yii::getAlias('@app'), $file);
$this->assertFileExists($expectedFile);
@@ -66,4 +69,10 @@ public function testGenerate($testFile)
$this->assertFileEquals($expectedFile, $actualFile, "Failed asserting that file contents of\n$actualFile\nare equal to file contents of\n$expectedFile");
}
}
+
+ protected function createGenerator($configFile)
+ {
+ $config = require $configFile;
+ return new ApiGenerator($config);
+ }
}
diff --git a/tests/unit/MigrationsGeneratorTest.php b/tests/unit/MigrationsGeneratorTest.php
new file mode 100644
index 00000000..409cf77c
--- /dev/null
+++ b/tests/unit/MigrationsGeneratorTest.php
@@ -0,0 +1,87 @@
+prepareTempDir();
+ $this->mockApplication($this->mockDbSchemaAsEmpty());
+ $generator = new MigrationsGenerator();
+ $models = $generator->generate($dbModels);
+ $model = \array_values($models)[0];
+ self::assertInstanceOf(MigrationModel::class, $model);
+ self::assertEquals($expected[0]->fileName, $model->fileName);
+ self::assertEquals($expected[0]->dependencies, $model->dependencies);
+ self::assertCount(count($expected[0]->upCodes), $model->upCodes);
+ self::assertCount(count($expected[0]->downCodes), $model->downCodes);
+ self::assertEquals(trim($expected[0]->getUpCodeString()), trim($model->getUpCodeString()));
+ self::assertEquals(trim($expected[0]->getDownCodeString()), trim($model->getDownCodeString()));
+ }
+
+ public function tableSchemaStub(string $tableName):?TableSchema
+ {
+ $stub = [];
+ return $stub[$tableName] ?? null;
+ }
+
+ public function simpleDbModelsProvider():array
+ {
+ $dbModel = new DbModel([
+ 'name' => 'dummy',
+ 'tableName' => 'dummy',
+ 'attributes' => [
+ (new Attribute('id'))->setPhpType('int')->setDbType(Schema::TYPE_PK)
+ ->setRequired(true)->setReadOnly(true),
+ (new Attribute('title'))->setPhpType('string')
+ ->setDbType('string')
+ ->setUnique(true)
+ ->setSize(60)
+ ->setRequired(true),
+ (new Attribute('article'))->setPhpType('string')->setDbType('text')->setDefault(''),
+ ],
+ ]);
+ $codes = str_replace(PHP_EOL,
+ PHP_EOL . MigrationBuilder::INDENT,
+ VarDumper::export([
+ 'id' => '$this->primaryKey()',
+ 'title' => '$this->string(60)->notNull()->unique()',
+ 'article' => '$this->text()->null()->defaultValue("")',
+ ]));
+ $expect = new MigrationModel($dbModel, true, [
+ 'dependencies' => [],
+ 'upCodes' => [
+ "\$this->createTable('{{%dummy}}', $codes);",
+ ],
+ 'downCodes' => [
+ "\$this->dropTable('{{%dummy}}');",
+ ],
+ ]);
+ return [
+ [
+ [$dbModel],
+ [$expect],
+ ],
+ ];
+ }
+}
diff --git a/tests/unit/MultiDbFreshMigrationTest.php b/tests/unit/MultiDbFreshMigrationTest.php
new file mode 100644
index 00000000..0a86726b
--- /dev/null
+++ b/tests/unit/MultiDbFreshMigrationTest.php
@@ -0,0 +1,122 @@
+set('db', Yii::$app->maria);
+ $this->assertInstanceOf(MySqlSchema::class, Yii::$app->db->schema);
+ $testFile = Yii::getAlias('@specs/blog.php');
+ $this->runGenerator($testFile, $dbName);
+ $expectedFiles = $this->findExpectedFiles($testFile, $dbName);
+ $actualFiles = $this->findActualFiles();
+ $this->assertEquals($expectedFiles, $actualFiles);
+ $this->compareFiles($expectedFiles, $testFile);
+ }
+
+ public function testPostgres()
+ {
+ $dbName = 'pgsql';
+ Yii::$app->set('db', Yii::$app->pgsql);
+ $this->assertInstanceOf(PgSqlSchema::class, Yii::$app->db->schema);
+ $testFile = Yii::getAlias('@specs/blog.php');
+ $this->runGenerator($testFile, $dbName);
+ $expectedFiles = $this->findExpectedFiles($testFile, $dbName);
+ $actualFiles = $this->findActualFiles();
+ $this->assertEquals($expectedFiles, $actualFiles);
+ $this->compareFiles($expectedFiles, $testFile);
+ }
+
+ public function testMysql()
+ {
+ $dbName = 'mysql';
+ Yii::$app->set('db', Yii::$app->mysql);
+ $this->assertInstanceOf(MySqlSchema::class, Yii::$app->db->schema);
+ $testFile = Yii::getAlias('@specs/blog.php');
+ $this->runGenerator($testFile, $dbName);
+ $expectedFiles = $this->findExpectedFiles($testFile, $dbName);
+ $actualFiles = $this->findActualFiles();
+ $this->assertEquals($expectedFiles, $actualFiles);
+ $this->compareFiles($expectedFiles, $testFile);
+ }
+
+ protected function setUp()
+ {
+ if (getenv('IN_DOCKER') !== 'docker') {
+ $this->markTestSkipped('For docker env only');
+ }
+ $this->prepareTempDir();
+ $this->mockApplication();
+ parent::setUp();
+ }
+
+ protected function tearDown()
+ {
+ parent::tearDown();
+ if (getenv('IN_DOCKER') === 'docker') {
+ $this->destroyApplication();
+ }
+ }
+
+ protected function runGenerator($configFile, string $dbName)
+ {
+ $config = require $configFile;
+ $config['migrationPath'] = "@app/migrations_{$dbName}_db/";
+ $generator = new ApiGenerator($config);
+ self::assertTrue($generator->validate(), print_r($generator->getErrors(), true));
+
+ $codeFiles = $generator->generate();
+ foreach ($codeFiles as $file) {
+ $file->save();
+ }
+ }
+
+ protected function compareFiles(array $expected, string $testFile)
+ {
+ foreach ($expected as $file) {
+ $expectedFile = str_replace('@app', substr($testFile, 0, -4), $file);
+ $actualFile = str_replace('@app', Yii::getAlias('@app'), $file);
+ self::assertFileExists($expectedFile);
+ self::assertFileExists($actualFile);
+ $this->assertFileEquals($expectedFile, $actualFile, "Failed asserting that file contents of\n$actualFile\nare equal to file contents of\n$expectedFile");
+ }
+ }
+
+ protected function findActualFiles():array
+ {
+ $actualFiles = array_map(function($file) {
+ return '@app' . substr($file, strlen(Yii::getAlias('@app')));
+ },
+ FileHelper::findFiles(Yii::getAlias('@app'), ['recursive' => true]));
+ \sort($actualFiles);
+ return $actualFiles;
+ }
+
+ protected function findExpectedFiles(string $testFile, string $dbName):array
+ {
+ $expectedFiles = array_map(function($file) use ($testFile) {
+ return '@app' . substr($file, strlen($testFile) - 4);
+ },
+ FileHelper::findFiles(substr($testFile, 0, -4), ['recursive' => true]));
+
+ $expectedFiles = array_filter($expectedFiles,
+ function($file) use ($dbName) {
+ return strpos($file, 'models') !== false || strpos($file, $dbName) !== false;
+ });
+ \sort($expectedFiles);
+ return $expectedFiles;
+ }
+}
diff --git a/tests/unit/MultiDbSecondaryMigrationTest.php b/tests/unit/MultiDbSecondaryMigrationTest.php
new file mode 100644
index 00000000..0627ccc1
--- /dev/null
+++ b/tests/unit/MultiDbSecondaryMigrationTest.php
@@ -0,0 +1,123 @@
+set('db', Yii::$app->maria);
+ $this->assertInstanceOf(MySqlSchema::class, Yii::$app->db->schema);
+ $testFile = Yii::getAlias('@specs/blog_v2.php');
+ $this->runGenerator($testFile, $dbName);
+ $expectedFiles = $this->findExpectedFiles($testFile, $dbName);
+ $actualFiles = $this->findActualFiles();
+ $this->assertEquals($expectedFiles, $actualFiles);
+ $this->compareFiles($expectedFiles, $testFile);
+ }
+
+ public function testPostgres()
+ {
+ $dbName = 'pgsql';
+ Yii::$app->set('db', Yii::$app->pgsql);
+ $this->assertInstanceOf(PgSqlSchema::class, Yii::$app->db->schema);
+ $testFile = Yii::getAlias('@specs/blog_v2.php');
+ $this->runGenerator($testFile, $dbName);
+ $expectedFiles = $this->findExpectedFiles($testFile, $dbName);
+ $actualFiles = $this->findActualFiles();
+ $this->assertEquals($expectedFiles, $actualFiles);
+ $this->compareFiles($expectedFiles, $testFile);
+ }
+
+ public function testMysql()
+ {
+ $dbName = 'mysql';
+ Yii::$app->set('db', Yii::$app->mysql);
+ $this->assertInstanceOf(MySqlSchema::class, Yii::$app->db->schema);
+ $testFile = Yii::getAlias('@specs/blog_v2.php');
+ $this->runGenerator($testFile, $dbName);
+ $expectedFiles = $this->findExpectedFiles($testFile, $dbName);
+ $actualFiles = $this->findActualFiles();
+ $this->assertEquals($expectedFiles, $actualFiles);
+ $this->compareFiles($expectedFiles, $testFile);
+ }
+
+ protected function setUp()
+ {
+ if (getenv('IN_DOCKER') !== 'docker') {
+ $this->markTestSkipped('For docker env only');
+ }
+ $this->prepareTempDir();
+ $this->mockApplication();
+ parent::setUp();
+ }
+
+ protected function tearDown()
+ {
+ parent::tearDown();
+ if (getenv('IN_DOCKER') === 'docker') {
+ $this->destroyApplication();
+ }
+ }
+
+ protected function runGenerator($configFile, string $dbName)
+ {
+ $config = require $configFile;
+ $config['migrationPath'] = "@app/migrations_{$dbName}_db/";
+ $config['generateModels'] = false;
+ $generator = new ApiGenerator($config);
+ self::assertTrue($generator->validate(), print_r($generator->getErrors(), true));
+
+ $codeFiles = $generator->generate();
+ foreach ($codeFiles as $file) {
+ $file->save();
+ }
+ }
+
+ protected function compareFiles(array $expected, string $testFile)
+ {
+ foreach ($expected as $file) {
+ $expectedFile = str_replace('@app', substr($testFile, 0, -4), $file);
+ $actualFile = str_replace('@app', Yii::getAlias('@app'), $file);
+ self::assertFileExists($expectedFile);
+ self::assertFileExists($actualFile);
+ $this->assertFileEquals($expectedFile, $actualFile, "Failed asserting that file contents of\n$actualFile\nare equal to file contents of\n$expectedFile");
+ }
+ }
+
+ protected function findActualFiles():array
+ {
+ $actualFiles = array_map(function($file) {
+ return '@app' . substr($file, strlen(Yii::getAlias('@app')));
+ },
+ FileHelper::findFiles(Yii::getAlias('@app'), ['recursive' => true]));
+ \sort($actualFiles);
+ return $actualFiles;
+ }
+
+ protected function findExpectedFiles(string $testFile, string $dbName):array
+ {
+ $expectedFiles = array_map(function($file) use ($testFile) {
+ return '@app' . substr($file, strlen($testFile) - 4);
+ },
+ FileHelper::findFiles(substr($testFile, 0, -4), ['recursive' => true]));
+
+ $expectedFiles = array_filter($expectedFiles,
+ function($file) use ($dbName) {
+ return strpos($file, $dbName) !== false;
+ });
+ \sort($expectedFiles);
+ return $expectedFiles;
+ }
+}
diff --git a/tests/unit/TypeResolverTest.php b/tests/unit/TypeResolverTest.php
new file mode 100644
index 00000000..1850bd0d
--- /dev/null
+++ b/tests/unit/TypeResolverTest.php
@@ -0,0 +1,71 @@
+'integer', 'format'=>'int64']), true, YiiDbSchema::TYPE_BIGPK],
+ [new Schema(['type'=>'integer', 'format'=>'int32']), true, YiiDbSchema::TYPE_PK],
+ [new Schema(['type'=>'boolean']), false, YiiDbSchema::TYPE_BOOLEAN],
+ [new Schema(['type'=>'number', 'format'=>'float']), false, YiiDbSchema::TYPE_FLOAT],
+ [new Schema(['type'=>'number', 'format'=>'double']), false, YiiDbSchema::TYPE_DOUBLE],
+ [new Schema(['type'=>'integer', 'format'=>'date-time']), false, YiiDbSchema::TYPE_INTEGER],
+ [new Schema(['type'=>'string']), false, YiiDbSchema::TYPE_TEXT],
+ [new Schema(['type'=>'string', 'x-db-type'=>'varchar']), false, YiiDbSchema::TYPE_STRING],
+ [new Schema(['type'=>'string', 'x-db-type'=>'JSON']), false, YiiDbSchema::TYPE_JSON],
+ [new Schema(['type'=>'string', 'x-db-type'=>'tsvector']), false, 'tsvector'],
+ [new Schema(['type'=>'string', 'enum'=>['a', 'b', 'c']]), false, 'string'],
+ [new Schema(['type'=>'string', 'format'=>'email']), false, YiiDbSchema::TYPE_STRING],
+ [new Schema(['type'=>'string', 'maxLength'=>100]), false, YiiDbSchema::TYPE_STRING],
+ [new Schema(['type'=>'string', 'maxLength'=>10000]), false, YiiDbSchema::TYPE_TEXT],
+ [new Schema(['type'=>'string', 'format'=>'date-time']), false, YiiDbSchema::TYPE_DATETIME],
+ ];
+ }
+
+ /**
+ * @dataProvider forPhpTypeDataProvider
+ * @param \cebe\openapi\spec\Schema $property
+ * @param string $expected
+ */
+ public function testResolvePhpType(Schema $property, string $expected):void
+ {
+ self::assertEquals($expected, TypeResolver::schemaToPhpType($property));
+ }
+
+ public function forPhpTypeDataProvider():array
+ {
+ return [
+ [new Schema(['type'=>'integer', 'format'=>'int64']), 'int'],
+ [new Schema(['type'=>'integer']), 'int'],
+ [new Schema(['type'=>'boolean']), 'bool'],
+ [new Schema(['type'=>'number']), 'float'],
+ [new Schema(['type'=>'number', 'format'=>'float']), 'float'],
+ [new Schema(['type'=>'number', 'format'=>'double']), 'double'],
+ [new Schema(['type'=>'string']), 'string'],
+ [new Schema(['type'=>'string', 'format'=>'date']), 'string'],
+ [new Schema(['type'=>'array']), 'array'],
+ [new Schema(['type'=>'string', 'x-db-type'=>'json']), 'array'],
+ [new Schema(['type'=>'integer', 'x-db-type'=>'int[]']), 'array'],
+ ];
+ }
+
+}
\ No newline at end of file
diff --git a/tests/unit/ValidatorRulesBuilderTest.php b/tests/unit/ValidatorRulesBuilderTest.php
new file mode 100644
index 00000000..ac73b03e
--- /dev/null
+++ b/tests/unit/ValidatorRulesBuilderTest.php
@@ -0,0 +1,47 @@
+ 'dummy',
+ 'tableName' => 'dummy',
+ 'attributes' => [
+ (new Attribute('id'))->setPhpType('int')->setDbType(Schema::TYPE_PK)
+ ->setRequired(true)->setReadOnly(true),
+ (new Attribute('title'))->setPhpType('string')
+ ->setDbType('string')
+ ->setUnique(true)
+ ->setSize(60)
+ ->setRequired(true),
+ (new Attribute('article'))->setPhpType('string')->setDbType('text')->setDefault(''),
+ (new Attribute('active'))->setPhpType('bool')->setDbType('boolean'),
+ (new Attribute('category'))->asReference('Category')
+ ->setRequired(true)->setPhpType('int')->setDbType('integer')
+ ],
+ ]);
+ $expected = [
+ new ValidationRule(['title', 'article'], 'trim'),
+ new ValidationRule(['title', 'category_id'], 'required'),
+ new ValidationRule(['category_id'], 'integer'),
+ new ValidationRule(['category_id'], 'exist', ['targetRelation'=>'Category']),
+ new ValidationRule(['title', 'article'], 'string'),
+ new ValidationRule(['active'], 'boolean'),
+ ];
+
+ $rules = (new ValidationRulesBuilder($model))->build();
+ $this->assertEquals($expected, $rules);
+ }
+
+
+}
\ No newline at end of file
diff --git a/tests/unit/items/ValidationRuleTest.php b/tests/unit/items/ValidationRuleTest.php
new file mode 100644
index 00000000..d3c12a60
--- /dev/null
+++ b/tests/unit/items/ValidationRuleTest.php
@@ -0,0 +1,74 @@
+assertEquals($expected, (string)$rule);
+ }
+
+ public function testRulesToString()
+ {
+ $rules = [
+ new ValidationRule(['foo'], 'required'),
+ new ValidationRule(['foo', 'bar'], 'trim'),
+ ];
+ $result = implode(",\n", $rules);
+ $this->assertEquals($result, " [['foo'], 'required'],\n [['foo', 'bar'], 'trim']");
+ }
+
+ public function dataProvider():array
+ {
+ return [
+ [
+ new ValidationRule(['foo'], 'required'),
+ " [['foo'], 'required']",
+ ],
+ [
+ new ValidationRule(['foo', 'bar'], 'trim'),
+ " [['foo', 'bar'], 'trim']",
+ ],
+ [
+ new ValidationRule(['foo'], 'string', ['min' => 1, 'max' => 500]),
+ " [['foo'], 'string', 'min' => 1, 'max' => 500]",
+ ],
+ [
+ new ValidationRule(['foo'], 'default', ['value' => null]),
+ " [['foo'], 'default', 'value' => null]",
+ ],
+ [
+ new ValidationRule(['foo'], 'filter', ['filter' => 'intval', 'skipOnEmpty' => true]),
+ " [['foo'], 'filter', 'filter' => 'intval', 'skipOnEmpty' => true]",
+ ],
+ [
+ new ValidationRule(['foo'], 'in', ['range' => ['one', 'two', 'three']]),
+ " [['foo'], 'in', 'range' => ['one', 'two', 'three']]",
+ ],
+ [
+ new ValidationRule(['foo'], 'exist', ['targetAttribute' => ['a2', 'a1' => 'a3']]),
+ " [['foo'], 'exist', 'targetAttribute' => ['a2', 'a1' => 'a3']]",
+ ],
+ [
+ new ValidationRule(['foo'], 'filter', [
+ 'filter' => function($v) {
+ return strtolower($v);
+ },
+ ]),
+ " [['foo'], 'filter', 'filter' => '']",
+ ],
+ ];
+ }
+}
diff --git a/tests/yii b/tests/yii
new file mode 100755
index 00000000..6a694787
--- /dev/null
+++ b/tests/yii
@@ -0,0 +1,7 @@
+#!/usr/bin/env php
+run();
+exit($exitCode);