Archive: Namaste PHP AMQP framework v1.0 (2017-2020)

952 days continuous production uptime, 40k+ tp/s single node.
Original corpo Bitbucket history not included — clean archive commit.
This commit is contained in:
2026-04-05 09:49:30 -07:00
commit 373ebc8c93
1284 changed files with 409372 additions and 0 deletions

View File

@@ -0,0 +1,315 @@
/**
* convertCacheMapDataToSchema() -- protected method
*
* this method takes an input array of payload data and checks to see if the current-loaded class has cacheMapping
* set (the cacheMap element has to be an array) and uses the map to convert the data from the public (cachemap)
* to private (schema) format.
*
* method requires two input parameters:
*
* - the payload data - which is a indexed array of associative array tuples
* - boolean toggle indicating if ALL fields are required to pass validation
*
* If the current class has cacheMapping, then we're going to spin through each tuple in the $_data parameter
* and look at each $key in the tuple -- if the $key exists as a member in the cacheMap, pull the key from cacheMap
* and store the new key and the old value in a temp array. If the key does not exist in the cacheMap then
* use the current (old) key/value pair.
*
* After each tuple is processed,copy the new vector in to a temporary matrix which will eventually be returned
* to the calling client.
*
* in all other (fail) cases, a null is returned.
*
* NOTE:
* -----
* This method is not to be confused with gasCache->buildMappedDataArray() which converts schema to cacheMap.
*
* @author mike@givingassistant.org
* @version 1.0
*
* @param array $_data
* @param bool $_allFields
* @return null|array
*
* HISTORY:
* ========
* 07-13-17 mks CORE-464: original coding
* 02-06-18 mks _INF-139: support for disabled caching + no cache map
* 02-22-18 mks _INF-139: when cache is disabled, need to verify that submitted data included the class
* extension - if not, replace the old key with the old key + class extension
* 12-12-18 mks DB-77: fixed error in processing: when we have a journal recovery event, the restore
* query uses column literals instead of cache-mapped values. Added conditional code
* to check if the literal appears in the field list and, if so, validate that field
*/
protected function convertCacheMapDataToSchema(array $_data, bool $_allFields = false): ?array
{
$this->state = STATE_VALIDATION_ERROR;
$this->status = false;
$data = false;
$badData = false;
$loggerAvailable = (isset($this->logger) and $this->logger->available);
if (!is_array($_data)) {
$msg = basename(__METHOD__) . AT . __LINE__ . COLON . ERROR_DATA_INVALID_FORMAT;
if ($loggerAvailable)
$this->logger->data($msg);
else
consoleLog($this->res, CON_ERROR, $msg);
$this->eventMessages[] = $msg;
} elseif ($this->useCache and empty($this->cacheMap)) {
$msg = ERROR_CACHE_MAP_404 . COLON . $this->class;
if ($loggerAvailable)
$this->logger->data($msg);
else
consoleLog($this->res, CON_ERROR, $msg);
$this->eventMessages[] = $msg;
} elseif (!$this->useCache and (!isset($this->cacheMap) or empty($this->cacheMap))) {
$data = $_data[0];
foreach ($data as $key => $value) {
try {
$newKey = $this->addExtension($key);
} catch (TypeError $t) {
$msg = ERROR_TYPE_EXCEPTION . COLON . $t->getMessage();
if ($loggerAvailable)
$this->logger->error($msg);
else
consoleLog($this->res, CON_ERROR, $msg);
$this->eventMessages[] = $msg;
return null;
}
if (is_null($newKey)) return null;
if ($newKey != $key) {
$data[$newKey] = $value;
unset($data[$key]);
}
}
$this->state = STATE_SUCCESS;
$this->status = true;
return [$data];
} else {
$counter = 0;
for ($index = 0, $last = count($_data); $index < $last; $index++) {
$row = null;
foreach ($_data[$index] as $key => $value) {
$ck = array_search($key, $this->cacheMap);
if (false === $ck) {
$ck = array_key_exists($key, $this->cacheMap);
/*
* edge case - this case will be encountered in situations where we're using non-cache-mapped
* keys (e.g.: column literals) in a cached-class query where there exists a cache-map.
* Journaling saves the recovery query in literal (as opposed to cache-mapped) format... so, we
* need to accommodate the possibility where the data keys exist only in the $fieldList member
* and, if so, treat the keys as valid values once we've exhausted cache-map processing
*/
if (false === $ck) { // check to see if key is member of $fieldList
// first - see if we have an extension appended to the key - if not, add one
$newKey = $this->addExtension($key);
// then, check if the qualified key exists in the fieldList - if so, update the value of $ck
$ck = (in_array($newKey, $this->fieldList)) ? $newKey : false;
}
if (false === $ck) {
$msg = ERROR_DATA_INVALID_KEY . $key;
$this->eventMessages[] = $msg;
if ($loggerAvailable)
$this->logger->data($msg);
else
consoleLog($this->res, CON_ERROR, $msg);
if ($_allFields) $badData = true;
$ck = $key;
}
}
if (is_array($value) and !empty($this->subCollections) and array_key_exists($ck, $this->subCollections)) {
try {
$value = $this->convertCacheMapDataToSchema($value);
} catch (TypeError $t) {
$msg = ERROR_TYPE_EXCEPTION . COLON . $t->getMessage();
if ($loggerAvailable)
$this->logger->error($msg);
else
consoleLog($this->res, CON_ERROR, $msg);
return null;
}
if (false === $value) {
$msg = sprintf(ERROR_SUB_C_V_NULL, $ck);
$this->logger->warn($msg);
$this->eventMessages[] = $msg;
}
}
if (false !== $ck) $row[$ck] = $value;
}
if (!empty($row)) $data[$counter++] = $row;
}
if (($_allFields and !$badData and is_array($data)) or (!$_allFields and is_array($data))) {
$this->state = STATE_SUCCESS;
$this->status = true;
}
}
return( ($this->status) ? $data : null );
}
NOTE: dataScrub() wasn't deprecated, just eviscerated...
/**
* dataScrub() -- private method
*
* this method parses all of the data stored in the protected $data member and replaces keys with cleaned values
* (extensions stripped from keys) and critical values removed entirely.
*
* $_data -- call-by-reference variable that's the implicitly returned
*
* While processing the data rows, we make a recursive call back to this method if we encounter sub-arrays so
* that the sub-array keys can be stripped (method 1 only!) also.
*
* When generating the return data, for every row of data, we check each column to ensure it's not listed in the
* $hiddenColumns member array and, if it is, we remove it.
*
* For nosql-based collections, if we specify that we want the meta data, then we'll return the history
* sub-collection (aka meta data) so if meta isn't specified, the meta is dropped from the return set.
*
* There are no errors raised in this method. The success is implicitly defined in the $_data return structure.
*
* NOTE:
* =====
* Cache-Key Mapping is located in the private static method gasCache::buildMappedDataArray().
*
*
* @author mikegivingassistant.org
* @version 1.0
*
* @param $_data
*
* HISTORY:
* ========
* 06-22-17 mks original coding
* 08-14-17 mks CORE-493: removing meta param support (DB_HISTORY no longer supported)
*
*/
private function dataScrub(array &$_data): void
{
if (empty($_data)) {
$msg = ERROR_DATA_MISSING_ARRAY . STRING_DATA;
$this->eventMessages[] = $msg;
if (isset($this->logger) and $this->logger->available)
$this->logger->error($msg);
else
consoleLog($this->res, CON_ERROR, $msg);
return;
}
/*
* if we're requesting a clean data set, and we've not requested a key-mapping, then
* clean the data "old-school" style, stripping off extensions, pulling the mongo ID
* fields, in the return data set.
*/
for ($index = 0, $last = count($_data); $index < $last; $index++) {
if (!empty($_data[$index]) and is_array($_data[$index])) {
foreach ($_data[$index] as $key => $value) {
$newKey = str_replace($this->ext, '', $key);
if (is_array($value)) {
try {
$this->dataScrub($value);
} catch (TypeError $t) {
$msg = basename(__METHOD__) . AT . __LINE__ . COLON . ERROR_TYPE_EXCEPTION . COLON . $t->getMessage();
$this->eventMessages[] = $msg;
if (isset($this->logger) and $this->logger->available)
$this->logger->error($msg);
else
consoleLog($this->res, CON_ERROR, $msg);
}
return;
}
if ($newKey != $key and !in_array($newKey, $this->hiddenColumns)) {
$_data[$index][$newKey] = $value;
unset($_data[$index][$key]);
} elseif (in_array($newKey, $this->hiddenColumns)) {
unset($_data[$index][$key]);
}
}
}
}
}
/**
* cmData() -- public function
*
* This public function is a dirty little way to stuff whatever data is defined in $_payload to replace whatever
* is stored in the protected member: $data.
*
* The only requirement is that the input parameter be an array.
*
* The purpose of this method is to store the cacheMap key(s) into the $data payload right before the broker
* event, that generated the data payload, finished processing an releases the class memory assigned to the object.
*
*
* @author mike@givingassistant.org
* @version 1.0
*
* @param array $_payload
*
*
* HISTORY:
* ========
* 03-04-19 mks DB-116: original coding
*
*/
public function cmData(array $_payload): void
{
$this->data = $_payload;
}
/**
* dumpRecord() -- public core method
*
* Sometimes, you need to know what's in the $data payload and, since it's protected, you can't access it directly
* without going through one of the other methods that filters the payload.
*
* This method allows you to dump a row of data from the $data array to stdout. If you don't specify a row (as
* the only input parameter, then you will dump the first (0th) row in the array. However, if the array is empty,
* we'll out a message to that effect.
*
*
* @author mike@givingassistant.org
* @version 1.0
*
* @param int $_row
*
*
* HISTORY:
* ========
* 10-24-18 mks DB-67: original coding
*
*
*/
public function dumpRecord(int $_row = 0): void
{
if (empty($this->data))
echo INFO_NO_DATA_IN_DATA;
else
var_export($this->data[$_row]);
}
/**
* validateStatus() -- public method
*
* Simple method that takes a single input parameter, a string, and returns a boolean value corresponding to
* whether or not the string-value is present in the validStatus member array.
*
*
* @author mike@givingassistant.org
* @version 1.0
*
* @param string $_status
* @return bool
*
*
* HISTORY:
* ========
* 02-12-18 mks _INF-139: original coding
*
*/
public function validateStatus(string $_status): bool
{
return (in_array($_status, $this->validStatus));
}

View File

@@ -0,0 +1,143 @@
<?php
/**
* this class is used when we want to publish a request to the AdminIn broker. The class wraps all of the
* RabbitMQ initialization and communication work so you don't have to. Especially useful for unit testing.
*
*
* @author mike@givingassistant.org
* @version 1.0
*
* HISTORY:
* ========
* 06-15-17 mks original coding
* 07-05-17 mks eliminated recursive calls to the $logger entity; replaced $logger calls with console output
*
*/
use /** @noinspection PhpUnusedAliasInspection */ PhpAmqpLib\Connection\AMQPStreamConnection;
use /** @noinspection PhpUnusedAliasInspection */ PhpAmqpLib\Channel\AMQPChannel;
use PhpAmqpLib\Exception\AMQPTimeoutException;
use /** @noinspection PhpUnusedAliasInspection */ PhpAmqpLib\Message\AMQPMessage;
class gacAdminClientIn
{
/** @var PhpAmqpLib\Connection\AMQPStreamConnection */
private $rabbitConnection;
/** @var PhpAmqpLib\Channel\AMQPChannel */
private $rabbitChannel;
private $rabbitCorrelationID;
private $rabbitCallbackQueue;
private $queueName;
private $res = 'BACI: '; // Broker Admin Client In
public $status;
/**
* __construct() -- public method
*
* this is the constructor for the class. it requests an admin resource from the resource manager and declares
* a client-side connection to the service.
*
* there is an optional input parameter -- $_fw (from-where) that inserts a string into the queue label allowing
* easy identification of the requesting source.
*
* the method returns no values. It only sets the class' status member variable, a Boolean, on success or fail,
* accordingly.
*
*
* @author mike@givingassistant.org
* @version 1.0
*
*
* @param $_fw - "from where" - tweaks queue label to identify request origin
*
*
* HISTORY:
* ========
* 06-15-17 mks original coding
*
*/
public function __construct($_fw = 'AdminClientIn')
{
register_shutdown_function(array($this, STRING_DESTRUCTOR));
$this->status = false;
var_dump(debug_backtrace());
$this->queueName = gasResourceManager::$cfgAdmin[CONFIG_BROKER_QUEUE_TAG] . BROKER_QUEUE_AI;
// $this->rabbitConnection = gasResourceManager::fetchResource(RESOURCE_ADMIN);
$this->rabbitConnection = new AMQPStreamConnection('localhost', 5672, 'namaste', 'oSZL8Cby', 'mdev');
if (is_null($this->rabbitConnection)) return;
$this->rabbitChannel = $this->rabbitConnection->channel();
$label = uniqid('gacAdminInClient<' . $_fw . '>:');
list($this->rabbitCallbackQueue, ,) = $this->rabbitChannel->queue_declare($label, false, false, false, false); // was: f,f,f,t
$this->status = true;
return;
}
/**
* call() -- public method
*
* This method is invoked outside of the class and is the entry point for publishing a message request to the
* AdminIn broker. It creates a new AMQP message and publishes it to the queue (defined in the constructor),
* and then exits, returning a true message indicating that the messages was successfully published.
*
* Since the AdminIN broker is a fire-n-forget broker, there are no return messages to block-and-wait on.
*
* If an exception is raised by this class, then a false value will be returned.
*
* NOTE: the true/false return values are not, in any way, a reflection of the processing success/failure on the
* remote service. The general RoT is that if we can publish the request, then we can only assume that the request
* was successfully consumed and processed.
*
* @author mike@givingassistant.org
* @version 1.0
*
* @param $_data
*
* HISTORY:
* ========
* 06-15-17 mks original coding
*
*/
public function call($_data)
{
$this->rabbitCorrelationID = uniqid();
$res = false;
$rabbitMessage = new AMQPMessage((string)$_data);
try {
$rabbitMessage = new AMQPMessage((string)$_data, array(BROKER_CORRELATION_ID => $this->rabbitCorrelationID, BROKER_REPLY_TO => $this->rabbitCallbackQueue));
$this->rabbitChannel->basic_publish($rabbitMessage, '', $this->queueName);
$res = true;
} catch (AMQPTimeoutException $e) {
echo getDateTime() . CON_ERROR . $this->res . ERROR_BROKER_EXCEPTION_TIMEOUT . PHP_EOL;
echo getDateTime() . CON_ERROR . $this->res . $e->getMessage() . PHP_EOL;
} catch (\PhpAmqpLib\Exception\AMQPRuntimeException $e) {
echo getDateTime() . CON_ERROR . $this->res . ERROR_BROKER_EXCEPTION_RUNTIME . PHP_EOL;
echo getDateTime() . CON_ERROR . $this->res . $e->getMessage() . PHP_EOL;
} catch (AMQPException $e) {
echo getDateTime() . CON_ERROR . $this->res . ERROR_BROKER_EXCEPTION . PHP_EOL;
echo getDateTime() . CON_ERROR . $this->res . $e->getMessage() . PHP_EOL;
} catch (Exception $e) {
echo getDateTime() . CON_ERROR . $this->res . ERROR_BROKER_EXCEPTION . PHP_EOL;
echo getDateTime() . CON_ERROR . $this->res . $e->getMessage() . PHP_EOL;
}
$this->rabbitChannel->close();
$this->rabbitConnection->close();
$this->status = $res;
}
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.
if (is_object($this->rabbitChannel)) {
$this->rabbitChannel->close();
$this->rabbitConnection->close();
}
}
}

View File

@@ -0,0 +1,149 @@
/**
* getNoSQLResource() -- private static method
*
* this method initializes the nosql resource by attempting to connect to the nosql service. if the connection
* attempt succeeds, then mark the resource as available. Otherwise, post an error-fatal and explicitly mark
* the service as not-available and return.
*
* NOTE:
* -----
* This resource allocation exists outside of the resource manager because the resource manager instantiates
* this class in it's constructor. Were you to request a resource from the resource manager, you'd end-up in
* a circular reference and if the whole thing does not come to an immediate shuddering stop, then it would
* certainly blow-up the first time you attempt to log an error. So, tl;dr: do not attempt to 'fix' this as
* it's not broken.
*
* @author mike@givingassistant.org
* @version 1.0
*
* @return Aws\DynamoDb\DynamoDbClient|null
*
* HISTORY:
* ========
* 06-09-17 mks original coding
*
*/
private function getNoSQLResource():Aws\DynamoDb\DynamoDbClient
{
global $eos;
$options = null;
date_default_timezone_set(TIME_TIMEZONE);
if (empty($this->config)) {
echo getDateTime() . CON_ERROR . $this->res . ERROR_CONFIG_RESOURCE_404 . CONFIG_DATABASE_DDB . $eos;
return(null);
}
$noSQLConfig = $this->config[CONFIG_DATABASE_DDB_APPSERVER];
$credentials = [
STRING_KEY => $noSQLConfig[CONFIG_DATABASE_DDB_APPSERVER_KEY_ID],
STRING_SECRET => $noSQLConfig[CONFIG_DATABASE_DDB_APPSERVER_ACCESS_KEY]
];
/*
* Requests to DynamoDB are made over HTTP(S), and this does not require that you establish an upfront
* connection. When you create the client object, you are not making a connection to DynamoDB, you are just
* configuring an HTTP client that will make requests to DynamoDB.
*/
$awsConfig = new Aws\Sdk([
STRING_ENDPOINT => $noSQLConfig[CONFIG_DATABASE_DDB_APPSERVER_DSN] . ':' . $noSQLConfig[CONFIG_DATABASE_DDB_APPSERVER_PORT],
STRING_REGION => $noSQLConfig[CONFIG_DATABASE_DDB_APPSERVER_REGION],
STRING_VERSION => $noSQLConfig[CONFIG_DATABASE_DDB_APPSERVER_VERSION],
STRING_CREDS => $credentials
]);
return $awsConfig->createDynamoDb();
}
/**
* getLog() - public method
*
* getLog is the method that is used to fetch log (or Metrics) records from the mongo collection.
*
* because ddb limits queries to 1MB returns, and the paint is still wet on this schema, for now I'm
* going to limit queries for log-file fetching to just the last N records created within the last hour and
* we'll just grab up to the limit of the records returned - which should still be a significant number of
* records...
*
* This method reads the last X records created in the last hour (since this method is mainly used for
* providing HTML output to the log-reader) and wraps the data in HTML table rows intended for the logDump
* utility.
*
* todo -- pagination support? Query by error-code? Query by eventID? Query by class?
*
* @author mshallop@pathway.com
* @version 1.0
*
* @param string $_what defaults to the log template -- should be over-ridden for metrics template
* @return null|string
* @throws Exception
*
* HISTORY:
* ========
* 06-07-17 mks original coding
* 06-14-17 mks refactored for ddb
*
*/
public function getLog(string $_what = TEMPLATE_CLASS_LOGS):string
{
$result = null;
$returnData = null;
$marshaler = new Marshaler(); // black-box son converter
$lastHour = time() - NUMBER_ONE_HOUR_SEC;
if ($_what != TEMPLATE_CLASS_LOGS and $_what != TEMPLATE_CLASS_METRICS) $_what = TEMPLATE_CLASS_LOGS;
$eav = $marshaler->marshalJson('{":ts" :' . $lastHour . '}');
$params = [
DDB_STRING_TABLE_NAME => $this->collectionName,
DDB_STRING_KEY_COND_EXPR => LOG_CREATED . ' > :ts',
DDB_STRING_EXPR_ATTR_VALS => $eav
];
try {
$result = $this->connection->query($params);
} catch (DynamoDbException $e) {
$this->errStack[] = __FILE__ . ':' . __LINE__ . ':' . __METHOD__ . ':' . $this->class . ':' .
ERROR_FATAL . ' caught cursor exception: ' . $e->getMessage();
self::throwFatal();
}
if (!is_null($result)) {
foreach ($result[DDB_STRING_ITEMS] as $row) {
$returnData .= '<div class="rowMeta">'; // note: css is defined in the utilities directory
$returnData .= '(' . $row[(DB_PKEY . $this->ext)] . ') - ';
// $returnData .= date(TIME_DATE_FORMAT, $row[(META_SESSION_DATE . self::$ext)]->sec) . ' - ';
// add error label as a span: warn/error/fatal...
$returnData .= self::getErrorLabel($row[(LOG_LEVEL . $this->ext)]);
$returnData .= ' ' . $row[(ERROR_FILE . $this->ext)] . '(' . $row[(ERROR_LINE . $this->ext)] . ')';
$cd = '';
if (!empty($row[(ERROR_CLASS . $this->ext)])) $cd = ' class[' . $row[(ERROR_CLASS . $this->ext)] . ']';
if (!empty($row[(ERROR_METHOD . $this->ext)])) $cd .= '.method(' . $row[(ERROR_METHOD . $this->ext)] . ')</div>';
$returnData .= $cd;
/*
if ($row[(ERROR_TYPE . self::$ext)] == ERROR_TRACE) {
$returnData .= '</div>';
}
*/
$returnData .= '<div class="rowData">' . htmlentities($row[(ERROR_MESSAGE . $this->ext)]);
if ($_what == TEMPLATE_CLASS_METRICS) {
$returnData .= ' - ' . $row[(DB_TIMER . $this->ext)] . ' or ';
$returnData .= ($row[(DB_TIMER . $this->ext)] * NUMBER_MS_PER_SEC) . 'ms';
}
$returnData .= '</div>';
$returnData .= '<div class="rowHist">';
foreach($row[(DB_HISTORY . $this->ext)] as $histRec) {
$returnData .= date('Y-M-d h:i:s', $histRec[META_SESSION_DATE]->sec);// . ' (';
if (!is_null($row[(LOG_EVENT_GUID . $this->ext)]))
$returnData .= ', Event ID: ' . $row[(LOG_EVENT_GUID . $this->ext)];
// $returnData .= $histRec[META_SESSION_EVENT] . ') from (';
// $returnData .= $histRec[META_SESSION_IP] . '): ';
// $returnData .= ((isset($histRec[META_SESSION_ID])) ? $histRec[META_SESSION_ID] : $histRec[META_CLIENT]) . '<br />';
}
$returnData .= '</div><br />';
}
}
return ($returnData);
}

View File

@@ -0,0 +1,68 @@
/**
* validateMeta() -- public method
*
* This method requires one input parameter:
*
* $_meta -- a key-value paired array containing the current meta data payload
*
* first we validate the input parameter to ensure we're working with valid data object. If not, we're going to
* immediately return a false value and set the gacMetrics property (stopProcessing) to true. This allows us
* to signal the gacFactory class that a processing error had occurred.
*
* Otherwise, spin through the meta data that was passed to the method and compare each key in the array to the
* list of "authorized" keys defined for the current class. If a key does not exist in the authoritative index,
* then remove that key from the input-meta data and record the event in the log file and in the gacFactory
* class eventMessages property.
*
* Method returns a boolean value that indicates if meta data was validated.
*
* Since the meta data array is passed as a call-by-reference variable, dropped fields will propagate back to the
* calling client. A list of dropped fields, if any, will be stored in the eventMessages container.
*
*
* @author mike@givingassistant.org
* @version 1.0
*
* @param array $_meta
* @return bool
*
* HISTORY:
* ========
* 06-21-17 mks original coding
* 10-05-17 mks CORE-584: added validation for META_SKIP and META_LIMIT
*
*/
public function validateMeta(array &$_meta):bool
{
if (!is_array($_meta) or empty($_meta)) {
$this->logger->error(ERROR_DATA_META_REQUIRED);
$this->eventMessages[] = ERROR_DATA_META_REQUIRED;
return(false);
}
foreach ($_meta as $key => $value) {
if (!array_key_exists($key, $this->fields)) {
unset($_meta[$key]);
$msg = sprintf(NOTICE_META_DISCARD, $key);
$this->eventMessages[] = $msg;
if ($this->debug) $this->logger->debug($msg);
} else {
switch ($key) {
case META_SKIP :
case META_LIMIT :
if (!is_numeric($value)) {
$msg = ERROR_DATA_FIELD_DROPPED . $key;
$this->eventMessages[] = $msg;
if ($this->debug) $this->logger->debug($msg);
$msg = sprintf(ERROR_DATA_TYPE_MISMATCH_DETAILS, $key, DATA_TYPE_INTEGER, gettype($value));
$this->eventMessages[] = $msg;
if ($this->debug) $this->logger->debug($msg);
unset($_meta[$key]);
}
break;
}
}
}
return(true);
}

View File

@@ -0,0 +1,55 @@
/**
* deBSON() -- private method
*
* MongoDB has a problem fetching data as it tends to BSON serialize all properties within a record for
* non-packed record arrays. So, this method, which is recursive, takes the current data payload of N-records,
* and traverses all the records looking for declared array fields (fieldTypes).
*
* When found, that field type is force-cast to type = array and, if the field itself is another array, such
* as a sub-collection, the method will recursively call itself.
*
* As of this time, version 1.0.0, this method is only called from the _fetchRecords() method.
*
* @author mike@givingassistant.org
* @version 1.0
*
* @param $_data
*
* HISTORY:
* ========
* 08-29-17 mks CORE-494: original coding
* 11-26-18 mks DB-55: added error processing if a key is not a member of the current class
*
*/
private function deBSON(&$_data)
{
if (is_array($_data) and array_key_exists(0, $_data)) {
for ($index = 0, $limit = count($_data); $index < $limit; $index++) {
foreach ($_data[$index] as $column => &$value) {
if ($this->fieldTypes[$column] == DATA_TYPE_ARRAY) {
if (!is_scalar($value)) {
foreach ($value as &$rec) {
if (!is_scalar($rec)) $rec = (array) $rec;
}
$this->deBSON($value);
}
$_data[$index][$column] = (array) $value;
}
}
}
} elseif (is_array($_data)) {
foreach ($_data as $key => $val) {
if (array_key_exists($key, $this->fieldTypes) and $this->fieldTypes[$key] == DATA_TYPE_ARRAY) {
if (is_array($val))
$this->deBSON($val);
$_data[$key] = (array) $val;
} elseif (!array_key_exists($key, $this->fieldTypes)) {
$msg = ERROR_DATA_FIELD_NOT_MEMBER . $key;
$this->eventMessages[] = sprintf(STUB_LOC, basename(__FILE__),__METHOD__, __LINE__) . COLON . $msg;
$this->logger->data($msg);
}
}
}
}

View File

@@ -0,0 +1,709 @@
<?php
/**
* BindParams -- helper class
*
* this is a helper class for the gacMySQL class, specifically for generating dynamic prepared statements.
*
* this class, when instantiated, creates storage for a prepared statement's type and values. When we want to
* create the prepared statement, we use call_user_func_array() and use the output from this method to generate
* the arguments that are normally passed in a prepared statement.
*
* using this class allows a data payload to be dynamically parsed and validated - allows a client to update
* a sub-set of a table without having to explicitly enumerate every column in the table.
*
* @author mike@givingassistant.org
* @version 1.0
*
* HISTORY:
* --------
* 06-29-17 mks original coding
*
*/
class BindParams {
private $values = array();
private $types = '';
/**
* add() -- public method
*
* this method accepts two parameters as input - the type of the variable and the value of the variable. In
* this instance, when I say variable, I am referring to a mysql table column.
*
* if, for some unknown reason, type is a value not allowed, reset it to type 's' which should cover-up most
* mistakes.
*
* $value as a call-by-reference to suppress a PHP warning message.
*
* @author mike@givingassistant.org
* @version 1.0
*
* @param $type
* @param $value
*
* HISTORY:
* --------
* 06-29-17 mks original coding
*
*/
public function add(string $type, &$value)
{
switch ($type) {
case 'd' :
case 'i' :
case 'b' :
case 's' :
break;
default :
$type = 's';
}
$this->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.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.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.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.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.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();
}
}

View File

@@ -0,0 +1,709 @@
<?php
/**
* BindParams -- helper class
*
* this is a helper class for the gacMySQL class, specifically for generating dynamic prepared statements.
*
* this class, when instantiated, creates storage for a prepared statement's type and values. When we want to
* create the prepared statement, we use call_user_func_array() and use the output from this method to generate
* the arguments that are normally passed in a prepared statement.
*
* using this class allows a data payload to be dynamically parsed and validated - allows a client to update
* a sub-set of a table without having to explicitly enumerate every column in the table.
*
* @author mike@givingassistant.org
* @version 1.0
*
* HISTORY:
* --------
* 06-29-17 mks original coding
*
*/
class BindParams {
private $values = array();
private $types = '';
/**
* add() -- public method
*
* this method accepts two parameters as input - the type of the variable and the value of the variable. In
* this instance, when I say variable, I am referring to a mysql table column.
*
* if, for some unknown reason, type is a value not allowed, reset it to type 's' which should cover-up most
* mistakes.
*
* $value as a call-by-reference to suppress a PHP warning message.
*
* @author mike@givingassistant.org
* @version 1.0
*
* @param $type
* @param $value
*
* HISTORY:
* --------
* 06-29-17 mks original coding
*
*/
public function add(string $type, &$value)
{
switch ($type) {
case 'd' :
case 'i' :
case 'b' :
case 's' :
break;
default :
$type = 's';
}
$this->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();
}
}

View File

@@ -0,0 +1,193 @@
/**
* cacheByTokenList() -- private method
*
* This method requires a single input parameter -- that's an array of tokens in the following format:
*
* array (
* 0 =>
* array (
* 'token_tst' => '2DB9636A-C14D-F2C9-7CDA-E7808C1EA600',
* ),
* )
*
* This method is used from the update event -- when we've already completed the update successfully and the
* current class has been populated with the successful update-query results, which we wish to preserve.
*
* The updated records, represented by the token list, has to be re-cached. So this method is going to exec
* a SELECT query to fetch the updated records for caching. This is a prepared query.
*
* Since we don't want to overwrite the results of the update query in the current class object, we're going to
* clone the object, execute the select query from that object, and transfer the results over to the original
* class before releasing the cloned object.
*
* Prior to said release, we're going to call the method to process the data members and cache the records and,
* on return, transfer the cache keys (if caching is enabled for the class) or the data.
*
* The method returns a boolean indicating success or failure for all of the operation.
*
*
* @author mike@givingassistant.org
* @version 1.0
*
* @param array $_tList
* @return bool
*
* HISTORY:
* ========
* 10-31-17 mks CORE-586: original coding
*
*/
private function cacheByTokenList(array $_tList): bool
{
if (!is_array($_tList)) {
$this->eventMessages[] = ERROR_DATA_ARRAY_NOT_ARRAY . STRING_TOKEN;
return false;
}
// clone the current object so we don't overwrite any of the current class members & zero-out the important bits
$tObj = clone $this;
$tObj->queryVariables = null;
$tObj->strQuery = '';
$tObj->queryResults = '';
$tObj->count = 0;
$tObj->dbEvent = DB_EVENT_SELECT;
/*
* build the query to fetch the record based on the token list which looks like:
*
* array (
* 0 =>
* array (
* 'token_xxx' => '2DB9636A-C14D-F2C9-7CDA-E7808C1EA600',
* ),
* )
*/
$query = 'SELECT /* ' . basename(__FILE__) . COLON . __METHOD__ . AT . __LINE__ . ' */ ';
$query .= '* FROM ';
if (!isset($tObj->template->dbObjects[PDO_VIEWS][PDO_VIEW_BASIC . $tObj->collectionName])) {
$query .= $tObj->collectionName;
} else {
$query .= PDO_VIEW_BASIC . $tObj->collectionName;
}
$query .= ' ';
$query .= 'WHERE ' . STRING_TOKEN . $tObj->ext . ' IN (';
foreach ($_tList as $record) {
$query .= '?, ';
$tObj->queryVariables[] = $record[(STRING_TOKEN . $tObj->ext)];
}
$query = rtrim($query, ', ');
$query .= ') ';
if (!$tObj->useDeletes) {
$query .= 'AND status' . $tObj->ext . ' != ?';
$tObj->queryVariables[] = STATUS_DELETED;
}
$tObj->strQuery = $query;
try {
$tObj->executePreparedQuery();
if (!$tObj->status) {
$this->eventMessages = array_merge($this->eventMessages, $tObj->eventMessages);
return false;
}
$tObj->data = $tObj->queryResults;
if (!$tObj->returnFilteredData()) {
$this->eventMessages = array_merge($this->eventMessages, $tObj->eventMessages);
$this->eventMessages[] = ERROR_RFD_CORE_FAIL;
return false;
}
} catch (Throwable $t) {
$msg = ERROR_THROWABLE_EXCEPTION . COLON . $t->getMessage();
$this->eventMessages[] = $msg;
if (isset($this->logger) and $this->logger->available)
$this->logger->error($msg);
else
consoleLog($this->res, CON_ERROR, $msg);
$this->state = STATE_FRAMEWORK_WARNING;
return false;
}
// copy data from the clone to the original
$this->eventMessages = array_merge($this->eventMessages, $tObj->eventMessages);
$this->cacheKeys = $tObj->cacheKeys;
$this->data = $tObj->data;
$tObj->__destruct();
unset($tObj);
return true;
}
/**
* getCacheTokenListQuery() -- private method
*
* This method is called from the update and delete methods for when we need to generate a list of affected tokens
* for these operations. We need to generate this list b/c the operation will modify the records and, if the
* records exist in cache, they should be removed.
*
* The method requires one input parameter which is the list of tokens we're going to build as a result of the
* query. As such, all of the query elements must have been built prior to invoking this method as those member
* elements are used to build this SELECT query...
*
* We'll execute the prepared select query and store the results in an array which is implicitly returned to the
* calling client.
*
* The method proper returns a boolean to indicate success or fail in processing as a null value returned for
* the token list is permissible.
*
*
* @author mike@givingassistant.org
* @version 1.0
*
* @param array|null $_tokenList
* @return boolean
*
* HISTORY:
* ========
* 10-26-17 mks CORE-586: original coding
*
*/
private function getCacheTokenListQuery(array &$_tokenList = null): bool
{
$rc = false;
$cq = 'SELECT /* ' . basename(__FILE__) . COLON . __METHOD__ . AT . __LINE__ . ' */ ';
$cq .= DB_TOKEN . $this->ext . ' ';
$cq .= 'FROM ' . $this->collectionName . ' ';
$cq .= 'WHERE ' . $this->where . ' ';
if (!is_null($this->queryOrderBy)) {
$cq .= 'ORDER BY ' . $this->queryOrderBy . ' ';
}
if (!is_null($this->queryLimit)) {
$cq .= 'LIMIT ' . $this->queryLimit;
}
try {
$this->dbEvent = DB_EVENT_NAMASTE_READ;
$this->strQuery = $cq;
$this->executePreparedQuery();
if ($this->status) {
$_tokenList = $this->queryResults;
// foreach ($this->queryResults as $key ) {
// $_tokenList[] = $key;
// }
if (empty($_tokenList)) $_tokenList = null;
$rc = true;
} else {
$this->eventMessages[] = ERROR_PDO_CQ_QUERY;
$this->state = STATE_DB_ERROR;
}
return $rc;
} catch (Throwable $t) {
$msg = sprintf(ERROR_EXCEPTION . COLON . $this->strQuery[0]);
consoleLog(RES_PDO, CON_ERROR, $msg);
$this->eventMessages[] = $msg;
$this->eventMessages[] = $t->getMessage();
if (isset($this->logger) and $this->logger->available) {
$this->logger->warn($msg);
$this->logger->warn($t->getMessage());
} else {
consoleLog($this->res, CON_ERROR, $msg);
consoleLog($this->res, CON_ERROR, $t->getMessage());
}
$this->status = false;
$this->state = STATE_DB_ERROR;
}
return false;
}

View File

@@ -0,0 +1,204 @@
/**
* convertCacheMap() -- private static method
*
* This private method is the gateway/entry-point for cacheMapping on data payloads. The function has the following
* required input parameters:
*
* $_data -- this is the payload to be cacheMapped. This should be an indexed array of one, or more, assoc arrays
* $_dir -- string value indicating if the data is incoming (IN) or outbound (OUT)
* $_map -- this is the class-specific vector of cacheMapped settings pulled from the global cacheMap
* $_type -- this defines the data payload as either record-data or query-data
* $_errs -- this is a call-by-reference array that allows us to propagate error messages back up the stack
*
* The function looks at the contents of most of the input parameters and validates the content returning a null
* if any of the params fail validation while also adding messages to the error stack and by publishing a message
* to the error logger.
*
* Once validation is complete, we pass all of the input params to a second private function, allowing for
* recursion in that function, and hopefully get back an array (which is passed though back up to the calling
* client) that is successfully cacheMapped.
*
*
* @author mike@givingassistant.org
* @version 1.0
*
* @param array $_data
* @param string $_dir
* @param array $_map
* @param string $_type
* @param array $_errs
* @return array|null
*
*
* HISTORY:
* ========
* 02-25-19 mks DB-116: original coding
*
*/
private static function convertCacheMap(array $_data, string $_dir, array $_map, string $_type, array &$_errs): ?array
{
// validate input param content -- input param type is implicitly validated by strong type decls
if ($_dir != IN and $_dir != OUT) {
$hdr = basename(__METHOD__) . AT . __LINE__ . COLON;
$msg = sprintf(ERROR_CACHE_DIRECTION, (string) $_dir);
$_errs[] = $msg;
static::$logger->data($hdr . $msg);
return null;
}
if ($_type != STRING_QUERY and $_type != STRING_DATA) {
$hdr = basename(__METHOD__) . AT . __LINE__ . COLON;
$msg = ERROR_CACHE_MAP_TYPE . $_type;
$_errs[] = $msg;
static::$logger->error($hdr . $msg);
return null;
}
if (!is_array($_data)) {
$hdr = basename(__METHOD__) . AT . __LINE__ . COLON;
$msg = $hdr . ERROR_DATA_ARRAY_NOT_ARRAY . STRING_DATA;
$_errs[] = $msg;
static::$logger->data($msg);
return null;
}
if (!is_array($_map)) {
$hdr = basename(__METHOD__) . AT . __LINE__ . COLON;
$msg = $hdr . ERROR_DATA_ARRAY_NOT_ARRAY . CACHE_MAP;
$_errs[] = $msg;
static::$logger->data($msg);
return null;
}
try {
return static::processCacheMap($_data, $_map, $_dir, $_type, $_errs);
} catch (TypeError $t) {
$hdr = basename(__METHOD__) . AT . __LINE__ . COLON;
$msg = $hdr . ERROR_TYPE_EXCEPTION;
$_errs[] = $msg;
static::$logger->warn($msg);
$msg = $hdr . $t->getMessage();
$_errs[] = $msg;
static::$logger->warn($msg);
return null;
}
}
/**
* processCacheMap() -- private static method with recursion
*
* This function is responsible for the cacheMapping for both incoming and outbound data payloads. It is a
* stand-alone function because of it's recursive nature -- when we encounter a sub-array within the payload,
* we must recursively call this method in order to process the sub-array (etc.).
*
* There are the following input parameters to this method, all of which are required:
*
* $_data -- this is the incoming/outgoing data payload. The invoking method has parsed, for example, the incoming
* data payload and, as an example, let's say there are three sub-arrays stored in the payload:
* STRING_QUERY_DATA, STRING_SORT_DATA and STRING_RETURN_DATA -- the invoking method will make a total of three
* calls to this method, one for each of the sub-arrays under BROKER_DATA. Note that $_data should be passed as
* an indexed array s.t. each tuple in the array is processed as a separate record.
*
* $_map -- this is the cacheMap for the targeted class. In other words, it is not the entire cacheMap but the
* named tuple for the current data class.
*
* $_dir -- this is a string value that may only be either IN or OUT (both are Namaste system constants). IN
* designates the payload as incoming while OUT designates the payload as outbound.
*
* $_type -- this is a string value, defined as either STRING_QUERY or STRING_DATA, and is verified in the
* the calling client. This value designates the type of payload to be mapped.
*
* $_es -- this is an array for the error-stack -- it's a call-by-reference parameter s.t. we can propagate any
* error messages back to the invoking client.
*
* The method returns an array which, under optimal conditions, returns a mirror of the incoming data payload
* save that the keys have been successfully cacheMapped.
*
* Any filed which fails cacheMapping (e.g.: not found) will be stored in the class static $badCacheFields and
* will be implicitly returned to the calling client for processing.
*
*
* @author mike@givingassistant.org
* @version 1.0
*
*
* @param array $_data -- indexed array of data to be cacheMapped; may contain more than one record
* @param array $_map -- cacheMap for the current data class
* @param string $_dir -- indicates the direction (flow) of the data: either INcoming or OUTbound
* @param string $_type - indicates payload type: either DATA or QUERY (validated in calling client)
* @param array $_es -- call-by-reference array for returning error messages to the calling client
* @return array|null -- returns the cacheMapped array or a null on error
*
*
* HISTORY:
* ========
* 02-25-19 mks DB-116: original coding
*
*/
private static function processCacheMap(array $_data, array $_map, string $_dir, string $_type, array &$_es): ?array
{
$data = null; // container to hold the cacheMapped record
$records = null; // container to hold all the cacheMapped records
// todo -- test for subCollection array existing in the subC setting... which means you have to add it to the cacheMap data
// this is where the cache-mapping magic happens...
foreach ($_data as $record => $recordData) {
foreach ($record as $column => &$value) {
if ($_dir == IN) { // todo -- map off the type...
// we're cache-mapping an incoming payload
if (in_array($column, $_map[CACHE_MAP])) {
if (is_array($value) and $_map[CACHE_SUBC] != STRING_NOT_DEFINED and array_key_exists(array_search($column, $_map[CACHE_MAP]), $_map[CACHE_SUBC])) {
// note: not checking for the case where $value is an array but $newKey is not defined in the
// cached SUBC definition for the class. This loose definition for sub-collections
// permits the user to store sub-arrays without inspection/validation/mapping.
try {
// we have to process $value recursively as a sub-array
$data[array_search($column, $_map[CACHE_MAP])] = static::processCacheMap($value, $_map, $_dir, $_type, $_es);
} catch (TypeError $t) {
$hdr = basename(__METHOD__) . AT . __LINE__ . COLON;
$msg = $hdr . ERROR_TYPE_EXCEPTION;
$_errs[] = $msg;
static::$logger->warn($msg);
$msg = $hdr . $t->getMessage();
$_errs[] = $msg;
static::$logger->warn($msg);
return null;
}
} else {
$data[array_search($column, $_map[CACHE_MAP])] = $value;
}
} else {
static::$badCacheFields[$column] = $value;
}
} else {
// we're cache-mapping an outbound payload so remove the class extension from the column name
$newKey = str_replace($_map[CACHE_EXT], '', $column);
if (array_key_exists($newKey, $_map[CACHE_MAP])) {
// note: not checking for the case where $value is an array but $newKey is not defined in the
// cached SUBC definition for the class. This loose definition for sub-collections
// permits the user to store sub-arrays without inspection/validation/mapping.
if (is_array($value) and $_map[CACHE_SUBC] != STRING_NOT_DEFINED and array_key_exists($newKey, $_map[CACHE_SUBC])) {
try {
// we have to process $value recursively as a sub-array
$data[$_map[CACHE_MAP][$newKey]] = static::processCacheMap($value, $_map, $_dir, $_type,$_es);
} catch (TypeError $t) {
$hdr = basename(__METHOD__) . AT . __LINE__ . COLON;
$msg = $hdr . ERROR_TYPE_EXCEPTION;
$_errs[] = $msg;
static::$logger->warn($msg);
$msg = $hdr . $t->getMessage();
$_errs[] = $msg;
static::$logger->warn($msg);
return null;
}
} else {
$data[$_map[CACHE_MAP][$newKey]] = $value;
}
} else {
static::$badCacheFields[$newKey] = $value;
}
}
}
}
if (!empty($data)) {
$records[] = $data;
unset($data);
} // todo: else?
return $records;
}