402 lines
13 KiB
PHP
402 lines
13 KiB
PHP
<?php namespace RainLab\Builder\Classes;
|
|
|
|
use Doctrine\DBAL\Types\Type;
|
|
use ApplicationException;
|
|
use ValidationException;
|
|
use SystemException;
|
|
use Exception;
|
|
use Validator;
|
|
use Lang;
|
|
use Schema;
|
|
use Str;
|
|
use Db;
|
|
|
|
/**
|
|
* Manages plugin database tables.
|
|
*
|
|
* @package rainlab\builder
|
|
* @author Alexey Bobkov, Samuel Georges
|
|
*/
|
|
class DatabaseTableModel extends BaseModel
|
|
{
|
|
public $columns = [];
|
|
|
|
/**
|
|
* @var string Specifies the database table model
|
|
*/
|
|
public $name;
|
|
|
|
protected static $fillable = [
|
|
'name',
|
|
'columns'
|
|
];
|
|
|
|
protected $validationRules = [
|
|
'name' => ['required', 'regex:/^[a-z]+[a-z0-9_]+$/', 'tablePrefix', 'uniqueTableName', 'max:64']
|
|
];
|
|
|
|
/**
|
|
* @var \Doctrine\DBAL\Schema\Table Table details loaded from the database.
|
|
*/
|
|
protected $tableInfo;
|
|
|
|
/**
|
|
* @var \Doctrine\DBAL\Schema\AbstractSchemaManager Contains the database schema
|
|
*/
|
|
protected static $schemaManager = null;
|
|
|
|
/**
|
|
* @var \Doctrine\DBAL\Schema\Schema Contains the database schema
|
|
*/
|
|
protected static $schema = null;
|
|
|
|
public static function listPluginTables($pluginCode)
|
|
{
|
|
$pluginCodeObj = new PluginCode($pluginCode);
|
|
$prefix = $pluginCodeObj->toDatabasePrefix();
|
|
|
|
$tables = self::getSchemaManager()->listTableNames();
|
|
|
|
return array_filter($tables, function($item) use($prefix) {
|
|
return Str::startsWith($item, $prefix);
|
|
});
|
|
}
|
|
|
|
public static function tableExists($name)
|
|
{
|
|
return self::getSchema()->hasTable($name);
|
|
}
|
|
|
|
/**
|
|
* Loads the table from the database.
|
|
* @param string $name Specifies the table name.
|
|
*/
|
|
public function load($name)
|
|
{
|
|
if (!self::tableExists($name)) {
|
|
throw new SystemException(sprintf('The table with name %s doesn\'t exist', $name));
|
|
}
|
|
|
|
$schema = self::getSchemaManager()->createSchema();
|
|
|
|
$this->name = $name;
|
|
$this->tableInfo = $schema->getTable($this->name);
|
|
$this->loadColumnsFromTableInfo();
|
|
$this->exists = true;
|
|
}
|
|
|
|
public function validate()
|
|
{
|
|
$pluginDbPrefix = $this->getPluginCodeObj()->toDatabasePrefix();
|
|
|
|
if (!strlen($pluginDbPrefix)) {
|
|
throw new SystemException('Error saving the table model - the plugin database prefix is not set for the object.');
|
|
}
|
|
|
|
$prefix = $pluginDbPrefix.'_';
|
|
|
|
$this->validationMessages = [
|
|
'name.table_prefix' => Lang::get('rainlab.builder::lang.database.error_table_name_invalid_prefix', [
|
|
'prefix' => $prefix
|
|
]),
|
|
'name.regex' => Lang::get('rainlab.builder::lang.database.error_table_name_invalid_characters'),
|
|
'name.unique_table_name' => Lang::get('rainlab.builder::lang.database.error_table_already_exists', ['name'=>$this->name]),
|
|
'name.max' => Lang::get('rainlab.builder::lang.database.error_table_name_too_long')
|
|
];
|
|
|
|
Validator::extend('tablePrefix', function($attribute, $value, $parameters) use ($prefix) {
|
|
$value = trim($value);
|
|
|
|
if (!Str::startsWith($value, $prefix)) {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
});
|
|
|
|
Validator::extend('uniqueTableName', function($attribute, $value, $parameters) {
|
|
$value = trim($value);
|
|
|
|
$schema = $this->getSchema();
|
|
if ($this->isNewModel()) {
|
|
return !$schema->hasTable($value);
|
|
}
|
|
|
|
if ($value != $this->tableInfo->getName()) {
|
|
return !$schema->hasTable($value);
|
|
}
|
|
|
|
return true;
|
|
});
|
|
|
|
$this->validateColumns();
|
|
|
|
return parent::validate();
|
|
}
|
|
|
|
public function generateCreateOrUpdateMigration()
|
|
{
|
|
$schemaCreator = new DatabaseTableSchemaCreator();
|
|
$existingSchema = $this->tableInfo;
|
|
$newTableName = $this->name;
|
|
$tableName = $existingSchema ? $existingSchema->getName() : $this->name;
|
|
|
|
$newSchema = $schemaCreator->createTableSchema($tableName, $this->columns);
|
|
|
|
$codeGenerator = new TableMigrationCodeGenerator();
|
|
$migrationCode = $codeGenerator->createOrUpdateTable($newSchema, $existingSchema, $newTableName);
|
|
if ($migrationCode === false) {
|
|
return $migrationCode;
|
|
}
|
|
|
|
$description = $existingSchema ? 'Updated table %s' : 'Created table %s';
|
|
return $this->createMigrationObject($migrationCode, sprintf($description, $tableName));
|
|
}
|
|
|
|
public function generateDropMigration()
|
|
{
|
|
$existingSchema = $this->tableInfo;
|
|
$codeGenerator = new TableMigrationCodeGenerator();
|
|
$migrationCode = $codeGenerator->dropTable($existingSchema);
|
|
|
|
return $this->createMigrationObject($migrationCode, sprintf('Drop table %s', $this->name));
|
|
}
|
|
|
|
public static function getSchema()
|
|
{
|
|
if (!self::$schema) {
|
|
self::$schema = self::getSchemaManager()->createSchema();
|
|
}
|
|
|
|
return self::$schema;
|
|
}
|
|
|
|
protected function validateColumns()
|
|
{
|
|
$this->validateColumnNameLengths();
|
|
$this->validateDupicateColumns();
|
|
$this->validateDubplicatePrimaryKeys();
|
|
$this->validateAutoIncrementColumns();
|
|
$this->validateColumnsLengthParameter();
|
|
$this->validateUnsignedColumns();
|
|
$this->validateDefaultValues();
|
|
}
|
|
|
|
protected function validateColumnNameLengths()
|
|
{
|
|
foreach ($this->columns as $column) {
|
|
$name = trim($column['name']);
|
|
|
|
if (Str::length($name) > 64) {
|
|
throw new ValidationException([
|
|
'columns' => Lang::get('rainlab.builder::lang.database.error_column_name_too_long',
|
|
['column' => $name]
|
|
)
|
|
]);
|
|
}
|
|
}
|
|
}
|
|
|
|
protected function validateDupicateColumns()
|
|
{
|
|
foreach ($this->columns as $outerIndex=>$outerColumn) {
|
|
foreach ($this->columns as $innerIndex=>$innerColumn) {
|
|
if ($innerIndex != $outerIndex && $innerColumn['name'] == $outerColumn['name']) {
|
|
throw new ValidationException([
|
|
'columns' => Lang::get('rainlab.builder::lang.database.error_table_duplicate_column',
|
|
['column' => $outerColumn['name']]
|
|
)
|
|
]);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
protected function validateDubplicatePrimaryKeys()
|
|
{
|
|
$keysFound = 0;
|
|
$autoIncrementsFound = 0;
|
|
foreach ($this->columns as $column) {
|
|
if ($column['primary_key']) {
|
|
$keysFound++;
|
|
}
|
|
|
|
if ($column['auto_increment']) {
|
|
$autoIncrementsFound++;
|
|
}
|
|
}
|
|
|
|
if ($keysFound > 1 && $autoIncrementsFound) {
|
|
throw new ValidationException([
|
|
'columns' => Lang::get('rainlab.builder::lang.database.error_table_auto_increment_in_compound_pk')
|
|
]);
|
|
}
|
|
}
|
|
|
|
protected function validateAutoIncrementColumns()
|
|
{
|
|
$autoIncrement = null;
|
|
foreach ($this->columns as $column) {
|
|
if (!$column['auto_increment']) {
|
|
continue;
|
|
}
|
|
|
|
if ($autoIncrement) {
|
|
throw new ValidationException([
|
|
'columns' => Lang::get('rainlab.builder::lang.database.error_table_mutliple_auto_increment')
|
|
]);
|
|
}
|
|
|
|
$autoIncrement = $column;
|
|
}
|
|
|
|
if (!$autoIncrement) {
|
|
return;
|
|
}
|
|
|
|
if (!in_array($autoIncrement['type'], MigrationColumnType::getIntegerTypes())) {
|
|
throw new ValidationException([
|
|
'columns' => Lang::get('rainlab.builder::lang.database.error_table_auto_increment_non_integer')
|
|
]);
|
|
}
|
|
}
|
|
|
|
protected function validateUnsignedColumns()
|
|
{
|
|
foreach ($this->columns as $column) {
|
|
if (!$column['unsigned']) {
|
|
continue;
|
|
}
|
|
|
|
if (!in_array($column['type'], MigrationColumnType::getIntegerTypes())) {
|
|
throw new ValidationException([
|
|
'columns' => Lang::get('rainlab.builder::lang.database.error_unsigned_type_not_int', ['column'=>$column['name']])
|
|
]);
|
|
}
|
|
}
|
|
}
|
|
|
|
protected function validateColumnsLengthParameter()
|
|
{
|
|
foreach ($this->columns as $column) {
|
|
try {
|
|
MigrationColumnType::validateLength($column['type'], $column['length']);
|
|
}
|
|
catch (Exception $ex) {
|
|
throw new ValidationException([
|
|
'columns' => $ex->getMessage()
|
|
]);
|
|
}
|
|
}
|
|
}
|
|
|
|
protected function validateDefaultValues()
|
|
{
|
|
foreach ($this->columns as $column) {
|
|
if (!strlen($column['default'])) {
|
|
continue;
|
|
}
|
|
|
|
$default = trim($column['default']);
|
|
|
|
if (in_array($column['type'], MigrationColumnType::getIntegerTypes())) {
|
|
if (!preg_match('/^\-?[0-9]+$/', $default)) {
|
|
throw new ValidationException([
|
|
'columns' => Lang::get('rainlab.builder::lang.database.error_integer_default_value', ['column'=>$column['name']])
|
|
]);
|
|
}
|
|
|
|
if ($column['unsigned'] && $default < 0) {
|
|
throw new ValidationException([
|
|
'columns' => Lang::get('rainlab.builder::lang.database.error_unsigned_negative_value', ['column'=>$column['name']])
|
|
]);
|
|
}
|
|
|
|
continue;
|
|
}
|
|
|
|
if (in_array($column['type'], MigrationColumnType::getDecimalTypes())) {
|
|
if (!preg_match('/^\-?([0-9]+\.[0-9]+|[0-9]+)$/', $default)) {
|
|
throw new ValidationException([
|
|
'columns' => Lang::get('rainlab.builder::lang.database.error_decimal_default_value', ['column'=>$column['name']])
|
|
]);
|
|
}
|
|
|
|
continue;
|
|
}
|
|
|
|
if ($column['type'] == MigrationColumnType::TYPE_BOOLEAN) {
|
|
if (!preg_match('/^0|1$/', $default)) {
|
|
throw new ValidationException([
|
|
'columns' => Lang::get('rainlab.builder::lang.database.error_boolean_default_value', ['column'=>$column['name']])
|
|
]);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
protected static function getSchemaManager()
|
|
{
|
|
if (!self::$schemaManager) {
|
|
self::$schemaManager = Schema::getConnection()->getDoctrineSchemaManager();
|
|
|
|
Type::addType('enumdbtype', 'RainLab\Builder\Classes\EnumDbType');
|
|
|
|
// Fixes the problem with enum column type not supported
|
|
// by Doctrine (https://github.com/laravel/framework/issues/1346)
|
|
$platform = self::$schemaManager->getDatabasePlatform();
|
|
$platform->registerDoctrineTypeMapping('enum', 'enumdbtype');
|
|
$platform->registerDoctrineTypeMapping('json', 'text');
|
|
}
|
|
|
|
return self::$schemaManager;
|
|
}
|
|
|
|
protected function loadColumnsFromTableInfo()
|
|
{
|
|
$this->columns = [];
|
|
$columns = $this->tableInfo->getColumns();
|
|
|
|
$primaryKey = $this->tableInfo->getPrimaryKey();
|
|
$primaryKeyColumns =[];
|
|
if ($primaryKey) {
|
|
$primaryKeyColumns = $primaryKey->getColumns();
|
|
}
|
|
|
|
foreach ($columns as $column) {
|
|
$columnName = $column->getName();
|
|
$typeName = $column->getType()->getName();
|
|
|
|
if ($typeName == EnumDbType::TYPENAME) {
|
|
throw new ApplicationException(Lang::get('rainlab.builder::lang.database.error_enum_not_supported'));
|
|
}
|
|
|
|
$item = [
|
|
'name' => $columnName,
|
|
'type' => MigrationColumnType::toMigrationMethodName($typeName, $columnName),
|
|
'length' => MigrationColumnType::doctrineLengthToMigrationLength($column),
|
|
'unsigned' => $column->getUnsigned(),
|
|
'allow_null' => !$column->getNotnull(),
|
|
'auto_increment' => $column->getAutoincrement(),
|
|
'primary_key' => in_array($columnName, $primaryKeyColumns),
|
|
'default' => $column->getDefault(),
|
|
'id' => $columnName,
|
|
];
|
|
|
|
$this->columns[] = $item;
|
|
}
|
|
}
|
|
|
|
protected function createMigrationObject($code, $description)
|
|
{
|
|
$migration = new MigrationModel();
|
|
$migration->setPluginCodeObj($this->getPluginCodeObj());
|
|
|
|
$migration->code = $code;
|
|
$migration->version = $migration->getNextVersion();
|
|
$migration->description = $description;
|
|
|
|
return $migration;
|
|
}
|
|
}
|