diff --git a/src/DBAL/ArrayType.php b/src/DBAL/ArrayType.php new file mode 100644 index 0000000..2c09e50 --- /dev/null +++ b/src/DBAL/ArrayType.php @@ -0,0 +1,141 @@ +members =& $properties[3]; + } + + // Get the Enum array as keys, values contains the index numbers + public function asKeys() // AKA flip() + { + return array_flip($this->members); + } + + // Get the Enum array with both keys AND values + public function asBoth() + { + return array_combine($this->members, $this->members); + } + + // pass null to the component for members to use + public function combine(array $keys = null, array $values = null) + { + return array_combine($keys ?: $this->members, $values ?: $this->members); + } + + // Creates a new array with Enum as keys, and $arr as values + public function combineWithValues(array $values) + { + return array_combine($this->members, $values); + } + + // Creates a new array with Enum as values, and $arr as keys + public function combineWithKeys(array $keys) + { + return array_combine($keys, $this->members); + } + + // Passed array must contain the same keys as our member array, used to verify that our array contains valid Enum entries + public function verifyKeys(array $arr) + { + // TODO + } + + // Passed array must contain the same Values as our member array, used to verify that our array contains valid Enum entries + public function verifyValues(array $arr) + { + // TODO + } + + + /** + * Iterator interface + */ + public function rewind() + { + return reset($this->members); + } + public function current() + { + return current($this->members); + } + public function key() + { + return key($this->members); + } + public function next() + { + return next($this->members); + } + public function valid() + { + return key($this->members) !== null; + } + + + /** + * Countable interface + */ + public function count() + { + return count($this->members); + } + + + // WARNING: I think I should switch the definition of `get`, + // if we query $obj['my_enum_value'] I think it should return the Enum index number ? + /** + * ArrayAccess interface + */ + public function offsetGet($idx) // eg. var_dump($obj['two']); + { + return $this->members[$idx]; + } + public function offsetSet($idx, $value) // eg. $obj['two'] = 'A value'; + { + throw new \Exception('Cannot set an Array Type value! This object is Immutable!'); + $this->members[$idx] = $value; + } + public function offsetExists($idx) // eg. isset($obj['two']) + { + return isset($this->members[$idx]); + } + public function offsetUnset($idx) // eg. unset($obj['two']); + { + throw new \Exception('Cannot unset an Array Type value! This object is Immutable!'); + unset($this->members[$idx]); + } + + + /** + * Test if a value is part of the Enum or Set + * This function is case-sensitive! + * TODO: Sets can include multiple values eg. VALUE1 | VALUE2 etc. + * We need to validate the whole set! + */ + function isValid($value) + { + return in_array($value, $this->members); + } + /** + * Test if a value is part of the Enum or Set + * This function is case-sensitive! + * TODO: Sets can include multiple values eg. VALUE1 | VALUE2 etc. + * We need to validate the whole set! + */ + function isMember($value) + { + return in_array($value, $this->members); + } +} diff --git a/src/DBAL/Table.php b/src/DBAL/Table.php index 56388d7..259a6ab 100644 --- a/src/DBAL/Table.php +++ b/src/DBAL/Table.php @@ -2,8 +2,6 @@ namespace Twister\DBAL; -use Twister\DBAL\Types; - class Table { protected $fields = null; @@ -18,86 +16,20 @@ class Table return $this->fields[$field]; } - public static function build($db, string $cache, string $schema, $table = null) + // $statement = 'UPDATE' | 'INSERT' ... UPDATE = partial check, INSERT = FULL (required non-nullable fields) check! + public function validate($statement) { - mysqli_report( MYSQLI_REPORT_STRICT ); - - $sql = 'SELECT ' . - 'TABLE_NAME,' . - 'COLUMN_NAME,' . - 'DATA_TYPE,' . // varchar - 'COLUMN_DEFAULT,' . - // 'NULLIF(IS_NULLABLE="YES",0),' .// NULL / 1 alternative - 'NULLIF(IS_NULLABLE,"NO"),' . // NULL / YES - // 'IS_NULLABLE,' . // NO / YES - 'COALESCE(CHARACTER_MAXIMUM_LENGTH, NUMERIC_SCALE) AS scale,' . // These two columns data NEVER overlap! - // 'CHARACTER_MAXIMUM_LENGTH,' . - // 'CHARACTER_OCTET_LENGTH,' . // eg. varchar(32) == CHARACTER_MAXIMUM_LENGTH = 32 && CHARACTER_OCTET_LENGTH = 96 - // 'NUMERIC_PRECISION,' . // eg. decimal(9,5) == NUMERIC_PRECISION = 9 && NUMERIC_SCALE = 5 - // 'NUMERIC_SCALE,' . // 5 (the decimal values, or 0 for integers) - 'CHARACTER_SET_NAME,' . // utf8 - // 'COLLATION_NAME,' . // utf8_general_ci - 'COLUMN_TYPE' . // int(10) unsigned / enum('','right','left') - // 'COLUMN_KEY,' . // PRI / UNI / MUL - // 'EXTRA' . // auto_increment / on update CURRENT_TIMESTAMP - ' FROM INFORMATION_SCHEMA.COLUMNS' . - ' WHERE TABLE_SCHEMA = "' . $schema . ($table ? (is_array($table) ? '" AND TABLE_NAME IN ("' . implode('","', $table) . '")' : '" AND TABLE_NAME = "' . $table) : null) . '"'; - ' ORDER BY TABLE_NAME, ORDINAL_POSITION'; - - $results = null; + return $this->fields[$field]; + } - $rows = $db->query($sql)->fetch_all(MYSQLI_NUM); // MYSQLI_ASSOC || MYSQLI_NUM - foreach ($rows as $row) - { - //$field = [$row[2], $row[3], $row[4] !== null]; - //... would have added more to the array ... but decided to make this realtime only for now! - //$results[$row[0]][$row[1]] = &$field; - $unsigned = strpos($row[7], 'unsigned') !== false; - switch($row[2]) - { - case 'int': $obj = new IntegerType('int', $row[3], $row[4] !== null, $unsigned ? 0 : -2147483648, $unsigned ? 4294967295 : 2147483647); - case 'tinyint': $obj = new IntegerType('tinyint', $row[3], $row[4] !== null, $unsigned ? 0 : -128, $unsigned ? 255 : 127); - case 'float': $obj = new FloatType('float', $row[3], $row[4] !== null, $row[5]); - case 'varchar': $obj = new StringType('varchar', $row[3], $row[4] !== null, $row[5], $row[6]); - case 'smallint': $obj = new IntegerType('smallint', $row[3], $row[4] !== null, $unsigned ? 0 : -32768, $unsigned ? 65535 : 32767); - case 'enum': $obj = new ArrayType('enum', $row[3], $row[4] !== null, $row[3]); - case 'mediumint': $obj = new IntegerType('mediumint', $row[3], $row[4] !== null, $unsigned ? 0 : -8388608, $unsigned ? 16777215 : 8388607); - case 'date': $obj = new DateType('date', $row[3], $row[4] !== null); - case 'bit': $obj = new IntegerType('bit', $row[3], $row[4] !== null, 0, 1); - case 'char': $obj = new StringType('char', $row[3], $row[4] !== null, $row[5], $row[6]); - case 'text': $obj = new StringType('text', $row[3], $row[4] !== null, 65535, $row[6]); - case 'timestamp': $obj = new DateType('timestamp', $row[3], $row[4] !== null); - case 'binary': $obj = new StringType('binary', $row[3], $row[4] !== null, $row[5], 0); - case 'double': $obj = new FloatType('double', $row[3], $row[4] !== null, $row[5]); - case 'tinytext': $obj = new StringType('tinytext', $row[3], $row[4] !== null, 255, $row[6]); - case 'set': $obj = new ArrayType('set', $row[3], $row[4] !== null, $row[3]); - case 'decimal': $obj = new FloatType('decimal', $row[3], $row[4] !== null, $row[5]); - case 'year': $obj = new IntegerType('year', $row[3], $row[4] !== null, 1970, 2070); // 4-digit format = 1901 to 2155, or 0000. - case 'varbinary': $obj = new StringType('varbinary', $row[3], $row[4] !== null, $row[5], 0); - case 'bigint': $obj = new IntegerType('bigint', $row[3], $row[4] !== null, $unsigned ? 0 : -9223372036854775808, $unsigned ? 18446744073709551615 : 9223372036854775807); - case 'datetime': $obj = new DateType('datetime', $row[3], $row[4] !== null); - case 'time': $obj = new DateType('time', $row[3], $row[4] !== null); - case 'mediumtext': $obj = new StringType('mediumtext', $row[3], $row[4] !== null, 16777215, $row[6]); - case 'longblob': $obj = new StringType('longblob', $row[3], $row[4] !== null, 4294967295, $row[6]); - case 'mediumblob': $obj = new StringType('mediumblob', $row[3], $row[4] !== null, 16777215, $row[6]); - case 'numeric': $obj = new FloatType('numeric', $row[3], $row[4] !== null, $row[5]); - case 'blob': $obj = new StringType('blob', $row[3], $row[4] !== null, 65535, $row[6]); - case 'tinyblob': $obj = new StringType('tinyblob', $row[3], $row[4] !== null, 255, $row[6]); - case 'longtext': $obj = new StringType('longtext', $row[3], $row[4] !== null, 4294967295, $row[6]); - } - $results[$row[0]][$row[1]] = &$obj; - } + public function fields() + { + return $this->fields; + } + public function getFields() + { + return $this->fields; + } -dump($results); -die(); -var_dump($tmp); -die(); - $columns = $db->get_array($sql, ['TABLE_NAME', 'COLUMN_NAME']); -var_dump($columns); - foreach ($columns as $table => $fields) - { - - } - } } diff --git a/src/DBAL/Type.php b/src/DBAL/Type.php new file mode 100644 index 0000000..f93f1cc --- /dev/null +++ b/src/DBAL/Type.php @@ -0,0 +1,73 @@ +properties =& $properties; + } + + /** + * Get table field/column property + * + * @param string $name + * @return mixed + */ + public function __get($name) + { + return $this->properties[$name]; + } + + /** + * Set table field/column property + * Values cannot be set, only callables + * + * @param string $name + * @param mixed $value + * @return void + */ + public function __set($name, $value) + { + if ( ! isset($this->properties[$name]) || is_callable($this->properties[$name])) + $this->properties[$name] = $value; + else + throw new \Exception("Cannot set protected property {$name}"); + } + + + public function __isset($name) + { + return isset($this->properties[$name]); + } + public function __unset($name) + { + unset($this->properties[$name]); + } + + + public function __call($method, $args) + { + return $this->properties[$method]($this, ...$args); // TEST !!! +/* + array_unshift($args, $this); + return call_user_func_array($this->properties[$method], $args); +*/ + } + public function __invoke() + { + return $this->properties['__invoke']($this); + } + public function setMethod($method, callable $callable) + { + $this->properties[$method] = $callable; + return $this; + } +} diff --git a/src/DBAL/Types/ArrayType.php b/src/DBAL/Types/ArrayType.php index fc4338f..bba9cb0 100644 --- a/src/DBAL/Types/ArrayType.php +++ b/src/DBAL/Types/ArrayType.php @@ -94,7 +94,7 @@ class ArrayType extends BaseType implements \Iterator, \Countable, \ArrayAccess * TODO: Sets can include multiple values eg. VALUE1 | VALUE2 etc. * We need to validate the whole set! */ - function isValid(string $value, bool $casesensitive = true) + function isValid($value) { static $combined = null; if ($combined === null) { @@ -122,8 +122,8 @@ class ArrayType extends BaseType implements \Iterator, \Countable, \ArrayAccess trigger_error('Invalid data type passed to getIndex()', E_USER_ERROR); } - function toSQL(&$value) + function toSQL($value) { - return value; + return $value; } } diff --git a/src/DBAL/Types/BaseType.php b/src/DBAL/Types/BaseType.php index db0d3ad..1700f3b 100644 --- a/src/DBAL/Types/BaseType.php +++ b/src/DBAL/Types/BaseType.php @@ -1,43 +1,9 @@ type = $type; @@ -38,7 +44,7 @@ class IntegerType extends BaseType // Either a NULL (if valid for the field), or ZERO or clamped to range! public function clamp($value) { - return isset($value) && is_numeric($value) ? min(max($value, $this->min), $this->max) : ($this->default ?: ($this->nullable ? null : 0)); + return isset($value) && is_numeric($value) ? min(max($value, $this->min), $this->max) : $this->default ?: ($this->nullable ? null : min(max(0, $this->min), $this->max)); } public function getMin() @@ -51,11 +57,6 @@ class IntegerType extends BaseType return $this->max; } - public function isUnsigned() - { - return $this->min >= 0; - } - public function getRange() { return [$this->min, $this->max]; diff --git a/src/DBAL/Types/StringType.php b/src/DBAL/Types/StringType.php index 1f865c8..3140bf9 100644 --- a/src/DBAL/Types/StringType.php +++ b/src/DBAL/Types/StringType.php @@ -34,7 +34,7 @@ class StringType extends BaseType return parent::isValid($value) && is_scalar($value) && strlen($value) <= $length; } - function toSQL(&$value) + function toSQL($value) { return $value; } diff --git a/src/Schema.php b/src/Schema.php new file mode 100644 index 0000000..c03eeb7 --- /dev/null +++ b/src/Schema.php @@ -0,0 +1,159 @@ +id->min +//Schema::users()->fields() + +class Schema +{ + private static $tables = null; + private static $db = null; + private static $schema = 'DATABASE()'; + private static $cache = null; + + // Set db to MySQL Connection object - if not set, then procedural style will be used + public static setConn($conn) + { + self::$db = $conn; + } + + // Set the database schema name + public static setSchema($schema) + { + self::$schema = $schema != 'DATABASE()' ? '"' . $schema . '"' : 'DATABASE()'; + } + + // Set the schema cache directory, where we can store files for the composer autoloader + public static setCache($cache) + { + self::$cache = $cache; + } + + public static __callStatic(string $table, array $args) + { + if ( ! isset($tables[$table])) + { + try + { + $class = 'Schema\\' . self::toPascalCase($table); + $tables[$table] = new $class(); + } + catch (\Exception $e) + { + $tables[$table] = self::build($table); + } + } + return $tables[$table]; + } + + /** + * Converts $table to PascalCase, preserving leading and trailing underscores '_' + * eg. nation_capitals__ => NationCapitals__ + */ + private static toPascalCase($table) + { + return str_repeat('_', strspn($table, '_')) . str_replace('_', '', ucwords($table, '_')) . str_repeat('_', strspn(strrev($table), '_')); + } + + public static function build($table = null, $db = null, string $cache = null) + { + mysqli_report( MYSQLI_REPORT_STRICT ); + + $cache = $cache ?: self::$cache; + + // here for potential caching possibilities !?!? Hopefully PHP can set an internal ref-counter and not re-define the functions each time! + static $methods = [ 'date' => function ($type, string $value) { return $value === null && $type->nullable || preg_match('~^\d\d\d\d-\d\d-\d\d$~', $value) === 1; }, // TODO: Add DateTime object validation! ie. instanceof DateTime + 'datetime' => function ($type, string $value) { return $value === null && $type->nullable || preg_match('~^\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d$~', $value) === 1; }, + 'time' => function ($type, string $value) { return $value === null && $type->nullable || preg_match('~^\d\d:\d\d:\d\d$~', $value) === 1; }, + 'time_ex' => function ($type, string $value) { return $value === null && $type->nullable || preg_match('~^-?\d?\d\d:\d\d:\d\d$~', $value) === 1; }, // From: http://www.mysqltutorial.org/mysql-time/ A TIME value ranges from -838:59:59 to 838:59:59. In addition, a TIME value can have fractional seconds part that is up to microseconds precision (6 digits). To define a column whose data type is TIME with a fractional second precision part, you use the following syntax: column_name TIME(N); + 'year' => function ($type, string $value) { return $value === null && $type->nullable || preg_match('~^\d\d\d\d$~', $value) === 1; }, + 'enum' => function ($type, string $value) { return in_array($value, $type->members); }, + 'int' => function ($type, $value) { return $value === null && $type->nullable || is_numeric($value) && $value >= $type->min && $value <= $type->max; }, + 'float' => function ($type, $value) { return $value === null && $type->nullable || is_numeric($value); }, + 'string' => function ($type, $value) { return $value === null && $type->nullable || is_string($value) && ($type->charset); }, + // '[0,1]' => function ($type, $value) { return $value === null && $type->nullable || is_numeric($value) && $value >= 0.0 && $value <= 1.0; }, // HYPOTHETICAL 'bit'! + 'clamp' => function ($type, $value) { return isset($value) && is_numeric($value) ? min(max($value, $type->min), $type->max) : $type->default ?: ($type->nullable ? null : min(max(0, $type->min), $type->max)); }, + 'isTrue' => function ($type) { return true; }, + 'isFalse' => function ($type) { return false; }, + 'isUnsigned' => function ($type) { return $type->min >= 0; }, + ]; + + $sql = 'SELECT ' . + 'TABLE_NAME,' . + 'COLUMN_NAME,' . + 'DATA_TYPE,' . // varchar + 'COLUMN_DEFAULT,' . + // 'NULLIF(IS_NULLABLE="YES",0),' .// NULL / 1 alternative + 'NULLIF(IS_NULLABLE,"NO"),' . // NULL / YES + // 'IS_NULLABLE,' . // NO / YES + 'COALESCE(CHARACTER_MAXIMUM_LENGTH, NUMERIC_SCALE) AS scale,' . // These two columns data NEVER overlap! + // 'CHARACTER_MAXIMUM_LENGTH,' . + // 'CHARACTER_OCTET_LENGTH,' . // eg. varchar(32) == CHARACTER_MAXIMUM_LENGTH = 32 && CHARACTER_OCTET_LENGTH = 96 + // 'NUMERIC_PRECISION,' . // eg. decimal(9,5) == NUMERIC_PRECISION = 9 && NUMERIC_SCALE = 5 + // 'NUMERIC_SCALE,' . // 5 (the decimal values, or 0 for integers) + 'CHARACTER_SET_NAME,' . // utf8 (binary == null) + // 'COLLATION_NAME,' . // utf8_general_ci + 'COLUMN_TYPE' . // int(10) unsigned / enum('','right','left') + // 'COLUMN_KEY,' . // PRI / UNI / MUL + // 'EXTRA' . // auto_increment / on update CURRENT_TIMESTAMP + ' FROM INFORMATION_SCHEMA.COLUMNS' . + ' WHERE TABLE_SCHEMA = DATABASE()' . ($table ? (is_array($table) ? ' AND TABLE_NAME IN ("' . implode('","', $table) . '")' : ' AND TABLE_NAME = "' . $table . '"') : null); + ' ORDER BY TABLE_NAME, ORDINAL_POSITION'; + + $result = null; + + $rows = $db->query($sql)->fetch_all(MYSQLI_NUM); // MYSQLI_ASSOC || MYSQLI_NUM + foreach ($rows as $row) + { + //$field = [$row[2], $row[3], $row[4] !== null]; + //... would have added more to the array ... but decided to make this realtime only for now! + //$result[$row[0]][$row[1]] = &$field; + $unsigned = strpos($row[7], 'unsigned') !== false; + switch($row[2]) + { // 5383 / 5996 fields (406 tables) are NOT nullable (nullable = 11%, NON-nullable = 89%) && 3022 fields have defaults (50%, 658 fields default is empty string, 1752 fields default = 0) + case 'int': $obj = new Type(['int', $row[3], $row[4] !== null, 'min' => $unsigned ? 0 : -2147483648, 'max' => $unsigned ? 4294967295 : 2147483647, 'isValid' => $methods['int'], 'clamp' => $methods['clamp']]); break; + case 'tinyint': $obj = new Type(['tinyint', $row[3], $row[4] !== null, 'min' => $unsigned ? 0 : -128, 'max' => $unsigned ? 255 : 127, 'isValid' => $methods['int'], 'clamp' => $methods['clamp']]); break; + case 'float': $obj = new Type(['float', $row[3], $row[4] !== null, 'precision' => $row[5], 'isValid' => $methods['float']]); break; + case 'varchar': $obj = new Type(['varchar', $row[3], $row[4] !== null, 'maxlen' => $row[5], 'charset' => $row[6]]); break; + case 'smallint': $obj = new Type(['smallint', $row[3], $row[4] !== null, 'min' => $unsigned ? 0 : -32768, 'max' => $unsigned ? 65535 : 32767, 'isValid' => $methods['int'], 'clamp' => $methods['clamp']]); break; + case 'enum': $obj = new Type(['enum', $row[3], $row[4] !== null, 'members' => self::convertToArray($row[7]), 'isValid' => $methods['enum']]); break; + case 'mediumint': $obj = new Type(['mediumint', $row[3], $row[4] !== null, 'min' => $unsigned ? 0 : -8388608, 'max' => $unsigned ? 16777215 : 8388607, 'isValid' => $methods['int'], 'clamp' => $methods['clamp']]); break; + case 'date': $obj = new Type(['date', $row[3], $row[4] !== null, 'isValid' => $methods['date']]); break; + case 'bit': $obj = new Type(['bit', $row[3] === null ? null : ($row[3] === "b'0'" ? 0 : 1), $row[4] !== null, 0, 1, 'isValid' => $methods['int'], 'clamp' => $methods['clamp']]); break; + case 'char': $obj = new Type(['char', $row[3], $row[4] !== null, 'maxlen' => $row[5], 'charset' => $row[6]]); break; + case 'text': $obj = new Type(['text', $row[3], $row[4] !== null, 'maxlen' => 65535, 'charset' => $row[6]]); break; + case 'timestamp': $obj = new Type(['timestamp', $row[3] === 'CURRENT_TIMESTAMP' ? null : $row[3], $row[4] !== null, 'isValid' => $methods['datetime']]); break; + case 'binary': $obj = new Type(['binary', $row[3], $row[4] !== null, 'maxlen' => $row[5], 'charset' => null]); break; + case 'double': $obj = new Type(['double', $row[3], $row[4] !== null, 'precision' => $row[5], 'isValid' => $methods['float']]); break; + case 'tinytext': $obj = new Type(['tinytext', $row[3], $row[4] !== null, 'maxlen' => 255, 'charset' => $row[6]]); break; + case 'set': $obj = new Type(['set', $row[3], $row[4] !== null, self::convertToArray($row[7])]); break; + case 'decimal': $obj = new Type(['decimal', $row[3], $row[4] !== null, 'precision' => $row[5], 'isValid' => $methods['float']]); break; + case 'year': $obj = new Type(['year', $row[3], $row[4] !== null, 'min' => 1970, 'max' => 2070, 'isValid' => $methods['int'], 'clamp' => $methods['clamp']]); break; // 4-digit format = 1901 to 2155, or 0000. + case 'varbinary': $obj = new Type(['varbinary', $row[3], $row[4] !== null, 'maxlen' => $row[5], 'charset' => null]); break; + case 'bigint': $obj = new Type(['bigint', $row[3], $row[4] !== null, 'min' => $unsigned ? 0 : -9223372036854775808, 'max' => $unsigned ? 18446744073709551615 : 9223372036854775807, 'isValid' => $methods['int'], 'clamp' => $methods['clamp']]); break; + case 'datetime': $obj = new Type(['datetime', $row[3], $row[4] !== null, 'isValid' => $methods['datetime']]); break; + case 'time': $obj = new Type(['time', $row[3], $row[4] !== null, 'isValid' => $methods['time']]); break; + case 'mediumtext': $obj = new Type(['mediumtext', $row[3], $row[4] !== null, 'maxlen' => 16777215, 'charset' => $row[6]]); break; + case 'longblob': $obj = new Type(['longblob', $row[3], $row[4] !== null, 'maxlen' => 4294967295, 'charset' => $row[6]]); break; + case 'mediumblob': $obj = new Type(['mediumblob', $row[3], $row[4] !== null, 'maxlen' => 16777215, 'charset' => $row[6]]); break; + case 'numeric': $obj = new Type(['numeric', $row[3], $row[4] !== null, 'precision' => $row[5], 'isValid' => $methods['float']]); break; + case 'blob': $obj = new Type(['blob', $row[3], $row[4] !== null, 'maxlen' => 65535, 'charset' => $row[6]]); break; + case 'tinyblob': $obj = new Type(['tinyblob', $row[3], $row[4] !== null, 'maxlen' => 255, 'charset' => $row[6]]); break; + case 'longtext': $obj = new Type(['longtext', $row[3], $row[4] !== null, 'maxlen' => 4294967295, 'charset' => $row[6]]); break; + } + $result[$row[0]][$row[1]] = $obj; + } + + if ( ! empty($cache)) + { + + } + + return is_string($table) ? $result[$table] : $result; + } + + // Converts enum() and set() to array + public static function convertToArray($value) + { + return explode('\',\'', substr($value, strpos($value, '(') + 2, -2)); + } +}