chimera-mark2-core-release/core/fox/baseClass.php

661 lines
23 KiB
PHP

<?php
namespace fox;
use Exception;
/**
*
* Class fox\baseClass
* @desc FOX Base Class
* @copyright MX STAR LLC 2021
* @version 4.0.0
* @author Pavel Dmitriev
* @license GPLv3
* @property-read mixed $changelog
* @property-read string $sqlSelectTemplate
* @property-read string $sqlCountSelectTemplate
*
**/
class baseClass extends dbStoredBase implements \JsonSerializable, jsonImportable
{
protected ?sql $sql = null;
protected $fillPrefix = "";
protected $changelog = null;
protected $__settings;
protected $__xId = null;
// id for xConstruct;
# if null - generated automatically
public static $baseSqlSelectTemplate = null;
# if null - generated automatically
public static $baseSqlCountSelectTemplate = null;
# primary index field. Default is id
public static $sqlIdx = "id";
# if true then DELETE when called delete() method, else mark as deleted
public static $allowDeleteFromDB = false;
# if !null - this field will set to 1 on delete() when $allowDeleteFromDB==false
public static $deletedFieldName = null;
protected $__sqlSelectTemplate = null;
protected $__sqlCountSelectTemplate = null;
# basic exclude props
protected static $excludePropsBase = [
'sql',
'changelog',
'__sqlSelectTemplate',
'__sqlCountSelectTemplate',
'fillPrefix'
];
# custom excluded props from export and var_dump
protected static $excludeProps = [];
public function getSqlSchema()
{
$rv = [];
foreach (static::$sqlColumns as $key => $conf) {
if ($conf["type"] !== "SKIP") {
$rv[$key] = static::$sqlColumns[$key];
}
}
foreach ($this as $key => $val) {
if (array_key_exists($key, static::$sqlColumns)) {
continue;
}
if (preg_match("/^[^_][^_].*/", $key) && (array_search($key, array_merge(static::$excludeProps, static::$excludePropsBase)) === false)) {
$type = null;
$idx = null;
$null = null;
switch (gettype($val)) {
case "NULL":
if ($key == static::$sqlIdx) {
$type = "INT";
$idx = "AI";
$null = false;
} else {
throw new \Exception("Invalid type conversion for key $key at " . get_class($this));
}
break;
case "array":
$type = "TEXT";
break;
case "integer":
$type = "INT";
break;
case "string":
$type = "VARCHAR(255)";
break;
case "boolean":
$type = "INT";
break;
case "object":
if (! empty($val::$SQLType)) {
$type = $val::$SQLType;
} elseif ($val instanceof stringExportable) {
$type = "VARCHAR(255)";
} elseif ($val instanceof \JsonSerializable) {
$type = "VARCHAR(255)";
} else {
throw new \Exception("Invalid type conversion for key $key at " . get_class($this));
}
break;
default:
throw new \Exception("Invalid type conversion for key $key at " . get_class($this));
break;
}
$rv[$key]["type"] = $type;
if ($idx) {
$rv[$key]["index"] = $idx;
}
if ($null !== null) {
$rv[$key]["nullable"] = $null;
}
if ($key == static::$sqlIdx) {
$rv[$key]["first"] = true;
} else {
$rv[$key]["first"] = false;
}
}
}
return $rv;
}
protected function checkSql()
{
if ($this->sql === null) {
$this->sql = sql::getConnection();
}
}
protected function __xConstruct()
{
return true;
}
public function __construct($id = null, ?namespace\sql $sql = null, $prefix = null, $settings = null, $loadDeleted=false)
{
# How to call from child template:
# parent::__construct($id, $sql, $prefix, $settings, $loadDeleted);
$this->__settings = $settings;
if (empty($this::$baseSqlSelectTemplate) && ! empty($this::$sqlTable)) {
$this->__sqlSelectTemplate = "select `i`.* from `" . $this::$sqlTable . "` as `i`";
} else {
$this->__sqlSelectTemplate = $this::$baseSqlSelectTemplate;
}
if (empty($this::$baseSqlCountSelectTemplate) && ! empty($this::$sqlTable)) {
$this->__sqlCountSelectTemplate = "select count(".static::$sqlIdx.") as `count` from `" . $this::$sqlTable . "` as `i`";
} else {
$this->__sqlCountSelectTemplate = $this::$baseSqlCountSelectTemplate;
}
if (isset($sql)) {
$this->sql = &$sql;
}
$this->fillPrefix = $prefix;
$this->__xId = $id;
if ($this->__xConstruct() === false) {
// stop autoload content if __xConstruct return FALSE;
return;
}
switch (gettype($id)) {
case "array":
$this->fillFromRow($id);
break;
case "string":
if ($this instanceof stringImportable) {
$this->__fromString($id);
} elseif (is_numeric($id)) {
$this->fill($id, $loadDeleted);
} elseif ($x = json_decode($id)) {
$this->fillFromRow($x);
} else {
throw new \Exception("Invalid input format", 597);
}
break;
case "integer":
$this->fill($id, $loadDeleted);
break;
case "NULL":
break;
default:
throw new \Exception("Invalid type " . gettype($id) . " for " . get_class($this) . "->__construct", 591);
break;
}
}
protected function fill($id, $loadDeleted=false)
{
if (! empty($this->__sqlSelectTemplate)) {
$this->checkSql();
$row = $this->sql->quickExec1Line($this->__sqlSelectTemplate . " where `i`." . $this::$sqlIdx . " = '" . $id . "'");
if (! empty($row)) {
$this->fillFromRow($row);
if (!$loadDeleted && static::$deletedFieldName && $this->{static::$deletedFieldName}) {
trigger_error("Record with " . (static::$sqlIdx) . " " . $id . " was deleted " . get_class($this));
throw new foxException("Not found",404);
}
} else {
throw new foxException("Not found",404);
}
} else {
throw new \Exception("Fill by ID not implemented in " . get_class($this), 592);
}
}
protected function fillFromRow($row)
{
foreach ($row as $key => $val) {
if (! empty($this->fillPrefix)) {
if (! preg_match("/^" . $this->fillPrefix . "/", $key)) {
continue;
}
$key = preg_replace("/^" . $this->fillPrefix . "/", "", $key);
}
if (property_exists($this, $key) || property_exists($this, "__" . $key)) {
if (property_exists($this, "__" . $key)) {
$key = "__" . $key;
}
if (gettype($this->{$key}) == 'boolean') {
$this->{$key} = $val == 1;
} elseif ((($this->{$key}) instanceof jsonImportable) || (($this->{$key}) instanceof stringImportable)) {
$typeof = get_class(($this->{$key}));
$this->{$key} = new $typeof($val);
} elseif (gettype($this->{$key}) == "array") {
if (gettype($val) == "string") {
$this->{$key} = (array) json_decode($val, true);
} elseif (gettype($val) == "array") {
$this->{$key} = $val;
} elseif ($val === null) {
$this->{$key} = [];
} elseif (gettype($val) == "object") {
$this->{$key} = (array) $val;
} else {
throw new Exception("Invalid type " . gettype($val) . " for " . $key . " in " . get_class($this));
}
} else {
$this->{$key} = $val;
}
}
}
}
public function save()
{
if (! $this->validateSave()) {
return false;
}
$this->checkSql();
if (property_exists($this, static::$sqlIdx) && ($this->{static::$sqlIdx} == null)) {
return $this->create();
} else {
$class = get_class($this);
if (is_numeric($this->{static::$sqlIdx})) {
$ref = new $class((int) $this->{static::$sqlIdx}, loadDeleted: true);
} else {
$ref = new $class($this->{static::$sqlIdx}, loadDeleted: true);
}
$this->changelog = "";
foreach ($this as $key => $val) {
if ((array_search($key, array_merge(static::$excludeProps, static::$excludePropsBase)) === false) && $ref->{$key} != $this->{$key}) {
$stringRef = (is_bool($ref->{$key}) || ! (is_object($ref->{$key}) || is_array($ref->{$key})));
$stringVal = (is_bool($val) || ! (is_object($val) || is_array($val)));
if (preg_match("/^_/",$key)) {
$this->changelog .= "key: " . $key . " changed \n";
} else {
$this->changelog .= "key: " . $key . " changed from " . ($stringRef ? (is_bool($ref->{$key}) ? ($ref->{$key} ? "true" : "false") : $ref->{$key}) : "<" . gettype($ref->{$key}) . ">") . " to " . ($stringVal ? (is_bool($val) ? ($val ? "true" : "false") : $val) : "<" . gettype($val) . ">") . ";\n ";
}
}
}
if (empty($this->changelog)) {
return true;
}
return $this->update();
}
}
public function delete()
{
if (property_exists($this, static::$sqlIdx) && ($this->{static::$sqlIdx} == null)) {
return false;
}
if (!$this->validateDelete()) {
throw new \Exception("ValidateDelete failed");
}
if (static::$allowDeleteFromDB) {
$this->checkSql();
$this->sql->quickExec("DELETE FROM `" . static::$sqlTable . "` where " . static::$sqlIdx . " = '" . $this->{static::$sqlIdx} . "'");
if (! (empty(static::$deletedFieldName))) {
$this->{static::$deletedFieldName} = true;
}
$this->{static::$sqlIdx} = null;
} elseif (! (empty(static::$deletedFieldName))) {
$this->checkSql();
$this->sql->quickExec("UPDATE `" . static::$sqlTable . "` set `" . static::$deletedFieldName . "`='1' where " . $this::$sqlIdx . " = '" . $this->{static::$sqlIdx} . "'");
$this->{static::$deletedFieldName} = true;
return true;
} else {
throw new \Exception("DELETE not implemented in " . get_class($this), 592);
}
}
protected function validateDelete()
{
return true;
}
protected function update()
{
if (! empty($this::$sqlTable)) {
$this->sql->prepareUpdate($this::$sqlTable);
}
if (empty($this::$sqlTable) || ! $this->updateAddParams()) {
throw new \Exception("Method update not implemented in " . get_class($this), 593);
}
$this->sql->paramClose($this::$sqlIdx . " = '" . $this->{static::$sqlIdx} . "'");
$this->sql->quickExecute();
return false;
}
protected function create()
{
if (! empty($this::$sqlTable)) {
$this->sql->prepareInsert($this::$sqlTable);
}
if (empty($this::$sqlTable) || ! $this->createAddParams()) {
throw new \Exception("Method create not implemented in " . get_class($this), 594);
}
$this->sql->paramClose();
$this->sql->quickExecute();
if (property_exists($this, static::$sqlIdx)) {
$this->{static::$sqlIdx} = $this->sql->getInsertId();
if (is_numeric($this->{static::$sqlIdx})) {
$this->fill((int) $this->{static::$sqlIdx});
} else {
$this->fill($this->{static::$sqlIdx});
}
}
return true;
}
protected function updateAddParams()
{
return $this->addParams();
}
protected function createAddParams()
{
return $this->addParams();
}
protected function addParams()
{
$this->checkSql();
foreach ($this->getSqlSchema() as $key => $conf) {
if ($key == static::$sqlIdx) {
continue;
}
if (property_exists($this, $key)) {
$val = $this->{$key};
} elseif (property_exists($this, "__" . $key)) {
$val = $this->{"__" . $key};
} else {
$val = null;
}
if (is_array($val)) {
$sqlVal = json_encode($val);
$sqlNull = empty($val);
} elseif (is_object($val) && ! ($val instanceof stringExportable)) {
if ($val instanceof \JsonSerializable) {
$sqlVal = json_encode($val);
$sqlNull = empty($val);
} else {
throw new Exception("Oups... $key is not jsonSerialiazable");
}
} elseif (is_object($val) && ($val instanceof stringExportable)) {
if ($val->isNull()) {
$sqlVal = null;
$sqlNull = true;
} else {
$sqlVal = (string) $val;
$sqlNull = ($val === null);
}
} elseif (is_bool($val)) {
$sqlVal = $val ? 1 : 0;
$sqlNull = false;
} else {
$sqlVal = (string) $val;
$sqlNull = ($val === null);
}
if ($sqlNull && array_key_exists("nullable", $conf) && $conf["nullable"] === false) {
throw new Exception("Field $key can't be null in " . get_class($this));
}
$this->sql->paramAdd($key, $sqlVal, $sqlNull);
}
return true;
}
protected function validateSave()
{
return true;
}
public function __get($key)
{
switch ($key) {
case "sqlSelectTemplate":
return $this->__sqlSelectTemplate;
break;
case "sqlCountSelectTemplate":
return $this->__sqlCountSelectTemplate;
break;
case "sql":
$this->checkSql();
return $this->sql;
case "changelog":
return $this->changelog;
break;
default:
if (property_exists($this, $key)) {
if ($this->{$key} instanceof stringExportable && !($this->{$key} instanceof \JsonSerializable)) {
return (string) $this->{$key};
} else {
return $this->{$key};
}
} else {
throw new \Exception("property $key not availiable for read in class " . get_class($this), 595);
}
}
}
public static function qGetSql() : sql
{
return (new static())->getSql();
}
public static function qGetSqlSelectTemplate() : string
{
return (new static())->__sqlSelectTemplate;
}
public function getSql() : sql
{
$this->checkSql();
return $this->sql;
}
public static function getCount($where = null)
{
$s = new static();
if (empty($s::$sqlTable)) {
throw new \Exception("Method getTotalCount not implemented in " . get_class($s), 691);
}
$sql = $s->getSql();
$res = $sql->quickExec1Line("select count(" . (empty(static::$sqlIdx) ? "*" : static::$sqlIdx) . ") as `cnt` from `" . static::$sqlTable . "`" . (empty($where) ? "" : " where $where"));
return $res["cnt"];
}
public function __set($key, $val)
{
switch ($key) {
case "settings":
$this->__settings = $val;
break;
default:
throw new \Exception("property $key not availiable for write in class " . get_class($this), 596);
break;
}
}
public function __debugInfo()
{
$rv = [];
foreach ($this as $key => $value) {
if (array_search($key, array_merge(static::$excludeProps, static::$excludePropsBase)) === false && ! preg_match("!^_!", $key)) {
if ($value instanceof stringExportable) {
if ($value->isNull()) {
$rv[$key] = null;
} else {
$rv[$key] = (string) $value;
}
} else {
$rv[$key] = $value;
}
}
}
return $rv;
}
public function export()
{
$rv = [];
foreach ($this as $key => $value) {
if (array_search($key, array_merge(static::$excludeProps, static::$excludePropsBase)) === false && ! preg_match("!^_!", $key)) {
if (($this->__get($key)) instanceof \JsonSerializable) {
$rv[$key] = $this->__get($key);
} elseif (($this->__get($key)) instanceof stringExportable) {
if ($this->__get($key)->isNull()) {
$rv[$key] = null;
} else {
$rv[$key] = (string) ($this->__get($key));
}
} else {
$rv[$key] = $this->__get($key);
}
}
}
return $rv;
}
public function jsonSerialize()
{
$rv = $this->export();
$rv["_type"] = get_class($this);
return $rv;
}
protected static function xSearch($where, $pattern, ?array $options, sql $sql) {
return ["where"=>$where, "join"=>null];
}
public static function search($pattern=null, $pageSize=null, $page=1, $options=[]) {
if (static::$sqlTable == null) {
throw new \Exception("Search not implemented for ".static::class);
}
if ($pattern instanceof request) {
@$page=$pattern->requestBody->page;
@$pageSize=$pattern->requestBody->pageSize;
$options=(array)$pattern->requestBody;
@$pattern=$pattern->requestBody->pattern;
}
$ref=new static();
$sql = $ref->getSql();
$where="";
if (!empty($pattern)) {
foreach (static::$sqlColumns as $key=>$val) {
if (!empty($val["search"])) {
switch (strtolower($val["search"])) {
case "strict":
$where.=(empty($where)?"":" OR ")."`$key`='".common::clearInput($pattern)."'";
break;
case "like":
$where.=(empty($where)?"":" OR ")."`$key` like '%".common::clearInput($pattern)."%'";
break;
case "start":
$where.=(empty($where)?"":" OR ")."`$key` like '%".common::clearInput($pattern)."'";
break;
case "invcode":
$where.=(empty($where)?"":" OR ")."`$key`='".UID::clear($pattern)."'";
break;
default:
break;
}
}
}
}
if (static::$deletedFieldName && empty($options["showDeleted"])) {
$where = (empty($where)?"":"(".$where.") AND ")."`".static::$deletedFieldName."` = 0";
}
$xRes=static::xSearch($where, $pattern, $options, $sql);
$where = array_key_exists("where",$xRes)?$xRes["where"]:"";
$join=array_key_exists("join",$xRes)?$xRes["join"]:"";
$groupBy=array_key_exists("group",$xRes)?$xRes["group"]:"";
$orderBy=array_key_exists("order",$xRes)?$xRes["order"]:"";
if (empty($orderBy) && array_key_exists("orderBy", $options)) {
@$orderDesc=strtoupper($options["orderDir"])=="DESC"?"DESC":"ASC";
$orderBy=common::clearInput($options["orderBy"])." ".$orderDesc;
}
$sqlQueryStringCount=$ref->sqlCountSelectTemplate.(empty($join)?"":" ".$join).(empty($where)?"":" WHERE ".$where).(empty($groupBy)?"":" GROUP BY ".$groupBy);
$rv=new searchResult();
$pageSize=is_numeric($pageSize)?((int)$pageSize):null;
$page=is_numeric($page)?((int)$page):1;
if ($pageSize) {
$rc=$sql->quickExec1Line($sqlQueryStringCount);
$rv->pages=ceil($rc["count"]/$pageSize);
} else {
$page=1;
$rv->pages=1;
}
if ($rv->pages < $page) { $page=$rv->pages; }
$rv->page=$page;
if ($pageSize!==null) {
if ($page<1) { $page=1;}
$limit = "LIMIT ".($pageSize*($page-1)).", ".$pageSize;
} else {
$limit="";
}
$sqlQueryString=$ref->sqlSelectTemplate.(empty($join)?"":" ".$join).(empty($where)?"":" WHERE ".$where).(empty($orderBy)?"":" ORDER BY ".$orderBy).(empty($groupBy)?"":" GROUP BY ".$groupBy).(empty($limit)?"":" ".$limit);
$res=$sql->quickExec($sqlQueryString);
$rv->setIndexByPage($page, $pageSize);
while ($row=mysqli_fetch_assoc($res)) {
$rv->push(new static($row));
}
return $rv;
}
protected static function log(string $instance, $method, string $message, ?user $user=null, ?string $refType=null, ?string $refId=null, string $msgCode=null, ?string $severity="INFO", $payload=null) {
return logEntry::add($instance, static::class, $method, $msgCode, $message, $severity, $user, $refType, $refId, $payload);
}
}