<?php
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @since 3.0.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Database\Schema;
use Cake\Database\Connection;
use Cake\Database\Exception;
use Cake\Database\Type;
/**
* Represents a single table in a database schema.
*
* Can either be populated using the reflection API's
* or by incrementally building an instance using
* methods.
*
* Once created TableSchema instances can be added to
* Schema\Collection objects. They can also be converted into SQL using the
* createSql(), dropSql() and truncateSql() methods.
*/
class TableSchema implements TableSchemaInterface, SqlGeneratorInterface
{
/**
* The name of the table
*
* @var string
*/
protected $_table;
/**
* Columns in the table.
*
* @var array
*/
protected $_columns = [];
/**
* A map with columns to types
*
* @var array
*/
protected $_typeMap = [];
/**
* Indexes in the table.
*
* @var array
*/
protected $_indexes = [];
/**
* Constraints in the table.
*
* @var array
*/
protected $_constraints = [];
/**
* Options for the table.
*
* @var array
*/
protected $_options = [];
/**
* Whether or not the table is temporary
*
* @var bool
*/
protected $_temporary = false;
/**
* Column length when using a `tiny` column type
*
* @var int
*/
const LENGTH_TINY = 255;
/**
* Column length when using a `medium` column type
*
* @var int
*/
const LENGTH_MEDIUM = 16777215;
/**
* Column length when using a `long` column type
*
* @var int
*/
const LENGTH_LONG = 4294967295;
/**
* Valid column length that can be used with text type columns
*
* @var array
*/
public static $columnLengths = [
'tiny' => self::LENGTH_TINY,
'medium' => self::LENGTH_MEDIUM,
'long' => self::LENGTH_LONG
];
/**
* The valid keys that can be used in a column
* definition.
*
* @var array
*/
protected static $_columnKeys = [
'type' => null,
'baseType' => null,
'length' => null,
'precision' => null,
'null' => null,
'default' => null,
'comment' => null,
];
/**
* Additional type specific properties.
*
* @var array
*/
protected static $_columnExtras = [
'string' => [
'fixed' => null,
'collate' => null,
],
'text' => [
'collate' => null,
],
'tinyinteger' => [
'unsigned' => null,
],
'smallinteger' => [
'unsigned' => null,
],
'integer' => [
'unsigned' => null,
'autoIncrement' => null,
],
'biginteger' => [
'unsigned' => null,
'autoIncrement' => null,
],
'decimal' => [
'unsigned' => null,
],
'float' => [
'unsigned' => null,
],
];
/**
* The valid keys that can be used in an index
* definition.
*
* @var array
*/
protected static $_indexKeys = [
'type' => null,
'columns' => [],
'length' => [],
'references' => [],
'update' => 'restrict',
'delete' => 'restrict',
];
/**
* Names of the valid index types.
*
* @var array
*/
protected static $_validIndexTypes = [
self::INDEX_INDEX,
self::INDEX_FULLTEXT,
];
/**
* Names of the valid constraint types.
*
* @var array
*/
protected static $_validConstraintTypes = [
self::CONSTRAINT_PRIMARY,
self::CONSTRAINT_UNIQUE,
self::CONSTRAINT_FOREIGN,
];
/**
* Names of the valid foreign key actions.
*
* @var array
*/
protected static $_validForeignKeyActions = [
self::ACTION_CASCADE,
self::ACTION_SET_NULL,
self::ACTION_SET_DEFAULT,
self::ACTION_NO_ACTION,
self::ACTION_RESTRICT,
];
/**
* Primary constraint type
*
* @var string
*/
const CONSTRAINT_PRIMARY = 'primary';
/**
* Unique constraint type
*
* @var string
*/
const CONSTRAINT_UNIQUE = 'unique';
/**
* Foreign constraint type
*
* @var string
*/
const CONSTRAINT_FOREIGN = 'foreign';
/**
* Index - index type
*
* @var string
*/
const INDEX_INDEX = 'index';
/**
* Fulltext index type
*
* @var string
*/
const INDEX_FULLTEXT = 'fulltext';
/**
* Foreign key cascade action
*
* @var string
*/
const ACTION_CASCADE = 'cascade';
/**
* Foreign key set null action
*
* @var string
*/
const ACTION_SET_NULL = 'setNull';
/**
* Foreign key no action
*
* @var string
*/
const ACTION_NO_ACTION = 'noAction';
/**
* Foreign key restrict action
*
* @var string
*/
const ACTION_RESTRICT = 'restrict';
/**
* Foreign key restrict default
*
* @var string
*/
const ACTION_SET_DEFAULT = 'setDefault';
/**
* Constructor.
*
* @param string $table The table name.
* @param array $columns The list of columns for the schema.
*/
public function __construct($table, array $columns = [])
{
$this->_table = $table;
foreach ($columns as $field => $definition) {
$this->addColumn($field, $definition);
}
}
/**
* {@inheritDoc}
*/
public function name()
{
return $this->_table;
}
/**
* {@inheritDoc}
*/
public function addColumn($name, $attrs)
{
if (is_string($attrs)) {
$attrs = ['type' => $attrs];
}
$valid = static::$_columnKeys;
if (isset(static::$_columnExtras[$attrs['type']])) {
$valid += static::$_columnExtras[$attrs['type']];
}
$attrs = array_intersect_key($attrs, $valid);
$this->_columns[$name] = $attrs + $valid;
$this->_typeMap[$name] = $this->_columns[$name]['type'];
return $this;
}
/**
* {@inheritDoc}
*/
public function removeColumn($name)
{
unset($this->_columns[$name], $this->_typeMap[$name]);
return $this;
}
/**
* {@inheritDoc}
*/
public function columns()
{
return array_keys($this->_columns);
}
/**
* Get column data in the table.
*
* @param string $name The column name.
* @return array|null Column data or null.
* @deprecated 3.5.0 Use getColumn() instead.
*/
public function column($name)
{
deprecationWarning('TableSchema::column() is deprecated. Use TableSchema::getColumn() instead.');
return $this->getColumn($name);
}
/**
* {@inheritDoc}
*/
public function getColumn($name)
{
if (!isset($this->_columns[$name])) {
return null;
}
$column = $this->_columns[$name];
unset($column['baseType']);
return $column;
}
/**
* Sets the type of a column, or returns its current type
* if none is passed.
*
* @param string $name The column to get the type of.
* @param string|null $type The type to set the column to.
* @return string|null Either the column type or null.
* @deprecated 3.5.0 Use setColumnType()/getColumnType() instead.
*/
public function columnType($name, $type = null)
{
deprecationWarning('TableSchema::columnType() is deprecated. Use TableSchema::setColumnType() or TableSchema::getColumnType() instead.');
if ($type !== null) {
$this->setColumnType($name, $type);
}
return $this->getColumnType($name);
}
/**
* {@inheritDoc}
*/
public function getColumnType($name)
{
if (!isset($this->_columns[$name])) {
return null;
}
return $this->_columns[$name]['type'];
}
/**
* {@inheritDoc}
*/
public function setColumnType($name, $type)
{
if (!isset($this->_columns[$name])) {
return $this;
}
$this->_columns[$name]['type'] = $type;
$this->_typeMap[$name] = $type;
return $this;
}
/**
* {@inheritDoc}
*/
public function hasColumn($name)
{
return isset($this->_columns[$name]);
}
/**
* {@inheritDoc}
*/
public function baseColumnType($column)
{
if (isset($this->_columns[$column]['baseType'])) {
return $this->_columns[$column]['baseType'];
}
$type = $this->getColumnType($column);
if ($type === null) {
return null;
}
if (Type::getMap($type)) {
$type = Type::build($type)->getBaseType();
}
return $this->_columns[$column]['baseType'] = $type;
}
/**
* {@inheritDoc}
*/
public function typeMap()
{
return $this->_typeMap;
}
/**
* {@inheritDoc}
*/
public function isNullable($name)
{
if (!isset($this->_columns[$name])) {
return true;
}
return ($this->_columns[$name]['null'] === true);
}
/**
* {@inheritDoc}
*/
public function defaultValues()
{
$defaults = [];
foreach ($this->_columns as $name => $data) {
if (!array_key_exists('default', $data)) {
continue;
}
if ($data['default'] === null && $data['null'] !== true) {
continue;
}
$defaults[$name] = $data['default'];
}
return $defaults;
}
/**
* {@inheritDoc}
* @throws \Cake\Database\Exception
*/
public function addIndex($name, $attrs)
{
if (is_string($attrs)) {
$attrs = ['type' => $attrs];
}
$attrs = array_intersect_key($attrs, static::$_indexKeys);
$attrs += static::$_indexKeys;
unset($attrs['references'], $attrs['update'], $attrs['delete']);
if (!in_array($attrs['type'], static::$_validIndexTypes, true)) {
throw new Exception(sprintf('Invalid index type "%s" in index "%s" in table "%s".', $attrs['type'], $name, $this->_table));
}
if (empty($attrs['columns'])) {
throw new Exception(sprintf('Index "%s" in table "%s" must have at least one column.', $name, $this->_table));
}
$attrs['columns'] = (array)$attrs['columns'];
foreach ($attrs['columns'] as $field) {
if (empty($this->_columns[$field])) {
$msg = sprintf(
'Columns used in index "%s" in table "%s" must be added to the Table schema first. ' .
'The column "%s" was not found.',
$name,
$this->_table,
$field
);
throw new Exception($msg);
}
}
$this->_indexes[$name] = $attrs;
return $this;
}
/**
* {@inheritDoc}
*/
public function indexes()
{
return array_keys($this->_indexes);
}
/**
* Read information about an index based on name.
*
* @param string $name The name of the index.
* @return array|null Array of index data, or null
* @deprecated 3.5.0 Use getIndex() instead.
*/
public function index($name)
{
deprecationWarning('TableSchema::index() is deprecated. Use TableSchema::getIndex() instead.');
return $this->getIndex($name);
}
/**
* {@inheritDoc}
*/
public function getIndex($name)
{
if (!isset($this->_indexes[$name])) {
return null;
}
return $this->_indexes[$name];
}
/**
* {@inheritDoc}
*/
public function primaryKey()
{
foreach ($this->_constraints as $name => $data) {
if ($data['type'] === static::CONSTRAINT_PRIMARY) {
return $data['columns'];
}
}
return [];
}
/**
* {@inheritDoc}
* @throws \Cake\Database\Exception
*/
public function addConstraint($name, $attrs)
{
if (is_string($attrs)) {
$attrs = ['type' => $attrs];
}
$attrs = array_intersect_key($attrs, static::$_indexKeys);
$attrs += static::$_indexKeys;
if (!in_array($attrs['type'], static::$_validConstraintTypes, true)) {
throw new Exception(sprintf('Invalid constraint type "%s" in table "%s".', $attrs['type'], $this->_table));
}
if (empty($attrs['columns'])) {
throw new Exception(sprintf('Constraints in table "%s" must have at least one column.', $this->_table));
}
$attrs['columns'] = (array)$attrs['columns'];
foreach ($attrs['columns'] as $field) {
if (empty($this->_columns[$field])) {
$msg = sprintf(
'Columns used in constraints must be added to the Table schema first. ' .
'The column "%s" was not found in table "%s".',
$field,
$this->_table
);
throw new Exception($msg);
}
}
if ($attrs['type'] === static::CONSTRAINT_FOREIGN) {
$attrs = $this->_checkForeignKey($attrs);
if (isset($this->_constraints[$name])) {
$this->_constraints[$name]['columns'] = array_unique(array_merge(
$this->_constraints[$name]['columns'],
$attrs['columns']
));
if (isset($this->_constraints[$name]['references'])) {
$this->_constraints[$name]['references'][1] = array_unique(array_merge(
(array)$this->_constraints[$name]['references'][1],
[$attrs['references'][1]]
));
}
return $this;
}
} else {
unset($attrs['references'], $attrs['update'], $attrs['delete']);
}
$this->_constraints[$name] = $attrs;
return $this;
}
/**
* {@inheritDoc}
*/
public function dropConstraint($name)
{
if (isset($this->_constraints[$name])) {
unset($this->_constraints[$name]);
}
return $this;
}
/**
* Check whether or not a table has an autoIncrement column defined.
*
* @return bool
*/
public function hasAutoincrement()
{
foreach ($this->_columns as $column) {
if (isset($column['autoIncrement']) && $column['autoIncrement']) {
return true;
}
}
return false;
}
/**
* Helper method to check/validate foreign keys.
*
* @param array $attrs Attributes to set.
* @return array
* @throws \Cake\Database\Exception When foreign key definition is not valid.
*/
protected function _checkForeignKey($attrs)
{
if (count($attrs['references']) < 2) {
throw new Exception('References must contain a table and column.');
}
if (!in_array($attrs['update'], static::$_validForeignKeyActions)) {
throw new Exception(sprintf('Update action is invalid. Must be one of %s', implode(',', static::$_validForeignKeyActions)));
}
if (!in_array($attrs['delete'], static::$_validForeignKeyActions)) {
throw new Exception(sprintf('Delete action is invalid. Must be one of %s', implode(',', static::$_validForeignKeyActions)));
}
return $attrs;
}
/**
* {@inheritDoc}
*/
public function constraints()
{
return array_keys($this->_constraints);
}
/**
* Read information about a constraint based on name.
*
* @param string $name The name of the constraint.
* @return array|null Array of constraint data, or null
* @deprecated 3.5.0 Use getConstraint() instead.
*/
public function constraint($name)
{
deprecationWarning('TableSchema::constraint() is deprecated. Use TableSchema::getConstraint() instead.');
return $this->getConstraint($name);
}
/**
* {@inheritDoc}
*/
public function getConstraint($name)
{
if (!isset($this->_constraints[$name])) {
return null;
}
return $this->_constraints[$name];
}
/**
* {@inheritDoc}
*/
public function setOptions($options)
{
$this->_options = array_merge($this->_options, $options);
return $this;
}
/**
* {@inheritDoc}
*/
public function getOptions()
{
return $this->_options;
}
/**
* Get/set the options for a table.
*
* Table options allow you to set platform specific table level options.
* For example the engine type in MySQL.
*
* @deprecated 3.4.0 Use setOptions()/getOptions() instead.
* @param array|null $options The options to set, or null to read options.
* @return $this|array Either the TableSchema instance, or an array of options when reading.
*/
public function options($options = null)
{
deprecationWarning('TableSchema::options() is deprecated. Use TableSchema::setOptions() or TableSchema::getOptions() instead.');
if ($options !== null) {
return $this->setOptions($options);
}
return $this->getOptions();
}
/**
* {@inheritDoc}
*/
public function setTemporary($temporary)
{
$this->_temporary = (bool)$temporary;
return $this;
}
/**
* {@inheritDoc}
*/
public function isTemporary()
{
return $this->_temporary;
}
/**
* Get/Set whether the table is temporary in the database
*
* @deprecated 3.4.0 Use setTemporary()/isTemporary() instead.
* @param bool|null $temporary whether or not the table is to be temporary
* @return $this|bool Either the TableSchema instance, the current temporary setting
*/
public function temporary($temporary = null)
{
deprecationWarning(
'TableSchema::temporary() is deprecated. ' .
'Use TableSchema::setTemporary()/isTemporary() instead.'
);
if ($temporary !== null) {
return $this->setTemporary($temporary);
}
return $this->isTemporary();
}
/**
* {@inheritDoc}
*/
public function createSql(Connection $connection)
{
$dialect = $connection->getDriver()->schemaDialect();
$columns = $constraints = $indexes = [];
foreach (array_keys($this->_columns) as $name) {
$columns[] = $dialect->columnSql($this, $name);
}
foreach (array_keys($this->_constraints) as $name) {
$constraints[] = $dialect->constraintSql($this, $name);
}
foreach (array_keys($this->_indexes) as $name) {
$indexes[] = $dialect->indexSql($this, $name);
}
return $dialect->createTableSql($this, $columns, $constraints, $indexes);
}
/**
* {@inheritDoc}
*/
public function dropSql(Connection $connection)
{
$dialect = $connection->getDriver()->schemaDialect();
return $dialect->dropTableSql($this);
}
/**
* {@inheritDoc}
*/
public function truncateSql(Connection $connection)
{
$dialect = $connection->getDriver()->schemaDialect();
return $dialect->truncateTableSql($this);
}
/**
* {@inheritDoc}
*/
public function addConstraintSql(Connection $connection)
{
$dialect = $connection->getDriver()->schemaDialect();
return $dialect->addConstraintSql($this);
}
/**
* {@inheritDoc}
*/
public function dropConstraintSql(Connection $connection)
{
$dialect = $connection->getDriver()->schemaDialect();
return $dialect->dropConstraintSql($this);
}
/**
* Returns an array of the table schema.
*
* @return array
*/
public function __debugInfo()
{
return [
'table' => $this->_table,
'columns' => $this->_columns,
'indexes' => $this->_indexes,
'constraints' => $this->_constraints,
'options' => $this->_options,
'typeMap' => $this->_typeMap,
'temporary' => $this->_temporary,
];
}
}
// @deprecated 3.4.0 Add backwards compat alias.
class_alias('Cake\Database\Schema\TableSchema', 'Cake\Database\Schema\Table');