diff options
Diffstat (limited to 'src/application/mystic/forum/Database.php')
| -rw-r--r-- | src/application/mystic/forum/Database.php | 194 | 
1 files changed, 153 insertions, 41 deletions
diff --git a/src/application/mystic/forum/Database.php b/src/application/mystic/forum/Database.php index 9b9cf55..7c9ac7a 100644 --- a/src/application/mystic/forum/Database.php +++ b/src/application/mystic/forum/Database.php @@ -6,10 +6,12 @@ namespace mystic\forum;  use DateTime;  use DateTimeImmutable;  use mystic\forum\attributes\Column; +use mystic\forum\attributes\DefaultValue;  use mystic\forum\attributes\NotNull;  use mystic\forum\attributes\PrimaryKey;  use mystic\forum\attributes\References;  use mystic\forum\attributes\Table; +use mystic\forum\attributes\Unique;  use mystic\forum\exceptions\DatabaseConnectionException;  use mystic\forum\orm\Entity;  use mystic\forum\utils\ArrayUtils; @@ -25,6 +27,25 @@ class Database {      private const PRIMARY_KEY = 0b0000_0001;      private const NOT_NULL    = 0b0000_0010;      private const REFERENCES  = 0b0000_0100; +    private const UNIQUE      = 0b0000_1000; +    private const DEFAULT     = 0b0001_0000; + +    protected static function encodeBinary(string $bin): string { +        return "\\x" . bin2hex($bin); +    } + +    protected static function decodeBinary(string $enc): string { +        return hex2bin(substr($enc, 2)); +    } + +    public static function generateId(int $length = 64): string { +        static $charset = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"; +        static $charsetLength = strlen($charset); +        $buf = ""; +        for ($i = 0; $i < $length; ++$i) +            $buf .= $charset[random_int(0, $charsetLength - 1)]; +        return $buf; +    }      public function __construct(string $connectionString) {          try { @@ -136,6 +157,14 @@ class Database {              $colType = self::getColumnType($rflProp);              $statement = "$colName $colType"; +            if (($colInfo["flags"] & self::DEFAULT) !== 0) { +                $statement .= " DEFAULT " . $colInfo["defaultValue"]; +            } + +            if (($colInfo["flags"] & self::UNIQUE) !== 0) { +                $statement .= " UNIQUE"; +            } +              if (($colInfo["flags"] & self::NOT_NULL) !== 0) {                  $statement .= " NOT NULL";              } @@ -159,15 +188,21 @@ class Database {              $flags = 0;              $attrs = $prop->getAttributes();              $reference = ""; +            $defaultValue = null;              foreach ($attrs as $attr) {                  $attrName = $attr->getName();                  if ($attrName === PrimaryKey::class) {                      $flags |= self::PRIMARY_KEY;                  } elseif ($attrName === NotNull::class) {                      $flags |= self::NOT_NULL; +                } elseif ($attrName === Unique::class) { +                    $flags |= self::UNIQUE;                  } elseif ($attrName === References::class) {                      $flags |= self::REFERENCES;                      $reference = $attr->newInstance()->__toString(); +                } elseif ($attrName === DefaultValue::class) { +                    $flags |= self::DEFAULT; +                    $defaultValue = $attr->newInstance()->defaultValue;                  }              } @@ -175,6 +210,8 @@ class Database {                  "propertyName" => $prop->getName(),                  "flags" => $flags,                  "reference" => $reference, +                "defaultValue" => $defaultValue, +                "columnType" => self::getColumnType($prop),              ]];          }, $rflEntity->getProperties(ReflectionProperty::IS_PUBLIC)));      } @@ -186,8 +223,10 @@ class Database {          return null;      } -    private static function stringifyValue(mixed $value): ?string { -        if (is_null($value)) +    private static function stringifyValue(mixed $value, string $columnType): ?string { +        if ($columnType === "bytea" && is_string($value)) +            return self::encodeBinary($value); +        elseif (is_null($value))              return null;          elseif (is_bool($value))              return $value ? "true" : "false"; @@ -201,6 +240,7 @@ class Database {      private static function assignValue(Entity &$entity, array $colProps, ?string $value): void {          $propName = $colProps["propertyName"]; +        $colType = $colProps["columnType"];          $prop = new \ReflectionProperty($entity, $propName);          $type = $prop->getType(); @@ -210,38 +250,43 @@ class Database {          $typeName = $type->getName();          if (!$type->isBuiltin() && $typeName !== \DateTime::class && $typeName !== \DateTimeImmutable::class)              throw new \RuntimeException("User-defined types cannot be converted to from a database type."); -        switch ($typeName) { -            case \DateTime::class: -                $typedValue = new \DateTime($value); -                break; -            case \DateTimeImmutable::class: -                $typedValue = new \DateTimeImmutable($value); -                break; -            case "true": -            case "false": -            case "bool": -                $typedValue = in_array(strtolower($value), ["true","yes","on","1"]) ? true : (in_array(strtolower($value), ["false","no","off","0"]) ? false : null); -                break; -            case "float": -                $typedValue = floatval($value); -                break; -            case "int": -                $typedValue = intval($value); -                break; -            case "string": -            case "null": -                $typedValue = strval($value); -                break; -            case "object": -            case "array": -            case "iterable": -            case "callable": -            case "mixed": -            case "never": -            case "void": -            default: -                throw new \RuntimeException("The type \"$typeName\" cannot be restored from the database"); -        } +        if ($value !== null) +            switch ($typeName) { +                case \DateTime::class: +                    $typedValue = new \DateTime($value); +                    break; +                case \DateTimeImmutable::class: +                    $typedValue = new \DateTimeImmutable($value); +                    break; +                case "true": +                case "false": +                case "bool": +                    $typedValue = in_array(strtolower($value), ["true","t","yes","on","1"]) ? true : (in_array(strtolower($value), ["false","f","no","off","0"]) ? false : null); +                    break; +                case "float": +                    $typedValue = floatval($value); +                    break; +                case "int": +                    $typedValue = intval($value); +                    break; +                case "string": +                case "null": +                    if ($colType === "bytea") { +                        $typedValue = self::decodeBinary(strval($value)); +                    } else { +                        $typedValue = strval($value); +                    } +                    break; +                case "object": +                case "array": +                case "iterable": +                case "callable": +                case "mixed": +                case "never": +                case "void": +                default: +                    throw new \RuntimeException("The type \"$typeName\" cannot be restored from the database"); +            }          $entity->{$propName} = $typedValue;      } @@ -249,7 +294,7 @@ class Database {          $values = [];          foreach ($cols as $colInfo) { -            $values []= self::stringifyValue($entity->{$colInfo["propertyName"]} ?? null); +            $values []= self::stringifyValue($entity->{$colInfo["propertyName"]} ?? null, $colInfo["columnType"]);          }          return $values; @@ -288,6 +333,67 @@ class Database {          return true;      } +    public function fetchWhere(Entity &$entity, string $columnName): bool { +        $entityClassName = get_class($entity); +        $tableName = self::getTableName($entityClassName); +        $reflClass = new ReflectionClass($entityClassName); +        $cols = self::getColumns($reflClass); +        if (!isset($cols[$columnName])) +            throw new \RuntimeException("Column $columnName does not exist!"); +        $query = "SELECT * FROM $tableName WHERE $columnName = \$1 LIMIT 1;"; +        $result = \pg_query_params($this->connection, $query, [ $entity->{$cols[$columnName]["propertyName"]} ]); +        if ($result === false) +            throw new \RuntimeException("Fetch failed: " . \pg_last_error($this->connection)); +        $row = \pg_fetch_assoc($result); +        \pg_free_result($result); +        if ($row === false) +            return false; +        foreach ($cols as $colName => $colProps) +            self::assignValue($entity, $colProps, $row[$colName]); +        return true; +    } + +    public function fetchAll(string $entityClassName): array { +        $tableName = self::getTableName($entityClassName); +        $reflClass = new ReflectionClass($entityClassName); +        $cols = self::getColumns($reflClass); +        $query = "SELECT * FROM $tableName;"; +        $result = \pg_query($this->connection, $query); +        if ($result === false) +            throw new \RuntimeException("Fetch failed: " . \pg_last_error($this->connection)); +        $items = []; +        while (($row = \pg_fetch_assoc($result)) !== false) { +            $entity = new $entityClassName(); +            foreach ($cols as $colName => $colProps) +                self::assignValue($entity, $colProps, $row[$colName]); +            $items []= $entity; +        } +        \pg_free_result($result); +        return $items; +    } + +    public function fetchCustom(string $entityClassName, string $customQuery, ?array $customQueryParams = null): array { +        $tableName = self::getTableName($entityClassName); +        $reflClass = new ReflectionClass($entityClassName); +        $cols = self::getColumns($reflClass); +        $query = "SELECT * FROM $tableName $customQuery;"; +        if ($customQueryParams === null) +            $result = \pg_query($this->connection, $query); +        else +            $result = \pg_query_params($this->connection, $query, $customQueryParams); +        if ($result === false) +            throw new \RuntimeException("Fetch failed: " . \pg_last_error($this->connection)); +        $items = []; +        while (($row = \pg_fetch_assoc($result)) !== false) { +            $entity = new $entityClassName(); +            foreach ($cols as $colName => $colProps) +                self::assignValue($entity, $colProps, $row[$colName]); +            $items []= $entity; +        } +        \pg_free_result($result); +        return $items; +    } +      public function delete(Entity &$entity): bool {          $entityClassName = get_class($entity);          $tableName = self::getTableName($entityClassName); @@ -313,14 +419,20 @@ class Database {          if ($primaryCol === null)              throw new \RuntimeException("Updating an entity requires a primary key column to be specified"); -        $filteredCols = array_filter(ArrayUtils::asPairs($cols), function($pair) { +        $filteredCols = array_values(array_filter(ArrayUtils::asPairs($cols), function($pair) {              return ($pair[1]["flags"] & self::PRIMARY_KEY) === 0; -        }); -        $colDef = implode(", ", array_map(function(array $pair): string { -            return implode("=", [ $pair[0], '$' . $pair[1] ]); -        }, $filteredCols)); +        })); +        // echo "<pre>"; +        // var_dump($filteredCols); +        // exit; +        $qCols = []; +        for ($i = 0; $i < count($filteredCols); ++$i) { +            $qCols []= implode("=", [ $filteredCols[$i][0], '$' . ($i + 2) ]); +        } +        $colDef = implode(", ", $qCols);          $query = "UPDATE $tableName SET $colDef WHERE $primaryCol = \$1;"; -        $result = \pg_query_params($this->connection, $query, array_map(fn($c) => $entity->{$c["propertyName"]}, $filteredCols)); +        //$theCols = ArrayUtils::assocFromPairs($filteredCols); +        $result = \pg_query_params($this->connection, $query, self::getColumnValues($entity, $cols));          if ($result === false)              throw new \RuntimeException("Update failed: " . \pg_last_error($this->connection));          $num_affected_rows = \pg_affected_rows($result);  |