Edit via SFTP
  1. <?php namespace ferret\data;
  2. /*
  3.   PURPOSE: Database parent classes
  4.   PART OF: db* database library
  5.   HISTORY:
  6.   2015-03-13 created
  7.   2019-09-22 much rewrite for v3 take 2 (now named v0.4)
  8.   2020-07-31 let's just give *all* rowsets the ability to store a native driver
  9.   * removed ifHasDriver; functions still exist in
  10.   * removed ifDrivenRowSpawner
  11.   * removed tDrivenTable in tables/table-db.php
  12.   * removed tDrivenRows, moved fx() to cMultiRow
  13.   2020-12-25 significant renaming of stuff
  14. */
  15. use ferret\cActionStatus;
  16. /*::::
  17.   PURPOSE:
  18.   STATIC functions create an object of the appropriate descendant class
  19.   DYNAMIC functions interact with the database
  20.   Does not make any assumptions about how the database works.
  21.   HISTORY:
  22.   2020-11-09 decided to make this descend from cActionStatus, for better status reporting
  23.   2021-04-17 now descends from cObjectStatus because restructuring
  24. */
  25. abstract class cDatabase extends \ferret\cObjectStatus {
  26.  
  27. public function __construct() {
  28. $this->CreateActionStatus();
  29. }
  30.  
  31. private $osAction;
  32. protected function CreateActionStatus() { $this->osAction = new cActionStatus(); }
  33. public function Action() : cActionStatus { return $this->osAction; }
  34.  
  35. /*----
  36.   ACTION: create a fully-initialized database Connection object from a spec string
  37.   RULES: all db Connection specs start with "<type>:", which determines which class to use
  38. After that, details are up to the Connection.
  39.   KLUGE: There doesn't seem to be any way to query an object to determine whether it implements
  40. an interface or uses a trait, so we have to go for instantiating the DB object and then
  41. checking to see whether it implments the method we need.
  42.   */
  43. static public function Instantiate(cDatabaseSpec $oSpec,$bAllowFail) {
  44. $sType = $oSpec->GetTypeString();
  45. if (self::TypeExists($sType)) {
  46. $sClass = self::GetTypeClass($sType,$bAllowFail);
  47.  
  48. $oDB = new $sClass; // make the Engine object
  49. $oDB->SetSpec($oSpec);
  50. return $oDB;
  51. } else {
  52. if ($bAllowFail) {
  53. return NULL;
  54. } else {
  55. throw new \exception("Ferreteria config error: Connection type [$sType] is not registered.");
  56. }
  57. }
  58. }
  59.  
  60. // -- STATIC: SETUP -- //
  61. // ++ STATIC: TYPES ++ //
  62.  
  63. static $arTypes;
  64. static public function RegisterType(string $sType,string $sClassName) { self::$arTypes[$sType] = $sClassName; }
  65. static protected function TypeExists(string $sType) : bool { return \fcArray::Exists(self::$arTypes,$sType); }
  66. static protected function GetTypeClass($sType,$bAllowFail) : string {
  67. if (self::TypeExists($sType)) {
  68. $sClass = self::$arTypes[$sType];
  69. return $sClass;
  70. } else {
  71. $sErrSfx = 'so cannot get a wrapper for protocol "'.$sType.'".';
  72. if (is_array(self::$arTypes)) {
  73. if (count(self::$arTypes) > 0) {
  74. // unknown db type
  75. return NULL; // 2020-02-23 This will error out now; maybe we should return a status object instead - TODO
  76. } else {
  77. throw new exception("Ferreteria config error: no database types have been defined, $sErrSfx");
  78. }
  79. } else {
  80. throw new exception("Ferreteria config error: Database type error has not been initialized, $sErrSfx");
  81. }
  82. }
  83. }
  84.  
  85. // -- STATIC: TYPES -- //
  86. // ++ CONNECTING ++ //
  87.  
  88. abstract public function Open();
  89. abstract public function Shut();
  90.  
  91. // -- CONNECTING -- //
  92. // ++ DATA READ/WRITE ++ //
  93.  
  94. /*----
  95.   FORMERLY FetchData
  96.   TODO: 2020-02-23 this should replace MakeTable
  97.   2021-01-21 Actually, I think they've both been replaced by MakeTable and MakeRow.
  98.   */
  99. #abstract public function FetchTableData(string $sTableClass,$id=NULL) : cRowKit; //
  100. abstract public function FetchRecordset(string $sql,cTabloid $tbl) : cSelectResult;
  101. abstract public function ExecuteAction(string $sql);
  102. abstract public function CountOfAffectedRows() : int;
  103. abstract public function CreatedID();
  104. abstract public function FetchNextResultRow(cMultiRowSerial $rs) : cRecordResult;
  105. abstract public function RewindResultRows(cMultiRowSerial $rs);
  106.  
  107. // -- DATA READ/WRITE -- //
  108. // ++ TRANSACTIONS ++ //
  109.  
  110. abstract public function TransactionOpen();
  111. abstract public function TransactionSave();
  112. abstract public function TransactionKill();
  113.  
  114. // -- TRANSACTIONS -- //
  115.  
  116. }
  117. /*::::
  118.   HISTORY:
  119.   2020-07-05 moved "use tHasTables;" here from cMySQL because all SQL DBs have tables,
  120.   and MW DBs are a use-case where we need to access tables without knowing what type
  121.   of DB engine we're using.
  122. */
  123. abstract class cDatabaseSQL extends cDatabase {
  124. use tHasTables;
  125.  
  126. // ++ PREPROCESSING ++ //
  127.  
  128. /*----
  129.   INPUT: non-NULL string value
  130.   OUTPUT: string value with quotes escaped, but NOT quoted
  131.   TODO: this is still a bad name for the action. I was originally thinking this, but now not sure:
  132.   (current) SanitizeString() -> NormalizeString(): sanitize string data but don't quote
  133.   (new) SanitizeString() = sanitize and always quote, because input is a string
  134.  
  135.   "Normalize" = make sure the value is safe to use as a value in SQL, but don't quote it
  136.   "Sanitize" = make sure the value can be used as-is in SQL without additional quoting
  137.   In any case, this and SanitizeValue() (and corresponding array fx) need better names.
  138.   Maybe SanitizeString() -> EscapeString().
  139.   TODO: What are the use-cases for this function?
  140.   */
  141. abstract public function SanitizeString(string $s);
  142. /*----
  143.   INPUT: any scalar value
  144.   OUTPUT: non-blank SQL-compatible string that equates to the input value
  145.   quoted if necessary
  146.   ACTION: Sanitizes, and encloses in quotes if needed;
  147.   returns 'NULL' if input is NULL.
  148.   This is equivalent to the functionality of
  149.   mysql_real_escape_string().
  150.   HISTORY:
  151.   2017-02-11 Now handles NULL properly. Also, decided there's no
  152.   need to sanitize a numeric.
  153.   */
  154. public function SanitizeValue($val) : string {
  155. if (is_null($val)) {
  156. return 'NULL';
  157. } else {
  158. if (is_numeric($val)) {
  159. return $val;
  160. } elseif (is_bool($val)) {
  161. return $val?'TRUE':'FALSE';
  162. } else {
  163. $s = $this->SanitizeString($val);
  164. return "'$s'";
  165. }
  166. }
  167. }
  168.  
  169. public function SanitizeValueArray(array $arVals) : array {
  170. $arOut = array();
  171. foreach ($arVals as $key => $val) {
  172. $arOut[$key] = $this->SanitizeValue($val);
  173. }
  174. return $arOut;
  175. }
  176.  
  177. // -- PREPROCESSING -- //
  178. }
  179. /*::::
  180.   NOTE: There's a temptation to type the $oConn as a \mysqli because that's what we're mainly using now,
  181.   but that's only one type of native object we should eventually be using. E.g. we'll eventually want
  182.   to support other types of DBs and drivers.
  183. */
  184. /* 2020-11-09 discontinuing this until another use-case besides cMySQL emerges
  185. trait tNativeObject {
  186.   protected function SetNativeObject($oConn) { $this->oNative = $oConn; }
  187.   protected function GetNativeObject() {
  188.   if (is_object($this->oNative)) {
  189.   return $this->oNative;
  190.   } else {
  191.   $sSchema = $this->GetSchemaString();
  192.   throw new exception("Trying to retrieve native db object for schema $sSchema, but it is not set.");
  193.   }
  194.   }
  195. }
  196. */
  197. trait tStandardSpec { // implements ifHasSpec
  198.  
  199. // ++ MAIN SETUP ++ //
  200.  
  201. /*----
  202.   RULES: spec includes everything after the "<type>:"
  203.   IMPLEMENTATION: "<user>:<password>@<host>/<schema>"
  204.   TODO: <schema> should be optional, but this is not yet coded.
  205.   RETURNS: nothing
  206.   */
  207. private $oSpec;
  208. public function SetSpec(cDatabaseSpec $oSpec) { $this->oSpec = $oSpec; }
  209. protected function GetSpec() : cDatabaseSpec { return $this->oSpec; }
  210. /*
  211.   $sHost = $oSpec->GetHostString();
  212.   $sUser = $oSpec->GetUserString();
  213.   $sPass = $oSpec->GetPassString();
  214.   $sSchema = $oSpec->GetSchemaString();
  215.  
  216.   // initialize it with these params
  217.   $this->SetHostString($sHost);
  218.   $this->SetUsername($sUser);
  219.   $this->SetPassword($sPass);
  220.   $this->SetSchemaString($sSchema);
  221.   }
  222.   */
  223.  
  224. // -- MAIN SETUP -- //
  225. // ++ SETUP FIELDS ++ //
  226.  
  227. /* 2020-11-09 replace all Get*() calls with GetSpec()->Get*()
  228.   protected function SetHostString(string $sVal) { $this->sHost = $sVal; }
  229.   protected function GetHostString() : string { return $this->sHost; }
  230.   protected function SetUsername(string $sVal) { $this->sUser = $sVal; }
  231.   protected function GetUsername() : string { return $this->sUser; }
  232.   protected function SetPassword(string $sVal) { $this->sPass = $sVal; }
  233.   protected function GetPassword() : string { return $this->sPass; }
  234.  
  235.   private $sSchema;
  236.   protected function SetSchemaString(string $sVal) { $this->sSchema = $sVal; }
  237.   // PUBLIC - exists only for debugging
  238.   public function GetSchemaString() : string { return $this->sSchema; }
  239.   */
  240.  
  241. // -- SETUP FIELDS -- //
  242. }
  243. /*::::
  244.   PURPOSE: the ability to spawn table-wrappers
  245.   TODO: determine if this even needs to be a trait, instead of just being part of cDatabaseSQL.
  246. */
  247. trait tHasTables {
  248. // ++ DATA OBJECTS ++ //
  249.  
  250. abstract protected function IsResultOk($poRes) : bool;
  251. /*----
  252.   NEW
  253.   INPUT:
  254.   $poRes should be either a Recordset wrapper object or boolean (output from native query() function)
  255.   $sRowsClass is the name of the class to use for the rowset wrapper
  256.   [OLD: $tbl is the Table wrapper object which should be used to instantiate the Recordset wrapper object.]
  257.   RETURNS: Recordset wrapper
  258.   * If query successful, Recordset wrapper object will be linked to a Table wrapper object, and will include the query results.
  259.   * If query failed, Recordset wrapper object will have 0 rows.
  260.   HISTORY:
  261.   2020-01-13 Significantly reworked -- arguments different, and now returns a status instead of a recordset/NULL
  262.   Edit: had to revert to original arguments, because we need a table to pass to the spawned recordset.
  263.   2020-07-07 Moved from cMySQL to tHasTables
  264.   */
  265. protected function ProcessResultset($poRes,cTabloid $tbl,string $sql) : cSelectResult {
  266. $rs = $tbl->SpawnRows(); // spawn a blank Recordset wrapper object
  267. $rs->RequireSpace();
  268. # echo 'RECORDSET CLASS: '.get_class($rs).'<br>';
  269. $ok = $this->IsResultOk($poRes);
  270. $os = new cSelectResult();
  271. $os->SetResults($sql,$ok,$tbl);
  272. if ($ok) {
  273. $rs->SetDriverBlob($poRes); // store mysqli_result in Recordset object = attach the data to the object
  274. $os->Rowset()->SetIt($rs);
  275. } else {
  276. $rs->SetDriverBlob(NULL); // no result to store
  277. $sErr = $this->ErrorString();
  278. $nErr = $this->ErrorNumber();
  279. $os->SetMessage($sErr);
  280. $os->SetNumber($nErr);
  281. //echo "<b>SQL</b>: $sql<br>";
  282. //echo "<b>DB Error</b>: $sErr<br>";
  283. //throw new \exception("Ferreteria/mysqli error: database query failed with error $nErr.");
  284. }
  285. return $os;
  286. }
  287. abstract public function CountOfReturnedRows(cMultiRow $rs) : int;
  288. /*----
  289.   NOTE: Adapted from Make() in db.v1
  290.   TODO: see notes on SetMissingPermit() - sometimes we want to upgrade an object rather than making a new one
  291.   ASSUMES:
  292.   * Table will connect itself to this database. (TODO: fix this assumption)
  293.   * If $id is not NULL, then $sTableClass must be a *single-keyed* table class and we will return a recordset object.
  294.   The table will be told to load the recordset whose row ID is $id (table->GetRow_fromKey($id)).
  295.   ...unless $id is KS_NEW_REC, in which case we will return a blank recordset.
  296.   HISTORY:
  297.   2017-08-28 Added KS_NEW_REC functionality -- but surely this must exist somewhere already.
  298.   2018-04-01 added $t->SetConnection($this)
  299.   2019-09-29 Renamed SetConnection() to SetDatabase(); now assuming the table-type will *always* have a Database
  300.   ...because if not, why is it being created by a Database object?
  301.   */
  302. static private $arTStat = array(); // array of Table Statuses
  303. /*----
  304.   RULES:
  305.   * If the array key exists, the element is a Table Status that already has a Table in it.
  306.   * There should never be >1 instance of the same Table class.
  307.   HISTORY:
  308.   2021-09-26 Changed $arTStat to static, because it wasn't detecting all duplicates.
  309.   2021-09-27 Even that didn't work; added AddSingle() function to cApp.
  310.   Keeping the object-cache here too, though, because this one stores statuses.
  311.   */
  312. public function MakeTable(string $sClassAsk) : cTableResult {
  313. if (array_key_exists($sClassAsk,self::$arTStat)) {
  314. // a Table of that class has already been created
  315. $ost = self::$arTStat[$sClassAsk];
  316. #echo "FOUND EXISTING TABLE for [<b>$sClassAsk</b>]<br>";
  317. } else {
  318. // that class of Table has not yet been created
  319. $ost = new cTableResult; // make the wrapper Space
  320. if (class_exists($sClassAsk)) {
  321. // attempt to create & cache it
  322. $ok = FALSE;
  323. $sClassBase = cTabloid::class;
  324. if (is_a($sClassAsk,$sClassBase,TRUE)) {
  325.  
  326. /*
  327.   //+DEBUG
  328.   echo "CREATING CLASS [$sClassAsk]<br>";
  329.   echo 'EXISTING OBJECTS:<ul>';
  330.   foreach (self::$arTStat as $sClass => $os) {
  331.   echo '<li>'.$sClass.' HAS IT:['.$os->HasIt().']</li>';
  332.   }
  333.   echo '</ul>';
  334.   //-DEBUG
  335.   */
  336.  
  337. $t = new $sClassAsk($this);
  338. $ost->SetIt($t);
  339. self::$arTStat[$sClassAsk] = $ost;
  340. $ok = TRUE;
  341.  
  342. $id = spl_object_id($t);
  343. $htID = \ferret\cEnv::BoldIt($id);
  344. #echo "CREATED NEW TABLE (ID $htID) for [<b>$sClassAsk</b>]<br>";
  345. } else {
  346. $sErr = "Requested class '$sClassAsk' is not a descendant of $sClassBase.";
  347. $arParents = class_parents($sClassAsk);
  348. #$htParents = \fcArray::RenderList($arParents,' &larr; ');
  349. $htParents = \ferret\cClassDebugger::RenderParentage($sClassAsk);
  350. echo "Parentage of <b>$sClassAsk</b>: $htParents";
  351. throw new \ferret\except\cUsage($sErr);
  352. }
  353. } else {
  354. #$tst = new $sClassAsk($this); // debugging
  355. // no code found for that class
  356. throw new \exception("Trying to wrap table with unknown class '$sClassAsk'. (Maybe the appropriate library module has not been requested?)");
  357. }
  358. }
  359. return $ost;
  360. }
  361.  
  362. /*----
  363.   INPUT:
  364.   $sClassAsk = name of Table class from which the Row should be retrieved
  365.   $id = ID of Row to read from Table, or string meaning new record
  366.   HISTORY:
  367.   2021-01-27 changed to return cRowStuff (now RowKit; was: cTableData), preserving Table info
  368.   in case caller wants it (a situation which has now come up with DropIn links)
  369.   2021-08-20 some careful reworking and renaming done; values now set via SetValues_fromStorage().
  370.   TODO: something like having one class for row-retrieval and another for PortBank stuff
  371.   ...or maybe Records should be assigned to the Storage PortIORow, rather than Native...
  372.   So much refactoring to do.
  373.   */
  374. private $arRows = array();
  375. public function MakeRow(string $sTableClass, int $id) : cRowKit {
  376. $osOut = new cRowKit(get_class($this));
  377. $ost = $this->MakeTable($sTableClass);
  378. if ($ost->HasIt()) {
  379. $t = $ost->GetIt();
  380. $osOut->Table()->SetIt($t);
  381. if ($id == \fcApp::Globals()->WebPath_ID_forNew()) {
  382. // new/empty recordset-wrapper wanted
  383. $rc = $t->SpawnRow();
  384. // 2021-04-05 I think this is supposed to be ->Record()->...
  385. $osOut->Record()->SetIt($rc);
  386. } else {
  387. // existing recordset-wrapper wanted
  388. if (!method_exists($t,'GetRow_fromKey')) {
  389. $sError = "GetRow_fromKey() method not found in class '$sTableClass'.";
  390. throw new \ferret\except\cInternal($sError);
  391. }
  392. $osOut->ID()->SetIt($id);
  393. $osrc = $t->GetRow_fromKey($id); // cSelectResult
  394. #$osrt->Record()->CopyIt($osr);
  395. #$osrt->Record()->CopyIt($osr->Record());
  396.  
  397. #echo 'OSRC: '.$osrc->Dump();
  398.  
  399. $rc = $osrc->Record()->GetIt(); // get the loaded Record object
  400. $osOut->Record()->SetIt($rc); // copy Record object to output RowKit
  401. // pass values through Storage format-translator:
  402. $rc->SetValues_fromStorage($rc->GetCells());
  403. }
  404. }
  405. return $osOut;
  406. }
  407.  
  408. public function MakeTableData(string $sTableClass,$id=NULL) : cTableResult {
  409. throw new \exception('2020-12-25 Call MakeTable() or MakeRow()');
  410. $out = new cTableResult();
  411. //if (empty($this->arTables)) { $this->arTables = array(); }
  412. if (array_key_exists($sTableClass,$this->arTables)) {
  413. // a Table of that class has already been created
  414. $t = $this->arTables[$sTableClass];
  415. $out->SetStatus(TRUE,$t);
  416. } else {
  417. // that class of Table has not yet been created
  418. if (class_exists($sTableClass)) {
  419. // attempt to create & cache it
  420. $ok = FALSE;
  421. $ksRequiredParent = __NAMESPACE__.'\cTabloid';
  422. if (is_subclass_of($sTableClass,$ksRequiredParent)) {
  423. $t = new $sTableClass();
  424. if (method_exists($t,'SetDatabase')) {
  425. // Some plugins have a DB Table, but some don't.
  426. $t->SetDatabase($this);
  427. }
  428. $this->arTables[$sTableClass] = $t;
  429. $out->SetStatus(TRUE,$t);
  430. $ok = TRUE;
  431. } else {
  432. $sErr = "Requested class '$sTableClass' is not a descendant of $ksRequiredParent.";
  433. }
  434. if (!$ok) {
  435. $arParents = class_parents($sTableClass);
  436. echo "Parentage of <b>$sTableClass</b>:".\fcArray::RenderList($arParents,' &larr; ');
  437. throw new \exception($sErr);
  438. }
  439. } else {
  440. $tst = new $sTableClass($this); // debugging
  441. // no code found for that class
  442. throw new \exception("Trying to wrap table with unknown class '$sTableClass'. (Maybe the appropriate library module has not been requested?)");
  443. }
  444. }
  445. if (is_null($id)) {
  446. // no Row requested; just fetching the Table
  447. $out->SetRowStatus(FALSE);
  448. } else {
  449. if ($id == \fcApp::Globals()->WebPath_ID_forNew()) {
  450. // new/empty recordset-wrapper wanted
  451. $rc = $t->SpawnRowWrapper();
  452. $out->SetRowStatus(TRUE,$rc);
  453. } else {
  454. // existing recordset-wrapper wanted
  455. if (!method_exists($t,'GetRow_fromKey')) {
  456. throw new \exception("Ferreteria internal error: GetRow_fromKey() method not found in class '$sTableClass'.");
  457. }
  458. $oRecStat = $t->GetRow_fromKey($id);
  459. $out->CopyRowStatus($oRecStat);
  460. }
  461. }
  462. return $out;
  463. }
  464. /*----
  465.   HISTORY:
  466.   2020-02-23 Rewriting MakeTableWrapper() as FetchData(), to return new cRowStuff (now RowKit) object
  467.   */
  468. public function FetchData(string $sTableClass,$id=NULL) : cRowKit {
  469. $ods = new cRowKit();
  470.  
  471. if (array_key_exists($sTableClass,$this->arTables)) {
  472. // a Table of that class has already been created
  473. $t = $this->arTables[$sTableClass];
  474. $ods->Table()->SetStatus(TRUE,$t);
  475. } else {
  476. // that class of Table has not yet been created
  477. if (class_exists($sTableClass)) {
  478. // attempt to create & cache it
  479. $ok = FALSE;
  480. $ksRequiredParent = __NAMESPACE__.'\cTabloid';
  481. if (is_subclass_of($sTableClass,$ksRequiredParent)) {
  482. $t = new $sTableClass();
  483. $t->SetDatabase($this);
  484. $this->arTables[$sTableClass] = $t;
  485. $ods->Table()->SetStatus(TRUE,$t);
  486. $ok = TRUE;
  487. } else {
  488. $sErr = "Requested class '$sTableClass' is not a descendant of $ksRequiredParent.";
  489. }
  490. if (!$ok) {
  491. $arParents = class_parents($sTableClass);
  492. echo "Parentage of <b>$sTableClass</b>:".\fcArray::RenderList($arParents,' &larr; ');
  493. throw new \exception($sErr);
  494. }
  495. } else {
  496. #$tst = new $sTableClass($this); // debugging
  497. // no code found for that class
  498. throw new \exception("Trying to wrap table with unknown class '$sTableClass'. (Maybe the appropriate library module has not been requested?)");
  499. }
  500. }
  501. $odsr = $ods->Row();
  502. if ($id == \fcApp::Globals()->WebPath_ID_forNew()) {
  503. // new/empty recordset-wrapper wanted
  504. $rc = $t->SpawnRowWrapper();
  505. $odsr->SetStatus(TRUE,$rc);
  506. } elseif (!is_null($id)) {
  507. // existing recordset-wrapper wanted
  508. if (!method_exists($t,'GetRow_fromKey')) {
  509. throw new \exception("Ferreteria internal error: GetRow_fromKey() method not found in class '$sTableClass'.");
  510. }
  511. $oRecStat = $t->GetRow_fromKey($id);
  512. $odsr->CopyStatus($oRecStat);
  513. } else {
  514. $odsr->SetStatus(FALSE);
  515. }
  516. return $ods;
  517. }
  518.  
  519. // -- DATA OBJECTS -- //
  520. }
  521.