diff --git a/src/Schema/Model.php b/src/Schema/Model.php new file mode 100644 index 0000000..87af424 --- /dev/null +++ b/src/Schema/Model.php @@ -0,0 +1,93 @@ +query('SELECT SQL_NO_CACHE data FROM sessions WHERE id = 0x' . $id . ' LIMIT 1')) + { + $row = $rs->fetch_row(); + $rs->free_result(); + return (string) $row[0]; + } + } + return ''; + } + + static function write($id, $data) + { + if (isset($_COOKIE[session_name()])) // WARNING: This will NOT write the session on the first page view! The cookie MUST be created/set first, which requires another page view before it works! This prevents bots from creating sessions! But during testing a new session_id() or browser, it can be confusing because you won't see a new session until your second page view! + { + $data = empty($data) ? 'NULL' : '"' . self::$_db->real_escape_string($data) . '"'; + self::$_db->real_query('INSERT INTO sessions (id, timestamp, persistent, data) VALUES (0x' . $id . ', UNIX_TIMESTAMP(), 0, ' . $data . ') ON DUPLICATE KEY UPDATE timestamp = UNIX_TIMESTAMP(), data = ' . $data); + } + return true; + } + + // This is ONLY required in the login page! + static function create_persistent_session() + { + self::$_db->real_query('INSERT INTO sessions (id, timestamp, persistent, data) VALUES (0x' . session_id() . ', UNIX_TIMESTAMP(), UNIX_TIMESTAMP(), NULL)'); + } + +/* + static function login($user_id, $persistent) + { + session_regenerate_id(true); + setcookie(session_name(), session_id(), $persistent ? 0x7fffffff : 0, '/'); + $_SESSION['id'] = $user_id; + self::$_db->real_query('INSERT INTO sessions (id, timestamp, persistent, user_id, data) VALUES (0x' . session_id() . ', UNIX_TIMESTAMP(), ' . ($persistent ? 'UNIX_TIMESTAMP(), ' : '0, ') . $user_id . ', "")'); + } +*/ + + // Taken from: http://www.php.net/manual/en/function.session-destroy.php + // `session_destroy() destroys all of the data associated with the current session. It does not unset any of the global variables associated with the session, or unset the session cookie.` + static function destroy($id) + { + self::$_db->real_query('DELETE FROM sessions WHERE id = 0x' . $id); + return true; + } + + static function gc($ttl) + { + self::$_db->real_query('DELETE FROM sessions WHERE timestamp < UNIX_TIMESTAMP() - ' . $ttl . ' AND persistent = 0'); // $ttl = 1440 (default) = 24 minutes + if (mt_rand(0, 99) == 0) + { + self::$_db->real_query('OPTIMIZE TABLE sessions'); // optional routine maintenance + self::$_db->real_query('FLUSH QUERY CACHE'); // optional routine maintenance + } + return true; + } +} diff --git a/src/Schema/Schema.php b/src/Schema/Schema.php index 7256d99..0bfc4ab 100644 --- a/src/Schema/Schema.php +++ b/src/Schema/Schema.php @@ -1,54 +1,88 @@ id->min -//Twister\Schema::users()->fields() +//$schema->worlds->id->min +//$schema->users() namespace Twister; class Schema { - private static $tables = null; - private static $db = null; - private static $schema = 'DATABASE()'; - private static $cache = null; + private $tables = null; + private $conn = null; + private $schema = null; + private $location = null; // (save) location? store? folder? directory? cache? + private $constraints = null; + private $lang = null; - // Set db to MySQL Connection object - if not set, then procedural style will be used - public static function setConn($conn) + public function __construct($conn = null, $schema = null, $location = null, $constraints = null, $lang = 'en') { - self::$db = $conn; + $this->setConn($conn)->setSchema($schema)->setLocation($location)->setConstraints($constraints)->setLang($lang); } - // Set the database schema name - public static function setSchema($schema) + public function __get($table) { - self::$schema = $schema != 'DATABASE()' ? '"' . $schema . '"' : 'DATABASE()'; - } - - // Set the schema cache directory, where we can store files for the composer autoloader - public static function setCache($cache) - { - self::$cache = $cache; - } - - public static function __callStatic(string $table, array $args) - { - if ( ! isset($tables[$table])) + if ( ! isset($this->tables[$table])) { try { - $class = 'Twister\\Schema\\' . self::toPascalCase($table); - $tables[$table] = new $class(); + $class = 'Twister\\Schema\\Tables\\' . self::toPascalCase($table); + $this->tables[$table] = new $class(); } catch (\Exception $e) { - $tables[$table] = self::build($table); + $this->tables[$table] = self::build($table); } } - return $tables[$table]; + return $this->tables[$table]; } /** - * Converts $table to PascalCase, preserving leading and trailing underscores '_' + * Set db to MySQL Connection object - if not set, then procedural style will be used + */ + public function setConn($conn) + { + self::$conn = $conn; + return $this; + } + + /** + * Set the database schema name + */ + public function setSchema($schema) + { + self::$schema = $schema != 'DATABASE()' ? '"' . $schema . '"' : 'DATABASE()'; + return $this; + } + + /** + * Set the schema cache directory, where we can store files for the composer autoloader + */ + public static function setLocation($location) + { + self::$location = $location; + return $this; + } + + /** + * Set the schema cache directory, where we can store files for the composer autoloader + */ + public static function setConstraints($constraints) + { + self::$location = $location; + return $this; + } + + /** + * Set the language used for translations, default is English + */ + public static function setLang($lang = 'en') + { + self::$lang = $lang; + return $this; + } + + /** + * Helper function to convert $table name to PascalCase, preserving leading and trailing underscores '_' * eg. nation_capitals__ => NationCapitals__ */ private static function toPascalCase($table) @@ -56,11 +90,22 @@ class Schema 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) + /** + * Force Table Schema Class (Cache) Rebuild + * There might be times where we need to force a table rebuild, eg. we change an Enum column at runtime! + */ + private static function forceRebuild($table) + { + throw new \Exception('TODO'); + } + + public function buildSchema($table = null) { mysqli_report( MYSQLI_REPORT_STRICT ); - $cache = $cache ?: self::$cache; + if ( ! is_dir($this->location) || ! is_writable($this->location)) { + throw new \Exception('Schema cache folder location `' . $this->location . '` must be a valid writable folder to build the table schema!'); + } static $methods = null; if ($methods === null) diff --git a/src/Schema/Table.php b/src/Schema/Table.php index e4a1b95..e5f6146 100644 --- a/src/Schema/Table.php +++ b/src/Schema/Table.php @@ -4,7 +4,9 @@ namespace Twister\Schema; abstract class Table { - protected $fields = null; + protected $name = null; + protected $hash = null; + protected $fields = null; public function __construct(array $fields = null) { @@ -13,9 +15,42 @@ abstract class Table public function __get($field) { - return $this->fields[$field]; + static $tr = null; + static $cache = null; + + if (isset($this->fields[$field])) + return $this->fields[$field]; + + $old_field = $field; +/* + + if ( ! isset($cache[$field])) + { + if ($tr === null) + { + foreach (range('A', 'Z') as $char) + $tr[$char] = '_' . strtolower($char); + } + $cache[$old_field] = $field = strtr(lcfirst($field), $tr); + } + + if (isset($this->fields[$field])) + return $this->fields[$field]; +*/ + throw new \Exception("Table `{$this->name}` doesn't contain a field called `$old_field`"); } + + public function tableName() + { + return $this->name; + } + public function tableHash() + { + return $this->hash; + } + + // $statement = 'UPDATE' | 'INSERT' ... UPDATE = partial check, INSERT = FULL (required non-nullable fields) check! public function validate($statement) { @@ -30,6 +65,25 @@ abstract class Table { return $this->fields; } + public function fieldNames() + { + return array_keys($this->fields); + } + public function getFieldNames() + { + return array_keys($this->fields); + } + + // get an array, with field names as keys, but ALL values set to NULL + // This can be useful for array_merge() to test if ALL the fields in an INSERT statement will be met, including those fields we don't explicitly set! like timestamp = CURRENT_TIMESTAMP / on update CURRENT_TIMESTAMP + public function emptyFields() + { + return array_fill_keys(array_keys($this->fields), null); + } + public function getEmptyFields() + { + return array_fill_keys(array_keys($this->fields), null); + } } diff --git a/src/Schema/Types/BaseType.php b/src/Schema/Types/BaseType.php index 0935e76..ab8d5db 100644 --- a/src/Schema/Types/BaseType.php +++ b/src/Schema/Types/BaseType.php @@ -5,7 +5,7 @@ namespace Twister\Schema\Types; abstract class BaseType { protected $properties = null; - +/* public function __construct(array $properties) { $properties['type'] = &$properties[0]; @@ -21,7 +21,7 @@ abstract class BaseType $this->default = $default; $this->nullable = $nullable; } - +*/ /** * Get table field/column property * diff --git a/src/Schema/Types/EnumType.php b/src/Schema/Types/EnumType.php index 2a4a379..7eb281a 100644 --- a/src/Schema/Types/EnumType.php +++ b/src/Schema/Types/EnumType.php @@ -4,10 +4,12 @@ namespace Twister\Schema\Types; class EnumType implements \Iterator, \Countable, \ArrayAccess { - private $properties = null; - private $members = null; + protected $properties = null; + private $members = null; - private static $isValid = null; // cache the default isValid function + public $required = false; // `required` is a publically changeable property (ie. we can override the default, unlike the other properties! moved here because of __set() restrictions) + + private static $isValid = null; // cache the default `isValid` function public function __construct(&$table, $name, $default, $nullable, array $members) { @@ -17,14 +19,34 @@ class EnumType implements \Iterator, \Countable, \ArrayAccess $this->properties['default'] = $default; $this->properties['nullable'] = $nullable; + $this->required = $default === null && ! $nullable; + if (self::$isValid === null) { - self::$isValid = function ($table, $type, $value) { $type }; + self::$isValid = function ($type, $value) + { + return $value !== null && in_array($value, $type->members) || $value === null && ($type->nullable || $this->default); + }; // in_array(null, ['']) === true ... therefore we MUST test `$value !== null` before the in_array() or we might get false positives } - $this->properties['isValid'] = self::$isValid + $this->properties['isValid'] = self::$isValid; $this->members = $members; } + // Returns an `ALTER TABLE` statement for the $members ... what about the table cache !?!? + // Setting $default to false will remove the default ... alternatively set the default to null !?!? + public function alterTable(array $members, $default = null) + { + //ALTER TABLE `fcm`.`worlds` + //CHANGE COLUMN `type` `type` ENUM('real', 'auto|mated', 'private', 'public', 'invitational', 'scenario', 'campaign', 'archived') NOT NULL DEFAULT 'public' ; + // TODO: $members cannot have any `\` characters! I think they are forbidden in Enums because MySQL removes them! + $default = $default ?? $this->properties['default']; + $default = $default === false ? null : ($default === null ? ' DEFAULT NULL' : ' DEFAULT \'' . $default . '\''); + return 'ALTER TABLE `' . $this->properties['table'] . '`' . + ' CHANGE COLUMN `' . $this->properties['name'] . '` `' . $this->properties['name'] . '` ENUM(\'' . implode('\', \'', $members) . '\')' . ($this->properties['nullable'] ? ' NULL' : ' NOT NULL') . $default; + } + + + /** * Get table field/column property * diff --git a/src/Schema/Types/IntegerType.php b/src/Schema/Types/IntegerType.php index 238fc27..1769603 100644 --- a/src/Schema/Types/IntegerType.php +++ b/src/Schema/Types/IntegerType.php @@ -4,30 +4,48 @@ namespace Twister\Schema\Types; class IntegerType extends BaseType { - public $min = null; - public $max = null; + protected $properties = null; -// public $auto_increment = null; + public $required = false; // `required` is a publically changeable property (ie. we can override the default, unlike the other properties! moved here because of __set() restrictions) + private static $isValid = null; // cache the default `isValid` function +/* private $clamp = function ($type, $value) {}; // callback function for `clamp` private $__invoke = function ($type, $value) {}; // callback function for `__invoke` private $toPHP = function ($type, $value) {}; // callback function for toPHP private $toSQL = function ($type, $value) {}; // callback function for toSQL private $valid = function ($type, $value) {}; // callback function for isValid - - public function __construct($type, $default, $nullable, $min, $max) +*/ + public function __construct(&$table, $name, $type, $default, $nullable, $min, $max, $auto = false) { - $this->type = $type; - $this->default = $default; - $this->nullable = $nullable; + $this->properties['table'] = $table; + $this->properties['name'] = $name; + $this->properties['type'] = $type; + $this->properties['default'] = $default; + $this->properties['nullable'] = $nullable; + $this->properties['min'] = $min; + $this->properties['max'] = $max; + $this->properties['autoincrement'] = $auto; + $this->properties['unsigned'] = $min === 0; - $this->min = $min; - $this->max = $max; + $this->required = $default === null && ! $nullable && ! $auto; + + if (self::$isValid === null) { + self::$isValid = function ($type, $value) + { + return $value !== null && is_string($value) && ($type->charset === 'latin1' ? strlen($value) : mb_strlen($value, 'utf8')) <= $this->maxlength || $value === null && ($type->nullable || $this->default); + }; // in_array(null, ['']) === true ... therefore we MUST test `$value !== null` before the in_array() or we might get false positives + } + $this->properties['isValid'] = self::$isValid; } + public function unsigned() + { + return $this->min === 0; + } public function isUnsigned() { - return $this->min >= 0; + return $this->min === 0; } public function filter($value) diff --git a/src/Schema/Types/StringType.php b/src/Schema/Types/StringType.php index 8c50af3..a979c8f 100644 --- a/src/Schema/Types/StringType.php +++ b/src/Schema/Types/StringType.php @@ -4,25 +4,34 @@ namespace Twister\Schema\Types; class StringType extends BaseType { - const CHARSET_BINARY = 0; // unused (the charset of BINARY fields is NULL!) ... we need to put BINARY data types inside string, for the maxlength ... - const CHARSET_LATIN1 = 1; - const CHARSET_UTF8 = 3; - const CHARSET_UTF8MB4 = 4; -// const CHARSET_UTF16 = 16; // unused -// const CHARSET_UTF32 = 32; // unused + protected $properties = null; + + public $required = false; // `required` is a publically changeable property (ie. we can override the default, unlike the other properties! moved here because of __set() restrictions) + + private static $isValid = null; // cache the default `isValid` function + + public function __construct(&$table, $name, $type, $default, $nullable, $length, $charset, $fixed = false) + { + $this->properties['table'] = $table; + $this->properties['name'] = $name; + $this->properties['type'] = $type; + $this->properties['default'] = $default; + $this->properties['nullable'] = $nullable; + $this->properties['length'] = $length; + $this->properties['charset'] = $charset; + $this->properties['fixed'] = $fixed; // fixed length - from Doctrine: `fixed (boolean): Whether a string or binary Doctrine type column has a fixed length. Defaults to false.` - public $maxlength = null; - public $charset = null; // public $binary = null; // needed? could be set by collation type: `utf8_bin` or data type `binary` ??? - function __construct($type, $default, $nullable, $maxlength, $charset) - { - $this->type = $type; - $this->default = $default; - $this->nullable = $nullable; + $this->required = $default === null && ! $nullable; - $this->maxlength = $maxlength; - $this->charset = $charset; + if (self::$isValid === null) { + self::$isValid = function ($type, $value) + { + return $value !== null && is_string($value) && ($type->charset === 'latin1' ? strlen($value) : mb_strlen($value, 'utf8')) <= $this->maxlength || $value === null && ($type->nullable || $this->default); + }; // in_array(null, ['']) === true ... therefore we MUST test `$value !== null` before the in_array() or we might get false positives + } + $this->properties['isValid'] = self::$isValid; } function isValid($value)