Created
July 8, 2017 18:37
-
-
Save cynthia/b59bc7439a8d7c289ee3c71409bcad24 to your computer and use it in GitHub Desktop.
Weird science experiment that makes every non-existent class in the current PHP execution context ORM backed with Redbean.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
/** | |
* UPO : Unidentified PHP Object (미확인 PHP 객체) | |
* | |
* Here be dragons. This abuses every odd PHP language feature known to man. | |
* which means it is highly experimental, and should *never* be used in a | |
* production environment. Failing to do so may expose your application to | |
* numerous security exploits, and the author takes no liability for damages | |
* caused by ignoring this warning. | |
* | |
* @file UPO.php | |
* @desc Main class for UPO | |
* @author Sangwhan Moon <sangwhan@iki.fi> | |
* @license 3-clause BSD | |
*/ | |
require_once("rb.php"); | |
error_reporting(E_ALL); | |
////////////////////////////////////////////////////////////////////////////// | |
// Constants | |
////////////////////////////////////////////////////////////////////////////// | |
define("GT", "Gt"); | |
define("ZZSEP", DIRECTORY_SEPARATOR); // DIRECTORY_SEPARATOR shorthand. | |
define("ZZIDF", "zn7q"); // Model prefix. | |
define("ZZCSP", "z9qx"); // Model internal token. | |
define("ZZMEM", "_"); // Model camel case replacement. | |
define("ZZMOD", "Model_" . ucfirst(ZZIDF)); // Redbean model name prefix. | |
// Shorthand for generated class path. | |
define("ZZGCP", dirname(__FILE__) . ZZSEP . "generated" . ZZSEP); | |
define("C_OPGT", "get"); | |
define("C_OPST", "set"); | |
define("C_OPFR", "from"); | |
define("C_OPAL", "all"); | |
define("T_OPRD", 0); | |
define("T_OPWR", 1); | |
define("T_OPID", 2); | |
define("T_DECL", 0); | |
define("T_SETP", 1); | |
define("T_GETP", 2); | |
define("T_IDXP", 3); | |
define("T_CINC", 4); | |
define("T_CDEC", 5); | |
define("T_CNIL", 6); | |
////////////////////////////////////////////////////////////////////////////// | |
// Config | |
////////////////////////////////////////////////////////////////////////////// | |
define("DB_STAGING", "sqlite:" . dirname(__FILE__) . ZZSEP . "../db/staging.db"); | |
define("DB_STAGING_U", "staging"); | |
define("DB_STAGING_P", "pepsiman"); | |
define("M_DBG", TRUE); | |
define("MODE_LAX", FALSE); | |
define("MODE_AGGRESSIVELY_STORE", FALSE); | |
define("MODE_WRITE_CLASSES", TRUE); | |
define("MODE_PURGE_ON_INIT", TRUE); | |
////////////////////////////////////////////////////////////////////////////// | |
// Autoloader and JIT class generation | |
////////////////////////////////////////////////////////////////////////////// | |
spl_autoload_register("UPO::__autoload"); | |
////////////////////////////////////////////////////////////////////////////// | |
// Bootstrap | |
////////////////////////////////////////////////////////////////////////////// | |
\R::setup(DB_STAGING, DB_STAGING_U, DB_STAGING_P); | |
if (MODE_PURGE_ON_INIT === TRUE) { | |
UPO::__purge_generated(); | |
} | |
class OperationNotPermittedException extends \Exception {} | |
class ReferenceCountMismatchException extends \Exception {} | |
class ObjectNotReadyException extends \Exception {} | |
class ObjectNotFoundException extends \Exception {} | |
class FromWithMultipleNotAllowedException extends \Exception {} | |
class ForbiddenObjectNameException extends \Exception {} | |
class NotImplementedException extends \Exception {} | |
////////////////////////////////////////////////////////////////////////////// | |
// Implementation | |
////////////////////////////////////////////////////////////////////////////// | |
class UPO | |
{ | |
private static $reference_count_ = 0; | |
private static $traces_ = array(); | |
private static $purged_ = FALSE; | |
private $instance_ = NULL; | |
private $bean_id_ = NULL; | |
private $cached_name_ = NULL; | |
//////////////////////////////////////////////////////////////////////////// | |
// Buit-in magic methods (from PHP) | |
//////////////////////////////////////////////////////////////////////////// | |
function __construct() | |
{ | |
$a = func_get_args(); | |
$i = func_num_args(); | |
if ($i == 1) { | |
$this->instance_ = \R::find(self::__tclazz(), $i); | |
} | |
$this->root_name_ = $this->__root_type(); | |
self::$reference_count_++; | |
self::__trace(T_CINC); | |
} | |
function __destruct() { | |
self::$reference_count_--; | |
self::__trace(T_CDEC); | |
if ($this->instance_ !== NULL) { | |
$this->instance_->__type = self::__clazz(); | |
$this->bean_id_ = \R::store($this->instance_); | |
} | |
if (self::$reference_count_ == 0) { | |
self::__trace(T_CNIL); | |
\R::close(); | |
} | |
} | |
public function __call($name, $arguments) | |
{ | |
// Don't handle __call coming from Redbean. Failing to do so wreaks havoc. | |
if ($name == "loadBean") return; | |
$op = explode(ZZMEM, | |
strtolower(preg_replace('/([a-z])([A-Z])/', '$1_$2', $name)), | |
2); | |
switch ($op[0]) | |
{ | |
case C_OPGT: | |
{ | |
if (!$this->instance_ && !MODE_LAX) { | |
throw new ObjectNotReadyException(); | |
} | |
else { | |
// Warning intentionally supressed, as we use the hard FALSE return | |
// value to determine if what was stored was a array or not. Ugly, | |
// but works. | |
@$temp = unserialize($this->instance_->$op[1]); | |
// IDs (Redbean identifiers) are not object properties, hence we do | |
// not trace them. | |
if ($op[1] == "id") { | |
$this->bean_id_ = \R::store($this->instance_); | |
return $this->bean_id_; | |
} | |
self::__trace(T_GETP, self::__tclazz(), $op[1]); | |
if ($temp !== FALSE) | |
return $temp; | |
return $this->instance_->$op[1]; | |
} | |
break; | |
} | |
case C_OPST: | |
{ | |
if (!$this->instance_) { | |
$this->instance_ = \R::dispense(self::__tclazz($this->root_name_)); | |
} | |
if (count($arguments) == 1) { | |
if (is_array($arguments[0])) { | |
self::__trace(T_SETP, self::__tclazz(), $op[1]); | |
$this->instance_->$op[1] = serialize($arguments[0]); | |
} | |
else { | |
self::__trace(T_SETP, self::__tclazz(), $op[1]); | |
$this->instance_->$op[1] = $arguments[0]; | |
} | |
} | |
if (count($arguments) > 1) { | |
self::__trace(T_SETP, self::__tclazz(), $op[1]); | |
$this->instance_->$op[1] = serialize($arguments); | |
} | |
if (MODE_AGGRESSIVELY_STORE) { | |
if ($this->instance_) { | |
$this->bean_id_ = \R::store($this->instance_); | |
} | |
} | |
break; | |
} | |
case C_OPFR: | |
{ | |
if (count($arguments) == 1) { | |
self::__trace(T_IDXP, self::__tclazz(), $op[1]); | |
$type = self::__clazz(); | |
// echo($op[1] . "\n" . $arguments[0] . "\n" . $type . "\n"); | |
$this->instance_ = $op[1] == "id" ? | |
R::load(self::__tclazz($this->root_name_), $arguments[0]) : | |
$this->instance_ = R::findOne(self::__tclazz($this->root_name_), | |
sprintf("%s = ? AND __type = ?", $op[1]), | |
[$arguments[0], $type]); | |
print_r($this->instance_); | |
} | |
if (count($arguments) > 1) { | |
// FIXME: Should Student->fromName("John", "Jane") return a Student | |
// instance with the name either John or Jane? Not sure. Don't do | |
// anything for now. | |
throw new FromWithMultipleNotAllowedException(); | |
} | |
break; | |
} | |
case C_OPAL: | |
{ | |
throw new NotImplementedException(); | |
} | |
default: | |
{ | |
if (!MODE_LAX) | |
throw new OperationNotPermittedException(); | |
} | |
} | |
} | |
public static function __callStatic($name, $arguments) | |
{ | |
$op = explode(ZZMEM, | |
strtolower(preg_replace('/([a-z])([A-Z])/', '$1_$2', $name)), | |
2); | |
switch ($op[0]) | |
{ | |
case C_OPST: | |
case C_OPGT: | |
{ | |
throw new NotImplementedException(); | |
break; | |
} | |
case C_OPFR: | |
{ | |
if (count($arguments) == 1) { | |
$class_name = self::__clazz(); | |
$ret = new $class_name; | |
$ret->$name($arguments[0]); | |
return $ret; | |
} | |
break; | |
} | |
case C_OPAL: | |
{ | |
throw new NotImplementedException(); | |
} | |
default: | |
{ | |
if (!MODE_LAX) | |
throw new OperationNotPermittedException(); | |
} | |
} | |
} | |
//////////////////////////////////////////////////////////////////////////// | |
// Helpers | |
//////////////////////////////////////////////////////////////////////////// | |
public static function __autoload($class_name) { | |
if (substr($class_name, 0, 2) == GT) { | |
// && substr($class_name, 0, 10) != ZZMOD) { | |
if (file_exists($class_name . ".php")) { | |
require_once($class_name . ".php"); | |
} | |
else if (file_exists(ZZGCP . $class_name . ".upo.php")) { | |
require_once(ZZGCP . $class_name . ".upo.php"); | |
} | |
else { | |
UPO::__declare_clazz($class_name); | |
} | |
} | |
} | |
public static function __declare_clazz() | |
{ | |
$a = func_get_args(); | |
$i = func_num_args(); | |
if ($i == 1) { | |
self::__trace(T_DECL, $a[0], "UPO"); | |
self::__declare_sclazz($a[0], "UPO"); | |
} | |
// NOTE: This code is not used internally. | |
else if ($i > 1) { | |
$parent_clazz = "UPO"; | |
for ($j = $i - 1; $j >= 0; $j--) { | |
self::__trace(T_DECL, $a[$j], $parent_clazz); | |
self::__declare_sclazz($a[$j], $parent_clazz); | |
$parent_clazz = $a[$j]; | |
} | |
} | |
else { | |
// This only happens if someone calls __declare_clazz with no args. | |
if (!MODE_LAX) | |
throw new OperationNotPermittedException(); | |
} | |
} | |
public static function __declare_sclazz($class_name, $parent_name) | |
{ | |
if (!preg_match("/^[a-zA-Z0-9_]+$/", $class_name . $parent_name)) { | |
throw new ForbiddenObjectNameException(); | |
} | |
$generated = "class " . $class_name . " extends " . $parent_name . | |
"\n{\n\t\n}\n"; | |
eval($generated); | |
} | |
public function flush() { | |
if ($this->instance_) { | |
$this->bean_id_ = \R::store($this->instance_); | |
} | |
} | |
public function __instance_bean() | |
{ | |
return $this->instance_; | |
} | |
public static function __purge_generated() | |
{ | |
$generated_classes = scandir(ZZGCP); | |
foreach ($generated_classes as $generated_class) { | |
if (substr($generated_class, -8) == ".upo.php") { | |
unlink(ZZGCP . $generated_class); | |
} | |
} | |
self::$purged_ = TRUE; | |
} | |
private static function __append_trace($type, $clazz, $func) | |
{ | |
if (!isset(self::$traces_[$clazz])) { | |
self::$traces_[$clazz] = array(); | |
} | |
if (!isset(self::$traces_[$clazz][$func])) { | |
self::$traces_[$clazz][$func] = | |
array(T_OPRD => 0, T_OPWR => 0, T_OPID => 0); | |
} | |
self::$traces_[$clazz][$func][$type]++; | |
} | |
private static function __trace() | |
{ | |
$a = func_get_args(); | |
switch ($a[0]) | |
{ | |
case T_DECL: | |
{ | |
if (M_DBG) | |
printf("[T_DECL]\t class %s extends %s dynamically declared.\n", | |
$a[1], $a[2]); | |
return; | |
} | |
case T_SETP: | |
{ | |
if (M_DBG) | |
printf("[T_SETP]\t %s property %s set.\n", $a[1], $a[2]); | |
self::__append_trace(T_OPWR, $a[1], $a[2]); | |
return; | |
} | |
case T_GETP: | |
{ | |
if (M_DBG) | |
printf("[T_GETP]\t %s property %s get.\n", $a[1], $a[2]); | |
self::__append_trace(T_OPRD, $a[1], $a[2]); | |
return; | |
} | |
case T_IDXP: | |
{ | |
if (M_DBG) | |
printf("[T_IDXP]\t %s property %s used as index.\n", $a[1], $a[2]); | |
self::__append_trace(T_OPID, $a[1], $a[2]); | |
return; | |
} | |
case T_CINC: | |
{ | |
if (M_DBG) | |
printf("[T_CINC]\t Reference count is %d. Last object was %s.\n", | |
self::$reference_count_, self::__clazz()); | |
return; | |
} | |
case T_CDEC: | |
{ | |
if (M_DBG) | |
printf("[T_CDEC]\t Reference count is %d. Last object was %s.\n", | |
self::$reference_count_, self::__clazz()); | |
return; | |
} | |
case T_CNIL: | |
{ | |
if (M_DBG) | |
printf("[T_CNIL]\t Reference clean! Attempting DB shutdown.\n"); | |
foreach (self::$traces_ as $internal_class_name => $trace) { | |
$class_name = self::__rclazz($internal_class_name, ZZCSP); | |
if (MODE_WRITE_CLASSES) { | |
$pclass = get_parent_class($class_name); | |
$gen = "class " . $class_name . " extends " . $pclass . "\n{\n"; | |
$class_file = fopen(ZZGCP . $class_name . ".upo.php", "w"); | |
fwrite($class_file, "<?php\n\n// Class generated by UPO.\n\n"); | |
fwrite($class_file, $gen); | |
} | |
foreach ($trace as $internal_member => $counter) { | |
$member = self::__rclazz($internal_member, ZZMEM); | |
if (M_DBG) | |
printf("[T_STAT]\t %s.%s read %d, written %d, index %d times\n", | |
$class_name, $member, $counter[T_OPRD], | |
$counter[T_OPWR], $counter[T_OPID]); | |
if ($pclass != "UPO") { | |
if ($counter[T_OPRD] > 0) | |
self::__append_trace(T_OPRD, $pclass, $member); | |
if ($counter[T_OPWR] > 0) | |
self::__append_trace(T_OPWR, $pclass, $member); | |
if ($counter[T_OPID] > 0) | |
self::__append_trace(T_OPID, $pclass, $member); | |
} | |
if (MODE_WRITE_CLASSES) { | |
$fl = ""; | |
$fl .= $counter[T_OPRD] > 0 ? "R" : ""; | |
$fl .= $counter[T_OPWR] > 0 ? "W" : ""; | |
$fl .= $counter[T_OPID] > 0 ? "I" : ""; | |
fwrite($class_file, "\t// @hint " . $fl . " " . $member . "\n"); | |
} | |
} | |
if (MODE_WRITE_CLASSES) { | |
fwrite($class_file, "}\n\n?>"); | |
fclose($class_file); | |
} | |
} | |
return; | |
} | |
} | |
} | |
private static function __clazz() | |
{ | |
return get_called_class(); | |
} | |
private static function __pclazz() | |
{ | |
return get_parent_class(self); | |
} | |
private static function __tclazz($clazz = NULL) | |
{ | |
return (ZZIDF . strtolower(preg_replace('/([a-z])([A-Z])/', "$1" . ZZCSP . | |
"$2", !$clazz ? get_called_class() : $clazz))); | |
} | |
private static function __rclazz($class_name, $token) | |
{ | |
// WORKAROUND: str_replace() is broken, so we do a domain specific | |
// implementation here. | |
if (strpos($class_name, ZZIDF) !== FALSE) { | |
$class_name = substr($class_name, strlen(ZZIDF)); | |
} | |
$camels = explode($token, $class_name); | |
$buffer = ""; | |
foreach ($camels as $i => $camel) { | |
$buffer .= ($i == 0 && $token == ZZMEM) ? $camel : ucfirst($camel); | |
} | |
return $buffer; | |
} | |
private function __root_type() | |
{ | |
$last_class = self::__clazz(); | |
if (get_parent_class($last_class) != "UPO") { | |
for (;;) { | |
if (get_parent_class($last_class) == "UPO") { | |
break; | |
} else { | |
$last_class = get_parent_class($last_class); | |
} | |
} | |
} | |
return $last_class; | |
} | |
private function __ready() | |
{ | |
return $this->instance_ === NULL; | |
} | |
} | |
?> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment