values[] = $value; $this->types .= $type; } public function isEmpty() { return((empty($this->values)) ? true : false); } public function checkOrd() { return((count($this->values) == strlen($this->types))); } /** * get() -- public method * * get() simply returns the two class variables as a string of output that's tailored to the input * requirement of mysqli::bind_param(). * * @author mike@givingassistant.org * @version 1.0 * * @return array * * HISTORY: * -------- * 06-29-17 mks Original coding * */ public function get() { return array_merge(array($this->types), $this->values); } public function refValues($arr) { $refs = array(); foreach($arr as $key => $value) $refs[$key] = &$arr[$key]; return $refs; } } class gacMySQL extends gaaNamasteCore { private $slaveConnection = null; // resource link to mysqli service protected $useSlaveServer = false; // should be overridden in the class instantiation protected $batchSize = PDO_RECORDS_PER_PAGE; protected $mySqlTypes = array(); protected $tip = false; // indicates if a transaction is already in progress protected $uniqueIndexes = null; protected $compoundIndexes = null; protected $exposedFields = null; protected $dbEvent; // used to track the different sql events protected $rowsAffected; // how many rows were affected by the sql query protected $queryResult; // container to hold return payload from mysqli protected $recordLimit; protected $serviceReady; // boolean indicating if the mysql service is ready // exceptions to the query-builder public $queryOrderBy; public $queryOrderByDirection; public $queryGroupBy; public $queryGroupByDirection; public $queryLimit; public $queryHaving; public $mysqlMasterAvailable; public $mysqlSlaveAvailable; // allowable operands for mysql public $operands = [ OPERATOR_EQ, OPERATOR_LTE, OPERATOR_GTE, OPERATOR_DNE, OPERATOR_LT, OPERATOR_GT, OPERATOR_NE ]; /** * __construct -- public method * * constructor for the mysql data instantiation class * * Three input parameters are supported for the constructor: * * $_template: the name of the template that establishes which data class will be instantiated * $_meta: the meta data payload as received from the broker - critical because it contains the name * of the class template we're going to be instantiating. * $_id: an optional parameter - if provided, we'll instantiate the class and then attempt to load * the record referenced by the primary key value (after evaluating the id type). * * Next, we going to assign mysql resources - based on the configuration, if we're supporting slave reads, make * the appropriate assignments so the correct resource is engaged for any particular query. * * Load the template properties into the class and set the class properties accordingly. * * Every mySQL table has two "primary keys" -- the traditional auto-incrementing integer, and a guid string. * The best-practices effort of "id's internally, guids externally" applies to mysql structures. * * When we instantiate the class and we receive an id, we have to evaluate if we're passed a string (guid) or an * integer (id) and adjust the current pkey pointer appropriately so that correct query is build deeper down. * * Therefore, mysql is the first and, of this writing, the only db instantiation class that has a floating pkey * value/type which is established on a data fetch at run-time. * * * @author mike@givingassistant.org * @version 1.0 * * @param string $_template * @param array $_meta * @param mixed $_id * * HISTORY: * -------- * 06-29-17 mks initial coding * */ public function __construct(string $_template, array $_meta, $_id = null) { register_shutdown_function(array($this, '__destruct')); parent::__construct(); if ($this->trace and $this->logger->available) { $this->logger->trace(STRING_ENT_METH . __METHOD__); if (!empty($_guid) and $this->debug) { $this->logger->debug('received guid: ' . $_guid); } } // validate the meta data payload if (empty($_meta)) { $this->state = STATE_META_ERROR; $this->logger->data(ERROR_DATA_META_REQUIRED); $this->eventMessages[] = ERROR_DATA_META_REQUIRED; return; } elseif (!array_key_exists(META_TEMPLATE, $_meta)) { $this->state = STATE_META_ERROR; $msg = ERROR_DATA_META_KEY_404 . META_TEMPLATE; $this->logger->data($msg); $this->eventMessages[] = $msg; return; } // invoke the parent constructor, load the mysql configuration parent::__construct(); $this->status = false; $this->config = gasConfig::$settings[CONFIG_DATABASE_MYSQL]; if (empty($this->config)) { $msg = ERROR_CONFIG_RESOURCE_404 . RESOURCE_MYSQL; $this->logger->warn($msg); $this->eventMessages[] = $msg; $this->state = STATE_RESOURCE_ERROR_MYSQL; return; } // load the template $this->templateName = STRING_CLASS_GAT . $_meta[META_TEMPLATE]; if (!$this->loadTemplate()) { $this->logger->warn(ERROR_TEMPLATE_INSTANTIATE . $_meta[META_TEMPLATE]); $this->state = STATE_TEMPLATE_ERROR; return; } $this->class = $_meta[META_TEMPLATE]; // set the class to the name of the requested data class // if we're passed an optional $_id, then evaluate which type of id we're working with and make // the appropriate assignments. if (!empty($_id)) { $_id = trim($_id); $_id = (is_numeric($_id)) ? abs(intval($_id)) : $_id; switch (gettype($_id)) { case DATA_TYPE_STRING : if (validateGUID($_id)) { if ($this->pKey != PKEY_GUID) { $msg = sprintf(ERROR_PKEY_TYPE, DATA_TYPE_STRING); $this->logger->error($msg); $this->state = STATE_DATA_TYPE_ERROR; $this->eventMessages[] = $msg; return; } } else { $msg = ERROR_INVALID_GUID . $_id; $this->eventMessages[] = $msg; $this->logger->error($msg); $this->state = STATE_DATA_ERROR; return; } break; case DATA_TYPE_INTEGER : if ($this->pKey != PKEY_ID) { $msg = sprintf(ERROR_PKEY_TYPE, DATA_TYPE_INTEGER); $this->logger->error($msg); $this->eventMessages[] = $msg; $this->state = STATE_DATA_TYPE_ERROR; return; } $this->pKey = PKEY_ID; break; default : $msg = sprintf(ERROR_PKEY_TYPE, gettype($_id)); $this->logger->error($msg); $this->eventMessages[] = $msg; $this->state = STATE_DATA_TYPE_ERROR; return; break; } } // establish and assign mysql connections if (gasResourceManager::$mySqlMasterAvailable) { $this->connection = gasResourceManager::fetchResource(RESOURCE_MYSQL_MASTER); $this->mysqlMasterAvailable = true; if (gasResourceManager::$mySqlSlaveAvailable) { $this->slaveConnection = gasResourceManager::fetchResource(RESOURCE_MYSQL_SLAVE); $this->mysqlSlaveAvailable = true; } else { $this->mysqlSlaveAvailable = false; } } else { $this->mysqlMasterAvailable = false; $this->state = STATE_RESOURCE_ERROR_MYSQL; return; } $this->queryOrderBy = null; $this->queryGroupBy = null; $this->queryLimit = null; $this->queryHaving = null; $this->queryGroupByDirection = null; $this->queryGroupByDirection = null; $this->serviceReady = true; // if we have an $_id, load the record if ($this->collectionName != NONE) { $this->buildIndexReference(); $this->setRowsReturnedLimit(); } } /** * buildIndexReference() -- private method * * this method looks at the table defined in the current class instantiation and fetches the schema * information about the table from mysql. * * Each returned array structure from the query looks like this: * * Array * ( * [Field] => email_usr * [Type] => varchar(50) * [Null] => NO * [Key] => UNI * [Default] => * [Extra] => * ) * * We're looking for the column 'Key' to be not-empty as this indicates that the table column is indexed * in some way. * * We want to save the indexed column information in a K->V paired array so that, when we're parsing * queries submitted to the mysql service, we can screen the query and prevent the execution of any * query that does not use the indexed columns of the table. * * The K->V associative array will be stored locally in the the $fieldTypes variable (declared in the core). * * The Key will contain the name of the indexed column, and the Value will have the mysql type definition * for that column. * * If the query execution generates a mysql error, set a WARN message and return. * If the query executes, but no indexed columns are returned, raise a WARN message. * * * @author mike@givingassistant.org * @version 1.0 * * HISTORY: * -------- * 06-30-17 mks original coding * */ private function buildIndexReference() { if ($this->trace) $this->logger->trace(STRING_ENT_METH . __METHOD__); $data = null; // generate the cache key appropriate to the class and see if we've already cached this info // based off a previous instantiation... $cKey = CACHE_NAMASTE_KEY . '_' . CACHE_MYSQL_TABLE_SCHEMA . '_' . $this->collectionName; if ($data = gasCache::get($cKey)) { $data = json_decode(gzuncompress($data), true); } else { $this->dbEvent = DB_EVENT_NAMASTE; $this->strQuery = 'SHOW COLUMNS FROM ' . $this->collectionName; $this->executeNonPreparedQuery(); if (!$this->rowsAffected) { $this->logger->warn(ERROR_SQL_FTL_INDEXES); $this->eventMessages[] = ERROR_SQL_FTL_INDEXES; } else { foreach($this->queryResults as $row) { $data[] = $row; } } gasCache::add($cKey, gzcompress(json_encode($data, true))); } if (!empty($data) and is_array($data)) { foreach ($data as $row) { @$this->mySqlTypes[$row[MYSQL_COLUMN_FIELD]] = $row[MYSQL_COLUMN_TYPE]; if (!empty($row[MYSQL_COLUMN_KEY])) { $this->indexes[] = $row[MYSQL_COLUMN_FIELD]; } if (@$row[MYSQL_COLUMN_KEY] == MYSQL_INDEX_PRIMARY or @$row[MYSQL_COLUMN_KEY] == MYSQL_INDEX_UNIQUE) { $this->uniqueIndexes[] = $row[MYSQL_COLUMN_FIELD]; } } } } /** * setRowsReturnedLimit() -- private method * * this function is called in the constructor for the current table instantiation. * * it looks at the information_schema table to get the average_row_length (arl) value for the table. * this is a gross calculation - the more data in the table, the more accurate the value. * * if we can get the arl value from the information_schema, then divide this number into the system * constant (max_data_returned) to see if the result is less-than or equal-to the system constant for the * number of rows returned per query... * * if the calculated value is smaller, then allow the system constants to remain -- if not, adjust the system * constant for the max_rows_returned so that the total amount of data remains under the system constant * max_data_returned. * * @author mike@givingassistant.org * @version 1.0 * * HISTORY: * 07-10-17 mks original coding * */ private function setRowsReturnedLimit() { if (gasConfig::$settings[ERROR_TRACE] and $this->logger->available) { $this->logger->trace(STRING_ENT_METH . __METHOD__); } $key = PDO_DATA_DEFINITION . '_' . PDO_AVG_ROW_LEN . '_' . $this->collectionName; $cacheData = null; $this->dbEvent = MYSQL_EVENT_META; if ($cacheData = gasCache::get($key)) { $cacheData = json_decode(gzuncompress($cacheData), true); $this->recordLimit = $cacheData[PDO_RECORDS_PER_PAGE]; } else { $schema = gasConfig::$settings[CONFIG_DATABASE][CONFIG_DATABASE_MYSQL][CONFIG_DATABASE_MYSQL_APPSERVER][CONFIG_DATABASE_MYSQL_MASTER][CONFIG_DATABASE_MYSQL_DB]; $this->strQuery = '-- noinspection SqlDialectInspection SELECT AVG_ROW_LENGTH FROM information_schema.tables WHERE table_schema = "' . $schema . '" AND table_name = "' . $this->collectionName . '"'; $this->recordLimit = PDO_RECORDS_PER_PAGE; $this->executeNonPreparedQuery(); if (($this->rowsAffected === 1) and (isset($this->queryResult[0][MYSQL_AVG_ROW_LENGTH]))) { $arl = $this->queryResult[0][MYSQL_AVG_ROW_LENGTH]; if (($arl * PDO_RECORDS_PER_PAGE) > MYSQL_MAX_DATA_RETURNED) { $this->recordLimit = intval(MYSQL_MAX_DATA_RETURNED / $arl); } } $cacheData[PDO_RECORDS_PER_PAGE] = $this->recordLimit; if (!gasCache::add(PDO_DATA_DEFINITION . '_' . PDO_AVG_ROW_LEN . '_' . $this->collectionName, gzcompress(json_encode($cacheData, true)), gasCache::$cacheTTL)) { $this->logger->warn('memcache:set failed - check log files'); } } } /** * executeNonPreparedQuery() -- private method * * this is the main method to execute all META and SELECT queries - any query that is not a prepared statement * will execute here. Basically, namaste queries. * * upon invocation, the string passed (implicitly through the member variable: $query) will be cleaned through * the common function, and then we'll evaluate the query based on the setting of the member variable $dbEvent. * If $dbEvent is not META and not SELECT, then we're going to return with a WARN message requiring the client * to use the prepared-query method. * * Next, parse the query and look for the "?" character - which is used as a place holder in prepared queries, * and, if found, reject the request and return with a WARN message. * * Call a private method to see if the slave server is enabled and, if so, use it if the current query contains * the SELECT keyword (meta queries will not use SELECT) and return the connection resource to a local variable. * * if query timers are enabled, then mark the start time and execute the query. record the end-time and log * the query through the parent::method(). * * Make a call to fetch the data as an associative array and post the results, along with the row count, to * class variables. * * if the query generated an mysql error, generate an WARN message and return. * * @author mike@givingassistant.org * @version 1.0 * * HISTORY: * -------- * 06-30-17 mks original coding * */ private function executeNonPreparedQuery() { if ($this->trace) $this->logger->trace(STRING_ENT_METH . __METHOD__); $startTime = floatval(0); if ($this->debug) { $this->logger->debug($this->strQuery); } // todo: can I exec this schema command using the read-slave? Do I want to? /** @var $dbLink mysqli() */ $dbLink = $this->connection; if ($this->useTimers) $startTime = floatval(0); $this->queryResults = null; if ($this->dbEvent != DB_EVENT_NAMASTE) { $this->strQuery = cleanQueryString($this->strQuery); } switch($this->dbEvent) { case DB_EVENT_NAMASTE : case DB_EVENT_SELECT : break; default : $this->logger->error(ERROR_SQL_NOT_PREP_STMNT); return; } if (stripos($this->strQuery, '?')) { $this->logger->warn(ERROR_SQL_LOST_PREP_QUERY); $this->logger->warn($this->strQuery); return; } if ($this->useTimers) { $startTime = gasStatic::doingTime(); } if ($result = $dbLink->query($this->strQuery)) { $this->rowsAffected = $result->num_rows; if ($this->useTimers) { $this->logger->metrics($this->strQuery, gasStatic::doingTime($startTime)); $this->logger->debug(MYSQL_ROWS_AFFECTED . $this->rowsAffected); } while ($row = $result->fetch_assoc()) { $this->queryResult[] = $row; } } else { $this->logger->warn('error expecting query: ' . $this->strQuery); } } /** * loadTemplate() -- private method * * this method is invoked by the constructor and serves to load the class template file, assimilating it into * the current instantiation. * * template loads are done on the schema-instantiation level, instead of the core, because of the changes in * the template file(s) across various schemas. * * the method will load the class template and set the class member variables controlled/referenced by the * template. * * successful loading of the template is determined by the return (boolean) value -- on error, a log message * will be generated so it's up to the developer to check logs on fail-returns to see why their template * file was not correctly assimilated. * * The template to be loaded is first derived in the constructor (post validation that the template file * exists) and is pulled from the member variable (also set in the constructor) within this method. * * * @author mike@givingassistant.org * @version 1.0 * * @return bool * * HISTORY: * ======== * 06-30-17 mks original coding * */ private function loadTemplate():bool { if ($this->trace) $this->logger->trace(STRING_ENT_METH . __METHOD__); try { /** @var gatTestMySQL template */ $this->template = new $this->templateName; } catch (Exception $e) { $this->logger->warn($e->getMessage()); $this->state = STATE_FRAMEWORK_FAIL; return (false); } if (!is_object($this->template)) { $this->logger->warn(ERROR_FILE_404 . $this->templateName); $this->setState(ERROR_FILE_404 . $this->templateName); return (false); } if ($this->template->schema != TEMPLATE_DB_PDO) { $this->logger->warn(ERROR_SCHEMA_MISMATCH . $this->template->schema . ERROR_STUB_EXPECTING . TEMPLATE_DB_PDO); $this->setState(ERROR_SCHEMA_MISMATCH . $this->templateName); return (false); } // transfer meta data info to current instantiation $this->schema = $this->template->schema; $this->collectionName = $this->template->collection; $this->ext = $this->template->extension; $this->useCache = $this->template->setCache; $this->useDeletes = $this->template->setDeletes; $this->useAuditing = $this->template->setAuditing; $this->useJournaling = $this->template->setJournaling; $this->allowUpdates = $this->template->setUpdates; $this->useDetailedHistory = $this->template->setHistory; $this->defaultStatus = $this->template->setDefaultStatus; $this->searchStatus = $this->template->setSearchStatus; $this->useLocking = $this->template->setLocking; $this->useTimers = ($this->template->setTimers and gasConfig::$settings[CONFIG_DATABASE][CONFIG_DATABASE_QUERY_TIMERS]); $this->pKey = $this->template->setPKey; $this->useToken = $this->template->setTokens; $this->cacheExpiry = $this->template->cacheTimer; if (isset($this->template->fields) and is_array($this->template->fields)) { foreach ($this->template->fields as $key => $value) { if ($key == DB_HISTORY) { $this->fieldList[] = $key; $this->fieldTypes[$key] = $value; } else { $this->fieldList[] = ($key . $this->ext); $this->fieldTypes[($key . $this->ext)] = $value; } } } if (isset($this->template->indexes) and is_array($this->template->indexes)) { foreach ($this->template->indexes as $key => $value) { $this->indexes[] = ($key . $this->ext); } } if (!is_null($this->template->cacheMap) and $this->useCache) { foreach ($this->template->cacheMap as $key => $value) { $this->cacheMap[($key . $this->ext)] = $value; } } elseif (!$this->useCache) { $this->cacheMap = null; if (!is_null($this->template->exposedFields)) { $this->exposedFields = $this->template->exposedFields; } } if (!is_null($this->template->uniqueIndexes)) $this->uniqueIndexes = $this->template->uniqueIndexes; if (!is_null($this->template->compoundIndexes)) $this->compoundIndexes = $this->template->compoundIndexes; if (!is_null($this->template->binFields)) { foreach ($this->template->binFields as $key) { $this->binaryFields[] = ($key . $this->ext); } } if ($this->template->selfDestruct) { unset($this->template); } return (true); } protected function _createRecord($_data) { } protected function _fetchRecords($_dd, $_rd = null, $_co = true, $_skip = 0, $_limit = 0, $_sort = null) { } protected function _updateRecord($_data){ } protected function _deleteRecord($_data) { } protected function _lockRecord() { } protected function _releaseLock() { } protected function _isLocked() { } /** * __destruct() -- public function * * class destructor * * @author mike@givingassistant.org * @version 1.0 * * HISTORY: * ======== * 06-29-17 mks original coding * */ public function __destruct() { // As of PHP 5.3.10 destructors are not run on shutdown caused by fatal errors. // // destructor is registered shut-down function in constructor -- so any recovery // efforts should go in this method. // there is no destructor method defined in the core abstraction class, hence // there is no call to that parent destructor in this class. parent::__destruct(); } }