false * @package app\components\db */ class SqlFunctionSchema { const SQL_PROCEDURE = 1; const SQL_FUNCTION = 2; protected $_arguments; protected $_functionName; protected $_objectType; protected $_errors; /** * @param $sqlFunctionName * @param int $objectType * @throws InvalidCallException|InvalidParamException */ function __construct($sqlFunctionName, $objectType = self::SQL_FUNCTION) { $objectTypeString = ($objectType === self::SQL_FUNCTION) ? 'FUNCTION' : 'PROCEDURE'; $this->_objectType = $objectType; $schemaCacheKey = 'SqlFunctionSchema.'. $objectTypeString .'.'.$sqlFunctionName; $rawSchema = false; if (\Yii::$app->params['sqlFunctionSchemaCaching']) { $rawSchema = \Yii::$app->cache->get($schemaCacheKey); } if (false === $rawSchema) { $rewSchema = []; $command = \Yii::$app->db->createCommand('SHOW CREATE '. $objectTypeString .' `'.$sqlFunctionName.'`'); $result = $command->queryOne(); if ($result) { $this->_functionName = ($objectType === self::SQL_FUNCTION) ? $result['Function'] : $result['Procedure'] ; $parts = []; $argSection = ''; if ($objectType === self::SQL_FUNCTION) { preg_match('/^CREATE [A-Za-z `=@_]*FUNCTION `*([A-Za-z_0-9]*)`*' .'\(([^#]*)\)[ ]*RETURN/', $result['Create Function'], $parts); if (count($parts) < 3) throw new InvalidCallException('Error on exec SQL statement'); $argSection = $parts[2]; } else { preg_match('/^CREATE [A-Za-z `=@_]*PROCEDURE [`]+([A-Za-z_0-9]*)[`]+' .'[ ]*\(([A-Za-z0-9_\'"\(\) ]*)\)[ \n]*(COMMENT|LANGUAGE|NOT|NO|CONTAINS|READS|MODIFIES|SQL|begin)/', $result['Create Procedure'], $parts); if (count($parts) < 3) throw new InvalidCallException('Error on exec SQL statement'); $argSection = $parts[2]; } $args = []; //preg_match_all('/([A-Za-z_]+)[ ]+([A-Za-z_]+)([\(\)A-Za-z0-9, \'\"]*)[,]*/', $argSection, $args); preg_match_all('/([A-Za-z_]+)[ ]+([A-Za-z_]+)(\([A-Za-z0-9, \'\"]*\)|[ ]+[A-Za-z]*|[.]*)[,]*/', $argSection, $args); for($i = 0, $l = count($args[0]); $i < $l; $i++) { $typeOptions = trim($args[3][$i],' ,)('); $rawSchema[] = [ 'name' => $args[1][$i], 'type' => $args[2][$i], 'typeOptions' => $typeOptions, 'index' => $i, ]; } } else { throw new InvalidParamException('Invalid SQL Function Name'); } $this->_arguments = $rawSchema; \Yii::$app->cache->set($schemaCacheKey, $rewSchema); } else { $this->_functionName = $sqlFunctionName; $this->_arguments = $rawSchema; } $this->_errors = []; } /** * Формує стрічку з аргументами для підстановки в запиті виклику SQL функції чи процедури * @param mixed $argsHash * @return bool|string */ public function bindArgs($argsHash) { $retArgs = []; $this->_errors = []; foreach($this->_arguments as $argInfo) { if (!isset($argsHash[$argInfo['name']]) && ($argsHash instanceof ActiveSqlViewModel && !$argsHash->exists($argInfo['name']))) { $this->_errors[$argInfo['name']] = \Yii::t('api', 'Argument "{argument}" for Sql function "{name}" is missing', ['argument' => $argInfo['name'], 'name' => $this->_functionName]); return false; } $val = $argsHash[$argInfo['name']]; $retArgs[] = (null === $val) ? 'NULL' : \Yii::$app->db->quoteValue($val); } return implode(',', $retArgs); } /** * Повертає метадані про схему аргументів відповідної SQL функції чи процедури * @return bool */ public function getRawSchema() { return $this->_arguments; } /** * Повертає назву поточної SQL функції чи процедури * @return mixed */ public function getFunctionName() { return $this->_functionName; } public function isFunction() { return (self::SQL_FUNCTION === $this->_objectType); } public function isProcedur() { return (self::SQL_PROCEDURE === $this->_objectType); } public function errors() { return $this->_errors; } public function hasError() { return (0 < count($this->_errors)); } /** * Хак для прямого виклику SQL функцій чи процедур, є можливість передати аргументи класичним способом * або через асоціативний масив, також можна передати модель для автозаповнення полів (бажано нащадка ActiveSqlViewModel). * Приклади: * SqlFunctionSchema::user_Login($userId, 'ukr', null); - виклик з простим способом передачі аргументів * SqlFunctionSchema::user_Login([ - виклик з передачою аргументів через масив * 'user_id_' => $userId, * 'browser_langid_' => 'ukr', * 'user_ip' => null, * ]); * * Для виклику процедур потрібно додати префікс 'CALL_' наприклад виклик SQL процедури 'search_Search': * SqlFunctionSchema::CALL_search_Search(); * * @param string $name * @param array $args * @return int */ public static function __callStatic($name, $args) { $type = self::SQL_FUNCTION; if (mb_strlen($name) > 5 && 'CALL_' === mb_substr($name, 0, 5)) { $type = self::SQL_PROCEDURE; $name = str_replace('CALL_', '', $name); } $schema = new SqlFunctionSchema($name, $type); $argsArea = ''; if (isset($args[0]) && is_array($args[0])) { $argsArea = $schema->bindArgs($args[0]); } else if (count($args) == count($schema->_arguments)) { $argsForCommand = []; foreach($schema->_arguments as $argument) { $argsForCommand[$argument['index']] = (null === $args[$argument['index']]) ? 'NULL' : \Yii::$app->db->quoteValue($args[$argument['index']]); } $argsArea = implode(',', $argsForCommand); } $command = \Yii::$app->db->createCommand( ((SqlFunctionSchema::SQL_FUNCTION === $type) ? 'SELECT ' : 'CALL ') . $name .'('.$argsArea.')'); return $command->execute(); } }