mirror of
https://github.com/twisterarmy/twister.git
synced 2025-03-12 21:31:26 +00:00
This commit is contained in:
parent
61ccd06e50
commit
f63ee8fe07
93
src/Schema/Model.php
Normal file
93
src/Schema/Model.php
Normal 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;
|
||||
}
|
||||
}
|
@ -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)
|
||||
|
@ -4,6 +4,8 @@ namespace Twister\Schema;
|
||||
|
||||
abstract class Table
|
||||
{
|
||||
protected $name = null;
|
||||
protected $hash = null;
|
||||
protected $fields = null;
|
||||
|
||||
public function __construct(array $fields = null)
|
||||
@ -13,8 +15,41 @@ abstract class Table
|
||||
|
||||
public function __get($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);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@ -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
|
||||
*
|
||||
|
@ -4,10 +4,12 @@ namespace Twister\Schema\Types;
|
||||
|
||||
class EnumType implements \Iterator, \Countable, \ArrayAccess
|
||||
{
|
||||
private $properties = 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
|
||||
*
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
Loading…
x
Reference in New Issue
Block a user