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. } }