Files
namaste/classes/gasCache.class.inc
gramps 373ebc8c93 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.
2026-04-05 09:49:30 -07:00

2129 lines
92 KiB
PHP

<?php
/**
* gasCache -- static class
*
* Class handles all memcache operations.
*
* All data is serialized JSON objects - which requires this class to fetch the items from memcache based on
* not only the data obfuscation, but also the key generation under which the object is stored.
*
* Keys are composed of five parts and all parts are required. The general format of key is:
*
* {FW_VERSION}_{CDN_VERSION}_{DATA_CLASS}_{RECORD_GUID}
*
* this class has full support for both generation keys based on the memcache version string and on the CDN version
* key, both of which are defined in the global configuration file. All "parts" of the key are separated by the
* underscore (_) character.
*
* To invalidate the current cache, one needs only to tweak one of these two key values in the configuration file.
* Old/invalid entries will be purged through time expiration under the cache management system.
*
* Note: Version key allows us to invalidate caches when changes are made to the release software. CDN keys
* allow us to invalidate caches when changes are made to the CDN server. Though as of initial coding, this is
* not a planned feature of the infrastructure - it is, however, supported in the interest of future expansion.
*
* Note that we're not ever going to delete anything from cache - this is because for all objects added to the
* cache, we've set an expiration timer of 1-hour -- items will naturally purge themselves via the memcache
* services, thus saving us the overhead of making a delete call.
*
* Programmer Notes:
* -----------------
* The $cacheName variable is defined in core-abstraction and is set in the class instantiation -- this was designed
* intentionally so as to NOT expose the collection/table name in the cache key.
*
* Generated keys should be self-documenting once one understands how they are constructed.
*
* Each method that returns a status requires an input parameter to be passed which represents an array error
* stack. If an error was raised during the method, it is the responsibility of the calling stub to validate
* the operation by parsing this ephemeral error stack variable.
*
*
* METHOD CATALOG:
* ---------------
* __construct: primary method, connect to memcached server, general initialization
* auto_connect: helper method: parses server list from configuration
* add_server: helper method: adds servers to memcache server pool
* add: primary method: adds new entry to cache or replaces if already exists
* set: primary method: identical to add() but replace is implicit
* get: primary method: fetch from cache method
* getByKey: primary method: fetches from cache based only on the passed key
* getAllKeys: helper method: returns array-list of all keys currently in cache
* delete: primary method: permanently deletes item (by key) from cache
* replace: primary method: replaces existing cached item with new item
* makeKey: primary method: build the cache storage key
* singleton: primary method: entry point for class instantiation
* dumpErrors: helper method: returns current error catalog as block of formatted text
* errorReset: helper method: resets the error stack with option to save to log
* getStats: helper method: queries cache server and returns cache stats
* flush_cache: helper method: deletes all items from cache
* __clone: helper method: prevents cloning of the singleton class
* __destruct: helper method: release class resources
*
*
* @author mike@givingassistant.org
* @version 1.0
*
* HISTORY:
* --------
* 06-09-17 mks original coding
* 03-02-18 mks CORE-680: deprecated trace logging
* 08-02-18 mks CORE-774: PHP7.2 Exception Handling
* 01-08-20 mks DB-150: PHP7.4 member variable type-casting
*
*/
class gasCache
{
private static ?gasCache $instance = null; // pointer to self for instantiation
private static Memcached $mco; // connector to memcache
private static string $class; // this class so we don't have to keep calling get_class()
private static bool $debug; // copied from global debug setting
private static array $config;
private static bool $isCluster;
public static ?array $errors;
public static bool $available; // indicates if the memcache servers are available
public static string $resultMessage; // result message array from memcached operation
public static int $resultCode;
public static int $cacheTTL; // make the default time-to-live publicly available
private static gacErrorLogger $logger; // Error Logger instantiation container
private static string $res = 'MEMC: '; // console resource id
/**
* __construct() -- private function for singleton class instantiation
*
* the constructor requires that the global config object be available so that we can read the memcache
* configuration... and that the global error object be available so that we can log errors. If either
* is unavailable, log messages to the console and (sic) the error stack and exit.
*
* Once the configuration is read, attempt to connect to the memcache server(s)...if we do not connect,
* generate a FATAL and exit.
*
* Read in the server list (support for memcache cluster, yes) and add these servers to the memcache
* service. as long as we can connect to at least one server, we're good - otherwise, generate a
* FATAL message and exit.
*
* Depending on the ultimate status of the object, output a status message to the console and return.
*
* @author mike@givingassistant.org
* @version 1.0
*
* HISTORY:
* --------
* 06-09-17 mks original coding
* 02-12-19 mks DB-116: cache the template cacheMaps into memcached
*
*/
private function __construct()
{
register_shutdown_function(array($this, '__destruct'));
try {
static::$logger = new gacErrorLogger();
} catch (Throwable $t) {
$msg = ERROR_THROWABLE_EXCEPTION . COLON . $t->getMessage();
static::$errors[] = $msg;
if (isset(static::$logger) and static::$logger->available)
static::$logger->error($msg);
else
consoleLog(static::$res, CON_ERROR, $msg);
return false;
}
if (!is_array(gasConfig::$settings[CONFIG_CACHE])) {
static::$logger->error(ERROR_CONFIG_RESOURCE_404 . CONFIG_CACHE);
exit(0);
}
// get configuration and initialize locals
static::$debug = gasConfig::$settings[CONFIG_DEBUG];
static::$class = get_class($this);
static::$available = true;
static::$config = gasConfig::$settings[CONFIG_CACHE];
static::$isCluster = intval(static::$config[CONFIG_CACHE_IS_CLUSTER]);
try {
static::$mco = new Memcached(static::$config[CONFIG_CACHE_SERVER][CONFIG_CACHE_SERVER_PERSISTENT_ID]);
static::$mco->setOption(Memcached::OPT_BINARY_PROTOCOL, false);
static::$cacheTTL = static::$config[CONFIG_CACHE_EXPIRES];
static::$resultMessage = static::$mco->getResultMessage();
if (!static::auto_connect()) {
static::$logger->error(ERROR_CACHE_RESOURCE_404);
static::$errors[] = ERROR_CACHE_RESOURCE_404;
static::$available = false;
}
} catch (Throwable $t) {
$msg = ERROR_THROWABLE_EXCEPTION . COLON . $t->getMessage();
static::$errors[] = $msg;
if (isset(static::$logger) and static::$logger->available)
static::$logger->error($msg);
else
consoleLog(static::$res, CON_ERROR, $msg);
static::$available = false;
}
// load the lookup-grid for the templates into memcached
if (!static::loadCacheMaps()) {
static::$logger->fatal(FAIL_CACHE_MAP_LOAD);
consoleLog(static::$res, CON_ERROR, FAIL_CACHE_MAP_LOAD);
}
// output state message to console dependant on if we successfully added a memcache server
if (isset($_SERVER['SHELL'])) {
if (static::$available) {
if (static::$debug) {
static::$logger->info(SUCCESS_CONNECT_CACHE);
consoleLog(static::$res, CON_SUCCESS, SUCCESS_CONNECT_CACHE . PHP_EOL);
}
} else {
consoleLog(static::$res, CON_ERROR, 'memcache reports no servers available');
}
}
return static::$available;
}
/**
* auto_connect() -- private method
*
* attempts to connect to each of the servers in the configuration file.
*
* posts status of each connection attempt to the log and adds an error to the error stack if a
* connection failed.
*
* returns a boolean indicating if ANY servers were successfully connected.
*
* NOTE:
* -----
* commented-out code is to support cache pools... if we're using cache pools/clusters, then update this code.
*
*
* @author mike@givingassistant.org
* @version 1.0
*
* @return bool
*
* HISTORY:
* --------
* 06-09-17 mks original coding
*
*/
private static function auto_connect(): bool
{
$connected = true;
// foreach(static::$config[CONFIG_CACHE_SERVER] as $key => $server) {
if (!static::add_server(gasConfig::$settings[CONFIG_CACHE][CONFIG_CACHE_SERVER])) {
static::$logger->warn(ERROR_CACHE_RESOURCE_404);
static::$errors[] = ERROR_CACHE_RESOURCE_404;
$connected = false;
}
// }
return($connected);
}
/**
* add_server() -- private method
*
* takes a single unit of memcache server configuration information and attempts to add that server to the
* memcache pool.
*
* input parameter is an array block of configuration information (../config/memcached.php)
*
* return value is a boolean indicated if we successfully added the named server to the memcache pool.
*
* Note:
* -----
* At this time of creation, pooled servers were not configured. However, the code is supporting them so, if
* GA is using a pool of servers, need to take a devOps break and get info about server instance weights.
*
* @author mike@givingassistant.org
* @version 1.0
*
* @param $_server
* @return bool
*
* HISTORY:
* --------
* 06-09-17 mks original coding
*
*/
private static function add_server($_server): bool
{
$hostName = null;
$portNum = null;
$weight = null;
extract($_server);
try {
if (static::$isCluster) {
/** @noinspection PhpUndefinedClassConstantInspection */
static::$mco->setOption(Memcached::OPT_CLIENT_MODE, Memcached::DYNAMIC_CLIENT_MODE);
}
return (static::$mco->addServer($hostName, $portNum, $weight));
} catch (Throwable $t) {
$msg = ERROR_THROWABLE_EXCEPTION . COLON . $t->getMessage();
static::$errors[] = $msg;
if (isset(static::$logger) and static::$logger->available)
static::$logger->error($msg);
else
consoleLog(static::$res, CON_ERROR, $msg);
static::$available = false;
}
return false;
}
/**
* fetchTemplateCacheMap() -- public static method
*
* This method is used to fetch the cacheMap vector for a specific $_template. The name of the template is
* the first parameter required. Template names should be in the form of gat{template_name}.
*
* The second parameter is optional: $_map, if overridden to true, will return the entire cacheMap instead of
* just the template specified in the first parameter.
*
* The method uses the locally instantiated $mco cache reference to directly query cache for the cacheMap. On
* successful retrieval, we return the vector of data referenced in the array by the key: $_template. If the
* key exists, we return that record back to the calling client.
*
* In all other cases, if an error is trapped, or if we're not able to fetch the template vector, the method
* will return a null value.
*
*
* @author mike@givingassistant.org
* @version 1.0
*
* @param null|string $_template - name of the template to fetch (format: "gat"{templateName})
* @param bool $_map - defaults to false, if true - returns the entire cacheMap instead
* @return array|null - returns the data record on success, null on fail
*
*
* HISTORY:
* ========
* 02-14-19 mks DB-116: original coding
* 02-25-19 mks DB-116: added second (and optional) input param to return entire cacheMap and fixed warning
* being generated if there was no cacheMap in cache; now returns null immediately
* 07-27-20 mks DB-156: refactored making template optional as a param, better error handling
*
*/
public static function fetchTemplateCacheMap(?string $_template = null, bool $_map = false): ?array
{
$msg = '';
$hdr = '';
try {
// first, fetch the cacheMap from cache
$cacheMap = static::$mco->get(CACHE_MAP);
if (is_null($cacheMap)) {
$hdr = sprintf(INFO_LOC, basename(__METHOD__), __LINE__);
$msg = ERROR_CACHE_FETCH_FAIL . CACHE_MAP;
} elseif (is_null($cacheMap = json_decode($cacheMap, true))) {
$hdr = sprintf(INFO_LOC, basename(__METHOD__), __LINE__);
$msg = ERROR_JSON_DECODE_FAIL;
}
if (strlen($msg)) {
consoleLog(static::$res, CON_ERROR, $msg);
static::$logger->warn($hdr . $msg);
return null;
}
} catch (Throwable $t) {
$msg = ERROR_CACHE_FETCH_FAIL . CACHE_MAP;
consoleLog(static::$res, CON_ERROR, $msg);
static::$logger->warn($msg);
return null;
}
// if we've request the entire cacheMap, return it...or if the cacheMap is null, return null
if ($_map) return $cacheMap;
// ...otherwise, just return the vector identified by $_template
if (array_key_exists($_template, $cacheMap)) {
return $cacheMap[$_template];
} else {
$hdr = sprintf(INFO_LOC, basename(__METHOD__), __LINE__);
$msg = sprintf(ERROR_CACHE_MAP_KEY_404, $_template);
consoleLog(static::$res, CON_ERROR, $msg);
static::$logger->warn($hdr . $msg);
return null;
}
}
/**
* mapIncomingData() -- public static method
*
* The purpose of this method is to perform the cacheMapping on all incoming data payloads to the Namaste
* service. Note that the invoking method already makes the check for AppServer so we don't have to. ;)
*
* The following input parameters are expected:
*
* $_data -- this is a call-by-reference value and should be the BROKER_DATA part of the $request payload
* $_map -- this is the data-template vector pulled from the global cacheMap container and contains all necessary
* data about the current class being cache-mapped.
* $_msg -- call-by-reference array acting as an error container for the invoking client
*
* Basically, this method gets the $request[BROKER_DATA] part of the payload. For all events, this payload will
* either be an associative array of data containers, each with a designated purpose, each requiring cacheMapping.
* If the array received in an indexed array, then the request is to create one, or more, new records and each
* tuple in the incoming indexed array is potentially a new record.
*
* If $_data is an indexed array, we'll handle that as it's own event by passing the entire array down to the
* cacheMapping function.
*
* Otherwise, we'll quality the keys of the associative array and well pass the arrays they reference down to the
* same cacheMapping routine we used for the create event.
*
* For both cases, the targeted (sub)array is replaced in it's entirety by the new cacheMapped array. However, we're
* not going to return the array in exactly the same format.
*
* We're going to create two sub-containers at the top level of the array keyed under the constants STATUS_VALID
* ('valid') and STATUS_INVALID ('invalid') respectively. If no fields were successfully cacheMapped, then
* the STATUS_VALID sub-container will be set to null. Otherwise, the container will be an array of the cacheMapped
* data. If any field failed to be cacheMapped, that field will be returned under the STATUS_INVALID container
* label. It is entirely the parent-processes' responsibility to mitigate any invalid fields.
*
*
* @author mike@givingassistant.org
* @version 1.0
*
* @param array $_data
* @param array $_map
* @param array $_msg
* @return bool
*
*
* HISTORY:
* ========
* 02-15-19 mks DB-116: original coding completed
* 04-21-20 mks ECI-107: updated for sub-collection fetch
*
*/
public static function mapIncomingData(array &$_data, array $_map, array &$_msg): bool
{
if (array_key_first($_data) === 0) {
// if the array is indexed...
try {
if (is_null($data = static::cacheMapIndexedArray($_data, $_map, false, $_msg))) {
$hdr = basename(__METHOD__) . AT . __LINE__ . COLON;
$msg = ERROR_CACHE_MAP_FAIL . STRING_INCOMING_DATA;
$_msg[] = $msg;
static::$logger->data($hdr . $msg);
return false;
}
$_data = $data;
} catch (TypeError $t) {
$hdr = basename(__METHOD__) . AT . __LINE__ . COLON;
$msg = ERROR_CACHE_MAP_FAIL . STRING_INCOMING_DATA;
$_msg[] = $msg;
static::$logger->data($hdr . $msg);
return false;
}
} else {
// ...else the array is associative - we want to process all the top level array keys and call the
// appropriate processing method (hint: STRING_RETURN_DATA is indexed)
$records = null;
foreach ($_data as $task => $value) {
switch ($task) {
case STRING_QUERY_DATA : // CRUD(fetch) event
case STRING_SORT_DATA : // CRUD(fetch) event
case STRING_UPDATE_DATA : // CRUD(update) event
case STRING_ORDER_BY_DATA : // Order-by directive
// edge case: a QUERY_DATA event with a null value is permissible
if ($task == STRING_QUERY_DATA and is_null($value)) return true;
// for all other situations, convert...
try {
if (is_null($data = static::cacheMapAssociativeArray($value, $_map, $_msg))) {
$hdr = basename(__METHOD__) . AT . __LINE__ . COLON;
$msg = ERROR_CACHE_MAP_FAIL . $task;
$_msg[] = $msg;
static::$logger->data($hdr . $msg);
return false;
}
$_data[$task] = $data;
} catch (TypeError $t) {
$hdr = basename(__METHOD__) . AT . __LINE__ . COLON;
$msg = ERROR_CACHE_MAP_FAIL . $task;
$_msg[] = $msg;
static::$logger->data($hdr . $msg);
return false;
}
break;
case STRING_DATA : // sub-Collection create event
try {
if (is_null($data = static::cacheMapIndexedArray($value, $_map, false, $_msg))) {
$hdr = basename(__METHOD__) . AT . __LINE__ . COLON;
$msg = ERROR_CACHE_MAP_FAIL . $task;
$_msg[] = $msg;
static::$logger->data($hdr . $msg);
return false;
}
$_data[$task] = $data;
} catch (TypeError $t) {
$hdr = basename(__METHOD__) . AT . __LINE__ . COLON;
$msg = ERROR_CACHE_MAP_FAIL . $task;
$_msg[] = $msg;
static::$logger->data($hdr . $msg);
return false;
}
break;
case STRING_RETURN_DATA :
try {
if (is_null($data = static::cacheMapIndexedArray($value, $_map, true, $_msg))) {
$hdr = basename(__METHOD__) . AT . __LINE__ . COLON;
$msg = ERROR_CACHE_MAP_FAIL . $task;
$_msg[] = $msg;
static::$logger->data($hdr . $msg);
return false;
}
$_data[$task] = $data;
} catch (TypeError $t) {
$hdr = basename(__METHOD__) . AT . __LINE__ . COLON;
$msg = ERROR_CACHE_MAP_FAIL . $task;
$_msg[] = $msg;
static::$logger->data($hdr . $msg);
return false;
}
break;
// sub-collection event GUIDs
case STRING_GUID_KEY :
case STRING_SUBC_GUID :
// if a guid-key field is present, it should contain a string value that is a valid guid
if (!validateGUID($value)) {
$hdr = basename(__METHOD__) . AT . __LINE__ . COLON;
$msg = ERROR_INVALID_GUID . $value;
$_msg[] = $msg;
static::$logger->data($hdr . $msg);
return false;
}
break;
case STRING_SUBC_FIELD :
// subC field has to be present in the cacheMap
if ($_map[CACHE_SUBC] == STRING_NOT_DEFINED or !in_array($value, $_map[CACHE_MAP])) {
$hdr = basename(__METHOD__) . AT . __LINE__ . COLON;
$msg = sprintf(ERROR_SUBC_KEY_404, $value);
$_msg[] = $msg;
static::$logger->data($hdr . $msg);
return false;
} else {
// $oldField = $task;
$mappedColumnName = array_search($value, $_map[CACHE_MAP]);
// validate the cacheMapped col name against the CACHE_SUBC keys ensuring it's a subC field
if (!array_key_exists($mappedColumnName, $_map[CACHE_SUBC])) {
$hdr = basename(__METHOD__) . AT . __LINE__ . COLON;
$msg = sprintf(ERROR_SUBC_RECORD_NO_KEY, $mappedColumnName);
$_msg[] = $msg;
static::$logger->data($hdr . $msg);
return false;
}
$_data[STRING_SUBC_FIELD] = $mappedColumnName;
}
break;
case STRING_QUERY_OPTIONS :
// this is used for the multi => true field, so let's just return true
case STRING_SUBC_COL :
case STRING_SUBC_DATA :
// keys for sub-collection fetch; just return true
return true;
break;
default:
// error -- unknown label/index in array payload
$msg = sprintf(ERROR_UNKNOWN_KEY, $task, BROKER_DATA);
$_msg[] = $msg;
static::$logger->data($msg);
return false;
break;
}
}
}
// // todo -- test that the unset won't undo all the other changes made to the $_data payload
// if (isset($oldField) and array_key_exists($oldField, $_data[STRING_SUBC_FIELD]))
// unset($_data[STRING_SUBC_FIELD][$oldField]);
return true;
}
/**
* mapOutboundPayload() -- public static method
*
* This method required the following input parameters:
*
* $_obj -- call-by-reference "copy" of the class object
* $_errs - call-by-reference error catalog
*
* If, during processing, there is a failure, we'll record an error associated with the failure and we'll return
* immediately. Because we only modify the $data member on complete success, the contents of $data remain
* untouched and can be returned back to the calling client.
*
* If the current class object supports caching, then we're going to fetch the cacheMap from cache and we'll
* use the cacheMap to convert the records currently stored in $data to the cacheMapped payload. (obfuscation)
*
* If we return successfully from the cacheMapping, we'll then spin through the payload and slice out chunks
* of the record payload in sizes of $queryRecordLimit and cache those records under a single key, and we will
* replace the contents of $data with the list of cacheKeys (1 key per every $queryRecordLimit records).
*
* The method returns a boolean value back to the calling client indicating if the cache operation completed
* successfully or not. If so, then as previously stated, the cache-key catalog will have replaced the
* contents of the $data member as an indexed array of string values.
*
*
* @author mike@givingassistant.org
* @version 1.0
*
* @param object $_obj
* @param array $_errs
* @return bool|null
*
*
* HISTORY:
* ========
* 03-05-19 mks DB-116: original coding completed
* 05-29-19 mks DB-122: added support for META_DONUT_FILTER, restricted retVal to bool only
* 06-27-19 mks DB-34: one last check to remove any records from being cached that have status = deleted
* 11-05-20 mks DB-171:
*
*/
public static function mapOutboundPayload(object &$_obj, array &$_errs): bool
{
$cacheKeys = [];
$vector = null;
$recordLimit = gasConfig::$settings[CONFIG_DATABASE][CONFIG_DATABASE_QUERY_RECORD_LIMIT];
// support for META_DO_CACHE -- if set, and set to false, then we're not caching the return payload
/** @var gacMongoDB | gacPDO $_obj */
$md = $_obj->getMetaDataPayload();
// if META_DONUT_FILTER is set, and is set to true, then do NOT filter (cachemap) the return payload
$doFilter = (isset($md[META_DONUT_FILTER]) and boolval($md[META_DONUT_FILTER])) ? false : (gasConfig::$settings[ENV_APPSERVER][CONFIG_IS_LOCAL] === 1);
$doCache = (isset($md[META_DO_CACHE])) ? boolval($md[META_DO_CACHE]) : $_obj->useCache;
// start the cache-mapping...
try {
// make a copy of the data for the conversion process
if (is_null($newData = $_obj->getData())) {
$hdr = sprintf(INFO_LOC, basename(__METHOD__), __LINE__);
$msg = ERROR_NO_DATA;
$_errs[] = $msg;
static::$logger->data($hdr . $msg);
return false;
}
// Map the payload if appServer ... this can be overridden only if META_DONUT_FILTER is set.
if ($doFilter) {
// grab the template map
if (is_null($vector = gasCache::fetchTemplateCacheMap(($md[META_TLTI] . $md[META_TEMPLATE])))) {
$_errs[] = ERROR_CACHE_MAP_404;
return false;
}
if (is_null($newData = static::cacheMapIndexedArray($newData, $vector, false, $_errs))) {
$hdr = basename(__METHOD__) . AT . __LINE__ . COLON;
$msg = $hdr . ERROR_CACHE_MAP_FAIL . $_obj->class . ' ' . STRING_DATA;
$_errs[] = $msg;
static::$logger->data($msg);
return false;
}
// save the cache-mapped data to the object $data member
$_obj->replaceData($newData);
}
} catch (TypeError $t) {
$hdr = sprintf(INFO_LOC, basename(__METHOD__), __LINE__);
$_errs[] = ERROR_TYPE_EXCEPTION;
@handleExceptionMessaging($hdr, $t->getMessage(), $foo, true);
return false;
}
/*
* here's the explanation about caching and cache-mapping...
*
* Each class has the ability to toggle caching -- which means that data returned to the client is cached
* if this setting is set to true. If set to false, the return data payload contains the actual data and not
* a cache key pointing to the data.
*
* This is not the same as cache-mapping. If this code is exec'ing on APPSERVER, then all return payloads
* are mapped so as to enforce schema obfuscation.
*
* The mapping can be overridden by use of the META_DONUT_FILTER which explicitly disables filtering. The
* return payload can still, however, be cached.
*
* THIS IS THE ONLY SETTING THAT CAN OVERRIDE DATA FILTERING ON APPSERVER!
*
* Other namaste services (Admin, Segundo, Tercero) do not, by design, cache-map return data payloads.
*
*/
// if doCache is true, then cache the data payload
if ($doCache) {
try {
// we have more than maxRecordLimit records in the payload so loop through the list
$numRecs = count($newData);
for ($index = 1, $iteration = 1; $index <= $numRecs; $index+=$recordLimit, $iteration++) {
$newSlice = array_slice($newData, ($index - 1), $recordLimit);
if (empty($newSlice)) {
$hdr = basename(__METHOD__) . AT . __LINE__ . COLON;
$msg = ERROR_DATA_SLICE . $iteration;
$_errs[] = $msg;
static::$logger->data($hdr . $msg);
// remove any slices previously cached
static::deleteSlices($cacheKeys);
return false;
} else {
// DB-34: remove any record that has a deleted status just in-case one slipped through...
$status = array_column($newSlice, CM_TST_FIELD_TEST_STATUS);
if ($status[0] == STATUS_DELETED) continue;
$keyList = array_keys($status, STATUS_DELETED);
if (!empty($keyList))
foreach ($keyList as $k)
unset($newSlice[$k]);
/*
* if there is only one record in the return payload, then we'll cache that record under
* it's own guid -- otherwise, generate a new guid under which to cache all the records.
*/
$newGUID = (count($newSlice) == 1) ? $newSlice[0][CM_TST_TOKEN] : guid();
$newSlice = jsonHandler($newSlice, JSON_DIR_ENC, $_errs, true);
$cacheTime = (isset($vector[CACHE_TIMER]) and is_numeric($vector[CACHE_TIMER])) ? intval($vector[CACHE_TIMER]) : gasConfig::$settings[CONFIG_DATABASE][CONFIG_DATABASE_QUERY_RECORD_LIMIT];
$newKey = static::add($newGUID, gzcompress($newSlice), $cacheTime);
if (empty($newSlice)) {
$hdr = basename(__METHOD__) . AT . __LINE__ . COLON;
$msg = ERROR_DATA_SLICE . $iteration;
$_errs[] = $msg;
static::$logger->data($hdr . $msg);
// remove any slices previously cached
static::deleteSlices($cacheKeys);
return false;
}
if (is_null($newKey)) {
$hdr = basename(__METHOD__) . AT . __LINE__ . COLON;
$msg = $hdr . ERROR_CACHE_ADD_FAIL . $newGUID . COLON . STRING_CLASS . COLON . $_obj->class;
$_errs[] = $msg;
static::$logger->warn($msg);
// remove previously cached slices
static::deleteSlices($cacheKeys);
return false;
}
}
$cacheKeys[] = $newKey;
}
$_obj->replaceData($cacheKeys);
} catch (TypeError | Throwable $t) {
$hdr = basename(__METHOD__) . AT . __LINE__ . COLON;
$_errs[] = ERROR_TYPE_EXCEPTION;
@handleExceptionMessaging($hdr, $t->getMessage(), $foo, true);
return false;
}
}
return true;
}
/**
* add() -- public method
*
* this is method which adds new information (data) to memcache.
*
* input parameters are a well-formed key, the data-payload to cache (pre-compressed(json)) and an optional
* expire parameter (in seconds) to keep the data in-cache.
*
* if the add fails and we're using Memcached, then the method will attempt to replace the data, assuming that
* the fail-return-code was one that indicated that the data already existed. (The add method will fail if the
* object key already exists.)
*
* the method returns the cache-key on success and a null on anything else.
*
*
* @author mike@givingassistant.org
* @version 1.0
*
* @param string $_key - the key should be the class name, underscore character, the the idHash key of the record
* @param string $_value - the value is a compressed(json) string object
* @param int $_expires - time to expire from cache in seconds
* @return null | string - indicates successful addition to cache
*
*
* HISTORY:
* --------
* 06-09-17 mks original coding
* 07-24-17 mks refactored to return either the cache-key or a null
*
*/
public static function add(string $_key = null, string $_value = null, int $_expires = NUMBER_CACHE_DEFAULT): ?string
{
if (!isset($_expires)) {
$_expires = static::$config[CONFIG_CACHE_EXPIRES];
}
if (empty($_key)) {
$msg = ERROR_PARAM_404 . UDASH . STRING_KEY;
static::$errors[] = $msg;
static::$logger->error($msg);
return(null);
}
if (empty($_value)) {
$msg = ERROR_PARAM_404 . UDASH . STRING_VALUE;
static::$errors[] = $msg;
static::$logger->error($msg);
return(null);
}
/** @var $mc Memcached() */
$mc = static::$mco;
// add a checksum to the payload if it's not a system key
static::addChecksum($_value);
$key = static::makeKey($_key);
$mc->set($key, $_value, $_expires);
static::$resultCode = $mc->getResultCode();
static::$resultMessage = $mc->getResultMessage();
if (static::$resultCode != 47 and static::$resultCode != 0) {
static::$logger->warn(ERROR_STUB_SET_MEMCACHED . static::$resultCode);
static::$logger->warn(ERROR_STUB_RESULT_CODE . static::$resultMessage);
} else {
static::$resultCode = 0;
}
return((static::$resultCode) ? null : $key);
}
/**
* sysAdd() -- public static method
*
* this method should only be called for use in system functions. the function requires two parameters:
*
* $_key: the key string (should be a guid)
* $_value: the guid string
*
* We're not validating or processing the inputs, just adding them to cache with a 0-expiry.
*
* @author mike@givingassistant.org
* @version 1.0
*
* @param string $_key
* @param $_value -- can be varying types (string, array, etc.)
* @return bool
*
* HISTORY:
* ========
* 08-21-17 mks CORE-500: original coding
* 12-06-17 mks CORE-591: removed secondary reference to cache resource object and trace message
* changed param-2 to type mixed from string
*
*/
public static function sysAdd(string $_key, $_value): bool
{
static::$mco->set($_key, $_value, 0);
static::$resultCode = static::$mco->getResultCode();
static::$resultMessage = static::$mco->getResultMessage();
if (static::$resultCode != 47 and static::$resultCode != 0) {
static::$logger->warn(ERROR_STUB_SET_MEMCACHED . static::$resultCode);
static::$logger->warn(ERROR_STUB_RESULT_CODE . static::$resultMessage);
return(false);
} else {
return(true);
}
}
/**
* sysGet() -- public static function
*
* this is another system-level function that's meant to be used sparingly for namaste/system-level storage.
* As such, there's not validation or processing on this, or the other system, function. We assume you know
* what you're doing and will handle errors appropriately.
*
*
* @author mike@givingassistant.org
* @version 1.0
*
* @param $_key
* @return string | array | null
*
* HISTORY:
* ========
* 08-21-17 mks CORE-500: original coding
* 12-06-17 mks CORE-591: removed secondary reference to resource object and trace message
* PHP7 function typedef headers
*
*/
public static function sysGet(string $_key)
{
return(static::$mco->get($_key));
}
/**
* sysDel() -- public static method
*
* the last of the system-level cache handlers, this function deletes a named key from cache. As a system
* function, we're not validating or processing input nor are we interested in the return code. Either it
* deletes the key, or it does not.
*
*
* @author mike@givingassistant.org
* @version 1.0
*
* @param $_key
*
* HISTORY:
* ========
* 08-21-17 mks CORE-500: original coding
* 12-06-17 mks CORE-591: removed secondary reference to resource object and trace message
* PHP7 function typedef headers
*
*/
public static function sysDel(string $_key): void
{
static::$mco->delete($_key);
}
/**
* addCheckSum() -- private static method
*
* this method will calculate the md5-checksum of a cache-payload ($_value) and will then
* decode and store the checksum internally to the payload under the index "checksum".
*
* the input parameter is the array payload - in gzip'd, json-encoded format. Once decoded,
* if the $_value is an array, then store the checksum as a new indexed-array value and re-assemble
* the payload.
*
* since the input parameter is passed by reference, the new payload is implicitly returned.
*
* todo return a success/fail code
*
* @author mike@givingassistant.org
* @version 1.0
*
* @param $_value
*
* HISTORY:
* ========
* 06-09-17 mks original coding
*
*/
private static function addChecksum(string &$_value)
{
$aryData = json_decode(gzuncompress($_value), true);
if (is_array($aryData)) {
$checksum = md5($_value);
$aryData[STRING_CHECKSUM] = $checksum;
$_value = gzcompress(json_encode($aryData));
}
}
/**
* validateChecksum() -- public static method
*
* this method requires three input parameters described as follows:
*
* $_data -- this is the unpacked data array presumably just fetched from cache
* $_checksum -- this is the checksum value that was previously extracted from the $_data
* $_errors -- array container implicitly returned with diagnostics
*
* This method is meant to take an array payload that was just fetched from cache, was unpacked and the checksum
* extracted (as evidenced by the input parameters) and passed to this method so that we can validate the calculated
* checksum.
*
* We're going to see if the array still contains the checksum index in case the client forgot to remove it and,
* if so, we'll do a validation check to ensure we're working with the correct array. If we are, then remove
* the checksum from the array container.
*
* Next calculate the checksum on the dynamically-repacked array and compare that value against the passed
* checksum parameter to ensure equivalency and generate the appropriate response.
*
* The return value, the boolean, indicates if the checksum correctly matched (true) -- anything else will
* return a boolean(false) value to the calling client.
*
*
* @author mike@givingassistant.org
* @version 1.0
*
* @param array $_data
* @param string $_checksum
* @param array $_errors
* @return bool
*
* HISTORY:
* ========
* 09-20-17 mks CORE-562: original coding
*
*/
public static function validateChecksum(array $_data, string $_checksum, array &$_errors): bool
{
if (array_key_exists(STRING_CHECKSUM, $_data) and $_data[STRING_CHECKSUM] != $_checksum) {
$_errors[] = ERROR_CACHE_CKSUM_DATA;
if (static::$debug) static::$logger->debug(ERROR_CACHE_CKSUM_DATA);
return(false);
} elseif (array_key_exists(STRING_CHECKSUM, $_data)) {
unset($_data[STRING_CHECKSUM]);
}
$cs = md5(gzcompress(json_encode($_data)));
if ($cs !== $_checksum) {
$msg = sprintf(ERROR_CACHE_CKSUM_MISMATCH, $_checksum, $cs);
$_errors[] = $msg;
if (static::$debug) static::$logger->debug($msg);
return(false);
}
return(true);
}
/**
* get() -- public method
*
* this is the method responsible for obtaining information from memcache based on the provided key.
*
* method will either return the data stored in-cache, or a boolean false indicating that the data was not found.
*
* this method should be checked before most database reads where the key in already known.
*
* @author mike@givingassistant.org
* @version 1.0
*
* @param string $_key
* @return null|string
*
* HISTORY:
* --------
* 06-09-17 mks original coding
* 07-31-17 mks refactored for explicit data typing
* 08-07-17 mks CORE-497: return explicit null if get() fails
*
*/
public static function get(string $_key = null): ?string
{
$data = null;
if (static::$mco) {
if (empty($_key)) {
$msg = ERROR_PARAM_404 . UDASH . STRING_KEY;
static::$logger->error($msg);
static::$errors[] = $msg;
return(null);
}
if (static::$debug) {
static::$logger->debug('Memcache: Get(' . static::makeKey($_key) . ')');
}
try {
$key = static::makeKey($_key);
$data = static::$mco->get($key);
} catch (TypeError $t) {
$msg = ERROR_TYPE_EXCEPTION . COLON . $t->getMessage();
static::$errors[] = $msg;
if (isset(static::$logger) and static::$logger->available)
static::$logger->error($msg);
else
consoleLog(static::$res, CON_ERROR, $msg);
}
return( (empty($data) ? null : $data) );
}
static::$errors[] = ERROR_CACHE_RESOURCE_404;
static::$logger->warn(ERROR_CACHE_RESOURCE_404);
return(null);
}
/**
* isCached() -- static public method
*
* This method checks to see if a record has been cached by it's guid value -- which is the first input parameter
* to the method. The second is a call-by-reference array that will allow us to return diagnostic messages to the
* calling client.
*
* The method validates the $_key as a guid and then attempts to get the record from cache. If successful, we
* return the cache-key back to the client. Otherwise a null is returned.
*
*
* @author mike@givingassistant.org
* @version 1.0
*
* @param string $_key
* @param array $_errors
* @return null|string
*
* HISTORY:
* ========
* 09-28-17 mks CORE-572: original coding
*
*/
public static function isCached(string $_key, array &$_errors): ?string
{
if (static::$mco) {
if (!validateGUID($_key)) {
$msg = ERROR_INVALID_GUID . $_key;
static::$logger->data($msg);
$_errors[] = $msg;
return null;
}
return (!is_null(static::get($_key)) ? static::makeKey($_key) : null);
}
$_errors[] = ERROR_CACHE_RESOURCE_404;
static::$logger->warn(ERROR_CACHE_RESOURCE_404);
return null;
}
/**
* getByKey() -- public method
*
* in some cases, we know all of the key and there's no need to build a key dynamically - this method provides
* fetch functionality without building a key by assuming that the calling stub passed in the entire key to
* the method.
*
* returns an error if no key was received or if we've lost the connection to the memcache instance.
*
* note that the returned data (if found in-cache) is returned unmodified from it's cached state.
*
* @author mike@givingassistant.org
* @version 1.0
* @noinspection PhpUnused
*
* @param $_key - string containing the cache-key value and can be of any length
* @return bool - returns boolean false on error or a data structure of cached data
*
* HISTORY:
* --------
* 06-09-17 mks initial coding
*
*/
public static function getByKey($_key)
{
if (static::$mco) {
if (empty($_key)) {
$msg = ERROR_PARAM_404. UDASH . STRING_KEY;
static::$logger->error($msg);
static::$errors[] = $msg;
return(false);
}
return(static::$mco->get($_key));
}
static::$errors[] = ERROR_CACHE_RESOURCE_404;
static::$logger->warn(ERROR_CACHE_RESOURCE_404);
return(false);
}
/**
* getAllKeys() -- public static method
*
* helper method that returns all keys currently stored in-cache
*
* @author mike@givingassistant.org
* @version 1.0
*
* @return array
*
* HISTORY:
* --------
* 06-09-17 mks initial coding
*
*/
public static function getAllKeys():array
{
return(static::$mco->getAllKeys());
}
/**
* delete() -- public method
*
* method that removes (permanently) an item (accessed by the KEY), from memcache.
*
* if the object exists in-cache by key, then it will be removed from cache and, if this happens, the
* method will return a boolean(true)...otherwise, a boolean(false) should be returned.
*
* @author mike@givingassistant.org
* @version 1.0
*
* @param $_key
* @param int $_expires
* @param bool $_makeKey - decides whether or not to prepare the key before deleting the cache value
* @return bool
*
* HISTORY:
* --------
* 06-09-17 mks original coding
*
*/
public static function delete($_key, $_expires = 0, $_makeKey = true)
{
if (empty($_key)) {
$msg = ERROR_PARAM_404 . UDASH . STRING_KEY;
static::$errors[] = $msg;
static::$logger->error($msg);
return(false);
}
if ($_makeKey) {
$key = static::makeKey($_key);
return(static::$mco->delete($key, $_expires));
} else {
return(static::$mco->delete($_key, $_expires));
}
}
/**
* flushTokens() -- public static method
*
* This method is used to remove a list of cached records based on their cache-key/GUID value.
*
* The input parameter to the method is a list of records, each record being a k->v paired associative array
* containing the field name: token{_ext} and referencing a record's GUID value.
*
* The method will spin through the arrays and will make a call to the static::delete() method in this class
* for every token value found.
*
* Data validation is minimal -- any validation error will silently return a boolean-false value to the calling
* client without notification as to why the request failed.
*
* If the token list is successfully processed, a boolean true is returned which does not infer that any cached
* records were actually deleted (they may have all expired if they existed) but only that processing of the
* list completed successfully.
*
*
* @author mike@givingassistant.org
* @version 1.0
*
* @param array $_list
* @return bool
* @noinspection PhpUnused
*
* HISTORY:
* ========
* 10-25-17 mks CORE-587: original coding
*
*/
public static function flushTokens(array $_list): bool
{
if (!is_array($_list)) return false;
// process the list of tokens
foreach ($_list as $record) {
if (count($record) > 1) return false;
foreach ($record as $key => $token) {
try {
@static::delete($token);
} catch (TypeError $t) {
$msg = ERROR_TYPE_EXCEPTION . COLON . $t->getMessage();
static::$errors[] = $msg;
if (isset(static::$logger) and static::$logger->available)
static::$logger->error($msg);
else
consoleLog(static::$res, CON_ERROR, $msg);
return false;
}
}
}
return true;
}
/**
* makeKey() --- private static method
*
* method makes a key from the configuration and the passed key-part.
*
* this is a function so that we have only one point in the code where we're generating keys.
*
* @author mike@givingassistant.org
* @version 1.0
*
* @param string $_part
* @return string
*
*
* HISTORY:
* --------
* 06-09-17 mks original coding
*
*/
private static function makeKey(string $_part):string
{
$preKey = gasConfig::$settings[CONFIG_APPLICATION_ID][CONFIG_ID_ENV] . UDASH;
// $pos = strpos($_part, $preKey);
$key = (strpos($_part, $preKey) === false) ? $preKey . $_part : $_part;
if (static::$debug) {
static::$logger->debug(STRING_KEY . COLON . $key);
}
return($key);
}
/**
* buildMappedDataArray() -- private method
*
* this method recursively builds a cacheMap array. (schema generates cacheMap)
*
* the method accepts (requires) three input parameters:
*
* $_data -- the array of key-value paired associative array containing the tuple data
* $_map -- the key-value paired associative array containing the classes' cache-map build in instantiation
* $_ext -- the class extension
*
* spin through each row of $_data array as a set of key->values...
* for every key found in the map index,
* assign the map index reference value (map[$key]) to the tmpArray variable and assign the data[key] (value).
* ensure a token exists and side-save it...
*
* if any data was successfully mapped, then build the return array and overwrite $_data with this structure.
*
* otherwise, for all other cases, a null value is returned and the $_data remains unchanged.
*
* @author mike@givingassistant.org
* @version 1.0
*
* @param $_data -- call by reference parameter - returns array of cached-mapped data
* @param $_map -- the cache map
* @param $_ext -- the class extension
* @return array|null
*
* HISTORY:
* ========
* 07-14-17 mks original coding
* 01-04-21 mks DB-180: recycled from deprecated, updated for CONS processing where we're receiving a non-
* cache-mapped payload, this sub checks the cacheMap container values instead of the
* keys, for matching input data fields - also updated exception handling
*
*/
public static function buildMappedDataArray(array &$_data, array $_map, string $_ext): ?array
{
$mappedData = null;
$keyList = null;
$tmpArray = null;
if (is_array($_data) and array_key_exists(0, $_data)) {
for ($index = 0, $last = count($_data); $index < $last; $index++) {
if (is_array($_data[$index])) {
foreach ($_data[$index] as $key => $value) {
// if we encounter a nexted sub-collection, recursively call self to process
if (is_array($value) and is_numeric(key($value))) {
try {
$tmpArray = static::buildMappedDataArray($value, $_map, $_ext);
} catch (Throwable | TypeError $t) {
$hdr = sprintf(INFO_LOC, basename(__METHOD__), __LINE__);
@handleExceptionMessaging($hdr, $t->getMessage(), $foo, true);
return null;
}
}
// convert to cacheMap keys
if (false === ($newKey = array_search($key, $_map))) {
static::$logger->data(ERROR_KEY_404 . $value);
} else {
if (is_array($tmpArray)) {
$mappedData[$index][$newKey] = $tmpArray;
$tmpArray = null;
} else {
$mappedData[$index][$newKey] = $value;
}
}
}
}
}
} elseif (is_array($_data)) {
foreach ($_data as $k => $v) {
if (array_key_exists($k, $_map)) {
$mappedData[$_map[$k]] = $v;
} elseif (array_key_exists(($k . $_ext), $_map)) {
$mappedData[$_map[($k . $_ext)]] = $v;
}
}
}
return($mappedData ?? null);
}
/**
* validateCacheKey() -- public static method
*
* this method validates a Namaste cache key which is in the format of:
*
* {currentEnvironment} + "_" + {guid}
*
* We validate the cache key by pulling the current environment value from the global configuration and seeing if
* that string exists within the key, which was passed as the input parameter. If the env string was not found
* at the beginning of the cache-key string, immediately return a false.
*
* Otherwise, invoke the general function: validateGUID by passing in the cacheKey with the env stripped out and
* pass the return from that method onto the calling client.
*
* Note: this does NOT validate system cache keys!
*
* @author mike@givingassistant.org
* @version 1.0
*
* @param string $_key
* @return bool|int
*
* HISTORY:
* ========
* 08-29-17 mks CORE-494: original coding (needed for unit testing)
*
*/
public static function validateCacheKey(string $_key)
{
$env = gasConfig::$settings[CONFIG_ID][CONFIG_ID_ENV] . UDASH;
return ((strpos($_key, $env) !== 0) ? false : validateGUID(ltrim($_key, $env)));
}
/**
* singleton() -- public method
*
* method to instantiate the singleton class (called externally)
*
* developer notes:
* ----------------
* it's the responsibility of the invoking stub to check for a non-null value in the class variable $instance.
* if you don't do this: bad things, man -- and it will all be on you.
*
* @author mike@givingassistant.org
* @version 1.0
*
* HISTORY:
* --------
* 06-09-17 mks initial coding
*
* @return mixed
*/
public static function singleton():self
{
if (static::$instance === null) {
$c = __CLASS__;
static::$instance = new $c();
}
return(static::$instance);
}
/**
* dumpErrors() -- public static method
*
* method to access the aggregated error messages within the class and return the errors as a single text block.
*
* @author mike@givingassistant.org
* @version 1.0
*
* @return string
*
* HISTORY:
* --------
* 03-29-13 mks original coding
*
*/
public static function dumpErrors():string
{
global $eos;
$errorBlock = '';
if (count(static::$errors)) {
foreach (static::$errors as $key) {
$errorBlock .= $key . $eos;
}
}
return($errorBlock);
}
/**
* errorReset() -- public static method
*
* resets the error stack - requires explicit call
*
* input param (_safe) will first dump the existing error catalog to the log file before clearing the stack.
* (default for this is off, so must be explicitly invoked.)
* if _safe is invoked, then default will always (and should) be console dump.
*
* @author mike@givingassistant.org
* @version 1.0
* @noinspection PhpUnused
*
* @param bool $_safe
*
* HISTORY:
* --------
* 03-29-13 mks original coding
*
*/
public static function errorReset(bool $_safe = false): void
{
if ($_safe) {
$errorDump = static::dumpErrors();
$errorDump = (is_null($errorDump)) ? SUCCESS_NO_ERRORS_FOUND : $errorDump;
static::$logger->debug($errorDump);
}
static::$errors = null;
}
/**
* get_stats() -- public static method
*
* this method returns the stats of the memcache server to the calling client.
* pid Process id of this server process
* uptime Number of seconds this server has been running
* time Current UNIX time according to the server
* version Version string of this server
* rusage_user Accumulated user time for this process
* rusage_system Accumulated system time for this process
* curr_items Current number of items stored by the server
* total_items Total number of items stored by this server ever since it started
* bytes Current number of bytes used by this server to store items
* curr_connections Number of open connections
* total_connections Total number of connections opened since the server started running
* connection_structures Number of connection structures allocated by the server
* cmd_get Cumulative number of retrieval requests
* cmd_set Cumulative number of storage requests
* get_hits Number of keys that have been requested and found present
* get_misses Number of items that have been requested and not found
* bytes_read Total number of bytes read by this server from network
* bytes_written Total number of bytes sent by this server to network
* limit_maxbytes Number of bytes this server is allowed to use for storage.
*
* @author mike@givingassistant.org
* @version 1.0
*
* HISTORY:
* --------
* 06-09-17 mks original coding
*
*/
public static function get_stats(): ?array
{
return(static::$mco->getStats());
}
/**
* flush() -- public method
*
* this method invokes the memcache flush() which removes all existing items from cache.
*
* please don't call this method unless you absolutely need to nuke all of the data.
*
* @author mike@givingassistant.org
* @version 1.0
*
* HISTORY:
* ========
* 06-09-17 mks original coding
*
*/
public static function flush_cache(): bool
{
return(static::$mco->flush());
}
/**
* smashCache() -- public static method
*
* This method is for mass-removing cached items. There are two input parameters to this method:
*
* $_listGUIDs - this is an indexed list of the cacheKeys or GUIDs that will be removed from cache
* $_errors - this is a call-by-reference container for error messages raised during processing
*
* We begin by setting the environment pre-key.
*
* Next, we check to see if the input array is associative - if not, we'll generate error messages, populate
* the $errors container and return a boolean(false) value back to the calling client.
*
* Next, we'll spin through all the keys in the list and check to see if we need to prepend the env onto the key
* creating a keyList in the process.
*
* Once that's completed, we'll pass the newly-created keyList to the memcached method deleteMulti().
*
* What's returned is an array of cacheKeys (keys) and values indicating the delete success or failure. For now,
* we really don't care about these values -- so we'll squash the return-payload and just return a Boolean(true).
*
* Programmer's Notes:
* -------------------
* The deleteMulti() method is incorrectly cast to return a Boolean value which is why the method (doc) reports
* three different return types and there's no return type specified in the function header
*
*
* @author mike@givingassistant.org
* @version 1.0
*
* @param array $_listGUIDs
* @param array $_errors
* @return bool
*
* HISTORY:
* ========
* 04-11-19 mks DB-116: original coding
*
*/
public static function smashCache(array $_listGUIDs, array &$_errors): bool
{
$preKey = gasConfig::$settings[CONFIG_APPLICATION_ID][CONFIG_ID_ENV] . UDASH;
if (array_key_first($_listGUIDs) !== 0) {
$hdr = basename(__METHOD__) . AT . __LINE__ . COLON;
$msg = ERROR_DATA_ARRAY_NOT_IDX . STRING_RECORD_GUIDS;
static::$logger->data($hdr . $msg);
$_errors[] = $msg;
return false;
}
$newList = null;
// if the keys are not prepped with the environment prepended, do so
foreach ($_listGUIDs as &$record)
$newList[] = (strpos($record, $preKey) === false) ? gasCache::makeKey($record) : $record;
// deleteMulti() is cast as returning a bool (even though it doesn't) so we'll do this in two lines
@static::$mco->deleteMulti($newList);
return true;
}
/**
* cacheMash() -- static public method
*
* This method was written to support the ability to cache an entire array of items in a single command instead
* of onsies. There are three input parameters required:
*
* $_data -- this should be the processed $data content
* $_expires -- This is an optional value that otherwise defaults to the system limit (in seconds)
* $_errors -- This is a call-by-reference used as an error container
*
* When we're caching records, we'll cache an amount up the limit set in the XML config (queryRecordLimit) - if the
* amount of records to be cached is less than this, then we'll simply cache a single payload of the records and
* return the guid key back to the calling client. If the record count exceeds the max limit, then we'll cache
* payload bundles in the amount of queryRecordLimit and we'll return a list of keys back to the calling client.
*
* We will return an array on successful cache even if only one record was cached.
*
* If an error was raised during processing, we return a null value. The following can cause a null return:
*
* -- if the input array is empty
* -- if the first key of the $_data array does not pass the guid-validation test
* -- if the setMulti() memcached call fails
*
*
* @author mike@givingassistant.org
* @version 1.0
*
* @param array $_data
* @param int $_expires
* @param array $_errors
* @return null|array
*
* HISTORY:
* ========
* 04-11-19 mks original coding
*
*/
public static function cacheMash(array $_data, int $_expires = NUMBER_CACHE_DEFAULT, array &$_errors = []): ?array
{
if (empty($_data) or !is_array($_data)) {
$hdr = basename(__METHOD__) . AT . __LINE__ . COLON;
$msg = ERROR_PARAM_404 . UDASH . STRING_VALUE;
static::$errors[] = $msg;
static::$logger->error($hdr . $msg);
return null;
}
$newData = null;
$keyList = null;
// get the record limit for splitting the payload
$recLimit = intval(gasConfig::$settings[CONFIG_DATABASE][CONFIG_DATABASE_QUERY_RECORD_LIMIT]);
if (count($_data <= $recLimit)) {
$keyList[] = static::makeKey(guid());
// $storedData = json_encode();
}
foreach ($_data as $record) {
foreach ($record as $key => $value) {
$newKey = static::makeKey($key);
$newData[$newKey] = $value;
$keyList[] = $key;
}
}
if (!static::$mco->setMulti($newData, $_expires)) {
$hdr = basename(__METHOD__) . AT . __LINE__ . COLON;
$msg = ERROR_CACHE_MASH_FAIL;
static::$logger->data($hdr . $msg);
$_errors[] = $msg;
return null;
}
return $keyList;
}
/**
* getCacheMap() -- public static method
*
* This method has the following input parameters:
*
* $_template - a string containing the template as presented in the brokers' meta-data payload
* $_tlti - a string containing the TLTI value passed in the brokers' meta-data payload
*
* This method was culled from functions.php when it became dual-purpose (called by tercero now, too) and
* was broken out so that it's accessible from multiple points.
*
* The method calls the gasStatic method to fetch the current template as defined by $_template (parameter)
* to see if one exists - if not, then we'll attempt to instantiate using the default TLTI value.
*
*
* @author mike@givingassistant.org
* @version 1.0
*
* @param string $_template
* @param string $_tlti
* @return array|null
*
*
* HISTORY:
* ========
* 09-21-20 mks DB-168: original coding
*
*/
public static function getCacheMap(string $_template, string &$_tlti = STRING_CLASS_GAT):?array
{
$vector = gasCache::fetchTemplateCacheMap(($_tlti . $_template));
if (is_null($vector) and $_tlti != STRING_CLASS_GAT) {
// did not fetch template based on partner tlti - see if this is a GA class instead...
$vector = gasCache::fetchTemplateCacheMap((STRING_CLASS_GAT . $_template));
$_tlti = STRING_CLASS_GAT;
}
return $vector;
}
/**
* deleteSlices() -- private static method
*
* This method is in support of mapOutboundPayload() -- where we're slicing the data payload into chunks and
* caching the chunk (collection of $queryRecordLimit) under a single key. This method spins through the array
* of previously-cached keys and calls the local cache-delete method to remove the slice.
*
* The method takes a single payload - this is the list of previously-cached keys.
*
* The method returns void.
*
*
* @author mike@givingassistant.org
* @version 1.0
*
* @param array $_keys
*
*
* HISTORY:
* ========
* 04-15-19 mks DB-116: original coding
*
*/
private static function deleteSlices(array $_keys): void
{
foreach ($_keys as $key)
static::delete($key);
}
/**
* loadCacheMaps() -- static private function
*
* This function is invoked from the constructor and it's intent is to load all of the template files, extract
* certain fields, and create a memcache db (grid), keyed off the template name, containing the following
* information about the template file:
*
* 1. SCHEMA -- defines the schema for the current template (mongoDB, mySQL, etc.)
* 2. SERVICE -- defines which service the data class can be instantiated on (namaste, segundo, admin, etc.)
* 3. IS_CACHED -- boolean indicating if the current class supports cache-mapping
* 4. CACHE_TIMER -- integer value indicating the amount of time the data class will be cached for
* 5. CACHE_MAP -- key-value associative array containing the cache map (public -> private)
* 6. CLOSED_CLASS -- indicates if a GA class can be instantiated by a Partner
*
* If any template files fails loading, generate both an log and console error message.
*
* Once we've spun through the list of templates, cache the data-grid into memcache by calling the
* memcached->set() function directly and store the entire mess under the key STRING_CACHE_MAP. Clever, right?
* The expiry for this array is 0 meaning that it won't expire.
*
* The method returns a boolean value indicating successful completion of the method.
*
* NOTES:
* ------
* All templates are cached. Whether or not cache has been enabled for a particular class will be resolved
* during cacheMap processing at the broker in/out level.
*
* Since PHP doesn't really have true static classes, it's ok for this to continuously refresh the memcached
* object ensuring we always have the latest templates stored and available. (Yes. Totally lame. I know.)
*
*
* @author mike@givingassistant.org
* @version 1.0
*
* @return bool
*
*
* HISTORY:
* ========
* 02-12-19 mks DB-116: original coding
* 07-27-20 mks DB-156: support for $closedClass template member setting
*
*/
private static function loadCacheMaps(): bool
{
$cacheMap = [];
try {
$templateList = gasStatic::loadValidTemplateNames();
// todo: delete next line
consoleLog(static::$res, CON_DEBUG, 'templateCount: ' . count($templateList));
/** @var gatProdRegistrations $obj */
foreach ($templateList as $template) {
/** @var gatTestMongo $obj */
$obj = new $template();
if (is_object($obj)) {
$cacheMap[$template][STRING_SCHEMA] = (isset($obj->schema)) ? $obj->schema : STRING_NOT_DEFINED;
$cacheMap[$template][STRING_SERVICE] = (isset($obj->service)) ? $obj->service : STRING_NOT_DEFINED;
$cacheMap[$template][STRING_IS_CACHED] = (isset($obj->setCache)) ? $obj->setCache : STRING_NOT_DEFINED;
$cacheMap[$template][CACHE_TIMER] = (isset($obj->cacheTimer)) ? $obj->cacheTimer : STRING_NOT_DEFINED;
$cacheMap[$template][CACHE_EXT] = (isset($obj->extension)) ? $obj->extension : STRING_NOT_DEFINED;
$cacheMap[$template][CACHE_SUBC] = (isset($obj->subC) and !empty($obj->subC)) ? $obj->subC : STRING_NOT_DEFINED;
$cacheMap[$template][CACHE_MAP] = (isset($obj->cacheMap)) ? $obj->cacheMap : STRING_NOT_DEFINED;
$cacheMap[$template][CACHE_CLOSED] = (isset($obj->closedClass)) ? $obj->closedClass : STRING_NOT_DEFINED;
if (is_object($obj)) $obj->__destruct();
unset($obj);
} else {
$msg = ERROR_TEMPLATE_INSTANTIATE . $template;
static::$logger->warn($msg);
consoleLog(static::$res, CON_ERROR, $msg);
}
}
if (!empty($cacheMap)) {
$mc = static::$mco;
// add a checksum to the payload if it's not a system key
if (true === $mc->set(CACHE_MAP, json_encode($cacheMap), 0)) {
return true;
}
}
static::$logger->warn(FAIL_CACHE_MAP_CACHE);
consoleLog(static::$res, CON_ERROR, FAIL_CACHE_MAP_CACHE);
return false;
} catch (Throwable | TypeError $t) {
$hdr = sprintf(INFO_LOC, basename(__METHOD__), __LINE__);
@handleExceptionMessaging($hdr, $t->getMessage(), $foo, true);
return false;
}
}
/**
* cacheMapAssociativeArray() -- private static method
*
* The purpose of this method is to convert an incoming associative array, where the keys at the top level of
* the array, are cacheMap key names, over to database schema. This function is used to processing incoming
* data payloads only.
*
* This method requires the following input parameters:
*
* $_data -- this is the incoming data payload to be mapped. This should be limited to the following named
* data payload containers: STRING_QUERY_DATA, STRING_SORT_DATA, and STRING_UPDATE_DATA.
* $_map -- this is an array containing the entire cacheMap catalog for the class named in the event meta data.
* $_errs -- a call-by-reference array container to store any processing errors raised in the method.
*
* We wrap processing in a TypeError exception handler because we're calling two API methods in processing. If
* a type error is raised, execution will conclude immediately and a null value will be returned to the calling
* client.
*
* If processing is successful, we return the replacement data array back to the calling client.
*
* NOTE:
* -----
* This is the second-version of the cache-replacement function. The first version was based on the container
* key values -- for the second version, the processing is based on the array type and not the content.
*
* @author mike@givingassistant.org
* @version 1.0
*
* @param array $_data
* @param array $_map
* @param array $_errs
* @return array|null
*
*
* HISTORY:
* ========
* 03-12-19 mks DB-116: original coding completed (second version)
*
*/
private static function cacheMapAssociativeArray(array $_data, array $_map, array &$_errs): ?array
{
$data = null;
$validOperands = [ OPERAND_AND, OPERAND_OR, OPERAND_NOT, OPERAND_NOR ];
try {
if (array_key_first($_data) === 0) {
// process an array of associative records
// for ($index = 0, $max = count($_data); $index < $max; $index++) {}
foreach ($_data as $record) {
foreach ($record as $key => $value) {
if (array_key_exists($key, $_map[CACHE_MAP])) {
$data[($key . $_map[CACHE_EXT])] = $value;
} elseif (in_array($key, $_map[CACHE_MAP])) {
$data[array_search($key, $_map[CACHE_MAP]) . $_map[CACHE_EXT]] = $value;
} else {
// key not in cache-map so we'll drop it
$hdr = basename(__METHOD__) . AT . __LINE__ . COLON;
$msg = sprintf(ERROR_CACHE_MAP_KEY_404, $key);
$_errs[] = $msg;
static::$logger->data($hdr . $msg);
}
}
}
} else {
foreach ($_data as $label => $content) {
if (array_key_exists($label, $_map[CACHE_MAP])) {
$data[$label . $_map[CACHE_EXT]] = $content; // does nothing b/c data is schema
} elseif (!in_array($label, $validOperands) and in_array($label, $_map[CACHE_MAP])) {
$data[array_search($label, $_map[CACHE_MAP]) . $_map[CACHE_EXT]] = $content;
} elseif (in_array($label, $validOperands)) {
$data[$label] = $content;
} else {
$hdr = basename(__METHOD__) . AT . __LINE__ . COLON;
$msg = sprintf(ERROR_CACHE_MAP_KEY_404, $label);
$_errs[] = $msg;
static::$logger->data($hdr . $msg);
}
}
}
} catch (TypeError $t) {
$hdr = basename(__METHOD__) . AT . __LINE__ . COLON;
$msg = ERROR_TYPE_EXCEPTION;
$_errs[] = $msg;
static::$logger->error($hdr . $msg);
static::$logger->error($hdr . $t->getMessage());
return null;
}
return $data;
}
/**
* cacheMapIndexedArray() -- private static method
*
* The purpose of this method is to convert an indexed array from cacheKeys to schemaKeys (aka cacheMapping).
* This method is used for the BROKER_REQUEST_CREATE payload as well STRING_RETURN_PAYLOAD containers. Note that
* this method is only used to processing incoming request data. Additionally, this method can process subC
* payloads (mongoDB), arrays and objects embedded within the data payload.
*
* The method has the following required input parameters:
*
* $_data -- this is the indexed array of payload data to be processed.
* $_map -- this is the cacheMap vector for the declared class.
* $_isRD -- this is a boolean value indicating is this is a STRING_RETURN_DATA payload which invokes easier
* (less-complicated) processing.
* $_errs -- call-by-reference array container for any errors raised during processing
*
* If successful, the method returns an array that's been converted from cacheKeys to schemaKeys. Otherwise,
* a null value is returned. Errors returns to the client are obtuse compared to the errors logged.
*
*
* @author mike@givingassistant.org
* @version 1.0
*
* @param array $_data
* @param array $_map
* @param bool $_isRD
* @param array $_errs
* @return array|null
*
*
* HISTORY:
* ========
* 03-13-19 mks DB-116: original coding completed
* 06-11-19 mks DB-122: fixed error in outbound object processing
* 06-18-19 mks DB-122: suppressed warning about unknown field in $_map
*
*/
private static function cacheMapIndexedArray(array $_data, array $_map, bool $_isRD, array &$_errs): ?array
{
$data = null;
try {
if ($_isRD) {
// this is STRING_RETURN_DATA processing:
// we're only mapping the values in the array and there is no recursive processing
for ($i = 0, $max = count($_data); $i < $max; $i++) {
if (in_array($_data[$i], $_map[CACHE_MAP])) {
$data[$i] = array_search($_data[$i], $_map[CACHE_MAP]) . $_map[CACHE_EXT];
} elseif (in_array(rtrim($_data[$i] . $_map[CACHE_EXT]), $_map[CACHE_MAP])) {
$data[$i] = array_search((rtrim($_data[$i], $_map[CACHE_EXT])), $_map[CACHE_MAP]);
}
}
} else {
// this is payload data -- it puts the extension in the basket...er, after the column name
foreach ($_data as $record) {
$rec = null;
foreach ($record as $column => $value) {
if (in_array($column, $_map[CACHE_MAP])) {
// this is inbound data because $column matches the cacheMap key, not the field
$schemaKey = array_search($column, $_map[CACHE_MAP]); // grab the schema key
if (array_key_exists(CACHE_SUBC, $_map) and is_array($_map[CACHE_SUBC]) and array_key_exists($schemaKey, $_map[CACHE_SUBC])) {
foreach ($value as $subCRecord) {
$subCRec = null;
if (is_array($subCRecord)) {
foreach ($subCRecord as $subCField => $subCValue) {
if (in_array($subCField, $_map[CACHE_MAP])) {
$subCRec[array_search($subCField, $_map[CACHE_MAP]) . $_map[CACHE_EXT]] = $subCValue;
} else {
$hdr = basename(__METHOD__) . AT . __LINE__ . COLON;
$msg = sprintf(ERROR_CACHE_MAP_KEY_404, $subCField);
$_errs[] = $msg;
static::$logger->data($hdr . $msg);
}
}
} else {
$hdr = sprintf(INFO_LOC, basename(__METHOD__), __LINE__);
$msg = ERROR_SUBC_SCALAR;
$_errs[] = $msg;
static::$logger->data($hdr . $msg);
}
if (!is_null($subCRec) and count($subCRec)) {
$rec[($schemaKey . $_map[CACHE_EXT])][] = $subCRec;
}
}
} elseif (is_array($value)) {
// we have an array field that is not a subCollection
static::cacheMapGrind($value, $_map, $_errs);
$rec[($schemaKey . $_map[CACHE_EXT])] = $value;
} elseif (is_object($value)) {
// we have an object field who's elements should be mapped
$aryData = null;
foreach ($value as $oKey => $oValue) {
if (is_array($oValue)) {
static::cacheMapGrind($oValue, $_map, $_errs);
$aryData[((array_search($oKey, $_map[CACHE_MAP])) . $_map[CACHE_EXT])] = $oValue;
} elseif (in_array($oKey, $_map[CACHE_MAP])) {
$aryData[((array_search($oKey, $_map[CACHE_MAP])) . $_map[CACHE_EXT])] = $oValue;
} else {
$hdr = basename(__METHOD__) . AT . __LINE__ . COLON;
$msg = sprintf(ERROR_CACHE_MAP_KEY_404, $oValue);
$_errs[] = $msg;
static::$logger->data($hdr . $msg);
}
}
if (!is_null($aryData)) {
if (in_array($column, $_map[CACHE_MAP])) {
$rec[array_search($column, $_map[CACHE_MAP]) . $_map[CACHE_EXT]] = (object)$aryData;
} else {
$rec[$column] = (object)$aryData;
}
}
} else {
$rec[($schemaKey . $_map[CACHE_EXT])] = $value;
}
} else {
// we're assuming this is an *OUTBOUND* payload since the key was not found in the cacheMap
$newKey = str_replace($_map[CACHE_EXT], '', $column); // remove class extension
// next, we need to process the outbound key against the cache map while checking for
// sub-collections and arrays -- if these are found, process after the container key
// has been mapped before mapping the container contents.
// Note that the framework does not return objects.
if (array_key_exists($newKey, $_map[CACHE_MAP])) {
$subCKey = $_map[CACHE_MAP][$newKey];
// check for sub-collection data
if (isset($_map[CACHE_SUBC]) and is_array($_map[CACHE_SUBC]) and array_key_exists($newKey, $_map[CACHE_SUBC]) and is_array($value)) {
for ($i = 0, $max = count($value); $i < $max; $i++) {
foreach ($value[$i] as $subColumn => $subValue) {
// strip the extension for the subCollection column
@$newSubKey = $_map[CACHE_MAP][str_replace($_map[CACHE_EXT], '', $subColumn)];
if (!empty($newSubKey))
$rec[$subCKey][$i][$newSubKey] = $subValue;
elseif (in_array($subColumn, $_map[CACHE_MAP]))
// border/edge case -- if data was inserted incorrectly,
// check for already mapped column name:
$rec[$subCKey][$i][$subColumn] = $subValue;
// else
// silently dropped the non-mapped field (no logging, no error message)
}
}
} elseif (is_array($value) or is_object($value)) {
// processing an array field that is not a sub-array (meaning: multiple records)
foreach ($value as $subKey => $subValue) {
$newSubKey = '';
if (array_key_exists(str_replace($_map[CACHE_EXT], '', $subKey), $_map[CACHE_MAP]))
$newSubKey = $_map[CACHE_MAP][str_replace($_map[CACHE_EXT], '', $subKey)];
if (!empty($newSubKey))
$rec[$subCKey][$newSubKey] = $subValue;
elseif (in_array($subKey, $_map[CACHE_MAP]))
$rec[$subCKey][$subKey] = $subValue;
}
} else {
// we have an outbound payload if the column data is schema (matches the key)
$rec[$_map[CACHE_MAP][$newKey]] = $value;
}
} /** @noinspection PhpStatementHasEmptyBodyInspection */ else {
// data is not in the schemaKey or the cacheKey array so it's invalid and will be
// dropped from the return payload -- we're not exposing this information to the user
// because it could reveal schema (e.g.: _id or id fields...) so this branch does
// no processing at the present time.
}
}
}
$data[] = $rec;
}
}
return $data;
} catch (TypeError $t) {
$hdr = basename(__METHOD__) . AT . __LINE__ . COLON;
$msg = ERROR_TYPE_EXCEPTION;
$_errs[] = $msg;
static::$logger->error($hdr . $msg);
static::$logger->error($hdr . $t->getMessage());
return null;
}
}
/**
* cacheMapGrind() -- private static method
*
* This method was written to fix an error (PD-18) in the processing of structures embedded in a collection. For
* objects, and arrays, this recursive processing routine was added allowing Namaste to support structures with
* nested structures. This expands the ability of Namaste to store more complex structures as previously structures
* were limited to a depth of 1.
*
* The method requires three input parameters:
*
* $_data -- call by reference array that is the array of data to be parsed
* $_map -- an array containing the cacheMap for the current data class
* $_errs -- call by reference array with the current error catalog
*
* The function loops through the $_data array and, if it finds a new value, within a key->value pair, that's an
* array, we'll make a recursive call to that the sub-array is validated (fields exist in the cache map).
*
*
* @author mike@givingassistant.org
* @version 1.0
*
* @param array $_data
* @param array $_map
* @param array $_errs
*
*
* HISTORY:
* ========
* 03-26-20 mks PD-18: original coding
*
*/
private static function cacheMapGrind(array &$_data, array $_map, array &$_errs):void
{
foreach ($_data as $k => &$v) {
if (is_int($k)) {
// numeric keys will not be in cache map b/c this is an indexed array
if (is_array($v)) {
// if we're dealing with a nested array, then make a recursive call
static::cacheMapGrind($v, $_map, $_errs);
}
} elseif (in_array($k, $_map)) {
$aryData[(array_search($k, $_map[CACHE_MAP]) . $_map[CACHE_EXT])] = $v;
} else {
$hdr = sprintf(INFO_LOC, basename(__METHOD__), __LINE__);
$msg = sprintf(ERROR_CACHE_MAP_KEY_404, $k);
$_errs[] = $msg;
static::$logger->data($hdr . $msg);
}
}
}
/**
* __clone()
*
* silently disallow the cloning function
*
* @return null
*/
private function __clone()
{
return null;
}
/**
* __destruct()
*
* closes the connection to memcache
*
*/
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.
}
}