Edit Configurations -> Arguments * * Put this in the text input: -k --env=dev --user=mshallop --password=einstein * * To debug w/out creating an application account: -k --env=dev * * * @author mike@givingassistant.org * @version 1.0 * * * HISTORY: * ======== * 07-02-18 mks CORE-1045: original coding * 05-21-19 mks DB-116: processing of templates updates s.t. template processing only happens in the * environment for which it is declared * 07-15-19 mks DB-124: support for arguments, limited app to cli, support for dynamically dropping indexes * 08-26-19 mks DB-111: re-write to support creation of a new *database* and database user, support for * environments hosted off localhost -> env-based processing, refactored output to consoleLog only * 10-09-19 mks DB-136: fixed bug where db name was getting appended to previous db name on every iteration * of the drop-index loop */ $_REDIRECT=false; // enable stdout require_once(dirname(__DIR__) . '/config/sneakerstrap.inc'); // load env global $topDir; // exit with an error message if the app was launched by a browser if (!empty($_REQUEST)) echo 'This app can only be run from the command line.' && exit; if (empty($argv)) printHelp($argv[0]) && exit; // --------------------------------------------------------------------------------------------------------------------- // lvar initialization $shortOptions = 'e:t::k::u::p::h::'; // get the cli options $longOptions = [ 'env:', 'template::', 'keep::', 'user::', 'password::', 'help::']; $options = getopt($shortOptions, $longOptions); $deleteDevTables = true; $singleFile = false; $couldBeADuplicateEnvUser = false; $env = NONE; $isNewInstallation = false; $templateDirectory = $topDir . DIR_CLASSES . DIR_TEMPLATE . '/'; //var_export($options);exit; $errors = null; $res = 'PDOC: '; // PDO Config /** @var PDO $resDB */ $resDB = null; $processTemplate = false; $error404 = ERROR_CONFIG_RESOURCE_404 . CONFIG_BROKER_SERVICES . ARROW; $dbObjects = [ PDO_VIEWS => STRING_VIEW, PDO_PROCEDURES => STRING_PROCEDURE, PDO_FUNCTIONS => STRING_FUNCTION, PDO_EVENTS => STRING_EVENT, PDO_TRIGGERS => STRING_TRIGGER ]; // generate a list of environments that are currently local and active foreach (gasConfig::$settings[CONFIG_REGISTERED_SERVICES] as $service => $enabled) if ($enabled) $envList[] = $service; $aryPDOData = [ ENV_ADMIN => null, ENV_APPSERVER => null, ENV_SEGUNDO => null, ENV_TERCERO => null]; $config = gasConfig::$settings[CONFIG_DATABASE][CONFIG_DATABASE_PDO]; if ($config[CONFIG_DATABASE_PDO_ENABLED] !== 1) { $hdr = basename(__FILE__) . AT . __LINE__ . COLON; consoleLog($res, CON_ERROR, $hdr . 'PDO is disabled in the current Namaste deployment...'); exit(1); } consoleLog($res, CON_SUCCESS, 'Starting mysql template processing...'); $curSvcEnv = gasConfig::$settings[CONFIG_ID][CONFIG_ID_ENV]; $currentReleaseVersion = number_format((float) gasConfig::$settings[CONFIG_ID][CONFIG_ID_VER], 1); // process the options: // ensure we have the required parameter (-e/--env) and subsequent argument if ((!isset($options['e']) and !isset($options['env'])) or (isset($options['h']) or isset($options['help']))) { printHelp($argv[0]); exit(); } // display the disclaimer printDisclaimer(); // todo -- uncomment the next line! //readline(); // wait until the user presses the enter key - discard the input // --------------------------------------------------------------------------------------------------------------------- // process the cli input params... $env = (isset($options['e'])) ? $options['e'] : ((isset($options['env'])) ? $options['env'] : ''); if ($env == NONE or ($env != 'dev' and $env != 'prod')) { $hdr = basename(__FILE__) . AT . __LINE__ . COLON; consoleLog($res, CON_ERROR, $hdr . 'Environment is invalid - must be either dev or prod.'); printHelp($argv[0]) && exit; } // check the template name if the -t option was specified if (isset($options['t']) or isset($options['template'])) { $file = (isset($options['t'])) ? $options['t'] : ((isset($options['template'])) ? $options['template'] : ''); if (empty($file)) { $hdr = basename(__FILE__) . AT . __LINE__ . COLON; consoleLog($res, CON_ERROR, $hdr . 'Template file name is blank.'); printHelp($argv[0]) && exit; } // we have a file name - validate that the file exists if (!file_exists($topDir . DIR_CLASSES . DIR_TEMPLATE . SLASH . $file)) { $hdr = basename(__FILE__) . AT . __LINE__ . COLON; consoleLog($res, CON_ERROR, $hdr . $file . ' could not be found in the template directory.'); printHelp($argv[0]) && exit; } $singleFile = true; } if (isset($options['k']) or isset($options['keep'])) $deleteDevTables = false; // --------------------------------------------------------------------------------------------------------------------- // extract the cli admin-account info from the command line parameters... // this is the user account that will create the db and bind the new user to the db... // so make sure that this user has db-level grant auths! $adminUser = (isset($options[STRING_USER])) ? $options[STRING_USER] : ''; $adminPass = (isset($options[STRING_PASSWORD])) ? $options[STRING_PASSWORD] : ''; // connect to the PDO resources based on configured environments - note that this call is NOT using resource // manager... the framework does not have to be running for this application to run try { if (!getPDOResources($adminUser, $adminPass)) { $hdr = basename(__FILE__) . AT . __LINE__ . COLON; consoleLog($res, CON_ERROR, $hdr . ERROR_PDO_CONNECT); exit; } } catch (TypeError $t) { $hdr = basename(__FILE__) . AT . __LINE__ . COLON; consoleLog($res, CON_ERROR, $hdr . ERROR_RESOURCE_404 . RESOURCE_PDO_MASTER); exit; } // create a new database and namaste user account if they do not already exist if (isset($options['u']) or isset($options['user'])) { if (!isset($options['p']) and !isset($options['password'])) { $hdr = basename(__FILE__) . AT . __LINE__ . COLON; consoleLog($res, CON_ERROR, $hdr . 'You must specify a password when providing a super user account name.'); printHelp($argv[0]); exit; } try { foreach ($envList as $env) { /** @var PDO $ptr */ // <-- so we don't get an IDE warning... $ptr = $aryPDOData[$env][STRING_DBR]; if (is_null($ptr)) continue; // if the request including the option to create a new Namaste (application) user account and password, // then check to see if user account already exists and, if so, stop execution... /** @noinspection SqlResolve */ $query = "SELECT Host FROM mysql.user WHERE User='" . $aryPDOData[$env][STRING_USER] . "'"; foreach ($ptr->query($query, PDO::FETCH_ASSOC) as $row) $results[] = $row['Host']; if (in_array($aryPDOData[$env][STRING_HOST], $results) or in_array(OPERATOR_REGEX, $results)) { $host = (in_array($aryPDOData[$env][STRING_HOST], $results)) ? $aryPDOData[$env][STRING_HOST] : OPERATOR_REGEX; $hdr = basename(__FILE__) . AT . __LINE__ . COLON; $msg = sprintf(INFO_DB_DUP_ENV_USER, $aryPDOData[$env][STRING_USER]) . $host; consoleLog($res, CON_ERROR, $hdr . $msg); $couldBeADuplicateEnvUser = true; } $ptr->beginTransaction(); /* * Namaste Super-User Account Creation * ------------------------------------ * The following code creates the Namaste Super-User which the requesting user identified using the --user * and --password input parameters. Because mySQL user management is a bit convoluted, here's how it's * broken-down for this app: * * 1. Create the application user which is "{userName}@{host}" where userName and host is pulled from both * the XML (host). This user is an application-user who's privileges are restricted to the named database * only. The user account is created using the password passed in via the command line parameter. * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ * IF THE $couldBeADuplicateEnvUser variable is false: * 2. Create the database user based off the Namaste application user but create the user using the "%" * host designation - we're creating this database-level user using the same password passed via the * XML that we used to create the application user. * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ * 3. Grant all privileges to the named (XML) Namaste database to the application user. * 4. Grant replication-slave privileges to the database-user level account so that the application can * access the read-slave for read requests. * 5. Flush the privileges (because of the create command). * 6. Use the targeted (switch to) database. */ $sql = "CREATE DATABASE " . $aryPDOData[$env][STRING_DBN] . ";"; if (!$couldBeADuplicateEnvUser) $sql .= "CREATE USER '" . $aryPDOData[$env][STRING_USER] . "'@'" . $aryPDOData[$env][STRING_HOST] . "' IDENTIFIED BY '" . $aryPDOData[$env][STRING_PASS] . "'; CREATE USER '" . $aryPDOData[$env][STRING_USER] . "'@'%' IDENTIFIED BY '" . $aryPDOData[$env][STRING_PASS] . "';"; $sql .= "GRANT ALL PRIVILEGES ON " . $aryPDOData[$env][STRING_DBN] . ".* to '" . $aryPDOData[$env][STRING_USER] . "'@'" . $aryPDOData[$env][STRING_HOST] . "' IDENTIFIED BY '" . $aryPDOData[$env][STRING_PASS] . "'; GRANT REPLICATION SLAVE ON *.* to '" . $aryPDOData[$env][STRING_USER] . "'@'% IDENTIFIED BY '" . $aryPDOData[$env][STRING_PASS] . "'; FLUSH PRIVILEGES; USE " . $aryPDOData[$env][STRING_DBN] . ";"; $ptr->exec($sql) or $ptr->rollBack() and die(print_r($ptr->errorInfo(), true)); $isNewInstallation = true; $ptr->commit(); } } catch (PDOException | Throwable $e) { $ptr->rollBack(); $hdr = basename(__FILE__) . AT . __LINE__ . COLON; consoleLog($res, CON_ERROR, ERROR_EXCEPTION); consoleLog($res, $e->getMessage()); exit; } } // generate the file list... honoring the user's request if they want to process a single template file if ($singleFile) { // single file $fileList = [ $templateDirectory . $file ]; } else { // all template files $fileList = glob($templateDirectory . '*.class.inc'); } // todo: check the $isNewInstallation var: if true, then we need to apply ALL of the version changes starting with 1.0 // I already created a JIRA ticket for the above issue: DB-132. foreach ($envList as $environment) { // if the current env is set and if that env has enabled PDO then... if (!empty($aryPDOData[$environment])) { foreach ($fileList as $filename) { /** @var PDO $resDB */ $resDB = $aryPDOData[$environment][STRING_DBR]; $transactionStarted = false; /** @var gatTestPDO $currentTemplate */ $currentTemplate = basename($filename); $currentTemplate = preg_replace("/" . STRING_CLASS_FILE_EXT . "/", "", $currentTemplate); $currentTemplate = new $currentTemplate(); // if not a PDO template, quietly move to next template if ($currentTemplate->schema != TEMPLATE_DB_PDO) { if (is_object($currentTemplate)) $currentTemplate->__destruct(); unset($currentTemplate); continue; } elseif (empty($currentTemplate->dbObjects) or !is_array($currentTemplate->dbObjects)) { $hdr = basename(__FILE__) . AT . __LINE__ . COLON; $msg = $hdr . ERROR_SQL_TEMPLATE_DBO_404; consoleLog($res, CON_ERROR, $msg); continue; } consoleLog($res, CON_SUCCESS, sprintf(INFO_PROCESSING_TEMPLATE, $currentTemplate->collection, $currentTemplate->service)); // set the correct PDO resource if ($currentTemplate->service == $environment) { $processTemplate = true; $database = gasConfig::$settings[CONFIG_ID][CONFIG_ID_ENV] . UDASH; $database .= $config[$environment][CONFIG_DATABASE_PDO_MASTER][CONFIG_DATABASE_PDO_DB]; } else { continue; } consoleLog($res, CON_SUCCESS, 'Processing PDO templates for env: ' . $environment); // --------------------------------------------------------------------------------------------------------- // Step 1: check to see if the current table exists and START THE TRANSACTION! <------------ pay attention // --------------------------------------------------------------------------------------------------------- try { // set attribute to allow multi-queries because transaction $resDB->setAttribute(PDO::ATTR_EMULATE_PREPARES, 0); $query = 'SHOW TABLES LIKE "' . $currentTemplate->collection . $currentTemplate->extension . '"'; $tableExists = $resDB->query($query)->rowCount() > 0; if (!$resDB->inTransaction()) $resDB->beginTransaction(); } catch (PDOException | Throwable $p) { $hdr = basename(__FILE__) . AT . __LINE__ . COLON; consoleLog($res, CON_ERROR, $hdr . ERROR_EXCEPTION); consoleLog($res, CON_ERROR, $hdr . $p->getMessage()); exit; } // Step 2: loop through PDO_SQL until we get to the current version - we'll only execute the SQL in this block! foreach ($currentTemplate->dbObjects[PDO_SQL] as $sqlBlock) { // check the release version and exit if the release version is missing from the template block if (!isset($sqlBlock[PDO_VERSION])) { // if there is no PDO_VERSION section set within *any* PDO_SQL block, it's a malformed template so exit... $msg = sprintf(ERROR_SQL_TEMPLATE_DBO_VER_404, $filename); try { @doRollBack($resDB, $msg); continue; } catch (PDOException | TypeError $t) { $hdr = basename(__FILE__) . AT . __LINE__ . COLON; consoleLog($res, CON_ERROR, $hdr . $t->getMessage()); @doRollBack($resDB, $hdr . $t->getMessage()); continue; } } else { // compare the current version block against the current release version -- skip block if not equal if (!empty($sqlBlock[PDO_VERSION])) { $rcCompare = strcmp($currentReleaseVersion, number_format((float)$sqlBlock[PDO_VERSION], 1)); } else { $hdr = basename(__FILE__) . AT . __LINE__ . COLON; $msg = sprintf(ERROR_TEMPLATE_MISSING_FIELD, $filename, PDO_VERSION); consoleLog($res, CON_ERROR, $hdr . $msg); continue; } if ($rcCompare !== 0) continue; // pull the table name from the sqlBlock $currentTable = $sqlBlock[PDO_TABLE]; // at this point, we have a PDO_SQL block that matches the current release version /* * Step 3: Start the transaction -- ALL SQL commands must process successfully or issue a rollback. * * However... * * Some databases, including MySQL, automatically issue an implicit COMMIT when a database * definition language (DDL) statement such as DROP TABLE or CREATE TABLE is issued within a * transaction. The implicit COMMIT will prevent you from rolling back any other changes within * the transaction boundary. */ try { if (!$resDB->inTransaction()) { if (false === $resDB->beginTransaction()) { $hdr = basename(__FILE__) . AT . __LINE__ . COLON; consoleLog($res, CON_ERROR, $hdr . ERROR_PDO_ST_FAIL); continue; } else { $transactionStarted = true; } } } catch (PDOException | Throwable $p) { $hdr = basename(__FILE__) . AT . __LINE__ . COLON; consoleLog($res, CON_ERROR, $hdr . $p->getMessage()); continue; } // Step 4: If the current table DNE, then pull the FC query for the current release version and execute if (!$tableExists) { // if the table does not exist, check the template $dbObjects[PDO_SQL][PDO_VERSION] to see if there is a // corresponding content for the PDO_SQL_FC declaration. If so, then this is the table CREATE syntax and // we need to execute this SQL. if (isset($sqlBlock[PDO_SQL_FC]) and !empty($sqlBlock[PDO_SQL_FC])) { if (!loadQuery($sqlBlock[PDO_SQL_FC])) { doRollBack($resDB, ERROR_PDO_FC_CREATE . COMMA . ERROR_PDO_CURRENT_TABLE . $currentTable); continue; } consoleLog($res, CON_SUCCESS, sprintf(INFO_PDO_DEPLOY, PDO_TABLE, $sqlBlock[PDO_TABLE])); } else { $msg = ERROR_PDO_CREATE_404 . $currentTemplate->collection; consoleLog($res, CON_ERROR, $msg); } } elseif (intval($currentReleaseVersion) == 1) { // Step 5.1: // we're installing version 1.0 of Namaste and the table exists... this means that the table // does exist and needs to be dropped - which is common in development environments so, if // there exists a drop component (and there should be) execute this first before executing the // SQL create statement: if (array_key_exists(STRING_DROP_CODE_DEV, $sqlBlock) and !empty($sqlBlock[STRING_DROP_CODE_DEV]) and (isset($sqlBlock[PDO_SQL_FC]) and !empty($sqlBlock[PDO_SQL_FC]))) { // do not delete the table if the user launched with -k|--keep option if ($deleteDevTables) { if (!loadQuery($sqlBlock[STRING_DROP_CODE_DEV])) { doRollBack($resDB, ERROR_PDO_DROP_DEV . COMMA . ERROR_PDO_CURRENT_TABLE . $currentTable); continue; } if (!loadQuery($sqlBlock[PDO_SQL_FC])) { doRollBack($resDB, ERROR_PDO_FC_CREATE . COMMA . ERROR_PDO_CURRENT_TABLE . $currentTable); continue; } if (!isset($sqlBlock[PDO_SQL_UPDATE]) or empty($sqlBlock[PDO_SQL_UPDATE])) { doRollBack($resDB, ERROR_PDO_UPDATE_404 . COMMA . ERROR_PDO_CURRENT_TABLE . $currentTable); continue; } consoleLog($res, CON_SUCCESS, sprintf(INFO_PDO_DEPLOY, PDO_TABLE, $sqlBlock[PDO_TABLE])); } else { // Step 5.2: Call the dropIndexes function to drop the table indexes and tweak the // update query in the sql block to remove the add primary key directive if (!dropIndexes($database, $currentTable, true)) { $hdr = basename(__FILE__) . AT . __LINE__ . COLON; consoleLog($res, CON_ERROR, $hdr . ERROR_PDO_INDEX_DROP . $currentTable); continue; } consoleLog($res, CON_SUCCESS, INFO_PDO_INDEXES_DROPPED . $currentTable); } } else { // Step 5.2: // actually, if the table exists and there's no drop command defined in the template, or // there's no create-table command defined, we can't continue so error-out and quit. $hdr = basename(__FILE__) . AT . __LINE__ . COLON; $msg = $hdr . ERROR_PDO_DROP_404 . $currentTemplate->collection; consoleLog($res, CON_ERROR, $msg); @doRollBack($resDB, $msg); continue; } } // Step 6: exec the update block if it exists for the current release version if (isset($sqlBlock[PDO_SQL_UPDATE]) and !empty($sqlBlock[PDO_SQL_UPDATE])) { if (!loadQuery($sqlBlock[PDO_SQL_UPDATE])) { doRollBack($resDB, sprintf(ERROR_PDO_UPDATE_FAIL, $currentTable, $currentReleaseVersion)); continue; } } // if there's no update block for the current release for the current template, that's ok! } } // sanity check: if we've not started a transaction, continue to the next template file if (!$resDB->inTransaction()) continue; // Step 6: Execute all the create-objects SQL statements found in the template. The release version is immaterial. // process views, procedures, functions, events and triggers (defined as an associative array above) foreach ($dbObjects as $dbObjectName => $dbObjectLabel) { if (isset($currentTemplate->dbObjects[$dbObjectName]) and is_array($currentTemplate->dbObjects[$dbObjectName]) and !empty($currentTemplate->dbObjects[$dbObjectName])) { foreach ($currentTemplate->dbObjects[$dbObjectName] as $name => $data) { // transaction-wrap all the function calls try { // it is possible to reach this point without having previously started a transaction so, // pause to see if one has been started and, if not, start one... if (!$resDB->inTransaction()) { if (false === $resDB->beginTransaction()) { $hdr = basename(__FILE__) . AT . __LINE__ . COLON; consoleLog($res, CON_ERROR, $hdr . ERROR_PDO_ST_FAIL); } else { $transactionStarted = true; } } // stored procedures, et all, require separate drop statements b/c PHP doesn't support embedded delimiter statements if (isset($data[STRING_DROP_CODE_DEV]) and is_string($data[STRING_DROP_CODE_DEV])) { if (!loadQuery($data[STRING_DROP_CODE_DEV])) exit(1); } if (isset($data[$dbObjectLabel]) and is_string($data[$dbObjectLabel])) { if (!loadQuery($data[$dbObjectLabel])) exit(1); consoleLog($res, CON_SUCCESS, sprintf(INFO_PDO_DEPLOY, $dbObjectName, $name)); } } catch (PDOException | Throwable $p) { $hdr = basename(__FILE__) . AT . __LINE__ . COLON; consoleLog($res, CON_ERROR, $hdr . $p->getMessage()); } } } else { consoleLog($res, CON_SUCCESS, sprintf(INFO_PDO_NO_DEPLOY, $currentTemplate->collection . $currentTemplate->extension, $dbObjectName)); } } // Step 7: commit all of the sql we just executed before proceeding to the next template file... try { $rc = $resDB->commit(); if (!$rc) { consoleLog($res, CON_ERROR, ERROR_PDO_COMMIT); @doRollBack($resDB, ERROR_PDO_COMMIT); exit(1); } } catch (PDOException $p) { consoleLog($res, CON_ERROR, $p->getMessage()); @doRollBack($resDB, $p->getMessage()); exit(1); } } } } // end env loop // if we reach this point, all templates were processed successfully - log a termination message and exit consoleLog($res, CON_SUCCESS, SUCCESS_PDO_TEMPLATE_PROCESSING . (string) $currentReleaseVersion); exit(0); // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ PROGRAM ENDS ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // LOCAL FUNCTIONS //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ /** * loadQuery() -- local function * * I call this code twice so I have it in a function -- there is one input parameter to the function - this is the * query to be loaded and sent for execution. * * The function accesses most of it's data from the heap by declaring it's lvars as globals - mostly because there * are five lvars and I didn't want to define them as a parameter list. * * The purpose, then, of this function is to execute the passed query that was extracted from the template file and * used to drop/create the mysql table. * * The method requires that we be covered under a transaction before executing the passed query. If we're not under * a transaction, as indicated by the appropriate and brilliantly-named global, then stop execution immediately and * return a false status after making a console log entry. * * If the sql fails to exec, or if any error or exception is raised, then we call the rollback function and return * a Boolean false back to the calling client. If the query executed successfully, a Boolean true is returned. * * * @author mike@givingassistant.org * @version 1.0 * * @param string $_query -- the query to be executed * @param bool $_needResults -- if this is a query that returns results, override the default * @return bool * * * HISTORY: * ======== * 05-22-19 mks DB-122: original coding * 07-18-19 mks DB-124: support for queries where result data is generated * 08-07-19 mks DB-124: enforcing transaction state * */ function loadQuery(string $_query, bool $_needResults = false): bool { global $res, $resDB; if (!$resDB->inTransaction()) { $hdr = basename(__FILE__) . AT . __LINE__ . COLON; consoleLog($res, CON_ERROR, $hdr . ERROR_PDO_NO_TRANS); return false; } try { $rc = ($_needResults) ? runQuery($resDB, $_query) : execQuery($resDB, $_query); if (!$rc) { $hdr = basename(__FILE__) . AT . __LINE__ . COLON; $msg = $hdr . ERROR_PDO_QUERY . $_query; @doRollBack($resDB, $msg); return false; } return true; } catch (TypeError $t) { $hdr = basename(__FILE__) . AT . __LINE__ . COLON; consoleLog($res, CON_ERROR, $hdr . $t->getMessage()); @doRollBack($resDB, $t->getMessage()); return false; } } /** * doRollBack() -- local function * * This function executes the transaction rollback command. Since there are so many places in the main processing * where a transaction failure is trapped, it made sense to put the code that actually rolls back the transaction * into it's own method. * * The method requires two input parameters: * * $_rc -- the PDO resource * $_msg - the transaction msg (why we're rolling back) * * If the rollback command fails, we'll output additional diagnostics. * * On exit, it sets the global variable: $transactionStarted to false. * * PROGRAMMER'S NOTES: * ------------------- * Some databases, including MySQL, automatically issue an implicit COMMIT when a database definition language * (DDL) statement such as DROP TABLE or CREATE TABLE is issued within a transaction. The implicit COMMIT will * prevent you from rolling back any other changes within the transaction boundary. * * The method is type void. * * * @author mike@givingassistant.org * @version 1.0 * * @param PDO $_rc * @param string $_msg * * * HISTORY: * ======== * 06-21-18 mks CORE-1045: original coding completed * */ function doRollBack(PDO $_rc, string $_msg): void { global $res, $transactionStarted; try { if (false === $_rc->rollBack()) { $hdr = basename(__FILE__) . AT . __LINE__ . COLON; consoleLog($res, CON_ERROR, $hdr . ERROR_PDO_ROLLBACK); } consoleLog($res, CON_ERROR, $_msg); consoleLog($res, CON_ERROR, INFO_TRX_ROLLBACK); } catch (PDOException $p) { consoleLog($res, CON_ERROR, $p->getMessage()); } $transactionStarted = false; } /** * dropIndexes() -- local function * * This method requires two input parameters: * * $_database -- the name of the current database being processed * $_currentTable -- omg! a self-documenting variable! * * We declare the $sqlBlock (pulled from the template) and the resource variable ($res) as global so that they can be * accessed within the function. If there's a index block defined in the template, then we're going to build the drop * index query by parsing out the database and tables names replacing them with the parameters passed into the function. * * We submit the query and, if the query failed to execute, return a boolean false. Otherwise, output a console * message and return a boolean true to the caller. * * Programmer's Notes: * ------------------- * This function was created b/c there are two conditions in which we'll eliminate indexes. We do NOT want to run the * drop-indexes command when we're in a dev env and the user has not specified the --keep option. * * There's also place-holder code in this method for removing the auto-increment feature of the primary key (id_{ext}) * presumably so that all indexes can be dropped and restored. I'd assume that this would be necessary under the * following conditions: * * 1. you're in a dev env * 2. you used the --keep|-k option to keep the table data * 3. you're on version 1.0 * * If ALL of the above conditions are met, then we'll have to remove the auto-increment attribute from the primary * key field and then drop the index. mariaDB does not allow you to drop the index unless this attribute has been * removed first. I'd assume that I'd add new params to the method, a switch telling the function that I want to * exec the drop-autoincrement attribute. * * * @author mike@givingassistant.org * @version 1.0 * * @param string $_database -- string containing the current database name * @param string $_currentTable -- string containing the current table name * @param bool $_dropAI -- flag, defaults to false, indicating if we're dropping the AI attribute from the pKey * @return bool * * HISTORY: * ======== * 07-31-19 mks DB-124: original coding * */ function dropIndexes(string $_database, string $_currentTable, bool $_dropAI = false): bool { /** var gatTestPDO $currentTemplate */ global $currentTemplate; global $sqlBlock, $res; if (!isset($sqlBlock[STRING_DROP_CODE_IDX]) or empty($sqlBlock[STRING_DROP_CODE_IDX])) { $hdr = basename(__FILE__) . AT . __LINE__ . COLON; consoleLog($res, CON_ERROR, $hdr . ERROR_DATA_ARRAY_EMPTY . COLON . STRING_DROP_CODE_IDX); return false; } /* ----------------------------------------------------------------------------------------------------------------- * the code in the next block is provided in-case there's every a time where we want to drop the primary key * when doing a sql-update. The primary key cannot be dropped like a regular index because of the auto-increment * attribute. So, this block executes a query that will drop the auto-increment attribute from the id_{ext} * column. -----------------------------------------------------------------------------------------------------------------*/ if ($_dropAI) { // we're not dropping the column, just the auto-increment attribute... $pkQuery = 'ALTER TABLE ' . STRING_TABLE_NAME_TAG . ' CHANGE XXXidXXX XXXidXXX int(11)'; // constant for the above place-holder is: STRING_TABLE_ID_TAG $pKey = $currentTemplate->collection . $currentTemplate->extension; if (empty($pKey)) { $hdr = basename(__FILE__) . AT . __LINE__ . COLON; consoleLog($res, CON_ERROR, $hdr . ERROR_PKEY_ID . STRING_TEMPLATE); return false; } // replace the table name with the current table name $pkQuery = str_replace(STRING_TABLE_NAME_TAG, $pKey, $pkQuery); // replace the id-field with the current table extension $pkQuery = str_replace(STRING_TABLE_ID_TAG, (PKEY_ID . $currentTemplate->extension), $pkQuery); if (empty($pkQuery)) { $hdr = basename(__FILE__) . AT . __LINE__ . COLON; consoleLog($res, CON_ERROR, $hdr . ERROR_PDO_QUERY_BUILD); return false; } else { // execute the query to drop the auto-increment attribute from the primary key if (!loadQuery($pkQuery)) { $hdr = basename(__FILE__) . AT . __LINE__ . COLON; consoleLog($res, CON_ERROR, $hdr . ERROR_PDO_DROP_AI . $_currentTable); return false; } // if we're dropping the auto-increment, we need to remove the ADD PRIMARY KEY... text from the sql // update string in the template so that we don't fail at adding the indexes $key = 'id' . $currentTemplate->extension; $targetString = 'ADD PRIMARY KEY (`' . $key . '`),'; $sqlBlock[PDO_SQL_UPDATE] = str_replace($targetString, '', $sqlBlock[PDO_SQL_UPDATE]); consoleLog($res, CON_SUCCESS, INFO_PDO_AI_ATTR_DROPPED); } } /* ----------------------------------END DROP AUTO-INCREMENT QUERY------------------------------------------------*/ // clear out the comment tag $query = str_replace('--', '', $sqlBlock[STRING_DROP_CODE_IDX]); // first, we need to parse and replace the placeholder strings with the actual values $query = str_replace(STRING_DB_NAME_TAG, $_database, $query); $query = str_replace(STRING_TABLE_NAME_TAG, $_currentTable, $query); if (!loadQuery($query, true)) { return false; } consoleLog($res, CON_SUCCESS, sprintf(INFO_PDO_DEPLOY, $query, $sqlBlock[PDO_TABLE])); return true; } /** * execQuery() -- local function * * this function handles all non-transaction and non-db-validation queries. All queries for creating and maintaining * tables are static and do not require preparation. * * Since we're just executing DDL statements (CREATE, ALTER), then there's no return data, per se, only information * is if the query successfully executed or not. * * Input parameters to the method are: * * $_rc -- this is the PDO resource (provisioned by the gasResourceManager) * $_query -- this is the SQL query(ies) that will be executed * * If a query raises a PDO exception, we'll trap the exception and, if transactions are currently active, then * we'll invoke the roll-back function (above). * * The method returns a BOOLEAN indicating if the query(ies) successfully executed to completion or not. * * * @author mike@givingassistant.org * @version 1.0 * * @param PDO $_rc * @param string $_query * @return mixed * * * HISTORY: * ======== * 06-21-16 mks CORE-1045: original coding completed * 07-18-19 mks DB-124: added ability to return query data for the drop-indexing command * */ function execQuery(PDO $_rc, string $_query) { global $eos, $res, $transactionStarted; try { $results = $_rc->exec($_query); return (!($results === false)); } catch (PDOException $p) { if ($transactionStarted) { try { @doRollBack($_rc, $p->getMessage()); } catch (TypeError $t) { $hdr = basename(__FILE__) . AT . __LINE__ . COLON; consoleLog($res, CON_ERROR, $hdr . $t->getMessage()); } } else { $msg = sprintf(ERROR_PDO_EXCEPTION, $_query); $hdr = basename(__FILE__) . AT . __LINE__ . COLON; consoleLog($res, CON_ERROR, $hdr . $eos. $msg . $_query); consoleLog($res, CON_ERROR, $p->getMessage()); } return false; } } /** * runQuery() -- local function * * This method is similar to the execQuery() function except that we prepare the query, tokenizing it, before * submitting the query to the sql engine. If you ever submit a query that requires params, you'll have to modify * this function by passing the placement variable as an argument to execute() assuming you've also added the array * variable (containing the parameter values) as an input parameter. * * The method currently requires two input parameters: * * $_rc - the database resource pointer * $_query - the query to be tokenized and executed * * Since the query is going to be a query pulled from the template, we're going to first parse and remove all the * new-line chars from the string. Next, we're going to build a system query that allows us to exec queries longer * (in length) than 10K. * * Then we prepare and execute the query. We fetch and return the results and check the results for errors, reporting * if same was found. * * The method returns a boolean value, true if everything was successful, otherwise false. * * * @author mike@givingassistant.org * @version 1.0 * * @param PDO $_rc * @param string $_query * @return bool * * * HISTORY: * ======== * 07-31-19 mks DB-124: original coding * 10-09-19 mks DB-136: fix for PHP warning * */ function runQuery(PDO $_rc, string $_query): bool { global $eos, $res; // clean-up the newline chars from the query string $query = str_replace("\n","", $_query); // remove multiple space chars from the query string $query = preg_replace('/\s\s+/', ' ', $query); if (empty($query)) { $hdr = basename(__FILE__) . AT . __LINE__ . COLON; consoleLog($res, CON_ERROR, $hdr . ERROR_PDO_QUERY_BUILD); return false; } // plan for overflow of the query if the table uses a lot of indexing, just in case... $sysQ = 'SET SESSION group_concat_max_len=10240'; try { if (false === $_rc->exec($sysQ)) { $hdr = basename(__FILE__) . AT . __LINE__ . COLON; consoleLog($res, CON_ERROR, $hdr . ERROR_PDO_QUERY . $sysQ); return false; } $tmp = $_rc->prepare($query); $tmp->execute(); $results = $tmp->fetchAll(); if ($results !== false) { $dropQuery = (isset($results) and isset($results[0]) and is_array($results[0])) ? reset($results[0]) : null; if (is_null($dropQuery)) return true; // no indexes to drop $results = $_rc->query($dropQuery); if (false === $results) return false; // need to clean up any unique fields still hanging around, including the primary key } else return false; } catch (PDOException | Throwable $e) { $msg = sprintf(ERROR_PDO_EXCEPTION, $_query); $hdr = basename(__FILE__) . AT . __LINE__ . COLON; consoleLog($res, CON_ERROR, $hdr . $eos. $msg . $_query); consoleLog($res, CON_ERROR, $e->getMessage()); return false; } return true; } /** * getPDOResources() -- internal method * * This is a "new" function courtesy of DB-111 and requires two input parameters: * * $_admin -- this is a string containing the name of the admin user for the mysql env. Optimally, this should be a * user account carrying global permissions allowing them to create new databases. * $_pass -- this is a string containing the password for the global admin user. * * If nulls are passed (which is possible if the requesting user does not provide this information via argv), then * we'll attempt to establish a database connection using the user/password account information stored in the XML * for the service we're attempting the connection. We'll also connect to the destination resource specifying * the database name for that resource. * * If the user/password is passed to the method, the assumption is that we need to create the database and the user * on the destination resource. We'll connect to the remote mysql resource without specifying the db name assuming * that will be created, along with the Namaste-admin-user account, post-invocation. * * This method uses a global array container - we loop through an array of valid envs and pull out the PDO host data * for that env and, if the env is set/enabled, then assign the param values to self-descriptive keys in the container. * This data includes the db resource pointer. * * If the connection attempt throws an exception, we process the exception and return a boolean(false) to the * calling client. * * If an unknown env is in the environment list, we proc an error and return a boolean(false). * * Otherwise, we return a boolean true and implicitly return the PDO connectors and the array containers for each * environment. * * Also, if Namaste and Admin are not enabled, generate an error and return a boolean(false) as these two envs are * minimally required for Namaste. * * * @author mike@givingassistant.org * @version 1.0 * * @param string $_admin * @param string $_pass * @return bool * * * HISTORY: * ======== * 08-29-19 mks DB-111: original coding * */ function getPDOResources(string $_admin, string $_pass): bool { global $res, $envList, $aryPDOData; $createDB = (empty($_admin) and empty($_pass)) ? false : true; foreach ($envList as $env) { if (isset(gasConfig::$settings[CONFIG_DATABASE][CONFIG_DATABASE_PDO][$env])) { $cfgOld = gasConfig::$settings[CONFIG_DATABASE][CONFIG_DATABASE_PDO][$env]; $cfg = $cfgOld[CONFIG_DATABASE_PDO_MASTER]; $aryPDOData[$env][STRING_HOST] = $cfg[CONFIG_DATABASE_PDO_HOSTNAME]; $aryPDOData[$env][STRING_PORT] = $cfg[CONFIG_DATABASE_PDO_PORT]; $aryPDOData[$env][STRING_USER] = $cfg[CONFIG_DATABASE_PDO_USERNAME]; $aryPDOData[$env][STRING_PASS] = $cfg[CONFIG_DATABASE_PDO_PASSWORD]; $aryPDOData[$env][STRING_DBN] = gasConfig::$settings[CONFIG_ID][CONFIG_ID_ENV] . UDASH . $cfg[CONFIG_DATABASE_PDO_DB]; if ($createDB) $dbs = 'mysql:host=' . $aryPDOData[$env][STRING_HOST] . COLON . $aryPDOData[$env][STRING_PORT]; else $dbs = 'mysql:host=' . $aryPDOData[$env][STRING_HOST] . COLON . $aryPDOData[$env][STRING_PORT] . ';dbname=' . $aryPDOData[$env][STRING_DBN]; } else { continue; // skip all processing for the current env b/c not configured } try { switch ($env) { case ENV_APPSERVER : case ENV_SEGUNDO : if ($cfgOld[CONFIG_DATABASE_PDO_ENABLED]) { $aryPDOData[$env][STRING_DBR] = new PDO($dbs, (empty($_admin)) ? $aryPDOData[$env][STRING_USER] : $_admin, (empty($_pass)) ? $aryPDOData[$env][STRING_PASS] : $_pass); } else { $hdr = basename(__FILE__) . AT . __LINE__ . COLON; consoleLog($res, CON_ERROR, $hdr . sprintf(ERROR_SQL_ENV_NOT_ENABLED, $env)); return false; } break; case ENV_TERCERO : case ENV_ADMIN : if ($cfgOld[CONFIG_DATABASE_PDO_ENABLED]) { $aryPDOData[$env][STRING_DBR] = new PDO($dbs, (empty($_admin)) ? $aryPDOData[$env][STRING_USER] : $_admin, (empty($_pass)) ? $aryPDOData[$env][STRING_PASS] : $_pass); } // no error messages if not enabled as segundo/tercero configs are not required like namaste/admin break; default : $hdr = basename(__METHOD__) . AT . __LINE__ . COLON; $msg = $hdr . sprintf(ERROR_UNKNOWN_KEY, $env, 'envList'); consoleLog($res, CON_ERROR, $msg); return false; break; } } catch (PDOException | Throwable $p) { $hdr = basename(__METHOD__) . AT . __LINE__ . COLON; $msg = $hdr . sprintf(ERROR_ARRAY_KEY_UNK, $env); consoleLog($res, CON_ERROR, $msg); $msg = $hdr . $p->getMessage(); consoleLog($res, CON_ERROR, $msg); return false; } } return true; } /** * printHelp() -- internal function * * This function displays the help screen for this app. It prints the help message to stdout and exists returning void. * * The input parameter is meant to be $argv[0]: the name of the script file. * * * @author mike@givingassistant.org * @version 1.0 * * @param string $_appName -- the name of the application, meant to be argv[0] * * * HISTORY: * ======== * 07-15-19 mks DB-124: original coding * */ function printHelp(string $_appName): void { echo PHP_EOL; echo "\e[1m" . $_appName . "\e[0m" . PHP_EOL . PHP_EOL; echo "\e[1mUsage:\e[0m" . PHP_EOL; echo '------' . PHP_EOL; echo $_appName . ' is a mySQL/PDO deployment script that analyzes the data class templates and executes the' . PHP_EOL; echo 'SQL code embedded in the templates to create (and/or drop) new tables, install indexes and database' . PHP_EOL; echo 'objects (views, stored-procedures, functions, etc.).' . PHP_EOL . PHP_EOL; echo 'The following are valid options for this program:' . PHP_EOL . PHP_EOL; echo "\t" . ' --env={dev|prod} REQUIRED: deploy into a dev (tables are truncated) or prod (table data kept) environments' . PHP_EOL; echo "\t" . ' --template={fileName} OPTIONAL: Deploy only for the template specified (e.g.: gatTestPDO.class.inc)' . PHP_EOL; echo "\t" . ' --user={username} OPTIONAL: If you need to create a new database, specify the super user name' . PHP_EOL; echo "\t" . ' --password={password} OPTIONAL: If you specify the super user name, specify the super user password' . PHP_EOL; echo "\t" . ' -k | --keep OPTIONAL: Deploy into dev env but keep the table data (no table truncation)' . PHP_EOL; echo "\t" . ' -h | --help Displays this help screen' . PHP_EOL . PHP_EOL . PHP_EOL; echo "\e[91mWhen you deploy in a development capacity: tables are destroyed and re-created dropping all records.\e[0m" . PHP_EOL; echo "\e[91mWhen you deploy in a production capacity: tables are retained but indexes are dropped and re-created.\e[0m" . PHP_EOL . PHP_EOL . PHP_EOL; echo 'When specifying the super user data, note that this is NOT the same as the gaAdmin account user defined in the XML.' . PHP_EOL; echo 'The super-user is a database account with admin privileges that can create new databases and users.' . PHP_EOL . PHP_EOL; echo 'In both modes, all database objects (views, stored-procedures, etc.) are dropped and re-created.' . PHP_EOL . PHP_EOL; echo 'All database modifications are done under the protection of a transaction. Either all of the table' . PHP_EOL; echo 'changes will be saved and committed, or none will be. You will have to validate by reading the report' . PHP_EOL; echo 'for each table that is processed by the program to determine success or failure.' . PHP_EOL . PHP_EOL; echo "\e[1mExamples:\e[0m" . PHP_EOL; echo '---------' . PHP_EOL; echo "\e[37m" . $_appName . ' -e dev -t gatProductRegistrations.class.inc -k' . "\e[0m" . PHP_EOL; echo 'This command will deploy into a dev environment but will limit the deployment to only processing the SQL directives' . PHP_EOL; echo 'for the gatProductRegistrations.class.inc class. (You have to provide the fully-qualified template file name!)' . PHP_EOL; echo 'The -k option tells the app to keep the table data; do not truncate the table.' . PHP_EOL . PHP_EOL; echo "\e[37m" . $_appName . ' -e prod' . "\e[0m" . PHP_EOL; echo 'Standard non-development deployment. All PDO (sql) tables will be updated but no data will be deleted.' . PHP_EOL . PHP_EOL; echo "\e[37m" . $_appName . "\e[0m" . PHP_EOL; echo 'Displays this message.' . PHP_EOL . PHP_EOL; } /** * function printDisclaimer() -- internal function * * This function simply prints-out a disclaimer to the cli user stating that processing commands in this script is * expected to take some reasonable amounts of time, depending on the amount of data in the table(s) being processed. * * The user is invited to display some patience during processing and is told to press the control-c button if they * wish to stop the program. * * * @author mike@givingassistant.org * @version 1.0 * * * HISTORY: * ======== * 08-07-19 mks DB-124: original coding * */ function printDisclaimer() { echo <<