Files
namaste/classes/gacPDO.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

3880 lines
177 KiB
PHP

<?php
/**
* gacPDO.class -- instantiation class
*
* -- should only be invoked by gacFactory when instantiating a new class
* -- extends gaaNamasteCore
*
* Used for database access to mariaDB (mysql) at time of creation - however, being a PDO based class, we can use
* it for all of the PDO-supported databases. Plus way-better handling of dynamic (run-time) prepared queries.
*
* Note:
* -----
* Resource allocation (connecting to the db) is handled by the gasResourceManager class.
*
*
* @author mike@givingassistant.org
* @version 1.0
*
* HISTORY:
* ========
* 09-13-17 mks CORE-561: original coding begins
* 03-02-18 mks CORE-680: deprecated trace logging
* 03-23-18 mks CORE-852: migration support
* 08-02-18 mks CORE-774: PHP7.2 exception handling
* 01-08-20 mks DB-150: PHP7.4 class member type-casting
*
*/
class gacPDO extends gaaNamasteCore
{
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
/// CLASS PROPERTIES ///
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
private ?object $dbcMain; // destructive database connection/resource
private ?object $dbcSlave; // non-destructive database connection/resource
private string $res; // console log output unique identifier/label
private bool $limitOverride; // used ad-hoc to override the system limit on query returns
private string $env; // defines the current environment
public array $dbObjects; // list of the stored-procedures, views, events, triggers, and functions
private bool $dbMainAvailable; // Boolean - indicates if the DB is currently available or not
private bool $dbSlaveAvailable; // Boolean - indicates if the slave DB is currently available or not
public array $queryVariables; // container of mixed types that correspond to a prepared query's fields
private bool $sidewaysDelete; // alert for an update that started as a delete request b/c soft deletes
protected ?array $aryQuery = null; // PDO uses arrays for queries - this is the container for the query
// exceptions to the query-builder
public ?string $queryOrderBy;
public ?string $queryGroupBy;
public int $queryLimit;
public int $querySkip;
public ?string $queryFields = '';
public ?array $queryHaving = null;
public int $recordsInserted;
public int $recordsDropped;
public string $where;
public string $dbEvent; // container for the current database event being processed
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
/// PUBLIC METHODS BEGIN HERE ///
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
/**
* gacPDO.class constructor.
*
* @author mike@givingassistant.org
* @version 1.0
*
* @param array $_meta
* @param string $_id
*
* HISTORY:
* ========
* 09-13-17 mks CORE-562: original coding
* 11-29-17 mks CORE-593: PDO Slave coding update, validate main and slave resources
* 11-27-18 mks DB-51: code to load a single record if the constructor receives a record guid
* also note that mySQL now only supports GUIDs as primary keys (even if it has an
* auto-incrementing integer indexed field). Also, moved stuff around for loading
* template and PDO resources.
*
*/
public function __construct(array $_meta, string $_id = '')
{
register_shutdown_function(array($this, '__destruct'));
$this->res = 'PDOC: ';
try {
parent::__construct();
} catch (Throwable $t) {
$msg = ERROR_THROWABLE_EXCEPTION . COLON . $t->getMessage();
$this->eventMessages[] = $msg;
if (isset($this->logger) and $this->logger->available)
$this->logger->error($msg);
else
consoleLog($this->res, CON_ERROR, $msg);
$this->state = STATE_FRAMEWORK_FAIL;
$this->status = false;
return;
}
$this->sidewaysDelete = false;
$this->recordsInserted = 0;
$this->recordsDropped = 0;
if (!empty($_guid) and $this->debug) $this->logger->debug(STRING_RECEIVED_GUID . $_guid);
// meta data transfer
$this->templateClass = $_meta[META_TEMPLATE];
$this->client = STRING_UNDEFINED;
if (!empty($_meta)) {
$this->metaPayload = $_meta;
if (isset($this->metaPayload[META_CLIENT])) {
$this->client = $this->metaPayload[META_CLIENT];
}
}
if (isset($this->metaPayload[META_EVENT_GUID])) {
$this->eventGUID = $this->metaPayload[META_EVENT_GUID];
} else {
$this->logger->info(ERROR_EVENT_GUID_404 . $this->class);
}
if (!isset($this->logger)) {
try {
$this->logger = new gacErrorLogger($_meta[META_EVENT_GUID] ?? null);
} catch (Throwable $t) {
$msg = ERROR_THROWABLE_EXCEPTION . COLON . $t->getMessage();
$this->eventMessages[] = $msg;
if (isset($this->logger) and $this->logger->available)
$this->logger->error($msg);
else
consoleLog($this->res, CON_ERROR, $msg);
$this->state = STATE_FRAMEWORK_FAIL;
$this->status = false;
return;
}
}
$this->status = false;
$this->limitOverride = false;
$this->env = gasConfig::$settings[CONFIG_ID][CONFIG_ID_ENV];
// load the PDO configuration from the XML
$this->config =gasConfig::$settings[CONFIG_DATABASE][CONFIG_DATABASE_PDO];
if (empty($this->config)) {
$msg = ERROR_CONFIG_RESOURCE_404 . RESOURCE_PDO_MASTER;
$this->logger->warn($msg);
$this->eventMessages[] = $msg;
$this->state = STATE_RESOURCE_ERROR_PDO;
return;
}
// load the template
// if we don't have a tlti set in the meta payload then the instantiation request originated outside of
// the SMAX API then we can assume the GA TLTI as a default
$this->templateName = (isset($_meta[META_TLTI])) ? $_meta[META_TLTI] . $_meta[META_TEMPLATE] : STRING_CLASS_GAT . $_meta[META_TEMPLATE];
try {
if (!$this->loadTemplate()) {
$this->logger->warn(ERROR_TEMPLATE_INSTANTIATE . $_meta[META_TLTI] . $this->templateClass);
$this->state = STATE_TEMPLATE_ERROR;
return;
}
if (!$this->logger->setService($this->dbService)) {
$this->eventMessages[] = ERROR_SERVICE_404 . $this->dbService;
$this->logger->fatal(sprintf(INFO_LOC, basename(__FILE__), __LINE__) . ERROR_SERVICE_404 . $this->dbService);
return;
}
// establish and assign PDO connections
if (!$this->getPDOResources()) return;
// get index and some query details about the class directly from the database -- state/status set in both
// of these methods...
if ($this->collectionName != NONE) {
$this->buildIndexReference();
if ($this->status)
$this->setRowsReturnedLimit();
}
} catch (Throwable $t) {
$msg = ERROR_THROWABLE_EXCEPTION . COLON . $t->getMessage();
$this->eventMessages[] = $msg;
if (isset($this->logger) and $this->logger->available)
$this->logger->error($msg);
else
consoleLog($this->res, CON_ERROR, $msg);
$this->state = STATE_FRAMEWORK_FAIL;
$this->status = false;
return;
}
$this->class = $this->templateName; // set the class to the name of the requested data class
// if the constructor received an id parameter, a GUID, validate and load the record if it exists...
if (!empty($_id)) {
if (!validateGUID($_id)) {
$msg = ERROR_INVALID_GUID . $_id;
$this->eventMessages[] = $msg;
$this->logger->error($msg);
$this->state = STATE_DATA_ERROR;
return;
}
$tweakedPerms = false; // did we override the audit controls for the fetch?
$oldAudit = $this->skipReadAudit; // save the old setting b/c we'll override if audit is enabled
// DB-51: populating a data class on instantiation if GUID passed to constructor
$query = [ DB_TOKEN => [ OPERAND_NULL => [ OPERATOR_EQ => [ $_id ]]]];
// if this is a system request, disable auditing/journaling for the fetch only
if ($this->useAuditing == AUDIT_NONDESTRUCTIVE and $this->metaPayload[META_CLIENT] == CLIENT_SYSTEM) {
$this->skipReadAudit = true;
$tweakedPerms = true;
}
try {
$this->_fetchRecords([STRING_QUERY_DATA => $query]);
} catch (TypeError $t) {
$this->eventMessages[] = sprintf(STUB_LOC, basename(__FILE__), __METHOD__, __LINE__) . ERROR_TYPE_EXCEPTION;
$this->eventMessages[] = $t->getMessage();
$this->logger->error($t->getMessage());
$this->state = STATE_FRAMEWORK_FAIL;
$this->status = false;
return;
}
if ($tweakedPerms) $this->skipReadAudit = $oldAudit;
}
}
/**
* _createRecord() -- public method
*
* This is the PDO method for adding new records to the SQL database.
*
* The method requires one input parameter: an array of 1 or more records to be added to the referenced table.
*
* The method algorithm is as follows:
*
* 1. load the payload data into the class member, which also performs the cache-mapping
* 2. inject the createdDate, status and guid into each record
* 3. validate the record count against the max-allowed records
* 4. parse the records to pad (insert null placeholders) for any skipped fields
* 5. build the prepared SQL query for the insert
* 6. call the function which executes the batch query insert
* 7. parse the return payload for cacheMapping and data return
*
* Almost all of the chunky bits listed are handled in either class or core methods and those are extensively
* documented in-case you're wondering where all the doc for this method is.
*
*
* @author mike@givingassistant.org
* @version 1.0
*
* @param array $_data
* @param string $_preValidated must be one of three constant values: DATA_NORM (default), DATA_MIG, or DATA_WH
*
* HISTORY:
* ========
* 10-23-17 mks CORE-585: original coding
* 04-28-18 mks _INF-188: support for meta_limit_override in warehousing request
* 05-23-18 mks _INF-188: rewrote query generation to overcome mysql's inability to batch process
* 07-23-18 mks CORE-1097: compliance with new mongo-resource retrieval
* 09-04-18 mks DB-48: changed to match re-declared method abstraction in core
* 11-05-18 mks DB-55: audit/journaling support added
*
*/
public function _createRecord(array $_data, string $_preValidated = DATA_NORM):void
{
try {
// ensure we still have an active connection - reconnect if we do not
if (is_null($this->dbcMain)) {
if (isset($this->isWHRequest) and $this->isWHRequest) {
$this->dbcMain = gasResourceManager::fetchResource(RESOURCE_MONGO_MASTER, ENV_SEGUNDO);
$res = RESOURCE_WH_COOL_PDO_MASTER;
} else {
$this->dbcMain = gasResourceManager::fetchResource(RESOURCE_PDO_MASTER);
$res = RESOURCE_PDO_MASTER;
}
if (is_null($this->dbcMain)) {
$error = ERROR_RESOURCE_404 . $res;
$this->eventMessages[] = $error;
if ($this->debug) $this->logger->debug($error);
$this->state = STATE_RESOURCE_ERROR;
$this->status = false;
return;
}
}
// all incoming data has already been validated (migration mapped if external source, fetched from a
// namaste table otherwise) so only do validation if data is coming from an unknown (user request) source
if ($_preValidated == DATA_NORM) {
// transfer the client-data payload into the class $data property with validation
// if the method returns a false, a processing error happened so return immediately
// note that data-array validation is handled in this method...
if (!$this->loadPayloadData($_data, DB_EVENT_CREATE)) return;
// reset state/status members
$this->state = STATE_DATA_ERROR;
$this->status = false;
} else {
$this->data = $_data;
}
// inject record GUIDs, createdDate and status into each payload record
if (!$this->newRecordDataInjections()) return;
// forced validation of the the number of tuples in the current container
if (count($this->data) != $this->count) {
$this->logger->warn(ERROR_DATA_INCONSISTENT_COUNT);
$this->count = count($this->data);
}
$this->state = STATE_DB_ERROR;
$this->status = false;
if ($this->count == 0) {
$this->logger->info(ERROR_DATA_ARRAY_EMPTY);
return;
}
if (!isset($this->metaPayload[META_LIMIT_OVERRIDE])) {
// ensure we're not inserting more than the max # of records allowed b/c batch processing eats memory
if ($this->count > $this->recordLimit) {
$msg = ERROR_RECORD_LIMIT_EXCEEDED . $this->recordLimit;
$this->eventMessages[] = $msg;
if ($this->debug) $this->logger->debug($msg);
$this->state = STATE_DATA_ERROR;
return;
}
} else {
$this->recordLimit = $this->metaPayload[META_LIMIT_OVERRIDE];
}
// prep the data for the insert command by padding missing columns in the data set with null values
// and re-ordering the data so that the field data is in the same order and the fields in the query
// DO NOT DO THIS IF THIS IS A WAREHOUSE REQUEST!
if (!isset($this->isWHRequest) or !$this->isWHRequest) $this->prepareDataForInsert();
// build the sql string for the insert
$sql = 'INSERT /* ' . __METHOD__ . AT . __LINE__ . ' */ ';
$sql .= 'INTO ' . $this->queryTable . ' (';
// _INF-188: changing PDO inserts to bulk mode b/c we can't guarantee that stupid mysql query data will
// all have the same columns and fields provided in the data array. (so much easier in mongo)
$queryList = null;
foreach ($this->data as $record) {
$query = $sql . implode(', ', array_keys($record)) . ') VALUES (';
for ($index = 0, $max = count($record); $index < $max; $index++)
$query .= '?, ';
$query = rtrim($query, ', ') . ')';
$queryList[] = $query;
}
$this->aryQuery = $queryList;
// process the query and the data payload (also assigns value to strQuery)
$this->executePreparedBatchInsertQuery();
// if we successfully created a record(s), process the return data set
if ($this->status) {
// if auditing is enabled, post the audit data built in executePreparedBatchInsertQuery()
// we want to do this before we filter the data...
if ($this->useAuditing) {
// if the audit event fails, we'll continue processing without raising an error (to the client)
if (!$this->registerAuditEvent(EVENT_NAME_AUDIT_CREATE)) {
$this->eventMessages[] = ERROR_AUDIT_GENERIC_FAIL;
consoleLog($this->res, CON_ERROR, ERROR_AUDIT_GENERIC_FAIL);
}
}
$rc = $this->returnFilteredData();
if (!$rc) {
// an error happened building the return data -- todo: how to handle this?
// do nothing for now b/c this usually works but we'll need to address it!!!
if ($this->debug) $this->logger->debug('RC: ' . $rc);
}
}
} catch (Throwable $t) {
$hdr = basename(__METHOD__) . AT . __LINE__ . COLON;
$msg = $hdr . ERROR_THROWABLE_EXCEPTION . COLON . $t->getMessage();
$this->eventMessages[] = $msg;
if (isset($this->logger) and $this->logger->available)
$this->logger->error($msg);
else
consoleLog($this->res, CON_ERROR, $msg);
$this->state = STATE_FRAMEWORK_FAIL;
$this->status = false;
return;
}
}
/**
* _fetchRecords() -- public method, defined in namasteCore
*
* this is one of the critical core (CRUD) methods that's defined as a protected abstraction in the namaste core
* parent class - just remember, all of the instantiation classes have to follow the same core definitions...
*
* this method requires one input parameter -- and that's the data payload as received by the broker for a fetch
* request, specifically, the read broker.
*
* this is a long method, mainly because mySQL has so many options and there's a lot of private methods written
* just to support this method.
*
* What the method, then, basically does, is take the input payload and break out the key components - some are
* required and some are optional, and then parses said components to build a SQL query that will be submitted
* to the database via the PDO API. Each relevant section within the method is commented for clarity.
*
* Success or failure for this method is determined by the state/status variables. If cache-mapping is enabled,
* then we'll return the cache key, otherwise, the $data member will hold the data set. This is the calling
* client's responsibility to evaluate and process.
*
* Documentation:
*
*
* Notes:
* ------
* This method is covered under unit testing.
*
* @author mike@givingassistant.org
* @version 1.0
*
* @param array $_data
*
* HISTORY:
* ========
* 10-10-17 mks CORE-584: original coding completed
* 10-13-17 mks CORE-584: using core::returnFilteredData() for consistent post-query processing, and added
* functionality for fetching the number of records returned by the query which is
* NOT the same thing as the number of records returned.
* 04-28-18 mks INF-188: meta_limit_override support for warehousing
* 11-06-18 mks DB-82: support for auditing
* 11-07-18 mks DB-83: check for meta_client == audit --> if so, skip the audit publish event
* 01-16-19 mks DB-103: Added CLIENT_UNIT to the check to skip returning filtered data
* 01-28-19 mks DB-107: Added CLIENT_AUDIT to the check to skip returning filtered data
* 02-01-19 mks DB-107: Fixed error where count of records returned not being correctly populated
* 02-19-19 mks DB-116: Deprecated cache-mapping/cache-fetching, fixed group-by/order-by processing error,
* deprecated code for evaluating which view to use as this is now set during class
* instantiation for all PDO class schemas.
* 06-18-19 mks DB-122: Formalized the recordsInQuery/recordsReturned/recordsInCollection member
* assignments.
*
*/
public function _fetchRecords(array $_data):void
{
$this->status = false;
$this->state = STATE_DATA_ERROR;
$sort = null;
$this->queryGroupBy = null;
$this->queryOrderBy = null;
// break-out the query bits from the data payload...
// load WHERE clause data from payload
$query = (isset($_data[STRING_QUERY_DATA])) ? $_data[STRING_QUERY_DATA] : null;
try {
// if necessary, inject a status filter into the request payload
list($query, $injection) = $this->softDeleteStatusInjection($query);
} catch (TypeError $t)
{
$msg = ERROR_TYPE_EXCEPTION . COLON . $t->getMessage();
$this->eventMessages[] = $msg;
if (isset($this->logger) and $this->logger->available)
$this->logger->error($msg);
else
consoleLog($this->res, CON_ERROR, $msg);
$this->state = STATE_FRAMEWORK_FAIL;
$this->status = false;
return;
}
if (!is_array($query) and !is_null($_data[STRING_QUERY_DATA])) {
$msg = sprintf(ERROR_PDO_QUERY_ELEMENT_DATA_TYPE, STRING_QUERY_DATA, gettype($query));
$this->eventMessages[] = $msg;
if ($this->debug) $this->logger->debug($msg);
return;
}
$oldQuery = $query;
// load HAVING data from payload
if (isset($_data[STRING_HAVING_DATA])) {
$this->queryHaving = $_data[STRING_HAVING_DATA];
if (!is_array($this->queryHaving)) {
$msg = sprintf(ERROR_PDO_QUERY_ELEMENT_DATA_TYPE, STRING_HAVING_DATA, gettype($this->queryHaving));
$this->eventMessages[] = $msg;
if ($this->debug) $this->logger->debug($msg);
return;
}
}
// load LIMIT value, if set: from meta-payload or from record limit
if (isset($this->metaPayload[META_LIMIT])) {
if (!is_numeric($this->metaPayload[META_LIMIT])) {
$msg = ERROR_DATA_RANGE . META_LIMIT;
$this->eventMessages[] = $msg;
$this->eventMessages[] = sprintf(INFO_DATA_RESET, META_LIMIT) . $this->recordLimit;
$this->queryLimit = $this->recordLimit;
} else {
$this->queryLimit = intval($this->metaPayload[META_LIMIT]);
if ($this->queryLimit < 0 or $this->queryLimit > $this->recordLimit) {
$this->eventMessages[] = sprintf(INFO_DATA_RESET, META_LIMIT) . $this->recordLimit;
$this->queryLimit = $this->recordLimit;
}
}
} elseif (isset($this->metaPayload[META_LIMIT_OVERRIDE]) and (1 === intval($this->metaPayload[META_LIMIT_OVERRIDE]))) {
$this->recordLimit = gasConfig::$settings[CONFIG_BROKER_SERVICES][CONFIG_WH_RECS_PER_XFER];
$this->queryLimit = gasConfig::$settings[CONFIG_BROKER_SERVICES][CONFIG_WH_RECS_PER_XFER];
} else {
$this->queryLimit = $this->recordLimit;
}
// load and validate the SKIP value
if (isset($this->metaPayload[META_SKIP])) {
if (!is_numeric($this->metaPayload[META_SKIP])) {
$msg = ERROR_DATA_RANGE . META_SKIP;
$this->eventMessages[] = $msg;
$this->eventMessages[] = sprintf(ERROR_DATA_TYPE_MISMATCH_DETAILS, META_SKIP, DATA_TYPE_INTEGER, gettype($this->metaPayload[META_SKIP]));
$this->querySkip = 0;
} elseif (isset($this->metaPayload[META_LIMIT]) and $this->metaPayload[META_LIMIT] < 0) {
$msg = ERROR_DATA_RANGE . COLON . META_SKIP;
$this->eventMessages[] = $msg;
// todo -- does this condition qualify as a query rejection?
$this->querySkip = 0;
} else {
$this->querySkip = intval($this->metaPayload[META_SKIP]);
}
} else {
$this->querySkip = 0;
}
try {
// build the where discriminant part of the query
$query = $this->queryBuilder($query);
if (is_null($query)) {
$this->eventMessages[] = ERROR_DATA_QUERY_BUILD;
return;
}
$this->where = $query;
// build the "having" clause
if (!is_null($this->queryHaving)) {
$this->queryHaving = $this->queryBuilder($this->queryHaving);
if (is_null($this->queryHaving)) {
$this->eventMessages[] = ERROR_DATA_HAVING_BUILD;
return;
}
}
// build the return field list
if (isset($_data[STRING_RETURN_DATA])) {
$this->queryFields = $this->buildReturnFieldList($_data[STRING_RETURN_DATA]);
} else {
$this->queryFields = $this->buildReturnFieldList();
}
if (empty($this->queryFields)) {
$msg = ERROR_DATA_QUERY_BUILD . STRING_RETURN_DATA;
$this->eventMessages[] = $msg;
if ($this->debug) $this->logger->debug($msg);
return;
}
// build the order-by/group-by clauses
$groupByData = (isset($_data[STRING_GROUP_BY_DATA])) ? $_data[STRING_GROUP_BY_DATA] : array();
if (!isset($_data[STRING_ORDER_BY_DATA]) and isset($_data[STRING_SORT_DATA])) {
$orderByData = $_data[STRING_SORT_DATA];
} elseif (isset($_data[STRING_ORDER_BY_DATA])) {
$orderByData = $_data[STRING_ORDER_BY_DATA];
} else {
$orderByData = array();
}
// validate these as arrays...
if (!is_null($groupByData) and !is_array($groupByData)) {
$msg = ERROR_DATA_ARRAY_NOT_ARRAY . STRING_GROUP_BY_DATA;
$this->eventMessages[] = $msg;
if ($this->debug) $this->logger->debug($msg);
return;
}
if (!is_null($orderByData) and !is_array($orderByData)) {
$msg = ERROR_DATA_ARRAY_NOT_ARRAY . STRING_ORDER_BY_DATA;
$this->eventMessages[] = $msg;
if ($this->debug) $this->logger->debug($msg);
return;
}
// account for both input parameters being null
if (is_null($groupByData) and is_null($orderByData)) {
$this->queryOrderBy = null;
$this->queryGroupBy = null;
} else {
$this->buildGroupOrderBy($groupByData, $orderByData);
if ($this->state != STATE_SUCCESS) {
$this->eventMessages[] = ERROR_DATA_GROUP_ORDER_BY_BUILD;
if ($this->debug) $this->logger->debug(ERROR_DATA_GROUP_ORDER_BY_BUILD);
return;
}
}
if (!$this->status) {
if (!$injection) {
// we didn't inject a query so log the failure and return
$this->logger->error(ERROR_DATA_QUERY_BUILD);
$this->logger->error(json_encode($query));
return;
} elseif (!empty($oldQuery)) {
// attempt to build the original query
$query = $this->queryBuilder($oldQuery);
if ($this->debug) $this->logger->debug($query);
if (!$this->status) {
$this->logger->error(ERROR_DATA_QUERY_BUILD);
$this->logger->error(json_encode($oldQuery));
return;
}
}
}
} catch (Throwable $t) {
$msg = ERROR_THROWABLE_EXCEPTION . COLON . $t->getMessage();
$this->eventMessages[] = $msg;
if (isset($this->logger) and $this->logger->available)
$this->logger->error($msg);
else
consoleLog($this->res, CON_ERROR, $msg);
$this->state = STATE_FRAMEWORK_FAIL;
$this->status = false;
return;
}
// determine the "from" part of the query - if not given, this will default the basic view that is
// native to every class (via the template definition: $dbObjects)
// if (empty($_data[STRING_FROM_DATA])) {
// if (!isset($this->template->dbObjects[PDO_VIEWS][PDO_VIEW_BASIC . $this->collectionName])) {
// $this->queryTable = $this->collectionName;
// } else {
// $this->queryTable = PDO_VIEW_BASIC . $this->collectionName;
// }
// } else {
// // validate the name of the supplied view against the current view list
// if (array_key_exists($_data[STRING_FROM_DATA], $this->template->dbObjects[PDO_VIEWS])) {
// $this->queryTable = $_data[STRING_FROM_DATA];
// } else {
// // return an error since we can't assume the responsibility of deriving what the client intended
// $msg = sprintf(ERROR_VIEW_404, $_data[STRING_FROM_DATA], $this->class);
// $this->eventMessages[] = $msg;
// if ($this->debug) $this->logger->debug($msg);
// $this->state = STATE_VALIDATION_ERROR;
// return;
// }
// }
// reset from queryBuilder()
$this->status = false;
$this->state = STATE_DATA_ERROR;
// BUILD THE PDO QUERIES
// all namaste queries include a comment identifying where in the source code the query originated
// BUILDING the query-record counter: how many total records will be returned from this query
$rcQuery = 'SELECT /* QUERY_COUNT: ' . basename(__METHOD__) . AT . __LINE__ . '*/ ';
$rcQuery .= 'count(*) AS ' . STRING_NUM_RECS . ' ';
$rcQuery .= 'FROM ' . $this->queryTable . ' ';
// BUILDING the regular query
$this->strQuery = 'SELECT /* ' . basename(__METHOD__) . AT . __LINE__ . ' */ ';
$this->strQuery .= $this->queryFields . ' '; // assigns the return fields to the query
$this->strQuery .= 'FROM ' . $this->queryTable . ' '; // the view or table name
$this->strQuery .= 'WHERE ' . $this->where . ' '; // the query discriminant
$rcQuery .= 'WHERE ' . $this->where . ' ';
if (isset($this->queryGroupBy) and !is_null($this->queryGroupBy)) {
$this->strQuery .= 'GROUP BY ' . $this->queryGroupBy . ' ';
$rcQuery .= 'GROUP BY ' . $this->queryGroupBy . ' ';
}
if (isset($this->queryOrderBy) and !is_null($this->queryOrderBy)) {
$this->strQuery .= 'ORDER BY ' . $this->queryOrderBy . ' ';
$rcQuery .= 'ORDER BY ' . $this->queryOrderBy . ' ';
}
// limit, skip is always enabled set so check for non-0 values
// do NOT add the skip/limit params to the record-count query!
if ($this->querySkip and $this->queryLimit) {
$this->strQuery .= 'LIMIT ' . $this->querySkip . COMMA . $this->queryLimit;
} elseif ($this->queryLimit) {
$this->strQuery .= 'LIMIT ' . $this->queryLimit;
}
try {
// set the event
$this->dbEvent = DB_EVENT_SELECT;
if (is_null($this->queryVariables)) {
// non-prepared query
$this->executeNonPreparedQuery();
$this->getQueryCount($rcQuery);
} else {
// prepared query
$this->executePreparedQuery();
$this->getPreparedQueryCount($rcQuery);
}
// the query was successful! -- process the payload data (cacheMapping), generate data counters
if ($this->status) {
if (is_null($this->queryResults) or empty($this->queryResults)) {
// search was successful but returned no records
$this->state = STATE_NOT_FOUND;
$this->status = true;
$this->eventMessages[] = INFO_QUERY_RETURNED_NO_DATA;
return;
}
$this->data = $this->queryResults;
$this->getActiveRecordCount();
$this->count = (!empty($this->data)) ? count($this->data) : 0;
$this->recordsReturned = $this->count;
// If auditing is set to read level, then submit the audit request
if ($this->useAuditing == AUDIT_NONDESTRUCTIVE) {
$this->auditRecordList = $this->data;
if (!$this->registerAuditEvent(EVENT_NAME_AUDIT_FETCH)) {
$this->eventMessages[] = ERROR_AUDIT_GENERIC_FAIL;
consoleLog($this->res, CON_ERROR, ERROR_AUDIT_GENERIC_FAIL);
}
}
// check to see if the filterData bypass was set
if ((isset($this->metaPayload[META_DONUT_FILTER]) and $this->metaPayload[META_DONUT_FILTER] == 1) and
($this->metaPayload[META_CLIENT] == CLIENT_SYSTEM or
$this->metaPayload[META_CLIENT] == CLIENT_UNIT or
$this->metaPayload[META_CLIENT] == CLIENT_AUDIT))
return;
// otherwise return a filtered data-set according to cache settings
if (!$this->returnFilteredData()) {
$this->logger->warn(ERROR_RFD_CORE_FAIL);
$this->state = STATE_FRAMEWORK_FAIL;
$this->status = false;
}
} // todo -- else???
} catch (Throwable $t) {
$msg = ERROR_THROWABLE_EXCEPTION . COLON . $t->getMessage();
$this->eventMessages[] = $msg;
if (isset($this->logger) and $this->logger->available)
$this->logger->error($msg);
else
consoleLog($this->res, CON_ERROR, $msg);
$this->state = STATE_FRAMEWORK_FAIL;
$this->status = false;
return;
}
}
/**
* _updateRecord() -- public method
*
* This method is normally called by the write broker for an update event - the method will process the client
* payload and will generate an update query that will affect 1, or more, rows in the PDO data set.
*
* The input parameter is the data payload as passed from the client. There must be, at a minimum, two sub-arrays
* present in the payload:
*
* STRING_QUERY_DATA for building the query where-discriminant
* STRING_UPDATE_DATA the key-value list of fields/values that will be updated
*
* Optional parameter:
*
* STRING_ORDER_BY_DATA for the order-by discriminant
*
* And, finally, if META_LIMIT is defined in the meta payload, the number of records that can be updated will
* be limited by this parameter. Note that, when validating META_LIMIT, we do not check an upper-boundry, only
* that the value is greater than 0.
*
* At the top of the method, we'll init some lvars and validate the input parameter as well as verifying that
* the required parameters are present and are present as array types.
*
* Next, we process the data payload and begin assembling the query components.
*
* If the current class uses soft-deletes and there's no STATUS field in the where discriminant, we're going to
* inject one (WHERE status != DELETED) into the query. todo: modify when status field is present
*
* In the fields to be updated, once that data has been validated and passes the protected fields check, add
* the lastAccessedDate to the list of column to be updated.
*
* Build the prepared query and pass it off to the execute prepared query method and update the cache (there's a
* to-do in that section of the code) before returning control back to the calling client.
*
*
* @author mike@givingassistant.org
* @version 1.0
*
* @param array $_data
*
* HISTORY:
* ========
* 10-27-17 mks CORE-586: original coding
* 05-21-18 mks _INF-188: fixed error with updates for (non)cached soft delete updates
* 11-29-18 mks DB-55: re-ordered query-building (component) flow, added audit support
* 02-05-19 mks DB-107: fixed error where we weren't fetching a list of updated records prior to
* calling the audit event, causing the audit event to fail
* Qualified the status injection on status field not being in the update data
* 02-15-19 mks DB-116: removed cache-mapping support; this is now handled at broker-services level,
* pre-update, we're now establishing a list of tokens that will be impacted by the
* update query and, if the query is unbound/unspecific, then we'll inject this token
* list in the update, and the post-update fetch queries for consistency
* 11-06-20 mks DB-171: define meta data field for current service since we're calling validateMetaData
* directly without first calling the firstPassPayloadValidation() method which is
* how brokerService is normally set in the meta data payload
*
*/
public function _updateRecord(array $_data):void
{
$this->state = STATE_DATA_ERROR;
$this->status = false;
$where = null;
$dataCopy = null;
$tokenQuery = null;
$updateData = null;
$recordList = null;
$tokenList = null;
// ensure the $_data input parameter is an array
if (empty($_data) or !is_array($_data)) {
$this->eventMessages[] = ERROR_DATA_404;
if ($this->debug) $this->logger->debug(ERROR_DATA_MISSING_ARRAY . STRING_DATA);
return;
}
// check for required fields in the $_data payload and qualify them as arrays
if (!array_key_exists(STRING_QUERY_DATA, $_data)) {
$msg = ERROR_DATA_KEY_404 . STRING_QUERY_DATA;
$this->eventMessages[] = $msg;
if ($this->debug) $this->logger->debug($msg);
return;
} elseif (!is_array($_data[STRING_QUERY_DATA])) {
$msg = ERROR_DATA_ARRAY_NOT_ARRAY . STRING_QUERY_DATA;
$this->eventMessages[] = $msg;
if ($this->debug) $this->logger->debug($msg);
return;
}
if (!array_key_exists(STRING_UPDATE_DATA, $_data)) {
$msg = ERROR_DATA_KEY_404 . STRING_UPDATE_DATA;
$this->eventMessages[] = $msg;
if ($this->debug) $this->logger->debug($msg);
return;
} elseif (!is_array($_data[STRING_UPDATE_DATA])) {
$msg = ERROR_DATA_ARRAY_NOT_ARRAY . STRING_UPDATE_DATA;
$this->eventMessages[] = $msg;
if ($this->debug) $this->logger->debug($msg);
return;
}
// if the current class uses soft deletes, we need to inject a "AND STATUS <> DELETED" qualifier
if (!$this->useDeletes) { // if the current class uses soft-deletes
// inject a status filter if one doesn't already exist in the update payload and we're using a soft-delete
// class, and if there's no status field in the discriminant
if (!array_key_exists((DB_STATUS . $this->ext), $_data[STRING_UPDATE_DATA])
and (!array_key_exists(DB_STATUS, $_data[STRING_UPDATE_DATA]))
and (!array_key_exists((DB_STATUS . $this->ext), $_data[STRING_QUERY_DATA]))) {
$injected = true;
if (!isset($_data[STRING_QUERY_DATA][DB_STATUS])) {
$_data[STRING_QUERY_DATA][DB_STATUS] = [OPERAND_NULL => [OPERATOR_DNE => [STATUS_DELETED]]];
} else {
$injected = false;
}
if ($injected) {
$_data[STRING_QUERY_DATA][OPERAND_AND] = null;
}
} elseif ((isset($_data[STRING_UPDATE_DATA][DB_STATUS]) and $_data[STRING_UPDATE_DATA][DB_STATUS] == STATUS_DELETED)
or (isset($_data[STRING_UPDATE_DATA][DB_STATUS . $this->ext]) and $_data[STRING_UPDATE_DATA][DB_STATUS . $this->ext] == STATUS_DELETED)) {
$this->sidewaysDelete = true;
}
}
// start validating the query components - because PHP7 and type-cast parameters, these are exception wrapped
try {
// since we're doing an update, we need to fetch a list of the records that will be affected by the
// update so that, post-query, we can remove them from cache. If auditing is enabled, we'll use the data
// (from the original query) in audit-journal processing.
// Additionally, we're going to clone the current object so that current data is not affected and we'll
// copy over the payload from the cloned object to the current object.
$clone = clone $this;
$clone->cloneSquelch();
if ($clone->metaPayload[META_CLIENT] == CLIENT_AUDIT) $clone->useDeletes = true;
if (!isset($clone->metaPayload[META_BROKER_SERVICE]) or empty($clone->metaPayload[META_BROKER_SERVICE]))
$clone->metaPayload[META_BROKER_SERVICE] = (isset($this->dbService) and !empty($this->dbService)) ? $this->dbService : ENV_APPSERVER;
$clone->_fetchRecords($_data);
if (!$clone->status) {
$hdr = basename(__METHOD__) . AT . __LINE__ . COLON;
$this->eventMessages[] = ERROR_CLONE_QUERY;
$this->logger->error($hdr . ERROR_CLONE_QUERY . $clone->strQuery);
$this->state = STATE_DB_ERROR;
$clone->__destruct();
unset($clone);
return;
} elseif ($clone->count) {
// note that we're not de-allocating $clone on success... build the auditRecordList and TokenLists
$this->auditRecordList = $clone->data;
if (count($this->auditRecordList)) {
foreach ($this->auditRecordList as $record)
$this->tokenList[] = $record[(DB_TOKEN . $this->ext)];
if (count($this->tokenList))
$tokenQuery[CM_TST_TOKEN] = [ OPERAND_NULL => [ OPERATOR_IN => $this->tokenList ]];
}
} elseif ($clone->count == 0) {
$this->queryResults[] = INFO_QUERY_RETURNED_NO_DATA;
$clone->__destruct();
unset($clone);
$this->state = STATE_NOT_FOUND;
$this->status = true;
return;
}
// inject the token list into the submitted query as an IN component
if (!array_key_exists(CM_TST_TOKEN, $_data[STRING_QUERY_DATA]) and !empty($tokenQuery)) {
if ((count($_data[STRING_QUERY_DATA]) == 1) or ((count($_data[STRING_QUERY_DATA]) > 1) and !array_key_exists(OPERAND_AND, $_data[STRING_QUERY_DATA]))) {
// we've got a "simple" single-field query, or a complex query without and OPERATOR_AND so append:
$_data[CM_TST_TOKEN] = $tokenQuery[CM_TST_TOKEN];
// $_data[CM_TST_TOKEN] = [ OPERAND_NULL => [ OPERATOR_IN => $this->tokenList ]];
$_data[OPERAND_AND] = null;
} elseif (array_key_exists(OPERAND_AND, $_data[STRING_QUERY_DATA])) {
// we have a complex query into which we have to inject the token list before OPERATOR_AND
foreach ($_data[STRING_QUERY_DATA] as $key => $elementList) {
if ($key == OPERAND_AND) {
$dataCopy[CM_TST_TOKEN] = $tokenQuery[CM_TST_TOKEN];
// $dataCopy[CM_TST_TOKEN] = [ OPERAND_NULL => [ OPERATOR_IN => $this->tokenList ]];
$dataCopy[$key] = $elementList;
} else {
$dataCopy[$key] = $elementList;
}
}
$_data[STRING_QUERY_DATA] = $dataCopy;
}
}
// build the query discriminant (required parameter)
if (!$this->buildWhereDiscriminant($_data[STRING_QUERY_DATA])) return;
// validate the query update data (required parameter)
$updateData = $_data[STRING_UPDATE_DATA];
if (false === $this->checkProtectedFields($updateData)) {
$this->state = STATE_VALIDATION_ERROR;
$this->status = false;
return; // event messages and log errors happened in the method called above
}
// check to see if optional parameters were passed and, if they exist, validate
// if GROUP-BY data is provided, validate so that the query is stored in $this->queryOrderBy
if (isset($_data[STRING_ORDER_BY_DATA]) and !$this->buildOrderBy($_data[STRING_ORDER_BY_DATA])) return;
// load LIMIT value, if set: from meta-payload
if (isset($this->metaPayload[META_LIMIT])) {
if (!is_numeric($this->metaPayload[META_LIMIT])) {
$msg = ERROR_DATA_RANGE . META_LIMIT;
$this->eventMessages[] = $msg;
$this->eventMessages[] = sprintf(INFO_DATA_RESET, META_LIMIT) . $this->recordLimit;
$this->queryLimit = 0;
} else {
$this->queryLimit = intval($this->metaPayload[META_LIMIT]);
if ($this->queryLimit < 0) {
$this->eventMessages[] = sprintf(INFO_DATA_RESET, META_LIMIT) . $this->recordLimit;
$this->queryLimit = 0;
}
}
} else {
$this->queryLimit = 0;
}
// inject an update for the lastAccessedDate field which is protected and is why this comes
// after the checkProtectedFields() call...
$updateData[(DB_ACCESSED . $this->ext)] = date('Y-m-d G:i:s');
} catch (Throwable $t) {
$msg = ERROR_THROWABLE_EXCEPTION . COLON . $t->getMessage();
$this->eventMessages[] = $msg;
if (isset($this->logger) and $this->logger->available)
$this->logger->error($msg);
else
consoleLog($this->res, CON_ERROR, $msg);
$this->state = STATE_FRAMEWORK_FAIL;
$this->status = false;
return;
}
// checkpoint -- we've all the query elements -- start building the query
try {
$pp = $this->queryVariables; // positional parameters: temp assignment
$this->queryVariables = [];
$query = 'UPDATE /* ' . basename(__FILE__) . COLON . __METHOD__ . AT . __LINE__ . ' */ ';
$query .= $this->queryTable . ' SET ';
// todo - a view is not guaranteed to work for all update queries and needs to be addressed
// for more info: http://www.mysqltutorial.org/create-sql-updatable-views.aspx
foreach ($updateData as $column => $value) {
$query .= $column . ' = ?, ';
$this->queryVariables[] = $value;
}
$query = rtrim($query, ', ');
// merge the where-discriminant query variables onto the end of the query-variable container so that the
// order of the variable placement is consistent with the place-holders within the query string.
if (is_array($pp)) {
$this->queryVariables = array_merge($this->queryVariables, $pp);
unset($pp);
}
if (!is_null($this->where)) $query .= ' WHERE ' . $this->where . ' ';
// order-by and limit are supported in update queries even if you think they're not...
if (!empty($this->queryOrderBy)) $query .= 'ORDER BY ' . $this->queryOrderBy . ' ';
if (!empty($this->queryLimit)) $query .= 'LIMIT ' . $this->queryLimit;
$this->strQuery = $query;
// submit the query
$this->dbEvent = ($this->sidewaysDelete) ? DB_EVENT_DELETE : DB_EVENT_UPDATE;
$this->executePreparedQuery();
if ($this->status) {
// if auditing is enabled, copy of the original records and invoke the audit/journal processing
if ($this->useAuditing > AUDIT_NOT_ENABLED) {
$auditEvent = ($this->dbEvent == DB_EVENT_UPDATE) ? EVENT_NAME_AUDIT_UPDATE : EVENT_NAME_AUDIT_DELETE;
if (!$this->registerAuditEvent($auditEvent)) {
$this->eventMessages[] = ERROR_AUDIT_GENERIC_FAIL;
consoleLog($this->res, CON_ERROR, ERROR_AUDIT_GENERIC_FAIL);
}
}
// if this was a straight-update then fetch the updated records and restore the DB-EVENT.
if (!$this->sidewaysDelete) {
$payload = [
BROKER_DATA => [ STRING_QUERY_DATA => $tokenQuery ],
BROKER_META_DATA => $clone->metaPayload
];
if (!validateMetaData($payload, $this->eventMessages)) {
$hdr = basename(__METHOD__) . AT . __LINE__ . COLON;
$msg = ERROR_META_VALIDATION_SECOND_PASS;
$this->eventMessages[] = $msg;
$this->logger->error($hdr . $msg);
$this->state = STATE_DATA_ERROR;
$this->status = false;
return;
}
// fetching the records will update them in cache
// change this to the IN query using the $auditRecordList
$clone->cloneSquelch();
$clone->_fetchRecords($payload[BROKER_DATA]);
if (!$clone->status) {
$hdr = basename(__METHOD__) . AT . __LINE__ . COLON;
$msg = ERROR_PDO_FETCH;
$this->eventMessages[] = $msg;
$this->logger->error($hdr . $msg);
$this->logger->error($hdr . $clone->strQuery);
if (is_object($clone)) $clone->__destruct();
unset($clone);
$this->state = STATE_DB_ERROR;
$this->status = false;
return;
}
$this->eventMessages = array_merge($this->eventMessages, $clone->eventMessages);
$this->data = $clone->data;
$this->count = $clone->count;
if (!$this->returnFilteredData()) {
$this->eventMessages[] = ERROR_DATA_PROCESSING;
$this->state = STATE_DATA_ERROR;
}
if (is_object($clone)) $clone->__destruct();
unset($clone);
} else {
// if the update resulted in removing the record(s), we need to build a token list and cacheSmash
foreach ($this->auditRecordList as $record)
$tokenList[] = $record[DB_TOKEN . $this->ext];
if (!gasCache::smashCache($tokenList, $this->eventMessages))
$this->logger->error(ERROR_CACHE_SMASH_FAIL_SYSTEM . $this->strQuery);
}
} else {
$hdr = basename(__METHOD__) . AT . __LINE__ . COLON;
$msg = ERROR_PDO_QUERY . $query;
$this->logger->error($hdr . $msg);
$this->eventMessages[] = $msg;
$this->state = STATE_DATA_ERROR;
}
} catch (Throwable $t) {
$msg = ERROR_THROWABLE_EXCEPTION . COLON . $t->getMessage();
$this->eventMessages[] = $msg;
if (isset($this->logger) and $this->logger->available)
$this->logger->error($msg);
else
consoleLog($this->res, CON_ERROR, $msg);
$this->state = STATE_FRAMEWORK_FAIL;
$this->status = false;
return;
}
}
/**
* _deleteRecord() -- public method
*
* This is the method called by the brokers for a delete event. It is normally called by the delete-event broker
* when a PDO class is the targeted instantiation.
*
* This method first checks to see if the class has hard deletes enabled and, if it does not, it immediately passes
* the request over to the update method so that we can toggle the status of the records returned by the query to
* DELETED instead of permanently removing the records.
*
* If we're proceeding with the hard delete, we enter the validation phase of the $_data payload:
*
* 1. ensure we have a where clause that defines a range of records to be deleted - reject the request
* if this payload does not exist (limited effectiveness)
* todo: select query count of affected rows != count(*) for the table
* 2. build the where clause
* 3. build the group-by clause if provided
* 4. build the limit clause if provide
*
* Next, we need to build a select query based on the discriminants just validated, and fetch a list of the tokens
* representing records to be deleted. This is so that, post delete, we can update (remove) said records if they
* are cached individually.
*
* todo: batch-cached records -- how to handle? a record-registration process?
*
* Then, build the delete query and pass it off to the preparedQuery method for execution. On successful return,
* take the token list we generated in the select query and search cache for these records stored individually and
* remove them from cache if they exist.
*
* On successful processing, the class returns the number of rows deleted in $this->rowsAffected and a text-string
* to the same effect in $this->queryResults.
*
* Notes:
* ------
* multi-table deletes are not supported
* partition deletes are not supported
* table references are not supported
*
* reference: https://dev.mysql.com/doc/refman/5.7/en/delete.html
*
*
* @author mike@givingassistant.org
* @version 1.0
*
* @param array $_data -- payload data as received by the broker
*
* HISTORY:
* ========
* 10-25-17 mks CORE-587: original coding
* 10-26-17 mks CORE-586: code reduction
* 02-01-19 mks DB-107: fixed bug where the record to be deleted isn't pre-fetched, when we pass the
* soft-delete data class to the update command, we pull a copy of the record so that the
* update method will complete successfully.
* 02-21-19 mks DB-116: refactored to remove cache support, removed redundant fetch call, clone usage
*
*/
public function _deleteRecord(array $_data):void
{
$this->status = false;
$this->state = STATE_DATA_ERROR;
$where = null;
$tokenList = null;
// get a copy of the records that will be impacted by the delete request - return if the fetch request fails
// set-up a clone object for the fetch so as not to "pollute" the current class object.
$clone = clone $this;
$clone->cloneSquelch();
$clone->_fetchRecords($_data);
if (!$clone->status) {
$this->eventMessages[] = ERROR_PDO_FETCH;
$this->logger->info(ERROR_PDO_FETCH);
if (count($clone->eventMessages))
$this->eventMessages = array_merge($this->eventMessages, $clone->eventMessages);
if (is_object($clone)) $clone->__destruct();
unset($clone);
return;
}
if ($clone->count) {
$this->auditRecordList = $clone->data;
foreach ($this->auditRecordList as $record)
$this->tokenList[] = $record[(DB_TOKEN . $this->ext)];
} else {
// no records were returned in the query
$this->eventMessages[] = INFO_RECORD_NOT_FOUND;
$this->state = STATE_NOT_FOUND;
if (count($clone->eventMessages))
$this->eventMessages = array_merge($this->eventMessages, $clone->eventMessages);
if (is_object($clone)) $clone->__destruct();
unset($clone);
return;
}
if (is_object($clone)) $clone->__destruct();
unset($clone);
// next thing we want to do is check and see if hard-deletes are enabled for the current class because, if so,
// then we're going to inject a "status_ext = 'DELETED'" directive into the payload and then do a sideways
// hand-off to the _updateRecord() method instead.
if (!$this->useDeletes) {
if (isset($_data[STRING_UPDATE_DATA])) {
$msg = sprintf(ERROR_UPDATE_DATA_NOT_ALLOWED, STRING_DELETE);
$this->eventMessages[] = $msg;
$this->state = STATE_REQUEST_REJECTED;
$this->status = false;
if ($this->debug) $this->logger->debug($msg);
return;
}
// build and inject the status = deleted, lastAccessed = now() directives for the update query
// if you get tempted to add the lastAccessedData update here, don't...
$_data[STRING_UPDATE_DATA] = [ DB_STATUS => STATUS_DELETED ];
try {
$this->sidewaysDelete = true; // everything in update() procs off of this setting
if ($this->debug)
$this->logger->debug(sprintf(INFO_SIDEWAYS, DB_EVENT_DELETE, DB_EVENT_UPDATE));
// re-direct the delete request to the update method to toggle the record status
$this->_updateRecord($_data);
} catch (TypeError $t) {
$msg = ERROR_TYPE_EXCEPTION . COLON . $t->getMessage();
$this->eventMessages[] = $msg;
if (isset($this->logger) and $this->logger->available)
$this->logger->error($msg);
else
consoleLog($this->res, CON_ERROR, $msg);
$this->state = STATE_FRAMEWORK_FAIL;
$this->status = false;
}
return; // pass-thru the state/status from update to the calling client
}
// at this stage, we're using a hard-delete class so start by qualifying the where clause
// if there is no "where" clause in the payload, reject the request b/c namaste does not allow
// delete-all-records types of queries...
if (!isset($_data[STRING_QUERY_DATA])) {
$msg = ERROR_DATA_KEY_404 . STRING_QUERY_DATA;
$this->eventMessages[] = $msg;
if ($this->debug) $this->logger->debug($msg);
return;
}
try {
if (!$this->buildWhereDiscriminant($_data[STRING_QUERY_DATA])) return;
} catch (TypeError $t) {
$msg = ERROR_TYPE_EXCEPTION . COLON . $t->getMessage();
$this->eventMessages[] = $msg;
if (isset($this->logger) and $this->logger->available)
$this->logger->error($msg);
else
consoleLog($this->res, CON_ERROR, $msg);
$this->state = STATE_FRAMEWORK_FAIL;
$this->status = false;
}
if (is_null($this->where)) {
$this->eventMessages[] = ERROR_DATA_QUERY_BUILD;
if ($this->debug) $this->logger->debug(ERROR_DATA_QUERY_BUILD);
return;
}
try {
// if GROUP-BY data is provided, validate so that the query is stored in $this->queryOrderBy
if (isset($_data[STRING_ORDER_BY_DATA]) and !$this->buildOrderBy($_data[STRING_ORDER_BY_DATA])) return;
// load LIMIT value, if set: from meta-payload
if (isset($this->metaPayload[META_LIMIT])) {
if (!is_numeric($this->metaPayload[META_LIMIT])) {
$msg = ERROR_DATA_RANGE . META_LIMIT;
$this->eventMessages[] = $msg;
$this->eventMessages[] = sprintf(INFO_DATA_RESET, META_LIMIT) . $this->recordLimit;
$this->queryLimit = 0;
} else {
$this->queryLimit = intval($this->metaPayload[META_LIMIT]);
if ($this->queryLimit < 0) {
$this->eventMessages[] = sprintf(INFO_DATA_RESET, META_LIMIT) . $this->recordLimit;
$this->queryLimit = 0;
}
}
} else {
$this->queryLimit = 0;
}
// build the pdo query for delete
$query = 'DELETE /* ' . basename(__FILE__) . COLON . __METHOD__ . AT . __LINE__ . ' */ ';
$query .= 'FROM ' . $this->queryTable . ' ';
$query .= 'WHERE ' . $this->where . ' ';
if (!is_null($this->queryOrderBy)) {
$query .= 'ORDER BY ' . $this->queryOrderBy . ' ';
}
if ($this->queryLimit) {
$query .= 'LIMIT ' . $this->queryLimit;
}
$this->strQuery = $query;
// submit the query
$this->dbEvent = DB_EVENT_DELETE;
$this->executePreparedQuery();
if ($this->status) {
if (!$this->registerAuditEvent(EVENT_NAME_AUDIT_DELETE)) {
$this->eventMessages[] = ERROR_AUDIT_GENERIC_FAIL;
consoleLog($this->res, CON_ERROR, ERROR_AUDIT_GENERIC_FAIL);
}
if (!$this->returnFilteredData()) {
$this->eventMessages[] = ERROR_DATA_PROCESSING;
$this->state = STATE_DATA_ERROR;
}
// remove records from cache
@gasCache::smashCache($this->tokenList, $this->eventMessages);
}
} catch (Throwable | TypeError $t) {
$msg = ERROR_THROWABLE_EXCEPTION . COLON . $t->getMessage();
$this->eventMessages[] = $msg;
if (isset($this->logger) and $this->logger->available)
$this->logger->error($msg);
else
consoleLog($this->res, CON_ERROR, $msg);
$this->state = STATE_FRAMEWORK_FAIL;
$this->status = false;
}
}
/**
* _checkQuery() -- public function
*
* this function is mandated by the Namaste core abstraction. The method provides access to the query-builder
* functionality within the the data classes to validate a query submission from clients.
*
* The method requires a single input parameter: this is the array containing the query to be tested.
*
* The method returns a boolean indicating whether or not the query was successfully built... if it was built
* successfully, then store the query in the class member container.
*
*
* @author mike@givingassistant.org
* @version 1.0
*
* @param array $_query
* @return bool
*
*
* HISTORY:
* ========
* 04-23-18 mks _INF-188: Original coding
*
*/
public function _checkQuery(array $_query): bool
{
try {
$query = $this->queryBuilder($_query);
if (!is_null($query)) {
$this->strQuery = $query;
return true;
}
} catch (TypeError $t) {
$msg = ERROR_TYPE_EXCEPTION . COLON . $t->getMessage();
$this->eventMessages[] = $msg;
if (isset($this->logger) and $this->logger->available)
$this->logger->error($msg);
else
consoleLog($this->res, CON_ERROR, $msg);
$this->state = STATE_FRAMEWORK_FAIL;
$this->status = false;
}
return false;
}
/**
* _getQC() -- abstraction-declared public method
*
* This method is prototyped in the core abstraction.
*
* The method requires a single input parameter and this is the STRING_QUERY_DATA sub-array from the
* BROKER_DATA payload of a fetch event request.
*
* The method validates that the input data exists and is an array and then passes the parameter off to
* the query-builder method to build the SQL query.
*
* If the call to the query-builder method returns true, (which is just actually building the where clause), we
* will complete building the entire query and then submit it to the methods that exec the query -- which, in turn,
* is dependent on if we have a prepared (contains variables) or non-prepared query.
*
* If the query-execution method succeeds, we return a Boolean(true) to the calling client -- relevant data (the
* query count) is embedded in a class member variable and should be extracted by the calling client.
*
* In all cases where a Boolean(false) is returned, we populate the eventMessage stack and, if debug is enabled,
* also output a debug-log message.
*
*
* @author mike@givingassistant.org
* @version 1.0
*
* @param array $_data
* @return bool
*
*
* HISTORY:
* ========
* 04-30-18 mks _INF-188: original coding
*
*/
public function _getQC(array $_data):bool
{
if (empty($_data) or (!is_array($_data))) {
$error = ERROR_DATA_MISSING_ARRAY . BROKER_DATA;
$this->eventMessages[] = $error;
if ($this->debug) $this->logger->debug($error);
$this->state = STATE_DATA_ERROR;
return false;
}
if (!isset($_data[STRING_QUERY_DATA]) or (!is_array($_data[STRING_QUERY_DATA]))) {
$error = ERROR_DATA_KEY_404 . STRING_QUERY_DATA;
$this->eventMessages[] = $error;
if ($this->debug) $this->logger->debug($error);
$this->state = STATE_DATA_ERROR;
return false;
}
try {
$where = $this->queryBuilder($_data[STRING_QUERY_DATA]);
if (is_null($where)) {
$this->state = STATE_DATA_ERROR;
$this->eventMessages[] = ERROR_DATA_QUERY_BUILD;
if ($this->debug) $this->logger->debug(ERROR_DATA_QUERY_BUILD);
return false;
}
$query = 'SELECT /* ' . basename(__FILE__) . COLON_NS . __LINE__ . ' */ count(*) AS ' . STRING_NUM_RECS . ' ';
$query .= 'FROM ' . $this->collectionName . ' WHERE ' . $where;
$this->dbEvent = DB_EVENT_SELECT;
$this->strQuery = $query;
$this->executePreparedQuery();
if (!$this->getPreparedQueryCount($query)) {
$error = ERROR_PDO_COUNT_FETCH_FAIL . $this->strQuery;
$this->eventMessages[] = $error;
if ($this->debug) $this->logger->debug($error);
$this->state = STATE_DB_ERROR;
return false;
}
$this->getActiveRecordCount();
return true;
} catch (Throwable $t) {
$msg = ERROR_THROWABLE_EXCEPTION . COLON . $t->getMessage();
$this->eventMessages[] = $msg;
if (isset($this->logger) and $this->logger->available)
$this->logger->error($msg);
else
consoleLog($this->res, CON_ERROR, $msg);
$this->state = STATE_FRAMEWORK_FAIL;
$this->status = false;
}
return false;
}
/**
* execSP() -- public method
*
* This method allows the brokers to exec a stored-procedure. There is one input parameter to the method and
* that is an associative array with the following structure:
*
* $_data = [ STRING_PROCEDURE_NAME => {string}, STRING_PARAM_LIST => {array} ]
*
* Where the STRING_PROCEDURE_NAME is the name of the stored procedure (obtainable from the schema template report)
* to be invoked and STRING_PARAM_LIST is an indexed-array of parameter values that will be passed through to
* the stored-procedure as function-parameter values.
*
* Validation:
* The method validates the following:
* -- that $_data is an array
* -- that $_data is an array with 2 elements
* -- that $_data is an array and one of the two elements is STRING_PROCEDURE_NAME
* -- that $_data is an array and one of the two elements is STRING_PARAM_LIST
* -- that the name of the stored-procedure to be invoked is a member of the current class' stored-procedure
* catalog (of sp names)
*
* Next, we parse all of the stored-procedure template-defined parameters to generate a count of the number of
* IN and OUT parameters, respectively, and also generate a list of all of the OUT parameter names.
*
* Processing:
* Once validation is complete, we have three different use cases for processing:
* -- no parameters are passed to the SP
* -- only IN parameters are passed to the SP
* -- the passed parameters contain an OUT param
*
* Each of these three use-cases requires a different way of invoking the stored procedure and fetching the
* result set.
*
* As part of the data-payload validation process, we're comparing the count of the number of parameters
* received by the user request to the count of the number of parameters declared in the SP defined in
* the template. Note that this will only apply to IN params.
*
* Once we know which branch to take in the processing tree, we execute the code to build the stored
* procedure SQL invocation query, bind parameters if present, and then exec the query and fetch the
* results-set.
*
* The results are returned in $this->queryResults and not $this->data b/c we cannot limit the return
* data to just table column values. (e.g.: aggregation queries). As such, there is no caching of the
* return data for stored-procedure calls nor is there a limit on the number of records returned.
*
* @author mike@givingassistant.org
* @version 1.0
*
* @param $_data
*
* HISTORY:
* ========
* 11-10-17 mks CORE-588: original coding completed
*
*/
public function execSP($_data)
{
$this->state = STATE_DATA_ERROR;
$this->status = false;
$this->queryResults = [];
$startTime = floatval(0);
$outList = null;
// validate the input data as an array with two elements
if (!is_array($_data)) {
$msg = ERROR_DATA_ARRAY_NOT_ARRAY . STRING_QUERY_DATA;
$this->eventMessages[] = $msg;
if ($this->debug) $this->logger->debug($msg);
return;
}
if (count($_data) != 2) {
$msg = sprintf(ERROR_DATA_ARRAY_COUNT, 2, STRING_QUERY_DATA, count($_data));
$this->eventMessages[] = $msg;
if ($this->debug) $this->logger->debug($msg);
return;
}
// validate the data keys
if (!array_key_exists(STRING_PROCEDURE_NAME, $_data)) {
$msg = ERROR_DATA_KEY_404 . STRING_PROCEDURE_NAME;
$this->eventMessages[] = $msg;
if ($this->debug) $this->logger->debug($msg);
return;
}
if (!array_key_exists(STRING_PARAM_LIST, $_data)) {
$msg = ERROR_DATA_KEY_404 . STRING_PARAM_LIST;
$this->eventMessages[] = $msg;
if ($this->debug) $this->logger->debug($msg);
return;
}
// get the name of the stored-procedure or function and store the param list if it exists b/c null
// values are allowed (the client MUST still define the key using a null placeholder.)
$spName = $_data[STRING_PROCEDURE_NAME];
// ensure that we have a stored procedure by that name defined in the class template
if (!array_key_exists($spName, $this->template->dbObjects[PDO_PROCEDURES])) {
$msg = sprintf(ERROR_SP_404, $spName, $this->class);
$this->eventMessages[] = $msg;
if ($this->debug) $this->logger->debug($msg);
$this->state = STATE_VALIDATION_ERROR;
return;
}
$spParamList = $_data[STRING_PARAM_LIST];
$spParamCount = (is_array($spParamList)) ? count($spParamList) : 0;
// init the param count vars (template vars)
$templateInParamCount = 0;
$templateOutParamCount = 0;
if (is_array($this->template->dbObjects[PDO_PROCEDURES][$spName][STRING_TYPE_LIST]))
$templateTotalParamCount = count($this->template->dbObjects[PDO_PROCEDURES][$spName][STRING_TYPE_LIST]);
else
$templateTotalParamCount = 0;
// get the parameter distribution/assignment for the SP
if (!is_null($this->template->dbObjects[PDO_PROCEDURES][$spName][STRING_TYPE_LIST])) {
foreach ($this->template->dbObjects[PDO_PROCEDURES][$spName][STRING_TYPE_LIST] as $spTemplateName => $templateData) {
switch (key($templateData)) {
case STRING_IN :
$templateInParamCount++;
break;
case STRING_INOUT :
$templateInParamCount++;
$templateOutParamCount++;
$outList[] = $spTemplateName;
break;
case STRING_OUT :
$templateOutParamCount++;
$outList[] = $spTemplateName;
break;
default :
$msg = ERROR_DATA_INVALID_KEY . key($templateData);
$this->eventMessages[] = $msg;
if ($this->debug) $this->logger->debug($msg);
$this->state = STATE_VALIDATION_ERROR;
return;
break;
}
}
}
// init some common vars...
$query = 'CALL /* ' . basename(__FILE__) . COLON . __METHOD__ . AT . __LINE__ . '*/ ' . $spName . '(';
$this->dbEvent = $this->template->dbObjects[PDO_PROCEDURES][$spName][STRING_SP_EVENT_TYPE];
$this->queryVariables = [];
/** @var PDOStatement $statement */
$statement = null;
try {
/*
* we have to check for three different types of stored procedures:
*
* 1. SP with no input parameters (can exec as a non-prepared query
* 2. SP with only IN parameters
* 3. SP with OUT or INOUT parameters (may also have IN params)
*
* each of these three cases has to be coded differently because of the way PDO sets-up input parameters,
* and processes/delivers output results using OUT parameters.
*
* todo: check the return data set and return a row-count in-concurrence with the framework limit
*
*/
if (is_null($spParamList) and !$templateTotalParamCount) {
// if the param list is empty, and there are no return parameters, then this is a simple fetch SP
$this->strQuery = $query . ')';
$this->executeNonPreparedQuery();
if (!$this->status) return;
} elseif ($templateInParamCount and !$templateOutParamCount) {
// We have a stored procedure with only IN params
for ($index = 0; $index < $spParamCount; $index++) {
$query .= '?, ';
}
$query = rtrim($query, ', ');
$query .= ')';
$this->strQuery = $query;
// prepare the PDO query
if (!$this->prepareQuery($statement)) return;
// bind the PDO params
// don't care about the retVal since this SP has no OUT/INOUT params...
@$this->bindSPQV($statement, $spParamList, $spName);
// exec the PDO query
// exec the query
if ($this->useTimers) $startTime = gasStatic::doingTime();
$res = $statement->execute();
if ($this->useTimers) $this->logger->metrics($this->strQuery, gasStatic::doingTime($startTime));
if ($res === false) {
$msg = ERROR_PDO_EXEC . $this->strQuery;
$this->eventMessages[] = $msg;
if ($this->debug) $this->logger->debug($msg);
$this->state = STATE_DB_ERROR;
return;
}
$data = $statement->fetchAll();
foreach ($data as $row) {
$this->queryResults[] = $row;
}
} elseif ($templateOutParamCount) {
// INOUT/OUT stored-procedure
// build the query string
for ($index = 0; $index < $spParamCount; $index++) {
$query .= '?, ';
}
foreach ($outList as $field) {
$query .= AT . $field . ', ';
}
$query = rtrim($query, ', ');
$query .= ')';
$this->strQuery = $query;
// prepare the query
if (!$this->prepareQuery($statement)) return;
// bind the PDO params
$rc = $this->bindSPQV($statement, $spParamList, $spName);
if (!$rc) return; // errors are assigned in bingSPQV()
// exec the query
if ($this->useTimers) $startTime = gasStatic::doingTime();
$res = $statement->execute();
if ($res === false) {
$msg = ERROR_PDO_EXEC . $this->strQuery;
$this->eventMessages[] = $msg;
if ($this->debug) $this->logger->debug($msg);
$this->state = STATE_DB_ERROR;
return;
}
// build the second query to fetch the OUT param data
$query = 'SELECT /* ' . basename(__FILE__) . COLON . __METHOD__ . AT . __LINE__ . '*/ ';
foreach ($outList as $field) {
$query .= AT . $field . ' AS ' . $field . ', ';
}
$query = rtrim($query, ', ');
if ($this->useTimers) $startTime = gasStatic::doingTime();
$this->queryResults = $this->dbcMain->query($query)->fetchAll(PDO::FETCH_ASSOC);
if ($this->useTimers) $this->logger->metrics($query, gasStatic::doingTime($startTime));
// todo -- debug this and see what's stored in $data
} else {
// if code lands here, we've got an error in the submitted parameters
$this->eventMessages[] = ERROR_SP_PARAM_PROC_FAIL;
$this->logger->warn(ERROR_SP_PARAM_PROC_FAIL);
$this->state = STATE_FRAMEWORK_WARNING;
return;
}
$this->dbEvent = DB_EVENT_CALL_SP;
$this->state = STATE_SUCCESS;
$this->status = true;
} catch (PDOException $p) {
$msg = sprintf(ERROR_PDO_EXCEPTION, $query) . COLON . $p->getMessage();
$this->eventMessages[] = $msg;
if (isset($this->logger) and $this->logger->available)
$this->logger->error($msg);
else
consoleLog($this->res, CON_ERROR, $msg);
$this->state = STATE_FRAMEWORK_FAIL;
$this->status = false;
} catch (Throwable $t) {
$msg = ERROR_THROWABLE_EXCEPTION . COLON . $t->getMessage();
$this->eventMessages[] = $msg;
if (isset($this->logger) and $this->logger->available)
$this->logger->error($msg);
else
consoleLog($this->res, CON_ERROR, $msg);
$this->state = STATE_FRAMEWORK_FAIL;
$this->status = false;
}
}
/**
* setMigrationBit() -- public method
*
* This method is available to anyone to toggle the isMigration indicator. The method requires a boolean value
* as the sole input parameter and is enforced through typeErrors.
*
* The method returns void.
*
* @author mike@givingassistant.org
* @version 1.0
*
* @param bool $_bit
*
* HISTORY:
* ========
* 03-24-18 mks CORE-852: initial coding
*
*/
public function setMigrationBit(bool $_bit): void
{
$this->isMigration = $_bit;
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
/// PROTECTED METHODS BEGIN HERE ///
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
protected function _lockRecord()
{
consoleLog($this->res, CON_SYSTEM, ERROR_EMPTY_METHOD);
}
protected function _releaseLock()
{
consoleLog($this->res, CON_SYSTEM, ERROR_EMPTY_METHOD);
}
protected function _isLocked()
{
consoleLog($this->res, CON_SYSTEM, ERROR_EMPTY_METHOD);
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
/// PRIVATE METHODS BEGIN HERE ///
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
/**
* queryBuilder() -- private method
*
* Doc: https://givingassistant.atlassian.net/wiki/display/INF/Namaste+Data+Payloads
*
* this method requires a single input: the array containing the query elements as submitted by the client.
*
* This method has it's own wiki page - please refer to the wiki page for implementation/usage details.
*
* Basically, the query builder both processed and validates the query portion of a client query request.
* (Other methods handle validation of the projection and the extras.)
*
* For each level of the query tree, we start a loop for every element in that level. Each level is validated
* appropriately. For example, at the attribute level, we validate that the attribute is a declared index
* for the current class and, at the value level, we validate that the value type matches the type
* declared for that attribute.
*
* When we're processing a query tree with more than one branch, we store each branch in a collective array until
* we encounter an operand at the attribute level. The presence of the operand declares that the previous
* branches will be joined by this operand. This is stored in $complexQuery.
*
* Any errors, or rules violations, will cause an error message to be generated and copied to the eventMessages
* property and output to the log file, and execution will immediately return back to the calling client and,
* instead of an array return value, a null will be sent back to the client.
*
* The over success/fail of the method can be determined by:
*
* -- state property is set to 'success'
* -- status property is set to true
* -- a non-null array is returned
*
* If all of the above conditions are not met, then query building failed.
*
* -----------------------------------------------------------------------------------------------------------------
* NEW FOR PDO CLASSES: complex queries!
* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
* The PDO version of the query builder supports complex queries in the form of:
*
* -- ( Field(1) { operator } { value } {OPERAND} Field(2) { operator } { value } )
* { OPERAND }
* -- ( Field(3) { operator } { value } {OPERAND} Field(4) { operator } { value } )
* -----------------------------------------------------------------------------------------------------------------
*
*
* @author mike@givingassistant.org
* @version 1.0
*
* @param array $_query
* @return string|null
*
* HISTORY:
* ========
* 09-28-17 mks CORE-572: initial coding (port from gacMongo class)
* 10-24-17 mks CORE-587: fixed processing errors for single-field compound queries
*
*/
private function queryBuilder(array $_query): ?string
{
$this->state = STATE_VALIDATION_ERROR;
$this->status = false;
$this->queryVariables = [];
$query = '';
$queryThreads = null;
$joinOperand = null;
$complexQuery = null;
$haveQuery = false;
$validOperands = [
OPERAND_AND,
OPERAND_OR,
OPERAND_NOT,
OPERAND_NOR,
OPERAND_NULL
];
$opCodes = [
OPERATOR_EQ => PDO_EQ,
OPERATOR_NE => PDO_NE,
OPERATOR_DNE => PDO_NE,
OPERATOR_NIN => PDO_NIN,
OPERATOR_IN => PDO_IN,
OPERATOR_NULL => PDO_NULL,
OPERATOR_NOT_NULL => PDO_NOT_NULL,
OPERATOR_GT => PDO_GT,
OPERATOR_GTE => PDO_GTE,
OPERATOR_LTE => PDO_LTE,
OPERATOR_LT => PDO_LT,
OPERAND_AND => OPERAND_AND,
OPERAND_NOT => OPERAND_NOT,
OPERAND_OR => OPERAND_OR,
OPERAND_NOR => OPERAND_NOR
];
// validate the query array as an array
if (!is_array($_query)) {
$msg = ERROR_DATA_MISSING_ARRAY . STRING_QUERY;
$this->eventMessages[] = $msg;
$this->logger->data($msg);
return null;
}
// start parsing the array beginning with the attribute elements
foreach ($_query as $attribute => $operandLevel) { // (L1 -> L2)
$tmp = null;
$fieldName = '';
// not the first iteration so store the last built query thread
if (!empty($query)) {
$queryThreads[] = $query;
$query = '';
}
// test to see if we're working with a join operand instead of an attribute... if so, then we want
// to join all the previously-stored query strings with this join operand into a single query string...
if (in_array($attribute, $validOperands) and is_null($operandLevel)) {
// set the query join operand to create a compound or complex query
$joinOperand = $attribute;
// // join two groups of complex queries todo -- this next line is the one you want to remove
// if (strlen($complexQuery) and empty($queryThreads)) $complexQuery .= ' ' . $joinOperand . ' ';
// join the query threads to create a complex query
if (is_array($queryThreads)) {
for ($index = 0, $cc = count($queryThreads); $index < $cc; $index++) {
if (empty($complexQuery)) {
$complexQuery = $queryThreads[$index];
} else {
$complexQuery .= ' ' . $joinOperand . ' ' . $queryThreads[$index];
}
}
}
$queryThreads = null;
break;
}
// step 1 -- is in the field list?
if (in_array(($attribute . $this->ext), $this->fieldList)) {
// Check to see if this is non-mapped value with the extension...
$fieldName = $attribute . $this->ext;
} elseif (in_array($attribute, $this->fieldList)) {
// .. of if the extension was included...
$fieldName = $attribute;
} elseif (in_array($attribute, $this->cacheMap)) {
// or if the user submitted the cache-mapped field alias
$fieldName = array_search($attribute, $this->cacheMap);
}
// return if we couldn't resolve the attribute (field) name
if ($fieldName == '') {
$msg = sprintf(ERROR_QB_ATTRIBUTE_404, $attribute, $this->class);
$this->eventMessages[] = $msg;
$this->logger->data($msg);
$this->logger->data(json_encode($_query));
$this->state = STATE_DATA_ERROR;
return null;
}
// validate that the fieldName is an indexed discriminant -- if not, reject the query
if (!in_array($fieldName, $this->indexes)) {
$msg = sprintf(ERROR_QB_NOT_INDEXED_KEY, $attribute, $this->class);
$this->eventMessages[] = $msg;
$this->logger->data($msg);
$this->state = STATE_INDEX_ERROR;
return null;
}
// we've validated the attribute name - move into the operand processing
foreach ($operandLevel as $operand => $operatorLevel) { // (L2 -> L3)
// for the attribute, drop down (one level) and validate the operand
if (!in_array($operand, $validOperands)) {
// operand is invalid
$msg = ERROR_QB_INVALID_OPERAND . $operand;
$this->eventMessages[] = $msg;
$this->logger->error($msg);
$this->state = STATE_DATA_ERROR;
return null;
} elseif (count($operatorLevel) != 1 and $operand == OPERAND_NULL) {
// cannot submit a null operand with more than one operator => value elements
$msg = sprintf(ERROR_QB_VALUE_COUNT, $operand, 1);
$this->eventMessages[] = $msg;
$this->logger->error($msg);
$this->state = STATE_DATA_ERROR;
return null;
} else {
// remember the current operator (level 2)
$op = ($operand == OPERAND_NULL) ? OPERAND_NULL : $operand;
}
// process the operators (level 3) and their values (level 4)
$query = '(';
$loopCounter = count($operatorLevel);
$currentCount = 1;
foreach ($operatorLevel as $operator => $values) { // (L3 -> L4)
if (empty($opCodes[$operator])) {
$hdr = basename(__METHOD__) . AT . __LINE__ . COLON;
$msg = ERROR_QB_UNKNOWN_OPERATOR . $operator;
$this->logger->data($hdr . $msg);
$this->eventMessages[] = $msg;
$this->state = STATE_DATA_ERROR;
return null;
}
$query .= $fieldName . ' ' . $opCodes[$operator] . ' ';
if ($opCodes[$operator] == PDO_NULL or $opCodes[$operator] == PDO_NOT_NULL) {
$haveQuery = true;
} else {
if ($opCodes[$operator] == PDO_IN or $opCodes[$operator] == PDO_NIN) {
$query .= '(';
}
foreach ($values as $value) {
if (gettype($value) != $this->fieldTypes[$fieldName]) {
$msg = sprintf(ERROR_DATA_TYPE_MISMATCH_DETAILS, $fieldName, $this->fieldTypes[$fieldName], gettype($value));
$this->logger->data(ERROR_DATA_FIELD_DROPPED);
$this->logger->data($msg);
$this->eventMessages[] = ERROR_DATA_FIELD_DROPPED;
$this->eventMessages[] = $msg;
continue 2;
}
// $query .= ($currentCount < $loopCounter) ? '?, ' : '?';
$query .= '?, ';
$this->queryVariables[] = $value;
$haveQuery = true;
}
$query = rtrim($query, ', ');
if ($opCodes[$operator] == PDO_IN or $opCodes[$operator] == PDO_NIN) {
$query .= ')';
}
}
if ($currentCount++ < $loopCounter) {
$query .= ' ' . $op . ' ';
}
}
$query = rtrim($query, ', ');
$query .= ')';
}
}
if ($haveQuery) {
$this->state = STATE_SUCCESS;
$this->status = true;
} else {
$this->state = STATE_DATA_ERROR;
}
return($complexQuery ?? $query);
}
/**
* buildGroupOrderBy() -- private method
*
* This method builds both the group and order-by clauses of the query. The method requires two input parameters,
* both of which are arrays and that represent the broker payloads for the group-by and order-by payloads,
* respectively. (Note that the input parameters are vetted as arrays in the calling client code.)
*
* First, we build the group-by piece of the query -- both query discriminants have the same format
* requirements (with the exception of the WITH ROLLUP option for group-by), and both can contain multiple
* fields.
*
* Each field is handed off to a sub-routine in the core that validates the field as a class member and returns
* the fully-qualified field name if processing completes successfully.
*
* For any error that would prevent successful generation/processing for either query discriminant, we log the
* processing/validation error and immediately return processing control to the calling client.
*
* All fields are also required to be index fields for the class. Not being an indexed field will cause the
* request to be rejected.
*
* If we have a valid group-by and no order-by, we're going to assign a null value to the order-by query so
* that, in the calling client, we append "ORDER BY null" to the end of the group-by query so as to reduce the
* overhead of group-by sorting.
*
* The method does not explicitly return any value - however, if processing was successfully completed, then
* we set the class state to STATE_SUCCESS -- which the calling client should validate on return from this method.
*
* @author mike@givingassistant.org
* @version 1.0
*
* @param array|null $_gb
* @param array|null $_ob
*
* HISTORY:
* ========
* 10-09-17 mks original coding
* 10-24-17 mks CORE-587: A parameter declaration can be made to accept null if the default value
* of the parameter is set to null
* 12-05-18 mks DB-55: fixed bug - it's ok to pass-in an empty group-by with a not-empty order-by
*
*/
private function buildGroupOrderBy(array $_gb = null, array $_ob = null): void
{
// groupBy: validate data and process
if (empty($_gb)) {
$this->queryGroupBy = null; // nothing to do so do nothing
} elseif (is_array($_gb) and count($_gb) != 1) {
$msg = sprintf(ERROR_DATA_ARRAY_COUNT, 1, STRING_GROUP_BY_DATA, count($_gb));
$this->eventMessages[] = $msg;
if ($this->debug) $this->logger->debug($msg);
return;
} elseif (is_array($_gb) and !empty($_gb)) {
// get the gb-key and validate as a valid column/member
foreach ($_gb as $field => $value) {
// check for the with-rollup option; if exists, should be last array item so exit loop
if (strtoupper($field) == STRING_WITH_ROLLUP) {
$this->queryGroupBy .= ' ' . STRING_WITH_ROLLUP;
break;
}
try {
$gbKey = $this->cmFieldValidation($field);
} catch (TypeError $t) {
$msg = ERROR_TYPE_EXCEPTION . COLON . $t->getMessage();
$this->eventMessages[] = $msg;
if (isset($this->logger) and $this->logger->available)
$this->logger->error($msg);
else
consoleLog($this->res, CON_ERROR, $msg);
$this->state = STATE_FRAMEWORK_FAIL;
$this->status = false;
return;
}
if (is_null($gbKey)) return;
// ensure that the group-by field is an indexed column
if (!in_array($gbKey, $this->indexes)) {
$msg = sprintf(ERROR_GB_NOT_INDEXED_KEY, $gbKey, $this->class);
$this->eventMessages[] = $msg;
if ($this->debug) $this->logger->debug($msg);
$this->queryGroupBy = null;
return;
}
// validate the GB discriminants in the submitted payload
$this->queryGroupBy .= (empty($this->queryGroupBy)) ? $gbKey : ', ' . $gbKey;
if (!empty($_gb[$gbKey]) and strtoupper($_gb[$gbKey]) == STRING_SORT_ASC or strtoupper($_gb[$gbKey]) == STRING_SORT_DESC) {
$this->queryGroupBy .= $_gb[$gbKey];
}
}
}
// process order-by
if (is_null($_ob) or empty($_ob)) {
if (is_null($this->queryOrderBy)) {
$this->queryGroupBy = null;
$this->state = STATE_SUCCESS;
return;
}
// To avoid the overhead of sorting that GROUP BY produces, add ORDER BY NULL:
$this->queryOrderBy .= ' ORDER BY null';
$this->queryGroupBy = null;
$this->state = STATE_SUCCESS;
return;
} elseif (is_array($_ob)) {
foreach ($_ob as $field => $value) {
try {
$obKey = $this->cmFieldValidation($field);
} catch (TypeError $t) {
$msg = ERROR_TYPE_EXCEPTION . COLON . $t->getMessage();
$this->eventMessages[] = $msg;
if (isset($this->logger) and $this->logger->available)
$this->logger->error($msg);
else
consoleLog($this->res, CON_ERROR, $msg);
$this->state = STATE_FRAMEWORK_FAIL;
$this->status = false;
return;
}
if (is_null($obKey)) return;
// ensure that the order-by column is an indexed column
if (!in_array($obKey, $this->indexes)) {
$msg = sprintf(ERROR_GB_NOT_INDEXED_KEY, $obKey, $this->class);
$this->eventMessages[] = $msg;
if ($this->debug) $this->logger->debug($msg);
$this->queryOrderBy = null;
return;
}
$this->queryOrderBy .= (empty($this->queryOrderBy)) ? $obKey : ', ' . $obKey;
if (!empty($_ob[$obKey]) and strtoupper($_ob[$field]) == STRING_SORT_ASC or strtoupper($_ob[$field]) == STRING_SORT_DESC) {
$this->queryOrderBy .= ' ' . $_ob[$field];
} else {
$msg = ERROR_DATA_ORDER_BY_INVALID_VALUE . $value;
$this->eventMessages[] = $msg;
if ($this->debug) $this->logger->debug($msg);
return;
}
}
} else {
$msg = ERROR_DATA_ARRAY_NOT_ARRAY . STRING_ORDER_BY_DATA;
$this->eventMessages[] = $msg;
if ($this->debug) $this->logger->debug($msg);
return;
}
$this->state = STATE_SUCCESS;
}
/**
* buildReturnFieldList() -- private method
*
* This method is called from any generic CRUD query and is used to build an exact list of fieldNames to be
* returned from the class table. The input parameter to the method is the list of field names that was generated
* by the client - this list can be empty or an indexed array of field names.
*
* Since we don't know if we're dealing with cache-mapped names, or names that may or may-not have the extension
* properly appended, we're going to validate each field name against the cacheMap, the exposedFields list,
* or, finally, just the $fieldList. We'll also try to find the field using the provided field name with and
* without the class extension.
*
* If the $_fields data passed is empty, then were going to return the mySQL wildcard as the return string to
* return all fields. (Note that restricted fields will be filtered by the view invoked.)
*
* If the $_fields data passed is an array, iterate through every field name and validate it against the
* various member field lists until found. If the field name was not found, generate an event message.
*
* If not fields qualified, the method returns a null - otherwise it returns a string that will be used in
* the query.
*
*
* @author mike@givingassistant.org
* @version 1.0
*
* @param array|null $_fields
* @return null|string
*
* HISTORY:
* ========
* 10-05-17 mks CORE-584: original coding
*
*/
private function buildReturnFieldList(array $_fields = null): ?string
{
$rds = ''; // return data set
if (is_null($_fields)) {
// return all the fields
return '*';
}
if (!is_array($_fields)) {
$this->eventMessages[] = ERROR_DATA_MISSING_ARRAY . STRING_RETURN_DATA;
if ($this->debug) $this->logger->debug(ERROR_DATA_MISSING_ARRAY . STRING_RETURN_DATA);
return null;
}
foreach ($_fields as $columnName) {
$found = false;
// auto-filter out the id/id_{ext} field if supplied
if ($columnName == PDO_ID or $columnName == (PDO_ID . $this->ext)) continue;
if ($this->useCache and in_array($columnName, $this->cacheMap)) {
$field = array_search($columnName, $this->cacheMap);
$rds .= (empty($rds)) ? $field : COMMA . $field;
$found = true;
} elseif (!empty($this->exposedFields)) {
if (in_array($columnName, $this->exposedFields)) {
$found = true;
$rds .= (empty($rds)) ? $columnName : COMMA . $columnName;
} elseif (in_array(($columnName . $this->ext), $this->exposedFields)) {
$found = true;
$rds .= (empty($rds)) ? ($columnName . $this->ext) : COMMA . ($columnName . $this->ext);
}
} else {
if (in_array($columnName, $this->fieldList)) {
$found = true;
$rds .= (empty($rds)) ? $columnName : COMMA . $columnName;
} elseif (in_array(($columnName . $this->ext), $this->fieldList)) {
$found = true;
$rds .= (empty($rds)) ? ($columnName . $this->ext): COMMA . ($columnName . $this->ext);
}
}
if (!$found) {
$msg = sprintf(ERROR_DATA_INVALID_CLASS_MEMBER, $columnName, $this->class);
$this->eventMessages[] = $msg;
if ($this->debug) $this->logger->debug($msg);
}
}
return($rds ?? null);
}
/**
* loadTemplate() -- private method
*
* this method is invoked by the constructor and serves to load the class template file, assimilating it into
* the current instantiation.
*
* template loads are done on the schema-instantiation level, instead of the core, because of the tiny changes in
* the template file(s) across various schemas. In other words, even though this code looks similar to the
* similarly-named methods in the other instantiation classes, it is not; do not move this method to the core.
*
* the method will load the class template and set the class member variables controlled/referenced by the
* template.
*
* successful loading of the template is determined by the return (boolean) value -- on error, a log message
* will be generated so it's up to the developer to check logs on fail-returns to see why their template
* file was not correctly assimilated.
*
* The template to be loaded is first derived in the constructor (post validation that the template file
* exists) and is pulled from the member variable (also set in the constructor) within this method.
*
*
* @author mike@givingassistant.org
* @version 1.0
*
* @return bool
*
* HISTORY:
* ========
* 09-13-17 mks CORE-562: original coding
* 09-19-17 mks CORE-562: removing some field population b/c now handled in buildIndexReference()
* 06-08-18 mks CORE-1035: added migration and warehouse configuration data
* 06-12-18 mks CORE-1043: removed sql from dbObject in template prior to class assignment so as to
* maintain schema obfuscation.
* 05-09-18 mks DB-116: support for PDO view templates at load-time -- depending on the client type
* in $metaPayload, we assign the preferred view name for queries.
*/
private function loadTemplate():bool
{
try {
/** @var gatTestPDO template */
$this->template = new $this->templateName;
} catch (Throwable $t) {
$msg = ERROR_THROWABLE_EXCEPTION . COLON . $t->getMessage();
$this->eventMessages[] = $msg;
if (isset($this->logger) and $this->logger->available)
$this->logger->warn($msg);
else
consoleLog($this->res, CON_ERROR, $msg);
$this->state = STATE_FRAMEWORK_FAIL;
return false;
}
if (!is_object($this->template)) {
$this->logger->warn(ERROR_FILE_404 . $this->templateName);
$this->setState(ERROR_FILE_404 . $this->templateName);
return false;
}
if ($this->template->schema != TEMPLATE_DB_PDO) {
$this->logger->warn(ERROR_SCHEMA_MISMATCH . $this->template->schema . ERROR_STUB_EXPECTING . TEMPLATE_DB_PDO);
$this->setState(ERROR_SCHEMA_MISMATCH . $this->templateName);
return false;
}
// transfer meta data info to current instantiation
$this->dbService = $this->template->service;
$this->schema = $this->template->schema;
$this->collectionName = $this->template->collection . $this->template->extension;
// in this next section we establish the PDO view based on the requesting client type
if (isset($this->metaPayload) and isset($this->metaPayload[META_CLIENT])) {
switch ($this->metaPayload[META_CLIENT]) {
case CLIENT_AUDIT :
case CLIENT_SYSTEM :
$this->queryTable = PDO_VIEW_AUDIT . $this->collectionName;
break;
case CLIENT_CLIENT :
case CLIENT_CSR :
case CLIENT_UNIT :
case CLIENT_API :
default :
$this->queryTable = PDO_VIEW_BASIC . $this->collectionName;
break;
}
} else {
$this->eventMessages[] = ERROR_META_CLIENT_404;
return false;
}
$this->ext = $this->template->extension;
$this->useCache = $this->template->setCache;
$this->useDeletes = $this->template->setDeletes;
$this->useAuditing = $this->template->setAuditing;
$this->useJournaling = $this->template->setJournaling;
$this->allowUpdates = $this->template->setUpdates;
$this->useDetailedHistory = $this->template->setHistory;
$this->defaultStatus = $this->template->setDefaultStatus;
$this->searchStatus = $this->template->setSearchStatus;
$this->useLocking = $this->template->setLocking;
$this->useTimers = ($this->template->setTimers and gasConfig::$settings[CONFIG_DATABASE][CONFIG_DATABASE_QUERY_TIMERS]);
$this->pKey = $this->template->setPKey;
$this->useToken = $this->template->setTokens;
$this->debug = gasConfig::$settings[CONFIG_DEBUG];
$this->cacheExpiry = $this->template->cacheTimer;
// todo -- evaluate if we want to continue using the hidden-field functionality
if (isset($this->template->fields) and is_array($this->template->fields)) {
foreach ($this->template->fields as $key => $value) {
// if (!in_array($key, $this->hiddenColumns)) {
// $this->fieldList[] = ($key . $this->ext);
$this->fieldTypes[($key . $this->ext)] = $value;
// }
}
}
if (isset($this->template->protectedFields) and is_array($this->template->protectedFields)) {
foreach ($this->template->protectedFields as $field) {
if ($field != PDO_ID) {
$this->protectedFields[] = ($field . $this->ext);
}
}
}
if (isset($this->template->compoundIndexes) and is_array($this->template->compoundIndexes)) {
foreach ($this->template->compoundIndexes as $key => $subArray) {
foreach ($subArray as $keys) {
$this->compoundIndexes[$key][] = ($keys . $this->ext);
}
}
}
if (!is_null($this->template->cacheMap) and is_array($this->template->cacheMap)) {
foreach ($this->template->cacheMap as $key => $value) {
$this->cacheMap[($key . $this->ext)] = $value;
}
} else {
$this->cacheMap = [];
if (!empty($this->template->exposedFields) and is_array($this->template->exposedFields)) {
foreach ($this->template->exposedFields as $key) {
$this->exposedFields[] = ($key . $this->ext);
}
}
}
if (!is_null($this->template->binFields) and is_array($this->template->binFields)) {
foreach ($this->template->binFields as $key) {
$this->binaryFields[] = ($key . $this->ext);
}
}
// migration data
if (isset($this->template->migrationMap) and is_array($this->template->migrationMap)) {
$this->migrationConfig[STRING_MIGRATION_MAP] = $this->template->migrationMap;
} else {
$this->migrationConfig[STRING_MIGRATION_MAP] = sprintf(INFO_NOT_SET, STRING_MIGRATION_MAP);
}
$this->migrationConfig[STRING_MIGRATION_STATUS_KEY] = (isset($this->template->migrationStatusKV)) ? $this->template->migrationStatusKV : null;
$this->migrationConfig[STRING_MIGRATION_SORT_KEY] = (isset($this->template->migrationSortKey)) ? $this->template->migrationSortKey : null;
// warehouse data
$this->warehouseConfig = (isset($this->template->wareHouse)) ? $this->template->wareHouse : null;
if (is_array($this->template->dbObjects)) {
// remove sql schema -- should only be accessible to the deployment script and never exposed
// in a class instantiation object.
if (isset($this->template->dbObjects[PDO_SQL]) and is_array($this->template->dbObjects[PDO_SQL])) {
unset($this->template->dbObjects[PDO_SQL]);
}
foreach ($this->template->dbObjects[PDO_VIEWS] as &$view) {
if (array_key_exists(STRING_VIEW, $view)) {
unset($view[STRING_VIEW]);
}
}
// copy the cleaned template db objects into the current PDO class:
$this->dbObjects = $this->template->dbObjects;
}
if ($this->template->selfDestruct) {
$this->eventMessages[] = INFO_TEMPLATE_CLASS_DROPPED;
unset($this->template);
}
return true;
}
/**
* buildIndexReference() -- private method
*
* this method looks at the table defined in the current class instantiation and fetches the schema
* information about the table from mysql.
*
* The returned array structure for columns looks like this:
*
* Array
* (
* [Field] => email_usr
* [Type] => varchar(50)
* [Null] => NO
* [Key] => UNI
* [Default] =>
* [Extra] =>
* )
*
* We're looking for the column 'Key' to be not-empty as this indicates that the table column is indexed
* in some way. We're looking for values of UNI or PRI and these fields will populate the uniqueIndexes member.
*
* The second query we launch fetches the indexes from the table - we use this returned data to build the
* $indexes member list.
*
* The data ball we build, and will eventually cache, consists as as associate array of two elements - one
* containing the columns and the other containing the indexes for the class table.
*
* We want to save this information in an associative array so that, when we're parsing
* queries submitted to the mysql service, we can screen the query and prevent the execution of any
* query that does not use the indexed columns of the table.
*
* So, this method builds the following class data members:
*
* $indexes -- an indexed array listing all current indexes in the table regardless of type
* $uniqueIndexes -- an indexed array listing all unique indexes for the table
* $fieldList -- an indexed array listing all the fields (columns) for the table
*
* If query execution generates a mysql error, set a WARN message and return.
*
*
* @author mike@givingassistant.org
* @version 1.0
*
* HISTORY:
* ========
* 09-13-17 mks CORE-562: original coding
* 10-02-17 mks CORE-572: compensating for a mysql shortcoming where compound indexes aren't reported if
* their index-sequence is > 1 resulting in a significant refactoring.
* 03-01-18 mks CORE-689: explicitly setting state/status values
*
*/
private function buildIndexReference()
{
$data = null;
$fields = null;
$indexes = null;
$this->status = true;
$this->state = STATE_SUCCESS;
try {
// generate the cache key appropriate to the class and see if we've already cached this info
// based off a previous instantiation...
$cKey = CACHE_NAMASTE_KEY . '_' . CACHE_MYSQL_TABLE_SCHEMA . '_' . $this->collectionName;
if ($data = gasCache::get($cKey)) {
$data = json_decode(gzuncompress($data), true);
$checksum = $data[STRING_CHECKSUM];
unset($data[STRING_CHECKSUM]);
if (!gasCache::validateChecksum($data, $checksum, $this->eventMessages)) {
$msg = ERROR_CACHE_DATA_CHECKSUM . $cKey;
$this->logger->warn($msg);
$this->eventMessages[] = $msg;
$this->state = STATE_CACHE_ERROR;
$this->status = false;
}
} else {
// if we've not cached, or the cache processing failed, then fetch the data from the db
$this->dbEvent = DB_EVENT_NAMASTE_READ;
$this->strQuery = 'SHOW COLUMNS FROM ' . $this->collectionName;
$this->executeNonPreparedQuery();
if ($this->state != STATE_SUCCESS) {
$this->logger->warn(ERROR_SQL_FTL_COLUMNS);
$this->eventMessages[] = ERROR_SQL_FTL_INDEXES;
return;
} else {
foreach ($this->queryResults as $row) {
$data[STRING_COLUMNS][] = $row;
}
}
$this->strQuery = 'SHOW INDEX FROM ' . $this->collectionName;
$this->executeNonPreparedQuery();
if ($this->state != STATE_SUCCESS) {
$this->logger->warn(ERROR_SQL_FTL_INDEXES);
$this->eventMessages[] = ERROR_SQL_FTL_INDEXES;
return;
} else {
foreach ($this->queryResults as $row) {
$data[STRING_INDEXES][] = $row;
}
}
if (is_null(gasCache::add($cKey, gzcompress(json_encode($data, true))))) {
$this->state = STATE_CACHE_ERROR;
$this->status = false;
$msg = ERROR_CACHE_ADD_FAIL . $cKey;
$this->eventMessages[] = $msg;
$this->logger->warn($msg);
return;
}
}
// build the fieldList if we have data
if (!empty($data) and is_array($data)) {
foreach ($data[STRING_COLUMNS] as $row) {
$this->fieldList[] = $row[MYSQL_COLUMN_FIELD];
if (!empty($row[MYSQL_COLUMN_KEY])) {
if ($row[MYSQL_COLUMN_KEY] == MYSQL_INDEX_UNIQUE or $row[MYSQL_COLUMN_KEY] == MYSQL_INDEX_PRIMARY) {
$this->uniqueIndexes[] = $row[MYSQL_COLUMN_FIELD];
}
}
}
foreach ($data[STRING_INDEXES] as $row) {
if (!in_array($row[MYSQL_COLUMN_NAME], $this->indexes)) {
$this->indexes[] = $row[MYSQL_COLUMN_NAME];
}
}
} else {
$this->state = STATE_DATA_ERROR;
$this->status = false;
}
} catch (Throwable $t) {
$msg = ERROR_THROWABLE_EXCEPTION . COLON . $t->getMessage();
$this->eventMessages[] = $msg;
if (isset($this->logger) and $this->logger->available)
$this->logger->error($msg);
else
consoleLog($this->res, CON_ERROR, $msg);
$this->state = STATE_FRAMEWORK_FAIL;
$this->status = false;
}
}
/**
* setRowsReturnedLimit() -- private method
*
* this function is called in the constructor for the current table instantiation.
*
* it looks at the information_schema table to get the average_row_length (arl) value for the table.
* this is a gross calculation - the more data in the table, the more accurate the value.
*
* if we can get the arl value from the information_schema, then divide this number into the system
* constant (max_data_returned) to see if the result is less-than or equal-to the system constant for the
* number of rows returned per query...
*
* if the calculated value is smaller, then allow the system constants to remain -- if not, adjust the system
* constant for the max_rows_returned so that the total amount of data remains under the system constant
* max_data_returned.
*
* @author mike@givingassistant.org
* @version 1.0
*
* HISTORY:
* ========
* 09-20-17 mks CORE-562: original coding
*
*/
private function setRowsReturnedLimit()
{
$key = PDO_DATA_DEFINITION . '_' . PDO_AVG_ROW_LEN . '_' . $this->collectionName;
$cacheData = null;
$this->dbEvent = MYSQL_EVENT_META;
try {
if ($cacheData = gasCache::get($key)) {
$cacheData = json_decode(gzuncompress($cacheData), true);
$this->recordLimit = $cacheData[PDO_RECORDS_PER_PAGE];
} else {
$schema = gasConfig::$settings[CONFIG_ID][CONFIG_ID_ENV] . UDASH;
$schema .= $this->config[CONFIG_DATABASE_PDO_APPSERVER][CONFIG_DATABASE_PDO_MASTER][CONFIG_DATABASE_PDO_DB];
// create the admin query
$this->strQuery = '-- noinspection SqlDialectInspection
SELECT AVG_ROW_LENGTH
FROM information_schema.tables
WHERE table_schema = "' . $schema . '"
AND table_name = "' . $this->collectionName . '"';
$this->dbEvent = DB_EVENT_NAMASTE_READ;
$this->executeNonPreparedQuery();
if ($this->state) {
if (($this->rowsAffected === 1) and (isset($this->queryResults[0][MYSQL_AVG_ROW_LENGTH]))) {
$arl = $this->queryResults[0][MYSQL_AVG_ROW_LENGTH];
if (($arl * $this->recordLimit) > MYSQL_MAX_DATA_RETURNED) {
$this->recordLimit = intval(MYSQL_MAX_DATA_RETURNED / $arl);
}
}
$cacheData[PDO_RECORDS_PER_PAGE] = $this->recordLimit;
if (!gasCache::add(PDO_DATA_DEFINITION . '_' . PDO_AVG_ROW_LEN . '_' . $this->collectionName, gzcompress(json_encode($cacheData, true)), gasCache::$cacheTTL)) {
$this->logger->warn('memcache:set failed - check log files');
}
}
}
} catch (Throwable $t) {
$msg = ERROR_THROWABLE_EXCEPTION . COLON . $t->getMessage();
$this->eventMessages[] = $msg;
if (isset($this->logger) and $this->logger->available)
$this->logger->error($msg);
else
consoleLog($this->res, CON_ERROR, $msg);
$this->state = STATE_FRAMEWORK_FAIL;
$this->status = false;
}
}
/**
* executeNonPreparedQuery() -- private method
*
* this is the main method to execute all META and SELECT queries - any query that is not a prepared statement
* will execute here. Basically, namaste queries.
*
* upon invocation, the string passed (implicitly through the member variable: $query) will be cleaned through
* the common function, and then we'll evaluate the query based on the setting of the member variable $dbEvent.
* If $dbEvent is not META and not SELECT, then we're going to return with a WARN message requiring the client
* to use the prepared-query method.
*
* Next, parse the query and look for the "?" character - which is used as a place holder in prepared queries,
* and, if found, reject the request and return with a WARN message.
*
* Call a private method to see if the slave server is enabled and, if so, use it if the current query contains
* the SELECT keyword (meta queries will not use SELECT) and return the connection resource to a local variable.
*
* if query timers are enabled, then mark the start time and execute the query. record the end-time and log
* the query through the parent::method().
*
* Make a call to fetch the data as an associative array and post the results, along with the row count, to
* class variables.
*
* if the query generated an mysql error, generate an WARN message and return.
*
*
* @author mike@givingassistant.org
* @version 1.0
*
* HISTORY:
* --------
* 09-13-17 mks CORE-562: original coding
* 11-07-17 mks CORE-588: support for real-slaves
* 11-13-17 mks CORE-588: explicitly setting state/status, added error messages to eventMessages stack
* 11-29-17 mks CORE-593: allowing namaste system-read events to use the pdo slave resource
* 01-16-18 mks CORE-697: attempting to detect dropped PDO resource connection and reconnect
* 05-31-18 mks CORE-1011: update for new XML broker services configuration
*
*/
private function executeNonPreparedQuery(): void
{
$this->status = false;
$dbLink = $this->dbcMain;
$which = DB_MASTER;
// return immediately if this is a prepared query
if (stripos($this->strQuery, '?')) {
$this->eventMessages[] = ERROR_SQL_LOST_PREP_QUERY;
$this->eventMessages[] = $this->strQuery;
$this->logger->warn(ERROR_SQL_LOST_PREP_QUERY);
$this->logger->warn($this->strQuery);
$this->state = STATE_SCHEMA_ERROR;
return;
}
$startTime = floatval(0);
$this->queryResults = [];
// check to see if we can exec this schema command using the read-slave
/** @var $dbLink PDO */
if (gasResourceManager::$PDOSlaveEnabled and gasResourceManager::$PDOSlaveAvailable and ($this->dbEvent == DB_EVENT_SELECT or $this->dbEvent == DB_EVENT_NAMASTE_READ)) {
$dbLink = $this->dbcSlave;
$which = DB_SLAVE;
}
// if we've lost the connect (cannot ping) -- attempt to reconnect
if (!$this->ping($which)) {
$dbLink = gasResourceManager::reconnect($which);
if (is_null($dbLink)) {
$msg = ERROR_PDO_DROPPED;
consoleLog(RES_PDO, CON_ERROR, $msg);
$this->eventMessages[] = $msg;
$this->logger->warn($msg);
$this->state = STATE_DB_ERROR;
return;
}
}
if ($this->debug) {
$this->logger->debug($this->strQuery);
}
// do not clean the query string if this is a system-generated query
if ($this->dbEvent != DB_EVENT_NAMASTE_READ and $this->dbEvent != DB_EVENT_NAMASTE_WRITE and $this->dbEvent != DB_EVENT_NAMASTE_READ) {
$this->strQuery = cleanQueryString($this->strQuery);
}
// last validation -- may change/delete this later depending if we have DESTRUCTIVE, non-prepared queries...
switch($this->dbEvent) {
case DB_EVENT_NAMASTE_READ :
case DB_EVENT_SELECT :
// do nothing
break;
default :
$this->logger->error(ERROR_SQL_NOT_PREP_STMNT);
$this->eventMessages[] = ERROR_SQL_NOT_PREP_STMNT;
return;
break;
}
try {
// start the query timer if supported
if ($this->useTimers) $startTime = gasStatic::doingTime();
$this->queryResults = [];
foreach ($dbLink->query($this->strQuery) as $row) {
$this->queryResults[] = $row;
}
// log the query time if supported
if ($this->useTimers) $this->logger->metrics($this->strQuery, gasStatic::doingTime($startTime));
$this->rowsAffected = (!empty($this->queryResults)) ? count($this->queryResults) : 0;
$this->state = STATE_SUCCESS;
$this->status = true;
} catch (PDOException $p) {
$msg = sprintf(ERROR_PDO_EXCEPTION, $this->strQuery);
$this->eventMessages[] = $msg;
$this->eventMessages[] = $p->getMessage();
if (isset($this->logger) and $this->logger->available) {
$this->logger->warn($msg);
$this->logger->warn($p->getMessage());
} else {
consoleLog($this->res, CON_ERROR, $msg);
consoleLog($this->res, CON_ERROR, $p->getMessage());
}
$this->state = STATE_DB_ERROR;
} catch (Throwable $t) {
$msg = ERROR_THROWABLE_EXCEPTION . COLON . $t->getMessage();
$this->eventMessages[] = $msg;
if (isset($this->logger) and $this->logger->available)
$this->logger->error($msg);
else
consoleLog($this->res, CON_ERROR, $msg);
$this->state = STATE_FRAMEWORK_FAIL;
$this->status = false;
}
}
/**
* executePreparedQuery() -- private method
*
* This method has no input parameters and returns type void. Return values are implicitly returned via class
* members.
*
* The method is responsible for submitting a query, and it's array of positional parameters/variables to the
* PDO API for execution.
*
* On successful processing, we'll populate the $queryResults container with the data for processing by the
* calling client. The state/status variables will be set to indicate success and it's critical that either
* of these variable are evaluated by the calling client upon return.
*
* NOTES:
* ------
* PDO Fetch mode was set in the resourceManager on instantiation and is set to PDO::FETCH_ASSOC
*
*
* @author mike@givingassistant.org
* @version 1.0
*
* HISTORY:
* ========
* 10-10-17 mks CORE-584: original coding
* 10-25-17 mks CORE-587: updated processing of successful results based on the database event type
* 10-27-17 mks CORE-586: update for update event
* 11-06-17 mks CORE-588: support for read-slave
* 11-29-17 mks CORE-593: support for read-slave on namaste-read system events
* 01-11-18 mks INF-147: support for namaste events in switch case
* 11-29-18 mks DB-55: de-janked which PDO resource to request when preparing query
* 01-16-19 mks DB-103: successful query returning no records now returns state = NOT_FOUND
*
*/
private function executePreparedQuery(): void
{
$this->status = false;
$this->state = STATE_DB_ERROR;
/** @var PDOStatement $ptrRecord */
$ptrRecord = null;
$whichPDOService = RES_PDO_MASTER;
if (!stripos($this->strQuery, '?')) {
$this->logger->warn(ERROR_SQL_NOT_PREP_QUERY);
$this->logger->warn($this->strQuery);
return;
}
if ($this->debug) $this->logger->debug($this->strQuery);
$startTime = floatval(0);
$this->queryResults = [];
// check to see if we can exec this schema command using the read-slave
if (gasResourceManager::$PDOSlaveEnabled and gasResourceManager::$PDOSlaveAvailable and ($this->dbEvent == DB_EVENT_SELECT or $this->dbEvent == DB_EVENT_NAMASTE_READ)) {
$whichPDOService = RES_PDO_SECONDARY;
}
try {
// start the query timer
if ($this->useTimers) $startTime = gasStatic::doingTime();
// prepare the query
if (!$this->prepareQuery($ptrRecord, $whichPDOService)) return;
// execute the query
$ptrRecord->execute($this->queryVariables);
// stop the query timer
if ($this->useTimers) $this->logger->metrics($this->strQuery, gasStatic::doingTime($startTime));
// based on query type, load the results
switch ($this->dbEvent) {
case DB_EVENT_UPDATE :
case DB_EVENT_NAMASTE_WRITE :
case DB_EVENT_DELETE :
$this->queryResults[] = sprintf(PDO_UP_DELETE_RESULTS, $ptrRecord->rowCount(), $this->dbEvent);
$this->rowsAffected = $ptrRecord->rowCount();
break;
case DB_EVENT_SELECT :
case DB_EVENT_NAMASTE_READ :
while ($row = $ptrRecord->fetch(PDO::FETCH_ASSOC, PDO::FETCH_ORI_NEXT)) {
$this->queryResults[] = $row;
}
$this->rowsAffected = (is_array($this->queryResults)) ? count($this->queryResults) : 0;
break;
}
$this->state = ($this->rowsAffected > 0) ? STATE_SUCCESS : STATE_NOT_FOUND;
$this->status = true;
} catch (PDOException $p) {
$msg = sprintf(ERROR_PDO_EXCEPTION, $this->strQuery);
consoleLog(RES_PDO, CON_ERROR, $msg);
$this->eventMessages[] = $msg;
$this->eventMessages[] = $p->getMessage();
if (isset($this->logger) and $this->logger->available) {
$this->logger->warn($msg);
$this->logger->warn($p->getMessage());
} else {
consoleLog($this->res, CON_ERROR, $msg);
consoleLog($this->res, CON_ERROR, $p->getMessage());
}
$this->state = STATE_DB_ERROR;
} catch (Throwable $t) {
$msg = ERROR_THROWABLE_EXCEPTION . COLON . $t->getMessage();
$this->eventMessages[] = $msg;
if (isset($this->logger) and $this->logger->available)
$this->logger->error($msg);
else
consoleLog($this->res, CON_ERROR, $msg);
$this->state = STATE_FRAMEWORK_FAIL;
$this->status = false;
}
}
/**
* ping() -- private method
*
* This is a health-check method to ensure that the PDO service (mysql) has not timed-out.
*
* The method takes a single, optional, input parameter which is the MASTER/SLAVE indicator switch. If no
* parameter is provided, then the switch defaults to the mysql master.
*
* We execute a simple query (select 1) and record the results returned. If the value returned is the excepted
* integer value of 1, return a boolean(true) value... otherwise, boolean(false) is returned.
*
* The query is exception wrapped which will trigger logging on the exception and return a boolean(false) back
* to the calling client.
*
*
* @author mike@givingassistant.org
* @version 1.0
*
* @param int $_which
* @return bool
*
*
* HISTORY:
* ========
* 01-16-18 mks CORE-697: Original coding
* 03-01-18 mks CORE-689: fixed bug where ping should have pre-check for the link resource != null
*
*/
private function ping(int $_which = DB_MASTER): bool
{
/** @var PDO $dbLink */
$dbLink = ($_which) ? $this->dbcMain : $this->dbcSlave;
if (!is_null($dbLink)) {
try {
$rc = intval($dbLink->query('SELECT 1')->fetchColumn(0));
return (1 === $rc);
} catch (PDOException $p) {
$msg = sprintf(ERROR_PDO_EXCEPTION, 'SELECT 1');
consoleLog(RES_PDO, CON_ERROR, $msg);
$this->eventMessages[] = $msg;
$this->eventMessages[] = $p->getMessage();
if (isset($this->logger) and $this->logger->available) {
$this->logger->warn($msg);
$this->logger->warn($p->getMessage());
} else {
consoleLog($this->res, CON_ERROR, $msg);
consoleLog($this->res, CON_ERROR, $p->getMessage());
}
$this->state = STATE_DB_ERROR;
return false;
} catch (Throwable $t) {
$msg = ERROR_THROWABLE_EXCEPTION . COLON . $t->getMessage();
$this->eventMessages[] = $msg;
if (isset($this->logger) and $this->logger->available)
$this->logger->error($msg);
else
consoleLog($this->res, CON_ERROR, $msg);
$this->state = STATE_FRAMEWORK_FAIL;
$this->status = false;
}
}
return false;
}
/**
* executePreparedBatchInsertQuery() -- private method
*
* This method is called by the _createRecord() method and is used for all new record inserts regardless of the
* record count (number of new records to insert). There are no input parameters to the method and the method
* returns type void.
*
* All data has been validated prior to reaching this point. So, what we should have ready to go is:
*
* -- the query strings stored as an array in $strQuery member
* -- the data payload stored as an array in $data member
*
* Because mysql is functionally bankrupt when it comes to batch-queries, we've generated an array of prepared
* queries ($this->strQuery), each row containing a number of positional parameters matching the corresponding
* row entry in $data.
*
* Once we've initialized some function variables and handled debug log events, if enabled, we're going
* to enter the exception block to process the prepared query and the new data to be inserted into the table.
*
* After calling the prepare() method on the query, we start the DB transaction and then loop through the records
* stored in $data -- we pull the values from the associative array and pass that array into the query-execute
* method.
*
* If no PDO exception is raised, then we commit the transaction and populate the query results into the
* relevant class members before returning.
*
* Note that, if an exception is trapped, the error output will only show the first query in the $strQuery
* array in the output...
*
* Success in processing is determined by the state/status member variables and, as always, is the calling clients
* responsibility to verify on return.
*
*
* @author mike@givingassistant.org
* @version 1.0
*
*
* HISTORY:
* ========
* 10-23-17 mks CORE-585: original coding
* 01-16-18 mks CORE-697: support for prepareQuery() and console logging
* 03-27-18 mks CORE-852: added recordsInserted and recordsDropped support for migration report
* 05-23-18 mks _INF-188: support for pseudo-batch inserts
* 11-05-18 mks DB-55: support for audit/journaling, cleaned-up exception handling
* 01-16-19 mks DB-103: fixed bug in associative index keys for recovery query
* 01-22-20 mks DB-150: replaced strQuery with aryQuery because PHP7.4 strong type casting for class members
*
*/
private function executePreparedBatchInsertQuery(): void
{
/** @var PDOStatement $res */
$res = null;
$startTime = floatval(0);
if ($this->debug) $this->logger->debug(json_encode($this->aryQuery));
/** @var PDO $dbLink */
$dbLink = $this->dbcMain;
// validate we're working with queries stored as an array in $this->strQuery:
if (!is_array($this->aryQuery)) {
$error = ERROR_DATA_ARRAY_NOT_ARRAY . STRING_STR_QUERY;
$this->logger->error($error);
$this->eventMessages[] = $error;
$this->state = STATE_DATA_ERROR;
$this->status = false;
return;
}
// ...and that each query is a prepared query:
$counter = 0;
foreach ($this->aryQuery as $query) {
if (!stripos($query, '?')) {
$this->logger->warn(ERROR_SQL_NOT_PREP_QUERY);
$this->logger->warn($query);
$this->eventMessages[] = ERROR_SQL_NOT_PREP_QUERY . $query;
$this->state = STATE_FRAMEWORK_WARNING;
$this->status = false;
return;
}
if ($this->useAuditing) {
$t = $this->data[$counter][(DB_TOKEN . $this->ext)];
if ($this->useJournaling) {
$q = [ DB_TOKEN => [ OPERAND_NULL => [ OPERATOR_EQ => [$t]]]]; // DB-103 fix
$u = [ DB_STATUS => STATUS_DELETED ];
if ($this->useDeletes) {
// hard deletes
$this->auditUndoQueries[] = [
BROKER_REQUEST => BROKER_REQUEST_DELETE,
BROKER_DATA => [ STRING_QUERY_DATA => $q ],
BROKER_META_DATA => $this->metaPayload
];
} else {
// soft delete
$this->auditUndoQueries[] = [
BROKER_REQUEST => BROKER_REQUEST_UPDATE,
BROKER_DATA => [ STRING_QUERY_DATA => $q, STRING_UPDATE_DATA => $u ],
BROKER_META_DATA => $this->metaPayload
];
}
}
$this->auditRecordList[] = $t;
$this->auditCreateQueries[] = [
BROKER_REQUEST => BROKER_REQUEST_CREATE,
BROKER_DATA => [$this->data[$counter++]],
BROKER_META_DATA => $this->metaPayload
];
}
}
$this->queryResults = [];
$this->dbEvent = DB_EVENT_BULK_CREATE;
try {
// start the query timer
if ($this->useTimers) $startTime = gasStatic::doingTime();
$dbLink->beginTransaction(); // BEGIN TRANSACTION
$count = 0;
foreach ($this->data as $record) {
/** @var PDOStatement $statement */
$statement = $this->dbcMain->prepare($this->aryQuery[$count++]);
$rc = $statement->execute(array_values($record));
if ($rc)
$this->recordsInserted++;
else
$this->recordsDropped++;
}
$dbLink->commit(); // END TRANSACTION
if ($this->useTimers) $this->logger->metrics($this->aryQuery[0] . ' x' . $count . ' ' . STRING_TIMES, gasStatic::doingTime($startTime));
$this->queryResults[] = sprintf(PDO_BULK_INSERT_RESULTS, $this->recordsInserted, $this->recordsDropped);
$this->state = STATE_SUCCESS;
$this->status = true;
$this->strQuery = json_encode($this->aryQuery);
} catch (PDOException | Throwable $p) {
$dbLink->rollBack();
$hdr = basename(__METHOD__) . AT . __LINE__ . COLON;
$msg = sprintf(ERROR_PDO_EXCEPTION, $this->aryQuery[0]);
consoleLog(RES_PDO, CON_ERROR, $hdr . $msg);
consoleLog(RES_PDO, CON_ERROR, $hdr . $p->getMessage());
$this->eventMessages[] = $msg;
if (isset($this->logger) and $this->logger->available) {
$this->logger->warn($hdr . $msg);
$this->logger->warn($hdr . $p->getMessage());
}
$this->status = false;
$this->state = STATE_DB_ERROR;
}
}
/**
* getPreparedQueryCount() -- private method
*
* This method calculates the total number of rows returned from a prepared query. It's sole purpose is to fetch
* this value in order to aid in query pagination. As such, the query is slightly modified from the original
* query in that the skip/limit is removed, as is the query projection (replaced by count(*)).
*
* this method requires a single input parameter -- the query to be executed -- and returns a Boolean to indicate
* the processing success or the failure.
*
*
* @author mike@givingassistant.org
* @version 1.0
*
* @param $_query
* @return bool
*
*
* HISTORY:
* ========
* 10-13-17 mks CORE-584: original coding
*
*/
public function getPreparedQueryCount(string $_query): bool
{
$startTime = floatval(0);
/** @var $dbLink PDO */
$dbLink = $this->dbcMain;
$this->recordsInQuery = 0;
try {
if ($this->useTimers) $startTime = gasStatic::doingTime();
$ptrRecord = $dbLink->prepare($_query);
$ptrRecord->execute($this->queryVariables);
if ($this->useTimers) $this->logger->metrics($_query, gasStatic::doingTime($startTime));
foreach ($ptrRecord as $row) {
$this->recordsInQuery = $row[STRING_NUM_RECS];
}
if (is_null($this->recordsInQuery) or !is_numeric($this->recordsInQuery)) {
$this->eventMessages[] = ERROR_DATA_REC_COUNT_UNDEF;
if ($this->debug) $this->logger->debug(ERROR_DATA_REC_COUNT_UNDEF);
$this->recordsInQuery = 0;
}
return true;
} catch (PDOException $p) {
$msg = sprintf(ERROR_PDO_EXCEPTION, $_query);
$this->eventMessages[] = $msg;
$this->eventMessages[] = $p->getMessage();
if (isset($this->logger) and $this->logger->available) {
$this->logger->warn(STRING_QUERY . COLON . $_query);
if ($this->debug)
$this->logger->warn(STRING_QUERY_DATA . COLON . var_export($this->queryVariables, true));
$this->logger->warn($p->getMessage());
} else {
consoleLog($this->res, CON_ERROR, STRING_QUERY . COLON . $_query);
if ($this->debug)
consoleLog($this->res, CON_ERROR, var_export($this->queryVariables, true));
consoleLog($this->res, CON_ERROR, $p->getMessage());
}
$this->state = STATE_DB_ERROR;
return false;
} catch (Throwable $t) {
$msg = sprintf(ERROR_EXCEPTION . COLON . $this->strQuery[0]);
consoleLog(RES_PDO, CON_ERROR, $msg);
$this->eventMessages[] = $msg;
$this->eventMessages[] = $t->getMessage();
if (isset($this->logger) and $this->logger->available) {
$this->logger->warn($msg);
$this->logger->warn($t->getMessage());
} else {
consoleLog($this->res, CON_ERROR, $msg);
consoleLog($this->res, CON_ERROR, $t->getMessage());
}
$this->status = false;
$this->state = STATE_DB_ERROR;
}
return false;
}
/**
* getQueryCount() -- private method
*
* This method is called to calculate the total number of rows returned by a previously-executed non-prepared
* query. We've removed the projection data from the query, replacing it with: count(*), and removed the
* skip/limit discriminant.
*
* The method requires a single, string-value, input parameter which is the modified (to get the row count) query.
* The method returns a boolean indicating processing success or failure.
*
*
* @author mike@givingassistant.org
* @version 1.0
*
* @param string $_query
* @return bool
*
*
* HISTORY:
* ========
* 10-13-17 mks original coding
*
*/
private function getQueryCount(string $_query): bool
{
$startTime = floatval(0);
/** @var $dbLink PDO */
$dbLink = $this->dbcMain;
try {
if ($this->useTimers) $startTime = gasStatic::doingTime();
// fetchColumn returns the value of a single column of a single row
$this->recordsInQuery = $dbLink->query($_query)->fetchColumn();
if ($this->useTimers) $this->logger->metrics($_query, gasStatic::doingTime($startTime));
return true;
} catch (PDOException $p) {
$msg = sprintf(ERROR_PDO_EXCEPTION, $_query);
$this->eventMessages[] = $msg;
$this->eventMessages[] = $p->getMessage();
if (isset($this->logger) and $this->logger->available) {
$this->logger->warn(STRING_QUERY . COLON . $_query);
if ($this->debug)
$this->logger->warn(STRING_QUERY_DATA . COLON . var_export($this->queryVariables, true));
$this->logger->warn($p->getMessage());
} else {
consoleLog($this->res, CON_ERROR, STRING_QUERY . COLON . $_query);
if ($this->debug)
consoleLog($this->res, CON_ERROR, var_export($this->queryVariables, true));
consoleLog($this->res, CON_ERROR, $p->getMessage());
}
$this->state = STATE_DB_ERROR;
return false;
} catch (Throwable $t) {
$msg = sprintf(ERROR_EXCEPTION . COLON . $this->strQuery[0]);
consoleLog(RES_PDO, CON_ERROR, $msg);
$this->eventMessages[] = $msg;
$this->eventMessages[] = $t->getMessage();
if (isset($this->logger) and $this->logger->available) {
$this->logger->warn($msg);
$this->logger->warn($t->getMessage());
} else {
consoleLog($this->res, CON_ERROR, $msg);
consoleLog($this->res, CON_ERROR, $t->getMessage());
}
$this->status = false;
$this->state = STATE_DB_ERROR;
}
return false;
}
/**
* getRecordCount() -- private method
*
* this method is mainly invoked from the _fetchRecords() query -- since the client wants to know how many
* records there are in a collection whenever a read-query is performed, we're going to get that information
* within this method.
*
* There are no input parameters to the method and the method will return a boolean value to indicate the success
* or failure of executing the query.
*
* NOTES:
* ------
* todo -- We're using the table (collection) name and not the basic view, this should be changed to match the query
* todo -- should we be querying the read-slaves?
* If the table has hard-deletes enabled, filter the record count against the DELETED status
* Errors generated are stored in the event messages and are written to the logs as WARN level events
*
* @author mike@givingassistant.org
* @version 1.0
*
* @return bool
*
* HISTORY:
* ========
* 10-10-17 mks CORE-584: original coding
*
*/
private function getActiveRecordCount(): bool
{
/** @var $dbLink PDO */
$dbLink = $this->dbcMain;
$query = "SELECT /* " . basename(__FILE__) . COLON . __METHOD__ . COLON . __LINE__ . ' */ ';
$query .= 'count(1) AS recordCount FROM ' . $this->collectionName;
if (!$this->useDeletes) { // hard deletes
$query .= ' WHERE status' . $this->ext . ' ' . PDO_NE . ' "' . STATUS_DELETED . '"';
}
try {
// fetchColumn returns the value of a single column of a single row
$this->recordsInCollection = $dbLink->query($query)->fetchColumn();
return true;
} catch (PDOException $p) {
$msg = sprintf(ERROR_PDO_EXCEPTION, $this->strQuery);
$this->eventMessages[] = $msg;
$this->eventMessages[] = $p->getMessage();
if (isset($this->logger) and $this->logger->available) {
$this->logger->warn($msg);
$this->logger->warn($p->getMessage());
} else {
consoleLog($this->res, CON_ERROR, $msg);
consoleLog($this->res, CON_ERROR, $p->getMessage());
}
$this->state = STATE_DB_ERROR;
return false;
} catch (Throwable $t) {
$msg = sprintf(ERROR_EXCEPTION . COLON . $this->strQuery[0]);
consoleLog(RES_PDO, CON_ERROR, $msg);
$this->eventMessages[] = $msg;
$this->eventMessages[] = $t->getMessage();
if (isset($this->logger) and $this->logger->available) {
$this->logger->warn($msg);
$this->logger->warn($t->getMessage());
} else {
consoleLog($this->res, CON_ERROR, $msg);
consoleLog($this->res, CON_ERROR, $t->getMessage());
}
$this->status = false;
$this->state = STATE_DB_ERROR;
}
return false;
}
/**
* prepareDataForInsert() -- private method
*
* This method is called when we're preparing a data payload for insert into the database. Because the SQL create
* command doesn't allow for column variance, we're going to pad missing data elements with null values creating,
* in effect, a homogeneous data payload so that we can execute a batch-insert command in one query.
*
*
* @author mike@givingassistant.org
* @version 1.0
*
*
* HISTORY:
* ========
* 10-23-17 mks CORE-585: original coding
*
*/
private function prepareDataForInsert(): void
{
$data = [];
$counter = 0;
foreach ($this->data as &$record) {
foreach ($this->fieldList as $field) {
if ($field != (PDO_ID . $this->ext)) {
if (!array_key_exists($field, $record)) {
$record[$field] = null;
if ($this->debug) $this->logger->debug(sprintf(INFO_INSERTED_FIELD, $field));
}
$data[$counter][$field] = $record[$field];
}
}
$counter++;
}
$this->data = $data;
}
/**
* buildWhereDiscriminant() -- private method
*
* This method is called from the update and delete methods. The method requires a single input parameter:
*
* $_queryData: the query discriminant array passed in from the client
*
* This is a gateway function to queryBuilder() but is encapsulated in a TypeError exception trap and, to avoid
* code duplication in the two calling clients was broken-out into it's own method.
*
* The method returns a boolean value to indicate successful processing.
*
*
* @author mike@givingassistant.org
* @version 1.0
*
* @param array $_queryData
* @return bool
*
* HISTORY:
* ========
* 10-26-17 mks CORE-586: original coding
*
*/
private function buildWhereDiscriminant(array $_queryData): bool
{
$rc = false;
try {
$this->where = $this->queryBuilder($_queryData);
$rc = true;
} catch (TypeError $t) {
$msg = ERROR_TYPE_EXCEPTION . COLON . $t->getMessage();
$this->eventMessages[] = $msg;
if (isset($this->logger) and $this->logger->available)
$this->logger->error($msg);
else
consoleLog($this->res, CON_ERROR, $msg);
$this->state = STATE_FRAMEWORK_WARNING;
}
return $rc;
}
/**
* buildOrderBy() -- private method
*
* This method is called by the update and delete methods and is used to create the order-by part of a query.
*
* The method requires a single input parameter -- this is the order-by data payload as built by the end-user.
*
* The method is a gateway to the buildGroupOrderBy() method but, because it is exception wrapped for TypeErrors,
* it was moved to this method to reduce code duplication.
*
* The method returns a boolean to indicate success or fail in processing.
*
*
* @author mike@givingassistant.org
* @version 1.0
*
* @param array $_orderByData
* @return bool
*
* HISTORY:
* ========
* 10-26-17 mks CORE-586: original coding
*
*/
private function buildOrderBy(array $_orderByData): bool
{
$rc = false;
try {
$this->buildGroupOrderBy(null, $_orderByData);
$rc = true;
} catch (TypeError $t) {
$msg = ERROR_TYPE_EXCEPTION . COLON . $t->getMessage();
$this->eventMessages[] = $msg;
if (isset($this->logger) and $this->logger->available)
$this->logger->error($msg);
else
consoleLog($this->res, CON_ERROR, $msg);
$this->status = false;
$this->state = STATE_FRAMEWORK_WARNING;
}
return $rc;
}
/**
* prepareQuery() -- private method
*
* This method is used from the stored-procedure/function builder methods and is used to submit a query
* to the pdo::prepare() method.
*
* The method accepts two input parameters. One of which is a PDO Statement class object, generated
* success or failure of successfully preparing the query. The other indicates which PDO resource to use
* for the query where the resource is defined in the dbCatalog.php file as either DB_MASTER (1) or DB_SLAVE (0)
* with the parameter default set to DB_MASTER.
*
* The method exists to reduce the code footprint of this class.
*
* If the prepare statement throws an exception, within the exception handler, we're going to attempt to
* reconnect to the PDO master database assuming that the error was from a dropped connection. Within the
* exception handler, after we make the call to create a new connection resource, we'll test the return value
* and, if it is not null, we'll re-attempt the prepare statement.
*
* Because you can't next exception-handling, we'll have to check that the prepare didn't return a Boolean(false)
* value meaning that the statement was successfully prepared, or generated an error message.
*
* Also added explicit console logging as part of the error-processing.
*
*
* @author mike@givingassistant.org
* @version 1.0
*
* @param PDOStatement $_statement
* @param int $_which
* @return bool
*
*
* HISTORY:
* ========
* 11-07-17 mks CORE-588: original coding
* 01-16-18 mks CORE-697: dropped PDO master connection handler, added second parameter
* 03-27-18 mks CORE-852: improved the handling of cascading exception trapping
* 11-29-18 mks DB-55: fixed processing and ack $_which param
*
*/
private function prepareQuery(PDOStatement &$_statement = null, int $_which = RES_PDO_MASTER): bool
{
$this->state = STATE_SUCCESS;
$this->status = false;
/** @var PDO $resPDO */
$resPDO = $this->dbcMain;
// don't execute a query against the PDO slave if the query isn't a select query
if ($_which === RES_PDO_SECONDARY) {
if ($this->dbEvent != DB_EVENT_SELECT) {
$this->logger->error(ERROR_PDO_SLAVE_ERROR . $this->dbEvent);
$this->eventMessages[] = INFO_PDO_SLAVE_SWITCH;
$_which = RES_PDO_MASTER;
}
$resPDO = $this->dbcSlave;
}
// if we lost the PDO connection, attempt to reconnect to the proper resource
if (empty($resPDO) or !isset($resPDO)) {
if ($resPDO == RES_PDO_MASTER) {
// attempt to reconnect to the PDO resource
try {
$resPDO = gasResourceManager::reconnect(($_which == RES_PDO_MASTER) ? RESOURCE_PDO_MASTER : RESOURCE_PDO_SECONDARY);
if ($_which == RES_PDO_MASTER)
$this->dbcMain = $resPDO;
else
$this->dbcSlave = $resPDO;
$this->logger->warn(ERROR_PDO_RECONNECT);
} catch (TypeError $t) {
$msg = ERROR_TYPE_EXCEPTION . COLON . $t->getMessage();
$this->eventMessages[] = $msg;
if (isset($this->logger) and $this->logger->available) {
$this->logger->fatal(ERROR_PDO_RECONNECT_FAIL);
$this->logger->fatal($msg);
} else {
consoleLog($this->res, CON_ERROR, $msg);
}
$this->status = false;
$this->state = STATE_FRAMEWORK_FAIL;
return false;
}
}
}
try {
// errors in this prepare handled by the exception
$_statement = $this->dbcMain->prepare($this->strQuery);
} catch (PDOException $p) {
// failed to prepare the query - assume a dropped connection but capture diagnostics
$msg = sprintf(ERROR_PDO_PREPARE, $this->strQuery);
$this->eventMessages[] = $msg;
$this->eventMessages[] = $p->getMessage();
if (isset($this->logger) and $this->logger->available) {
$this->logger->warn($msg);
$this->logger->warn($p->getMessage());
} else {
consoleLog($this->res, CON_ERROR, $msg);
consoleLog($this->res, CON_ERROR, $p->getMessage());
}
$this->state = STATE_DB_ERROR;
return false;
} catch (Throwable $t) {
$msg = ERROR_THROWABLE_EXCEPTION . COLON . $t->getMessage();
$this->eventMessages[] = $msg;
if (isset($this->logger) and $this->logger->available)
$this->logger->error($msg);
else
consoleLog($this->res, CON_ERROR, $msg);
$this->state = STATE_FRAMEWORK_WARNING;
return false;
}
if (!is_null($this->dbcMain)) {
try {
$_statement = $this->dbcMain->prepare($this->strQuery);
if ($_statement === false) {
$msg = sprintf(ERROR_PDO_PREPARE_2, $this->strQuery);
$this->eventMessages[] = $msg;
if ($this->debug) $this->logger->debug($msg);
$this->state = STATE_DB_ERROR;
} else {
$this->status = true;
return true;
}
} catch (PDOException $p) {
$msg = sprintf(ERROR_PDO_PREPARE, $this->strQuery);
$this->eventMessages[] = $msg;
$this->eventMessages[] = $p->getMessage();
if (isset($this->logger) and $this->logger->available) {
$this->logger->warn($msg);
$this->logger->warn($p->getMessage());
} else {
consoleLog($this->res, CON_ERROR, $msg);
consoleLog($this->res, CON_ERROR, $p->getMessage());
}
$this->state = STATE_DB_ERROR;
return false;
} catch (Throwable $t) {
$msg = ERROR_THROWABLE_EXCEPTION . COLON . $t->getMessage();
$this->eventMessages[] = $msg;
if (isset($this->logger) and $this->logger->available)
$this->logger->error($msg);
else
consoleLog($this->res, CON_ERROR, $msg);
$this->state = STATE_FRAMEWORK_WARNING;
return false;
}
} // todo -- else?
return true;
}
/**
* bindSPQV() -- private function
*
* Bind Stored Procedure Query Variables
*
* This method requires three input parameters:
*
* $_statement -- which is a PDOStatement resource previous built (otherwise, we'd not have made it to this point)
* $_spParamList -- this is the indexed array of variable data supplied by the client in the broker call
* $_spName -- the name of the stored procedure to be invoked, as provided by the requesting client
*
* The method loops through the list of of the stored-procedure parameter types as defined in the template, for the
* current class, and calls the PDO::bindParam() method using positionally-dependent notation:
*
* PDO::bindParam( {intVal}, {Variable}, {PDO-TYPE} )
*
* ... where {intVal} is the position of the variable as it appears in the input-parameter list for the stored
* procedure using an index-starts-at-1 counter.
* ... {Variable} is the literal value received, sent from the client via a broker event
* ... {PDO-Type} is the pdo type declaration (e.g: PDO::PARAM_INT) pulled from the SP template definition
*
* NOTE:
* -----
* According to the PDO doc for PHP here: http://php.net/manual/en/pdo.constants.php
* There is not a declared type defined for float values -- however, there's a note for sqlLite that says to
* use PDO::PARAM_STR for floats...
*
* If the bindParam() method succeeds, we'll return boolean(true), otherwise a boolean(false) is returned.
*
*
* @author mike@givingassistant.org
* @version 1.0
*
* @param PDOStatement $_statement
* @param array $_spParamList
* @param string $_spName
* @return bool
*
*
* HISTORY:
* ========
* 11-08-17 mks CORE-588: original coding
*
*/
private function bindSPQV(PDOStatement $_statement, array $_spParamList, string $_spName): bool
{
$index = 0;
foreach ($this->template->dbObjects[PDO_PROCEDURES][$_spName][STRING_TYPE_LIST] as $key => $param) {
foreach ($param as $direction => $paramType) {
try {
if ($direction != STRING_OUT) {
$res = $_statement->bindParam(($index + 1), $_spParamList[$index], $paramType);
if ($res === false) {
$msg = sprintf(ERROR_PDO_BIND, (string)$_spParamList[$index], $this->strQuery, $index);
$this->eventMessages[] = $msg;
if ($this->debug) $this->logger->debug($msg);
return false;
}
}
} catch (PDOException $p) {
$msg = sprintf(ERROR_PDO_BIND, $_spParamList[$index], $this->strQuery, $index);
$this->eventMessages[] = $msg;
$this->eventMessages[] = $p->getMessage();
if (isset($this->logger) and $this->logger->available) {
$this->logger->error($msg);
$this->logger->error($p->getMessage());
} else {
consoleLog($this->res, CON_ERROR, $msg);
consoleLog($this->res, CON_ERROR, $p->getMessage());
}
return false;
} catch (TypeError $t) {
$msg = ERROR_TYPE_EXCEPTION . COLON . $t->getMessage();
$this->eventMessages[] = $msg;
if (isset($this->logger) and $this->logger->available)
$this->logger->error($msg);
else
consoleLog($this->res, CON_ERROR, $msg);
$this->status = false;
$this->state = STATE_FRAMEWORK_WARNING;
return false;
}
$index++;
}
}
return true;
}
/**
* getPDOResources() -- private method
*
* This method requires no input methods and returns a boolean which indicates if the PDO resource, minimally just
* the master, is available for queries.
*
* If the resourceManager has flagged the PDO resource as available, something that happens during bootstrapping,
* then we're going to verify that the connection is still active as, over a period of enough time, there is a risk
* that the PDO connection may timeout.
*
* First, then, is to ping the PDO master resource and, if it does not respond, attempt a reconnect. If we cannot
* reconnect, mark the connection as dropped/bad and return.
*
* If the connection is good, then repeat the process for the PDO slave resource. If the PDO slave resource, which
* must be enabled, is lost, we'll mark the resource as unavailable, but we will return a boolean(true) if the
* PDO-master resource is available.
*
* PDO MASTER PDO SLAVE RETURNS
* ---------------------------------------------
* available available true
* available not-available false
* not-available doesn't matter false
*
* Error messages are recorded to the console and admin logs, and to the eventMessages stack.
*
*
* @author mike@givingassistant.org
* @version 1.0
*
* @return bool
*
*
* HISTORY:
* ========
* 01-18-18 mks CORE-697: original coding
* 03-01-18 mks CORE-689: fixed bug where dbSlave resource wasn't being stored in the member variable
* 05-09-18 mks _INF-188: segundo service support
* 08-30-18 mks DB-50: sneakerstrap patching for lite-initialization
*
*/
private function getPDOResources(): bool
{
if (!gasResourceManager::$PDOAvailable and !gasResourceManager::$available)
$this->dbcMain = gasResourceManager::fetchResource(RESOURCE_PDO_MASTER);
if (!gasResourceManager::$PDOSlaveAvailable and !gasResourceManager::$available)
$this->dbcSlave = gasResourceManager::fetchResource(RESOURCE_PDO_SECONDARY);
switch ($this->dbService) {
case CONFIG_DATABASE_SERVICE_APPSERVER :
$resDBMain = RESOURCE_PDO_MASTER;
$service = CONFIG_DATABASE_PDO_APPSERVER;
$resDBSlave = RESOURCE_PDO_SECONDARY;
break;
case CONFIG_DATABASE_SERVICE_SEGUNDO :
$resDBMain = RESOURCE_WH_COOL_PDO_MASTER;
$service = CONFIG_DATABASE_PDO_SEGUNDO;
$resDBSlave = RESOURCE_WH_COOL_PDO_SECONDARY;
break;
default :
$error = (isset($this->dbService)) ? (ERROR_SERVICE_404 . $this->dbService) : ERROR_SERVICE_UNK;
$this->eventMessages[] = $error;
$this->logger->warn($error);
return false;
break;
}
if (gasResourceManager::$PDOAvailable) {
try {
$this->dbcMain = gasResourceManager::fetchResource($resDBMain);
// check if we've lost the connect (cannot ping) -- attempt to reconnect to the master
if (!$this->ping(DB_MASTER)) {
$dbLink = gasResourceManager::reconnect($resDBMain);
if (is_null($dbLink)) {
$msg = ERROR_PDO_DROPPED;
consoleLog(RES_PDO, CON_ERROR, $msg);
$this->eventMessages[] = $msg;
$this->logger->warn($msg);
$this->dbMainAvailable = false;
$this->state = STATE_RESOURCE_ERROR_PDO;
return false;
}
$this->dbcMain = $dbLink;
}
$this->dbMainAvailable = (is_object($this->dbcMain)) ? true : false;
if ($this->config[$service][CONFIG_DATABASE_PDO_USE_SECONDARY] and $this->dbMainAvailable) {
$this->dbcSlave = gasResourceManager::fetchResource($resDBSlave);
$this->dbSlaveAvailable = true;
// check to see that the slave connection is still active - attempt to reconnect on fail
if (!$this->ping(DB_SLAVE)) {
$this->dbcSlave = gasResourceManager::reconnect($resDBSlave);
if (is_null($this->dbcSlave)) {
$msg = ERROR_PDO_SLAVE_DROPPED;
consoleLog(RES_PDO, CON_ERROR, $msg);
$this->eventMessages[] = $msg;
$this->logger->warn($msg);
$this->dbSlaveAvailable = false;
}
}
} else {
$this->dbSlaveAvailable = false;
$this->dbcSlave = null;
}
} catch (Throwable $t) {
$msg = ERROR_THROWABLE_EXCEPTION . COLON . $t->getMessage();
$this->eventMessages[] = $msg;
if (isset($this->logger) and $this->logger->available)
$this->logger->error($msg);
else
consoleLog($this->res, CON_ERROR, $msg);
$this->status = false;
$this->state = STATE_FRAMEWORK_WARNING;
}
} else {
if (isset($this->logger) and $this->logger->available)
$this->logger->fatal(ERROR_RESOURCE_PDO_NOT_AVAIL);
else
consoleLog(RES_PDO, CON_SYSTEM, ERROR_RESOURCE_PDO_NOT_AVAIL);
$this->eventMessages[] = ERROR_RESOURCE_PDO_NOT_AVAIL;
$this->dbMainAvailable = false;
$this->dbSlaveAvailable = false;
$this->dbcSlave = null;
$this->dbcMain = null;
$this->state = STATE_RESOURCE_ERROR_PDO;
return false;
}
if ($this->dbMainAvailable) return true;
consoleLog(RES_PDO, CON_SYSTEM, ERROR_PDO_CONNECT);
return false;
}
/**
* checkProtectedFields() -- protected method
*
* This method checks the part of the query defined by STRING_UPDATE_DATA in an update event, for fields that
* the client wants to update, against the classes' protectedFields array.
*
* The method requires one input parameter: $_data -- an associative array of column names that have been
* cache-mapped-processed and their respective values (for the update query).
*
* If the $_data parameter submitted is not an array or is empty, return a false back to the calling client
* signalling that the data payload failed validation.
*
* If a field in $_data exists in the protectedFields list, then delete that key->value pair from the
* submitted array.
*
* If we remove all of the fields in the $_data array, then we'll return a false to the calling client to indicate
* that array failed (completely) validation.
*
* Otherwise, we have validate the update-portion of the query and so we'll return a Boolen true to the client.
*
*
* @author mike@givingassistant.org
* @version 1.0
*
* @param array $_data
* @return bool
*
* HISTORY:
* ========
* 09-11-17 mks CORE-558: original coding
* 10-26-17 mks CORE-586: moved to the core from mongo instantiation class
* 11-26-18 mks DB-90: fixed bug in that we're dropping a *row* of data instead of a protected field when
* there's a protected field violation (e.g.: just drop the field)
* 02-05-19 mks DB-107: bug: if last element in an array is unset b/c of protected field violation, then
* ($_data[0] = []) evals true... added check for this
* 03-29-19 mks DB-116: moved from core to PDO class and coded it specifically for a STRING_UPDATE_DATA
* payload, specific to PDO, check. Added check to ensure protected fields member
* exists and is correctly declared as an array
*
*/
protected function checkProtectedFields(array &$_data): bool
{
$loggerAvailable = (isset($this->logger) and $this->logger->available);
$msg = '';
if (empty($_data)) {
$msg = ERROR_DATA_ARRAY_EMPTY . COLON . STRING_DATA;
} elseif (!is_array($_data)) {
$msg = ERROR_DATA_ARRAY_NOT_ARRAY . STRING_DATA;
}
if (empty($this->protectedFields) or !is_array($this->protectedFields)) {
$hdr = basename(__METHOD__) . AT . __LINE__ . COLON;
$msg = ERROR_PF_404;
$this->eventMessages[] = $msg;
$this->logger->warn($hdr . $msg);
return false;
}
if (!empty($msg)) {
if ($loggerAvailable)
$this->logger->error($msg);
else
consoleLog($this->res, CON_ERROR, $msg);
$this->eventMessages[] = $msg;
return false;
}
// we'll copy all the $_data over to $dataCopy
$dataCopy = null;
// scan each key in the submitted data (field => value) and if the field exists in the protected-fields
// member for the class, then drop the field from the query array, log a message to the event-messages
// stack.
foreach ($_data as $key => $value) {
if (in_array($key, $this->protectedFields) or in_array(($key . $this->ext), $this->protectedFields)) {
$hdr = basename(__METHOD__) . AT . __LINE__ . COLON;
$msg = sprintf(ERROR_QB_PF_VIOL, $key);
$this->eventMessages[] = $msg;
if ($loggerAvailable) $this->logger->data($hdr . $msg);
} else {
if (in_array($key, $this->fieldList))
$dataCopy[$key] = $value;
elseif (in_array($key . $this->ext, $this->fieldList))
$dataCopy[($key . $this->ext)] = $value;
else {
// update field is not a valid member of the current data class, so drop the
// field from the update request while making note of the drop in eventMessages
$msg = ERROR_DATA_FIELD_NOT_MEMBER . $key;
$this->eventMessages[] = $msg;
}
}
}
$_data = $dataCopy;
return((empty($_data)) ? false : true);
}
}