From 9d923605d8165ffca558e148075ea6cd7e0270a5 Mon Sep 17 00:00:00 2001 From: Alex Cabal Date: Thu, 9 Dec 2021 13:13:16 -0600 Subject: [PATCH] Add database and ORM scaffolding --- lib/Artwork.php | 7 ++ lib/Collection.php | 1 + lib/Constants.php | 7 ++ lib/Core.php | 2 + lib/Db.php | 15 ++++ lib/DbConnection.php | 183 +++++++++++++++++++++++++++++++++++++++++ lib/OrmBase.php | 30 +++++++ lib/PropertiesBase.php | 33 ++++++++ 8 files changed, 278 insertions(+) create mode 100644 lib/Artwork.php create mode 100644 lib/Db.php create mode 100644 lib/DbConnection.php create mode 100644 lib/OrmBase.php create mode 100644 lib/PropertiesBase.php diff --git a/lib/Artwork.php b/lib/Artwork.php new file mode 100644 index 00000000..31530e8f --- /dev/null +++ b/lib/Artwork.php @@ -0,0 +1,7 @@ +GetLastInsertedId(); + } + + public static function Query(string $query, $args = []){ + if(!is_array($args)){ + $args = [$args]; + } + + return $GLOBALS['DbConnection']->Query($query, $args); + } +} diff --git a/lib/DbConnection.php b/lib/DbConnection.php new file mode 100644 index 00000000..ee040bbc --- /dev/null +++ b/lib/DbConnection.php @@ -0,0 +1,183 @@ + 2){ + $port = $matches[2]; + } + + $connectionString .= 'host=' . $host; + + if($port !== null){ + $connectionString .= ';port=' . $port; + } + } + else{ + $connectionString .= 'host=' . $host; + } + + if($defaultDatabase !== null){ + $connectionString .= ';dbname=' . $defaultDatabase; + } + + // Have to use ATTR_EMULATE_PREPARES = true, otherwise transactions don't work for some reason. + $params = [\PDO::ATTR_EMULATE_PREPARES => true, \PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION, \PDO::ATTR_PERSISTENT => false]; + + if($forceUtf8){ + $params[\PDO::MYSQL_ATTR_INIT_COMMAND] = 'set names utf8mb4 collate utf8mb4_unicode_ci;'; + } + + // We can't use persistent connections (connection pooling) because we would have race condition problems with last_insert_id() + $this->_link = new \PDO($connectionString, $user, $password, $params); + + $this->IsConnected = true; + } + catch(Exception $ex){ + if(SITE_STATUS == SITE_STATUS_DEV){ + var_dump($ex); + } + else{ + Logger::WriteErrorLogEntry('Error connecting to ' . $connectionString . '. Exception: ' . vds($ex)); + } + + if($require){ + print("Something crazy happened in our database. Drop us a line and let us know exactly what you were doing and we'll take a look.\n
The more detail you can provide, the better!"); + exit(); + } + } + } + + // Inputs: string $sql = the SQL query to execute + // array $params = an array of parameters to bind to the SQL statement + // Returns: a resource record or null on error + public function Query(string $sql, array $params = []){ + if(!$this->IsConnected){ + return; + } + + $this->QueryCount++; + $result = []; + $preparedSql = $sql; + + $handle = $this->_link->prepare($preparedSql); + if(!is_array($params)){ + $params = [$params]; + } + + $name = 0; + foreach($params as $parameter){ + $name++; + + if(is_a($parameter, 'DateTime')){ + $parameter = $parameter->format('Y-m-d H:i:s'); + } + + // MySQL strict mode requires 0 or 1 instead of true or false + // Can't use PDO::PARAM_BOOL, it just doesn't work + if(is_bool($parameter)){ + if($parameter){ + $parameter = 1; + } + else{ + $parameter = 0; + } + } + + if(is_int($parameter)){ + $handle->bindValue($name, $parameter, PDO::PARAM_INT); + } + else{ + $handle->bindValue($name, $parameter); + } + } + + $deadlockRetries = 0; + $done = false; + while(!$done){ + try{ + $result = $this->ExecuteQuery($handle, $preparedSql, $params); + $done = true; + } + catch(\PDOException $ex){ + if($ex->errorInfo[1] == 1213 && $deadlockRetries < 3){ // InnoDB deadlock, this is normal and happens occasionally. All we have to do is retry the query. + $deadlockRetries++; + + usleep(500000 * $deadlockRetries); // Give the deadlock some time to clear up. Start at .5 seconds + } + elseif(stripos($ex->getMessage(), '1064 offset out of bounds') !== false){ + $done = true; + // We reach here if Sphinx tries to get a record past its page limit. Just silently do nothing. + } + else{ + $done = true; + if(SITE_STATUS == SITE_STATUS_DEV){ + print($sql); + throw($ex); + } + else{ + Logger::WriteErrorLogEntry($ex->getMessage()); + Logger::WriteErrorLogEntry($preparedSql); + Logger::WriteErrorLogEntry(vds($params)); + } + } + } + } + + // If only one rowset is returned, change the result object + if(sizeof($result) == 1){ + $result = $result[0]; + } + + return $result; + } + + private function ExecuteQuery($handle, string $preparedSql, array $params){ + $handle->execute(); + + $result = []; + do{ + try{ + if($handle->columnCount() > 0){ + $result[] = $handle->fetchAll(\PDO::FETCH_OBJ); + } + } + catch(\PDOException $ex){ + // HY000 is thrown when there is no result set, e.g. for an update operation. + // If anything besides that is thrown, then send it up the stack + if($ex->errorInfo[0] != "HY000"){ + throw $ex; + } + } + }while($handle->nextRowset()); + + return $result; + } + + // Gets the last AUTO-INCREMENT id + public function GetLastInsertedId(){ + $id = $this->_link->lastInsertId(); + + if($id == 0){ + return null; + } + + return $id; + } +} diff --git a/lib/OrmBase.php b/lib/OrmBase.php new file mode 100644 index 00000000..b7043264 --- /dev/null +++ b/lib/OrmBase.php @@ -0,0 +1,30 @@ + $value){ + if(substr($property, strlen($property) - 9) == 'Timestamp'){ + if($value !== null){ + $object->$property = new DateTime($value, new DateTimeZone('UTC')); + } + else{ + $object->$property = null; + } + } + elseif(substr($property, strlen($property) - 5) == 'Cache'){ + $property = substr($property, 0, strlen($property) - 5); + $object->$property = $value; + } + else{ + $object->$property = $value; + } + } + + return $object; + } + + public static function FromRow($row){ + return self::FillObject(new static(), $row); + } +} diff --git a/lib/PropertiesBase.php b/lib/PropertiesBase.php new file mode 100644 index 00000000..5ad0b39d --- /dev/null +++ b/lib/PropertiesBase.php @@ -0,0 +1,33 @@ +$function(); + } + elseif(substr($var, 0, 7) == 'Display'){ + // If we're asked for a DisplayXXX property and the getter doesn't exist, format as escaped HTML. + if($this->$var === null){ + $target = substr($var, 7, strlen($var)); + $this->$var = Formatter::ToPlainText($this->$target); + } + + return $this->$var; + } + else{ + return $this->$var; + } + } + + public function __set($var, $val){ + $function = 'Set' . $var; + if(method_exists($this, $function)){ + $this->$function($val); + } + else{ + $this->$var = $val; + } + } +}