This commit is contained in:
therselman 2017-07-16 14:22:18 +02:00
parent 61ccd06e50
commit f63ee8fe07
7 changed files with 306 additions and 65 deletions

93
src/Schema/Model.php Normal file
View File

@ -0,0 +1,93 @@
<?php
namespace Twister\Schema;
abstract class Model
{
}
namespace Twister;
class Session
{
private static $_db = null;
function __construct(DB &$db)
{
session_set_save_handler('Session::open', 'Session::close', 'Session::read', 'Session::write', 'Session::destroy', 'Session::gc');
register_shutdown_function('session_write_close');
session_set_cookie_params(0, '/', null, true, true);
self::$_db = $db;
session_start();
}
static function open($sp, $sn)
{
return true;
}
static function close()
{
return true;
}
static function read($id)
{
if (isset($_COOKIE[session_name()]))
{
if (!ctype_xdigit($id) || strlen($id) !== 32) die('Invalid Session ID: ' . $id);
if ($rs = self::$_db->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;
}
}

View File

@ -1,54 +1,88 @@
<?php
//Twister\Schema::worlds()->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)

View File

@ -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);
}
}

View File

@ -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
*

View File

@ -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
*

View File

@ -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)

View File

@ -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)