diff --git a/CRM/Banking/PluginImpl/Matcher/RegexAnalyser.php b/CRM/Banking/PluginImpl/Matcher/RegexAnalyser.php index 497cb053..813b7b36 100644 --- a/CRM/Banking/PluginImpl/Matcher/RegexAnalyser.php +++ b/CRM/Banking/PluginImpl/Matcher/RegexAnalyser.php @@ -16,16 +16,19 @@ declare(strict_types = 1); +use Civi\Banking\Matcher\RegexAnalyser\RegexAnalyserActionHandlerInterface; +use Civi\Banking\Matcher\RegexAnalyser\RegexAnalyserMatchContext; + /** - * This matcher use regular expressions to extract information from the payment meta information + * This matcher uses regular expressions to extract information from the payment meta information */ class CRM_Banking_PluginImpl_Matcher_RegexAnalyser extends CRM_Banking_PluginModel_Analyser { /** * class constructor */ - public function __construct($config_name) { - parent::__construct($config_name); + public function __construct($plugin_dao) { + parent::__construct($plugin_dao); // read config, set defaults $config = $this->_plugin_config; @@ -41,11 +44,10 @@ public function __construct($config_name) { /** * this matcher does not really create suggestions, but rather enriches the parsed data + * + * @throws \CRM_Core_Exception */ public function analyse(CRM_Banking_BAO_BankTransaction $btx, CRM_Banking_Matcher_Context $context) { - $config = $this->_plugin_config; - $data_parsed = $btx->getDataParsed(); - // iterate through all rules foreach ($this->_plugin_config->rules as $rule) { if (empty($rule->fields)) { @@ -68,348 +70,65 @@ public function analyse(CRM_Banking_BAO_BankTransaction $btx, CRM_Banking_Matche // appy rule to all the fields listed... foreach ($fields as $field) { $matches = []; - $field_data = $this->getValue($field, NULL, NULL, $data_parsed, $btx); + $fieldData = $this->getValue($field, $btx) ?? ''; + if (!is_scalar($fieldData)) { + continue; + } // match the pattern on the given field data - $match_count = preg_match_all($pattern, (string) $field_data, $matches); + $matchCount = preg_match_all($pattern, (string) $fieldData, $matches); // and execute the actions for each match... - for ($i = 0; $i < $match_count; $i++) { + for ($i = 0; $i < $matchCount; $i++) { $this->logMessage("Rule '{$rule->pattern}' matched.", 'debug'); - $this->processMatch($matches, $i, $data_parsed, $rule, $btx); + $this->processMatch($matches, $i, $rule, $btx); } } } - - // save changes and that's it - $btx->setDataParsed($data_parsed); } /** * execute all the action defined by the rule to the given match * - * phpcs:disable Generic.Metrics.CyclomaticComplexity.MaxExceeded + * @param array> $matchData + * Matches of preg_match_all(). + * + * @throws \CRM_Core_Exception */ - public function processMatch($match_data, $match_index, &$data_parsed, $rule, $btx) { - // phpcs:enable + private function processMatch( + array $matchData, + int $matchIndex, + \stdClass $rule, + \CRM_Banking_BAO_BankTransaction $btx + ): void { + $matchContext = new RegexAnalyserMatchContext($matchData, $matchIndex, $rule, $this, $btx, $this->logger); + /** @var \Civi\Banking\Matcher\RegexAnalyser\RegexAnalyserActionHandlerInterface $actionHandler */ + $actionHandler = Civi::service(RegexAnalyserActionHandlerInterface::class); foreach ($rule->actions as $action) { - if ($action->action == 'copy') { - // COPY value from match group to parsed data - $data_parsed[$action->to] = $this->getValue($action->from, $match_data, $match_index, $data_parsed, $btx); - - } - elseif ($action->action == 'copy_append') { - // COPY value, but append to the target - $data_parsed[$action->to] .= $this->getValue($action->from, $match_data, $match_index, $data_parsed, $btx); - - } - elseif ($action->action == 'copy_ltrim_zeros') { - // COPY value, but remove leading zeros - $data_parsed[$action->to] = ltrim($this->getValue($action->from, $match_data, $match_index, $data_parsed, $btx), '0'); - - } - elseif ($action->action == 'set') { - // SET value regardless of the match context - $data_parsed[$action->to] = $action->value; - - } - elseif ($action->action == 'align_date') { - // ALIGN a date forwards or backwards - $value = $this->getValue($action->from, $match_data, $match_index, $data_parsed, $btx); - $data_parsed[$action->to] = CRM_Utils_BankingToolbox::alignDateTime($value, $rule->offset, $rule->skip); - - } - elseif ($action->action == 'unset') { - // UNSET a certain value - unset($data_parsed[$action->to]); - - } - elseif ($action->action == 'strtolower') { - // data to lowercase - $data_parsed[$action->to] = strtolower($this->getValue($action->from, $match_data, $match_index, $data_parsed, $btx)); - - } - elseif ($action->action == 'sha1') { - // reduce to SHA1 checksum - $data_parsed[$action->to] = sha1($this->getValue($action->from, $match_data, $match_index, $data_parsed, $btx)); - - } - elseif (substr($action->action, 0, 7) == 'sprint:') { - // format data - $data = $this->getValue($action->from, $match_data, $match_index, $data_parsed, $btx); - $format = substr($action->action, 7); - $data_parsed[$action->to] = sprintf($format, $data); - - } - elseif ($action->action == 'preg_replace') { - // perform preg_replace - if (empty($action->search_pattern) || !isset($action->replace)) { - error_log("org.project60.banking bad 'preg_replace' spec in plugin {$this->_plugin_id}."); - } - else { - $subject = $this->getValue($action->from, $match_data, $match_index, $data_parsed, $btx); - $data_parsed[$action->to] = preg_replace($action->search_pattern, $action->replace, (string) $subject); - } - - } - elseif ($action->action == 'calculate') { - // CALCULATE the new value with an php expression, using {}-based tokens - $expression = $action->from; - $matches = []; - while (preg_match('#(?P{[^}]+})#', $expression, $matches)) { - // replace variable with value - $token = trim($matches[0], '{}'); - $value = $this->getValue($token, $match_data, $match_index, $data_parsed, $btx); - $expression = preg_replace('#(?P{[^}]+})#', (string) $value, $expression, 1); - } - // phpcs:disable Drupal.Functions.DiscouragedFunctions.Discouraged - $data_parsed[$action->to] = eval("return $expression;"); - // phpcs:enable - - } - elseif ($action->action == 'map') { - // MAP a value given a list of replacements - $value = $this->getValue($action->from, $match_data, $match_index, $data_parsed, $btx); - if (isset($action->mapping->$value)) { - $data_parsed[$action->to] = $action->mapping->$value; - } - - } - elseif (substr($action->action, 0, 7) == 'lookup:') { - // LOOK UP values via API::getsingle - // parameters are in format: "EntityName,result_field,lookup_field" - $params = explode(',', substr($action->action, 7)); - $value = $this->getValue($action->from, $match_data, $match_index, $data_parsed, $btx); - $query = [$params[2] => $value, 'return' => $params[1]]; - if (!empty($action->parameters)) { - foreach ($action->parameters as $key => $value) { - $query[$key] = $value; - } - } - - // execute and log - $this->logger->setTimer('regex:lookup'); - - $this->logMessage("Calling API {$params[0]}.getsingle: " . json_encode($query), 'debug'); - $result = $this->executeAPIQuery($params[0], 'getsingle', $query, $action); - $this->logMessage('API result: ' . json_encode($result), 'debug'); - $this->logTime("API {$params[0]}.getsingle", 'regex:lookup'); - - if (empty($result['is_error'])) { - // something was found... copy value - $data_parsed[$action->to] = $result[$params[1]]; - } - - } - elseif (substr($action->action, 0, 4) == 'api:') { - /** - * Look up parameters via API call - * the 'action' format is: "::[:multiple]" - * EntityName the CiviCRM API entity - * action the CiviCRM API action - * result_field the field to take from the result - * multiple if this is given, multiple results will be added to the field, separated by comma - * otherwise the result will only be copied if exactly one match was found - * - * further attributes can be given as follows: - * const_ set the API parameter to a constant, e.g. const_contact_type = 'Individual' - * param_ set the API parameter to the value of another field, e.g. const_first_name = 'first_name' - */ - // compile query - $params = explode(':', substr($action->action, 4)); - $query = ['return' => $params[2]]; - foreach ($action as $key => $value) { - if (substr($key, 0, 6) == 'const_') { - $query[substr($key, 6)] = $value; - } - elseif (substr($key, 0, 6) == 'param_') { - $query[substr($key, 6)] = $this->getValue($value, $match_data, $match_index, $data_parsed, $btx); - } - elseif (substr($key, 0, 10) == 'jsonparam_') { - $query[substr($key, 10)] = json_decode($this->getValue($value, $match_data, $match_index, $data_parsed, $btx), TRUE); - } - elseif (substr($key, 0, 10) == 'jsonconst_') { - $query[substr($key, 10)] = json_decode($value, TRUE); - } - } - - // execute query - try { - $this->logger->setTimer('regex:api'); - $this->logMessage("Calling API {$params[0]}.{$params[1]}: " . json_encode($query), 'debug'); - $result = $this->executeAPIQuery($params[0], $params[1], $query, $action); - $this->logMessage('API result: ' . json_encode($result), 'debug'); - $this->logTime("API {$params[0]}.{$params[1]}", 'regex:api'); - - if (isset($params[3]) && $params[3] == 'multiple') { - // multiple values allowed - $results = []; - foreach ($result['values'] as $entity) { - $results[] = $entity[$params[2]]; - } - $data_parsed[$action->to] = implode(',', $results); - - } - else { - // only valid if it's the only value - if ($result['count'] == 1) { - $entity = reset($result['values']); - $data_parsed[$action->to] = $entity[$params[2]]; - } - } - } - catch (Exception $e) { - // TODO: this didn't work... how can we do this? - $this->logMessage("Exception in API {$params[0]}.{$params[1]}: " . $e->getMessage(), 'debug'); - } - - } - else { - error_log("org.project60.banking: RegexAnalyser - bad action: '" . $action->action . "'"); - } + $actionHandler->execute($action, $matchContext); } } /** - * Get the value either from the match context, or the already stored data + * Get the value either from $data_parsed, or the propagation value. */ - protected function getValue($key, $match_data, $match_index, $data_parsed, $btx = NULL) { + private function getValue(string $key, \CRM_Banking_BAO_BankTransaction $btx): mixed { + $dataParsed = $btx->getDataParsed(); // see https://github.com/Project60/org.project60.banking/issues/111 - if ($this->_plugin_config->variable_lookup_compatibility) { - if (!empty($match_data[$key][$match_index])) { - return $match_data[$key][$match_index]; - } - elseif (!empty($data_parsed[$key])) { - return $data_parsed[$key]; - } - else { - // try value propagation - $value = $this->getPropagationValue($btx, NULL, $key); - if ($value) { - return $value; - } - else { - $this->logMessage("RexgexAnalyser - Cannot find source '$key' for rule or filter.", 'debug'); - return ''; - } - } + $lookupCompatibility = $this->_plugin_config->variable_lookup_compatibility; + // @phpstan-ignore empty.notAllowed + if (!empty($dataParsed[$key]) || (!$lookupCompatibility && isset($dataParsed[$key]))) { + return $dataParsed[$key]; } else { - if (isset($match_data[$key][$match_index])) { - return $match_data[$key][$match_index]; - } - elseif (isset($data_parsed[$key])) { - return $data_parsed[$key]; + // try value propagation + $value = $this->getPropagationValue($btx, NULL, $key); + if (NULL === $value) { + $this->logMessage("RegexAnalyser - Cannot find source '$key' for rule or filter.", 'debug'); } - else { - // try value propagation - $value = $this->getPropagationValue($btx, NULL, $key); - if ($value) { - return $value; - } - else { - $this->logMessage("RexgexAnalyser - Cannot find source '$key' for rule or filter.", 'debug'); - return ''; - } - } - } - } - - /** - * execute API Query - * - * phpcs:disable Generic.Metrics.CyclomaticComplexity.TooHigh - */ - protected function executeAPIQuery($entity, $command, $query, $action) { - // phpcs:enable - $command = strtolower($command); - if (empty($action->sql) || !$action->sql || !in_array($command, ['get', 'getsingle'])) { - // execute via API - return civicrm_api3($entity, $command, $query); + return NULL; } - else { - // execute via SQL - // compile select - if (empty($query['return'])) { - $select_clause = '*'; - } - else { - $select_clause = $query['return']; - } - - // compile from - $from = $this->getTableName($entity); - - // compile where - $where_clauses = []; - $query_params = []; - foreach ($query as $key => $value) { - if (!in_array($key, ['return', 'sort', 'limit', 'option'], TRUE)) { - // TODO: support for sort, limit, etc. - // phpcs:disable Generic.CodeAnalysis.EmptyStatement.DetectedIf - if (is_array($value)) { - $this->logMessage('Support for arrays not implemented, will be ignored', 'warning'); - } - else { - $index = count($query_params) + 1; - $where_clauses[] = "`$key` = %$index"; - $query_params[$index] = [$value, 'String']; - } - } - } - if (empty($where_clauses)) { - $where_clause = 'TRUE'; - } - else { - $where_clause = '(' . implode(') AND (', $where_clauses) . ')'; - } - - // should there be a limit - if ($command == 'getsingle') { - $limit = 'LIMIT 1'; - } - elseif (!empty($query['limit'])) { - $limit = "LIMIT {$query['limit']}"; - } - else { - $limit = ''; - } - - // execute the query - $dao_query = CRM_Core_DAO::executeQuery("SELECT {$select_clause} FROM {$from} WHERE {$where_clause} {$limit};", $query_params); - if ($command == 'getsingle') { - if ($dao_query->fetch()) { - return $dao_query->toArray(); - } - else { - return civicrm_api3_create_error('Not found'); - } - } - // phpcs:disable Squiz.PHP.CommentedOutCode.Found - // $command == 'get' - // phpcs:enable - else { - $results = []; - while ($dao_query->fetch()) { - $results[] = $dao_query->toArray(); - } - return civicrm_api3_create_success($results); - } - } - } - - /** - * get the CiviCRM table name for an entity - */ - protected function getTableName($entity) { - // from: https://stackoverflow.com/questions/1993721/how-to-convert-camelcase-to-camel-case#1993772 - preg_match_all('!([A-Z][A-Z0-9]*(?=$|[A-Z][a-z0-9])|[A-Za-z][a-z0-9]+)!', $entity, $matches); - $ret = $matches[0]; - foreach ($ret as &$match) { - $match = $match == strtoupper($match) ? strtolower($match) : lcfirst($match); - } - return 'civicrm_' . implode('_', $ret); } } diff --git a/CRM/Banking/PluginModel/Base.php b/CRM/Banking/PluginModel/Base.php index 56475f52..b668e5f3 100755 --- a/CRM/Banking/PluginModel/Base.php +++ b/CRM/Banking/PluginModel/Base.php @@ -56,6 +56,8 @@ public static function displayName() { /** * class constructor + * + * @param \CRM_Banking_DAO_PluginInstance $plugin_dao */ public function __construct($plugin_dao) { $this->setDAO($plugin_dao); diff --git a/Civi/Banking/CompilerPass.php b/Civi/Banking/DependencyInjection/Compiler/ActionProviderPass.php similarity index 62% rename from Civi/Banking/CompilerPass.php rename to Civi/Banking/DependencyInjection/Compiler/ActionProviderPass.php index 522f018e..3135cf5b 100755 --- a/Civi/Banking/CompilerPass.php +++ b/Civi/Banking/DependencyInjection/Compiler/ActionProviderPass.php @@ -1,17 +1,33 @@ . + */ declare(strict_types = 1); -namespace Civi\Banking; +namespace Civi\Banking\DependencyInjection\Compiler; +use CRM_Banking_ExtensionUtil as E; use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; use Symfony\Component\DependencyInjection\ContainerBuilder; -use CRM_Banking_ExtensionUtil as E; /** * Compiler Class for action provider */ -class CompilerPass implements CompilerPassInterface { +class ActionProviderPass implements CompilerPassInterface { public function process(ContainerBuilder $container): void { if ($container->hasDefinition('action_provider')) { diff --git a/Civi/Banking/DependencyInjection/Compiler/RegexAnalyserActionHandlerPass.php b/Civi/Banking/DependencyInjection/Compiler/RegexAnalyserActionHandlerPass.php new file mode 100644 index 00000000..1b26e92f --- /dev/null +++ b/Civi/Banking/DependencyInjection/Compiler/RegexAnalyserActionHandlerPass.php @@ -0,0 +1,71 @@ +. + */ + +declare(strict_types = 1); + +namespace Civi\Banking\DependencyInjection\Compiler; + +use Civi\Banking\Matcher\RegexAnalyser\RegexAnalyserActionHandlerCollector; +use Civi\Banking\Matcher\RegexAnalyser\RegexAnalyserActionHandlerInterface; +use Civi\Core\ClassScanner; +use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; +use Symfony\Component\DependencyInjection\Compiler\ServiceLocatorTagPass; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Reference; + +final class RegexAnalyserActionHandlerPass implements CompilerPassInterface { + + public function process(ContainerBuilder $container): void { + $services = []; + + foreach (ClassScanner::get(['interface' => RegexAnalyserActionHandlerInterface::class]) as $class) { + if (RegexAnalyserActionHandlerCollector::class === $class) { + continue; + } + + $constantName = $class . '::NAME'; + if (!defined($constantName)) { + throw new \RuntimeException(sprintf('Constant "NAME" is missing in class "%s"', $class)); + } + + /** @var string $actionName */ + $actionName = constant($constantName); + if (isset($services[$actionName])) { + throw new \RuntimeException( + sprintf( + 'Duplicate action handler with action name "%s" (%s, %s)', + $actionName, + (string) $services[$actionName], + $class, + ) + ); + } + + if (!$container->has($class)) { + $container->autowire($class, $class); + } + + $services[$actionName] = new Reference($class); + } + + $container->register(RegexAnalyserActionHandlerInterface::class, RegexAnalyserActionHandlerCollector::class) + ->addArgument(ServiceLocatorTagPass::register($container, $services)) + ->setPublic(TRUE); + } + +} diff --git a/Civi/Banking/Matcher/RegexAnalyser/ActionHandlers/AlignDateRegexAnalyserActionHandler.php b/Civi/Banking/Matcher/RegexAnalyser/ActionHandlers/AlignDateRegexAnalyserActionHandler.php new file mode 100644 index 00000000..6350c46b --- /dev/null +++ b/Civi/Banking/Matcher/RegexAnalyser/ActionHandlers/AlignDateRegexAnalyserActionHandler.php @@ -0,0 +1,50 @@ +. + */ + +declare(strict_types = 1); + +namespace Civi\Banking\Matcher\RegexAnalyser\ActionHandlers; + +use Civi\Banking\Matcher\RegexAnalyser\RegexAnalyserActionHandlerInterface; +use Civi\Banking\Matcher\RegexAnalyser\RegexAnalyserMatchContext; + +/** + * ALIGN a date forwards or backwards. + */ +final class AlignDateRegexAnalyserActionHandler implements RegexAnalyserActionHandlerInterface { + + public const NAME = 'align_date'; + + public function execute(\stdClass $action, RegexAnalyserMatchContext $matchContext): void { + $value = $matchContext->getValue($action->from); + if (!is_string($value)) { + $matchContext->logMessage(sprintf('The value of "%s" for action "%s" must be a string', $action->from, self::NAME)); + } + else { + $matchContext->setParsedValue( + $action->to, + \CRM_Utils_BankingToolbox::alignDateTime( + $value, + $matchContext->rule->offset ?? '0', + $matchContext->rule->skip ?? [] + ) + ); + } + } + +} diff --git a/Civi/Banking/Matcher/RegexAnalyser/ActionHandlers/ApiRegexAnalyserActionHandler.php b/Civi/Banking/Matcher/RegexAnalyser/ActionHandlers/ApiRegexAnalyserActionHandler.php new file mode 100644 index 00000000..3e6f9cbe --- /dev/null +++ b/Civi/Banking/Matcher/RegexAnalyser/ActionHandlers/ApiRegexAnalyserActionHandler.php @@ -0,0 +1,208 @@ +. + */ + +declare(strict_types = 1); + +namespace Civi\Banking\Matcher\RegexAnalyser\ActionHandlers; + +use Civi\Banking\Matcher\RegexAnalyser\RegexAnalyserActionHandlerInterface; +use Civi\Banking\Matcher\RegexAnalyser\RegexAnalyserMatchContext; + +/** + * Look up parameters via API call. + * + * the 'action' format is: '::[:multiple]' + * EntityName the CiviCRM API entity + * action the CiviCRM API action + * result_field the field to take from the result + * multiple if this is given, multiple results will be added to the field, separated by comma + * otherwise the result will only be copied if exactly one match was found + * + * further attributes can be given as follows: + * const_ set the API parameter to a constant, e.g. const_contact_type = 'Individual' + * param_ set the API parameter to the value of another field, e.g. const_first_name = 'first_name' + */ +final class ApiRegexAnalyserActionHandler implements RegexAnalyserActionHandlerInterface { + + public const NAME = 'api'; + + private \CRM_Banking_Helpers_Logger $logger; + + public function __construct( + ?\CRM_Banking_Helpers_Logger $logger = NULL + ) { + $this->logger = $logger ?? \CRM_Banking_Helpers_Logger::getLogger(); + } + + // phpcs:ignore Generic.Metrics.CyclomaticComplexity.TooHigh + public function execute(\stdClass $action, RegexAnalyserMatchContext $matchContext): void { + // compile query + $params = explode(':', substr($action->action, 4)); + $query = ['return' => $params[2]]; + // @phpstan-ignore foreach.nonIterable + foreach ($action as $key => $value) { + if (str_starts_with($key, 'const_')) { + $query[substr($key, 6)] = $value; + } + elseif (str_starts_with($key, 'param_')) { + $query[substr($key, 6)] = $matchContext->getValue($value) ?? ''; + } + elseif (str_starts_with($key, 'jsonparam_')) { + $query[substr($key, 10)] = json_decode((string) ($matchContext->getValue($value) ?? ''), TRUE); + } + elseif (str_starts_with($key, 'jsonconst_')) { + $query[substr($key, 10)] = json_decode($value, TRUE); + } + } + + // execute query + try { + $this->logger->setTimer('regex:api'); + $matchContext->logMessage("Calling API {$params[0]}.{$params[1]}: " . json_encode($query), 'debug'); + $result = $this->executeAPIQuery($params[0], $params[1], $query, $action, $matchContext); + $matchContext->logMessage('API result: ' . json_encode($result), 'debug'); + $matchContext->logTime("API {$params[0]}.{$params[1]}", 'regex:api'); + + if (isset($params[3]) && $params[3] === 'multiple') { + // multiple values allowed + $results = []; + foreach ($result['values'] as $entity) { + $results[] = (string) $entity[$params[2]]; + } + $matchContext->setParsedValue($action->to, implode(',', $results)); + } + else { + // only valid if it's the only value + if ($result['count'] == 1) { + $entity = reset($result['values']); + $matchContext->setParsedValue($action->to, $entity[$params[2]]); + } + } + } + catch (\Exception $e) { + // @ignoreException + // TODO: this didn't work... how can we do this? + $matchContext->logMessage("Exception in API {$params[0]}.{$params[1]}: " . $e->getMessage(), 'debug'); + } + } + + /** + * execute API Query + * + * @param array $query + * + * @return array + * + * @throws \CRM_Core_Exception + * + * phpcs:disable Generic.Metrics.CyclomaticComplexity.TooHigh + */ + private function executeAPIQuery(string $entity, string $command, array $query, \stdClass $action, RegexAnalyserMatchContext $matchContext): array { + // phpcs:enable + $command = strtolower($command); + if (empty($action->sql) || !in_array($command, ['get', 'getsingle'], TRUE)) { + // execute via API + // @phpstan-ignore return.type + return civicrm_api3($entity, $command, $query); + } + else { + // execute via SQL + // compile select + if (empty($query['return'])) { + $select_clause = '*'; + } + else { + $select_clause = $query['return']; + } + + // compile from + $from = $this->getTableName($entity); + + // compile where + $where_clauses = []; + $query_params = []; + foreach ($query as $key => $value) { + if (!in_array($key, ['return', 'sort', 'limit', 'option'], TRUE)) { + // TODO: support for sort, limit, etc. + // phpcs:disable Generic.CodeAnalysis.EmptyStatement.DetectedIf + if (is_array($value)) { + $matchContext->logMessage('Support for arrays not implemented, will be ignored', 'warning'); + } + else { + $index = count($query_params) + 1; + $where_clauses[] = "`$key` = %$index"; + $query_params[$index] = [$value, 'String']; + } + } + } + if ([] === $where_clauses) { + $where_clause = 'TRUE'; + } + else { + $where_clause = '(' . implode(') AND (', $where_clauses) . ')'; + } + + // should there be a limit + if ($command === 'getsingle') { + $limit = 'LIMIT 1'; + } + elseif (!empty($query['limit'])) { + $limit = "LIMIT {$query['limit']}"; + } + else { + $limit = ''; + } + + // execute the query + /** @var \CRM_Core_DAO $dao_query */ + $dao_query = \CRM_Core_DAO::executeQuery("SELECT {$select_clause} FROM {$from} WHERE {$where_clause} {$limit};", $query_params); + if ($command === 'getsingle') { + if ($dao_query->fetch()) { + return $dao_query->toArray(); + } + else { + return civicrm_api3_create_error('Not found'); + } + } + // phpcs:disable Squiz.PHP.CommentedOutCode.Found + // $command == 'get' + // phpcs:enable + else { + $results = []; + while ($dao_query->fetch()) { + $results[] = $dao_query->toArray(); + } + return civicrm_api3_create_success($results); + } + } + } + + /** + * get the CiviCRM table name for an entity + */ + private function getTableName(string $entity): string { + // from: https://stackoverflow.com/questions/1993721/how-to-convert-camelcase-to-camel-case#1993772 + preg_match_all('!([A-Z][A-Z0-9]*(?=$|[A-Z][a-z0-9])|[A-Za-z][a-z0-9]+)!', $entity, $matches); + $ret = $matches[0]; + foreach ($ret as &$match) { + $match = $match === strtoupper($match) ? strtolower($match) : lcfirst($match); + } + return 'civicrm_' . implode('_', $ret); + } + +} diff --git a/Civi/Banking/Matcher/RegexAnalyser/ActionHandlers/CalculateRegexAnalyserActionHandler.php b/Civi/Banking/Matcher/RegexAnalyser/ActionHandlers/CalculateRegexAnalyserActionHandler.php new file mode 100644 index 00000000..b33810b7 --- /dev/null +++ b/Civi/Banking/Matcher/RegexAnalyser/ActionHandlers/CalculateRegexAnalyserActionHandler.php @@ -0,0 +1,63 @@ +. + */ + +declare(strict_types = 1); + +namespace Civi\Banking\Matcher\RegexAnalyser\ActionHandlers; + +use Civi\Banking\Matcher\RegexAnalyser\RegexAnalyserActionHandlerInterface; +use Civi\Banking\Matcher\RegexAnalyser\RegexAnalyserMatchContext; +use Webmozart\Assert\Assert; + +/** + * CALCULATE the new value with an php expression, using {}-based tokens. + */ +final class CalculateRegexAnalyserActionHandler implements RegexAnalyserActionHandlerInterface { + + public const NAME = 'calculate'; + + public function execute(\stdClass $action, RegexAnalyserMatchContext $matchContext): void { + $expression = $action->from; + Assert::string($expression, sprintf('"from" must be a string for the %s action', self::NAME)); + + $matches = []; + while (preg_match('#(?P{[^}]+})#', $expression, $matches)) { + // replace variable with value + $token = trim($matches[0], '{}'); + $value = $matchContext->getValue($token); + if (!is_scalar($value) && NULL !== $value) { + $matchContext->logMessage( + sprintf('The value of "%s" for action "%s" must be a scalar or null', $token, self::NAME) + ); + + return; + } + + $expression = preg_replace('#(?P{[^}]+})#', (string) $value, $expression, 1); + if (!is_string($expression)) { + $matchContext->logMessage(sprintf('Invalid expression "%s" for action "%s"', $expression, self::NAME)); + + return; + } + } + // phpcs:disable Drupal.Functions.DiscouragedFunctions.Discouraged + $matchContext->setParsedValue($action->to, eval("return $expression;")); + // phpcs:enable + } + +} diff --git a/Civi/Banking/Matcher/RegexAnalyser/ActionHandlers/CopyAppendRegexAnalyserActionHandler.php b/Civi/Banking/Matcher/RegexAnalyser/ActionHandlers/CopyAppendRegexAnalyserActionHandler.php new file mode 100644 index 00000000..a1586cac --- /dev/null +++ b/Civi/Banking/Matcher/RegexAnalyser/ActionHandlers/CopyAppendRegexAnalyserActionHandler.php @@ -0,0 +1,48 @@ +. + */ + +declare(strict_types = 1); + +namespace Civi\Banking\Matcher\RegexAnalyser\ActionHandlers; + +use Civi\Banking\Matcher\RegexAnalyser\RegexAnalyserActionHandlerInterface; +use Civi\Banking\Matcher\RegexAnalyser\RegexAnalyserMatchContext; + +/** + * COPY value, but remove leading zeros. + */ +final class CopyAppendRegexAnalyserActionHandler implements RegexAnalyserActionHandlerInterface { + + public const NAME = 'copy_append'; + + public function execute(\stdClass $action, RegexAnalyserMatchContext $matchContext): void { + $currentValue = $matchContext->getParsedValue($action->to); + $value = $matchContext->getValue($action->from); + + if (!is_scalar($currentValue) && NULL !== $currentValue) { + $matchContext->logMessage(sprintf('The parsed value of "%s" for action "%s" must be a scalar or null', $action->to, self::NAME)); + } + elseif (!is_scalar($value) && NULL !== $value) { + $matchContext->logMessage(sprintf('The value of "%s" for action "%s" must be a scalar or null', $action->from, self::NAME)); + } + else { + $matchContext->setParsedValue($action->to, $currentValue . $value); + } + } + +} diff --git a/Civi/Banking/Matcher/RegexAnalyser/ActionHandlers/CopyLtrimZerosRegexAnalyserActionHandler.php b/Civi/Banking/Matcher/RegexAnalyser/ActionHandlers/CopyLtrimZerosRegexAnalyserActionHandler.php new file mode 100644 index 00000000..2c61fbae --- /dev/null +++ b/Civi/Banking/Matcher/RegexAnalyser/ActionHandlers/CopyLtrimZerosRegexAnalyserActionHandler.php @@ -0,0 +1,43 @@ +. + */ + +declare(strict_types = 1); + +namespace Civi\Banking\Matcher\RegexAnalyser\ActionHandlers; + +use Civi\Banking\Matcher\RegexAnalyser\RegexAnalyserActionHandlerInterface; +use Civi\Banking\Matcher\RegexAnalyser\RegexAnalyserMatchContext; + +/** + * COPY value, but remove leading zeros. + */ +final class CopyLtrimZerosRegexAnalyserActionHandler implements RegexAnalyserActionHandlerInterface { + + public const NAME = 'copy_ltrim_zeros'; + + public function execute(\stdClass $action, RegexAnalyserMatchContext $matchContext): void { + $value = $matchContext->getValue($action->from) ?? ''; + if (!is_string($value)) { + $matchContext->logMessage(sprintf('The value of "%s" for action "%s" must be a string', $action->from, self::NAME)); + } + else { + $matchContext->setParsedValue($action->to, ltrim($value, '0')); + } + } + +} diff --git a/Civi/Banking/Matcher/RegexAnalyser/ActionHandlers/CopyRegexAnalyserActionHandler.php b/Civi/Banking/Matcher/RegexAnalyser/ActionHandlers/CopyRegexAnalyserActionHandler.php new file mode 100644 index 00000000..03429b9b --- /dev/null +++ b/Civi/Banking/Matcher/RegexAnalyser/ActionHandlers/CopyRegexAnalyserActionHandler.php @@ -0,0 +1,37 @@ +. + */ + +declare(strict_types = 1); + +namespace Civi\Banking\Matcher\RegexAnalyser\ActionHandlers; + +use Civi\Banking\Matcher\RegexAnalyser\RegexAnalyserActionHandlerInterface; +use Civi\Banking\Matcher\RegexAnalyser\RegexAnalyserMatchContext; + +/** + * COPY value from match group to parsed data. + */ +final class CopyRegexAnalyserActionHandler implements RegexAnalyserActionHandlerInterface { + + public const NAME = 'copy'; + + public function execute(\stdClass $action, RegexAnalyserMatchContext $matchContext): void { + $matchContext->setParsedValue($action->to, $matchContext->getValue($action->from) ?? ''); + } + +} diff --git a/Civi/Banking/Matcher/RegexAnalyser/ActionHandlers/LookupRegexAnalyserActionHandler.php b/Civi/Banking/Matcher/RegexAnalyser/ActionHandlers/LookupRegexAnalyserActionHandler.php new file mode 100644 index 00000000..f94f67c9 --- /dev/null +++ b/Civi/Banking/Matcher/RegexAnalyser/ActionHandlers/LookupRegexAnalyserActionHandler.php @@ -0,0 +1,57 @@ +. + */ + +declare(strict_types = 1); + +namespace Civi\Banking\Matcher\RegexAnalyser\ActionHandlers; + +use Civi\Banking\Matcher\RegexAnalyser\RegexAnalyserActionHandlerInterface; +use Civi\Banking\Matcher\RegexAnalyser\RegexAnalyserMatchContext; + +/** + * LOOK UP values via API::getsingle. + */ +final class LookupRegexAnalyserActionHandler implements RegexAnalyserActionHandlerInterface { + + public const NAME = 'lookup'; + + public function execute(\stdClass $action, RegexAnalyserMatchContext $matchContext): void { + // parameters are in format: "EntityName,result_field,lookup_field" + $params = explode(',', substr($action->action, 7)); + $value = $matchContext->getValue($action->from) ?? ''; + $query = [$params[2] => $value, 'return' => $params[1]]; + if (is_array($action->parameters ?? NULL)) { + $query += $action->parameters; + } + + // execute and log + $matchContext->setLogTimer('regex:lookup'); + + $matchContext->logMessage("Calling API {$params[0]}.getsingle: " . json_encode($query), 'debug'); + /** @var array $result */ + $result = civicrm_api3($params[0], 'getsingle', $query); + $matchContext->logMessage('API result: ' . json_encode($result), 'debug'); + $matchContext->logTime("API {$params[0]}.getsingle", 'regex:lookup'); + + if (!(bool) ($result['is_error'] ?? NULL)) { + // something was found... copy value + $matchContext->setParsedValue($action->to, $result[$params[1]]); + } + } + +} diff --git a/Civi/Banking/Matcher/RegexAnalyser/ActionHandlers/MapRegexAnalyserActionHandler.php b/Civi/Banking/Matcher/RegexAnalyser/ActionHandlers/MapRegexAnalyserActionHandler.php new file mode 100644 index 00000000..282e0d83 --- /dev/null +++ b/Civi/Banking/Matcher/RegexAnalyser/ActionHandlers/MapRegexAnalyserActionHandler.php @@ -0,0 +1,46 @@ +. + */ + +declare(strict_types = 1); + +namespace Civi\Banking\Matcher\RegexAnalyser\ActionHandlers; + +use Civi\Banking\Matcher\RegexAnalyser\RegexAnalyserActionHandlerInterface; +use Civi\Banking\Matcher\RegexAnalyser\RegexAnalyserMatchContext; +use Webmozart\Assert\Assert; + +/** + * MAP a value given a list of replacements. + */ +final class MapRegexAnalyserActionHandler implements RegexAnalyserActionHandlerInterface { + + public const NAME = 'map'; + + public function execute(\stdClass $action, RegexAnalyserMatchContext $matchContext): void { + Assert::isInstanceOf($action->mapping, \stdClass::class, sprintf('"mapping" must be an object for the %s action', self::NAME)); + + $value = $matchContext->getValue($action->from); + if (!is_string($value)) { + $matchContext->logMessage(sprintf('The value of "%s" for action "%s" must be a string', $action->from, self::NAME)); + } + elseif (property_exists($action->mapping, $value)) { + $matchContext->setParsedValue($action->to, $action->mapping->$value); + } + } + +} diff --git a/Civi/Banking/Matcher/RegexAnalyser/ActionHandlers/PregReplaceRegexAnalyserActionHandler.php b/Civi/Banking/Matcher/RegexAnalyser/ActionHandlers/PregReplaceRegexAnalyserActionHandler.php new file mode 100644 index 00000000..bedf8172 --- /dev/null +++ b/Civi/Banking/Matcher/RegexAnalyser/ActionHandlers/PregReplaceRegexAnalyserActionHandler.php @@ -0,0 +1,56 @@ +. + */ + +declare(strict_types = 1); + +namespace Civi\Banking\Matcher\RegexAnalyser\ActionHandlers; + +use Civi\Banking\Matcher\RegexAnalyser\RegexAnalyserActionHandlerInterface; +use Civi\Banking\Matcher\RegexAnalyser\RegexAnalyserMatchContext; + +/** + * perform preg_replace. + */ +final class PregReplaceRegexAnalyserActionHandler implements RegexAnalyserActionHandlerInterface { + + public const NAME = 'preg_replace'; + + public function execute(\stdClass $action, RegexAnalyserMatchContext $matchContext): void { + if ( + !is_string($action->search_pattern ?? NULL) + || '' === ($action->search_pattern) + || !is_string($action->replace ?? NULL) + ) { + $matchContext->logMessage(sprintf('Bad "%s" spec', self::NAME)); + + return; + } + + $subject = $matchContext->getValue($action->from); + if (!is_scalar($subject) && NULL !== $subject) { + $matchContext->logMessage(sprintf('The value of "%s" for action "%s" must be a string or null', $action->from, self::NAME)); + } + else { + $matchContext->setParsedValue( + $action->to, + preg_replace($action->search_pattern, $action->replace, (string) $subject) + ); + } + } + +} diff --git a/Civi/Banking/Matcher/RegexAnalyser/ActionHandlers/SetRegexAnalyserActionHandler.php b/Civi/Banking/Matcher/RegexAnalyser/ActionHandlers/SetRegexAnalyserActionHandler.php new file mode 100644 index 00000000..38f53720 --- /dev/null +++ b/Civi/Banking/Matcher/RegexAnalyser/ActionHandlers/SetRegexAnalyserActionHandler.php @@ -0,0 +1,37 @@ +. + */ + +declare(strict_types = 1); + +namespace Civi\Banking\Matcher\RegexAnalyser\ActionHandlers; + +use Civi\Banking\Matcher\RegexAnalyser\RegexAnalyserActionHandlerInterface; +use Civi\Banking\Matcher\RegexAnalyser\RegexAnalyserMatchContext; + +/** + * SET value regardless of the match context. + */ +final class SetRegexAnalyserActionHandler implements RegexAnalyserActionHandlerInterface { + + public const NAME = 'set'; + + public function execute(\stdClass $action, RegexAnalyserMatchContext $matchContext): void { + $matchContext->setParsedValue($action->to, $action->value); + } + +} diff --git a/Civi/Banking/Matcher/RegexAnalyser/ActionHandlers/Sha1RegexAnalyserActionHandler.php b/Civi/Banking/Matcher/RegexAnalyser/ActionHandlers/Sha1RegexAnalyserActionHandler.php new file mode 100644 index 00000000..2f5318f1 --- /dev/null +++ b/Civi/Banking/Matcher/RegexAnalyser/ActionHandlers/Sha1RegexAnalyserActionHandler.php @@ -0,0 +1,43 @@ +. + */ + +declare(strict_types = 1); + +namespace Civi\Banking\Matcher\RegexAnalyser\ActionHandlers; + +use Civi\Banking\Matcher\RegexAnalyser\RegexAnalyserActionHandlerInterface; +use Civi\Banking\Matcher\RegexAnalyser\RegexAnalyserMatchContext; + +/** + * reduce to SHA1 checksum. + */ +final class Sha1RegexAnalyserActionHandler implements RegexAnalyserActionHandlerInterface { + + public const NAME = 'sha1'; + + public function execute(\stdClass $action, RegexAnalyserMatchContext $matchContext): void { + $value = $matchContext->getValue($action->from) ?? ''; + if (!is_string($value)) { + $matchContext->logMessage(sprintf('The value of "%s" for action "%s" must be a string', $action->from, self::NAME)); + } + else { + $matchContext->setParsedValue($action->to, sha1($value)); + } + } + +} diff --git a/Civi/Banking/Matcher/RegexAnalyser/ActionHandlers/SprintRegexAnalyserActionHandler.php b/Civi/Banking/Matcher/RegexAnalyser/ActionHandlers/SprintRegexAnalyserActionHandler.php new file mode 100644 index 00000000..2c812461 --- /dev/null +++ b/Civi/Banking/Matcher/RegexAnalyser/ActionHandlers/SprintRegexAnalyserActionHandler.php @@ -0,0 +1,47 @@ +. + */ + +declare(strict_types = 1); + +namespace Civi\Banking\Matcher\RegexAnalyser\ActionHandlers; + +use Civi\Banking\Matcher\RegexAnalyser\RegexAnalyserActionHandlerInterface; +use Civi\Banking\Matcher\RegexAnalyser\RegexAnalyserMatchContext; + +/** + * format data. + */ +final class SprintRegexAnalyserActionHandler implements RegexAnalyserActionHandlerInterface { + + public const NAME = 'sprint'; + + public function execute(\stdClass $action, RegexAnalyserMatchContext $matchContext): void { + $format = substr($action->action, 7); + + $value = $matchContext->getValue($action->from); + if (!is_scalar($value) && NULL !== $value) { + $matchContext->logMessage( + sprintf('The value of "%s" for action "%s" must be a scalar or null', $action->from, self::NAME) + ); + } + else { + $matchContext->setParsedValue($action->to, sprintf($format, $value)); + } + } + +} diff --git a/Civi/Banking/Matcher/RegexAnalyser/ActionHandlers/StrtolowerRegexAnalyserActionHandler.php b/Civi/Banking/Matcher/RegexAnalyser/ActionHandlers/StrtolowerRegexAnalyserActionHandler.php new file mode 100644 index 00000000..02160031 --- /dev/null +++ b/Civi/Banking/Matcher/RegexAnalyser/ActionHandlers/StrtolowerRegexAnalyserActionHandler.php @@ -0,0 +1,43 @@ +. + */ + +declare(strict_types = 1); + +namespace Civi\Banking\Matcher\RegexAnalyser\ActionHandlers; + +use Civi\Banking\Matcher\RegexAnalyser\RegexAnalyserActionHandlerInterface; +use Civi\Banking\Matcher\RegexAnalyser\RegexAnalyserMatchContext; + +/** + * String to lowercase. + */ +final class StrtolowerRegexAnalyserActionHandler implements RegexAnalyserActionHandlerInterface { + + public const NAME = 'strtolower'; + + public function execute(\stdClass $action, RegexAnalyserMatchContext $matchContext): void { + $value = $matchContext->getValue($action->from) ?? ''; + if (!is_string($value)) { + $matchContext->logMessage(sprintf('The value of "%s" for action "%s" must be a string', $action->from, self::NAME)); + } + else { + $matchContext->setParsedValue($action->to, strtolower($value)); + } + } + +} diff --git a/Civi/Banking/Matcher/RegexAnalyser/ActionHandlers/UnsetRegexAnalyserActionHandler.php b/Civi/Banking/Matcher/RegexAnalyser/ActionHandlers/UnsetRegexAnalyserActionHandler.php new file mode 100644 index 00000000..6d512e67 --- /dev/null +++ b/Civi/Banking/Matcher/RegexAnalyser/ActionHandlers/UnsetRegexAnalyserActionHandler.php @@ -0,0 +1,37 @@ +. + */ + +declare(strict_types = 1); + +namespace Civi\Banking\Matcher\RegexAnalyser\ActionHandlers; + +use Civi\Banking\Matcher\RegexAnalyser\RegexAnalyserActionHandlerInterface; +use Civi\Banking\Matcher\RegexAnalyser\RegexAnalyserMatchContext; + +/** + * UNSET a certain value. + */ +final class UnsetRegexAnalyserActionHandler implements RegexAnalyserActionHandlerInterface { + + public const NAME = 'unset'; + + public function execute(\stdClass $action, RegexAnalyserMatchContext $matchContext): void { + $matchContext->removeParsedValue($action->to); + } + +} diff --git a/Civi/Banking/Matcher/RegexAnalyser/RegexAnalyserActionHandlerCollector.php b/Civi/Banking/Matcher/RegexAnalyser/RegexAnalyserActionHandlerCollector.php new file mode 100644 index 00000000..91125381 --- /dev/null +++ b/Civi/Banking/Matcher/RegexAnalyser/RegexAnalyserActionHandlerCollector.php @@ -0,0 +1,49 @@ +. + */ + +declare(strict_types = 1); + +namespace Civi\Banking\Matcher\RegexAnalyser; + +use Psr\Container\ContainerInterface; + +final class RegexAnalyserActionHandlerCollector implements RegexAnalyserActionHandlerInterface { + + public function __construct( + private readonly ContainerInterface $container, + ) {} + + public function execute(\stdClass $action, RegexAnalyserMatchContext $matchContext): void { + [$actionName] = explode(':', $action->action, 2); + if (!$this->container->has($actionName)) { + throw new \InvalidArgumentException(sprintf('Unknown action "%s"', $actionName)); + } + + $this->getAction($actionName)->execute($action, $matchContext); + } + + private function getAction(string $actionName): RegexAnalyserActionHandlerInterface { + if (!$this->container->has($actionName)) { + throw new \InvalidArgumentException(sprintf('Unknown action "%s"', $actionName)); + } + + // @phpstan-ignore return.type + return $this->container->get($actionName); + } + +} diff --git a/Civi/Banking/Matcher/RegexAnalyser/RegexAnalyserActionHandlerInterface.php b/Civi/Banking/Matcher/RegexAnalyser/RegexAnalyserActionHandlerInterface.php new file mode 100644 index 00000000..3eeb3b11 --- /dev/null +++ b/Civi/Banking/Matcher/RegexAnalyser/RegexAnalyserActionHandlerInterface.php @@ -0,0 +1,34 @@ +. + */ + +declare(strict_types = 1); + +namespace Civi\Banking\Matcher\RegexAnalyser; + +interface RegexAnalyserActionHandlerInterface { + + /** + * @param \stdClass $action + * @param \Civi\Banking\Matcher\RegexAnalyser\RegexAnalyserMatchContext $matchContext + * + * @throws \CRM_Core_Exception + * @throws \InvalidArgumentException + */ + public function execute(\stdClass $action, RegexAnalyserMatchContext $matchContext): void; + +} diff --git a/Civi/Banking/Matcher/RegexAnalyser/RegexAnalyserMatchContext.php b/Civi/Banking/Matcher/RegexAnalyser/RegexAnalyserMatchContext.php new file mode 100644 index 00000000..1143427e --- /dev/null +++ b/Civi/Banking/Matcher/RegexAnalyser/RegexAnalyserMatchContext.php @@ -0,0 +1,119 @@ +. + */ + +declare(strict_types = 1); + +namespace Civi\Banking\Matcher\RegexAnalyser; + +final class RegexAnalyserMatchContext { + + /** + * @param array> $matchData + * Matches of preg_match_all(). + */ + public function __construct( + private readonly array $matchData, + private readonly int $matchIndex, + public readonly \stdClass $rule, + private readonly \CRM_Banking_PluginImpl_Matcher_RegexAnalyser $analyser, + private readonly \CRM_Banking_BAO_BankTransaction $btx, + private readonly \CRM_Banking_Helpers_Logger $logger, + ) {} + + /** + * Get the value either from the match context, or the already stored data + */ + public function getValue(string $key): mixed { + $matchedValue = $this->getMatchedValue($key); + $dataParsed = $this->btx->getDataParsed(); + // see https://github.com/Project60/org.project60.banking/issues/111 + if ($this->analyser->getConfig()->variable_lookup_compatibility) { + // @phpstan-ignore empty.notAllowed + if (!empty($matchedValue)) { + return $matchedValue; + } + // @phpstan-ignore empty.notAllowed + elseif (!empty($dataParsed[$key])) { + return $dataParsed[$key]; + } + else { + // try value propagation + $value = $this->analyser->getPropagationValue($this->btx, NULL, $key); + if ($value) { + return $value; + } + else { + $this->analyser->logMessage("RegexAnalyser - Cannot find source '$key' for rule or filter.", 'debug'); + + return NULL; + } + } + } + else { + if (NULL !== $matchedValue) { + return $matchedValue; + } + elseif (isset($dataParsed[$key])) { + return $dataParsed[$key]; + } + else { + // try value propagation + $value = $this->analyser->getPropagationValue($this->btx, NULL, $key); + if (NULL === $value) { + $this->analyser->logMessage("RegexAnalyser - Cannot find source '$key' for rule or filter.", 'debug'); + } + + return $value; + } + } + } + + public function getMatchedValue(string $key): ?string { + return $this->matchData[$key][$this->matchIndex] ?? NULL; + } + + public function getParsedValue(string $key): mixed { + return $this->btx->getDataParsed()[$key] ?? NULL; + } + + public function removeParsedValue(string $key): void { + $dataParsed = $this->btx->getDataParsed(); + unset($dataParsed[$key]); + $this->btx->setDataParsed($dataParsed); + } + + public function setParsedValue(string $key, mixed $value): void { + $this->btx->setDataParsed([$key => $value] + $this->btx->getDataParsed()); + } + + /** + * set a timer for later use with logTime + */ + public function setLogTimer(string $timer): void { + $this->logger->setTimer($timer); + } + + public function logMessage(string $message, string $logLevel = 'debug'): void { + $this->analyser->logMessage($message, $logLevel); + } + + public function logTime(string $process, string $timer): void { + $this->analyser->logTime($process, $timer); + } + +} diff --git a/banking.php b/banking.php index ae9912ef..46f61f23 100755 --- a/banking.php +++ b/banking.php @@ -19,20 +19,20 @@ // phpcs:disable PSR1.Files.SideEffects.FoundWithSymbols require_once 'banking.civix.php'; require_once 'banking_options.php'; -require_once 'CRM/Banking/Helpers/URLBuilder.php'; -require_once 'CRM/Banking/Helpers/OptionValue.php'; // phpcs:enable -use Symfony\Component\DependencyInjection\ContainerBuilder; +use Civi\Banking\DependencyInjection\Compiler\ActionProviderPass; +use Civi\Banking\DependencyInjection\Compiler\RegexAnalyserActionHandlerPass; +use Civi\Core\ClassScanner; use CRM_Banking_ExtensionUtil as E; +use Symfony\Component\DependencyInjection\ContainerBuilder; /** * Implements hook_civicrm_container(). */ -function banking_civicrm_container(ContainerBuilder $container) { - if (class_exists('Civi\Banking\CompilerPass')) { - $container->addCompilerPass(new Civi\Banking\CompilerPass()); - } +function banking_civicrm_container(ContainerBuilder $container): void { + $container->addCompilerPass(new ActionProviderPass()); + $container->addCompilerPass(new RegexAnalyserActionHandlerPass()); } /** @@ -84,6 +84,14 @@ function banking_civicrm_pageRun(&$page) { } } +/** + * @param list $classes + */ +function banking_civicrm_scanClasses(array &$classes): void { + // @phpstan-ignore parameterByRef.type + ClassScanner::scanFolders($classes, __DIR__, 'Civi/Banking/Matcher', '\\'); +} + /** * Replace (some of) the summary blocks on the banking review page * diff --git a/composer.json b/composer.json index 58df26ab..946412f6 100644 --- a/composer.json +++ b/composer.json @@ -21,7 +21,8 @@ "require": { "php": "^8.1", "civicrm/civicrm-core": ">=5.76", - "civicrm/civicrm-packages": ">=5.76" + "civicrm/civicrm-packages": ">=5.76", + "webmozart/assert": "^1 || ^2" }, "autoload": { "classmap": ["ComposerHelper.php"] diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index ef537eae..573d451c 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -7620,68 +7620,9 @@ parameters: count: 1 path: CRM/Banking/PluginImpl/Matcher/RecurringContribution.php - - - message: '#^Access to an undefined property object\:\:\$from\.$#' - identifier: property.notFound - count: 1 - path: CRM/Banking/PluginImpl/Matcher/RegexAnalyser.php - - - - message: '#^Access to an undefined property object\:\:\$replace\.$#' - identifier: property.notFound - count: 1 - path: CRM/Banking/PluginImpl/Matcher/RegexAnalyser.php - - - - message: '#^Access to an undefined property object\:\:\$to\.$#' - identifier: property.notFound - count: 2 - path: CRM/Banking/PluginImpl/Matcher/RegexAnalyser.php - - - - message: '#^Call to an undefined method object\:\:fetch\(\)\.$#' - identifier: method.notFound - count: 2 - path: CRM/Banking/PluginImpl/Matcher/RegexAnalyser.php - - - - message: '#^Call to an undefined method object\:\:toArray\(\)\.$#' - identifier: method.notFound - count: 2 - path: CRM/Banking/PluginImpl/Matcher/RegexAnalyser.php - - - - message: '#^Call to function in_array\(\) requires parameter \#3 to be set\.$#' - identifier: function.strict - count: 1 - path: CRM/Banking/PluginImpl/Matcher/RegexAnalyser.php - - message: '#^Construct empty\(\) is not allowed\. Use more strict comparison\.$#' identifier: empty.notAllowed - count: 10 - path: CRM/Banking/PluginImpl/Matcher/RegexAnalyser.php - - - - message: '#^Foreach overwrites \$value with its value variable\.$#' - identifier: foreach.valueOverwrite - count: 1 - path: CRM/Banking/PluginImpl/Matcher/RegexAnalyser.php - - - - message: '#^In method "CRM_Banking_PluginImpl_Matcher_RegexAnalyser\:\:processMatch", caught "Exception" must be rethrown\. Either catch a more specific exception, add a "throw" clause in the "catch" block to propagate the exception or add a "// @ignoreException" comment\.$#' - count: 1 - path: CRM/Banking/PluginImpl/Matcher/RegexAnalyser.php - - - - message: '#^Loose comparison via "\=\=" is not allowed\.$#' - identifier: equal.notAllowed - count: 23 - path: CRM/Banking/PluginImpl/Matcher/RegexAnalyser.php - - - - message: '#^Method CRM_Banking_PluginImpl_Matcher_RegexAnalyser\:\:__construct\(\) has parameter \$config_name with no type specified\.$#' - identifier: missingType.parameter count: 1 path: CRM/Banking/PluginImpl/Matcher/RegexAnalyser.php @@ -7691,138 +7632,12 @@ parameters: count: 1 path: CRM/Banking/PluginImpl/Matcher/RegexAnalyser.php - - - message: '#^Method CRM_Banking_PluginImpl_Matcher_RegexAnalyser\:\:executeAPIQuery\(\) has no return type specified\.$#' - identifier: missingType.return - count: 1 - path: CRM/Banking/PluginImpl/Matcher/RegexAnalyser.php - - - - message: '#^Method CRM_Banking_PluginImpl_Matcher_RegexAnalyser\:\:executeAPIQuery\(\) has parameter \$action with no type specified\.$#' - identifier: missingType.parameter - count: 1 - path: CRM/Banking/PluginImpl/Matcher/RegexAnalyser.php - - - - message: '#^Method CRM_Banking_PluginImpl_Matcher_RegexAnalyser\:\:executeAPIQuery\(\) has parameter \$command with no type specified\.$#' - identifier: missingType.parameter - count: 1 - path: CRM/Banking/PluginImpl/Matcher/RegexAnalyser.php - - - - message: '#^Method CRM_Banking_PluginImpl_Matcher_RegexAnalyser\:\:executeAPIQuery\(\) has parameter \$entity with no type specified\.$#' - identifier: missingType.parameter - count: 1 - path: CRM/Banking/PluginImpl/Matcher/RegexAnalyser.php - - - - message: '#^Method CRM_Banking_PluginImpl_Matcher_RegexAnalyser\:\:executeAPIQuery\(\) has parameter \$query with no type specified\.$#' - identifier: missingType.parameter - count: 1 - path: CRM/Banking/PluginImpl/Matcher/RegexAnalyser.php - - - - message: '#^Method CRM_Banking_PluginImpl_Matcher_RegexAnalyser\:\:getTableName\(\) has no return type specified\.$#' - identifier: missingType.return - count: 1 - path: CRM/Banking/PluginImpl/Matcher/RegexAnalyser.php - - - - message: '#^Method CRM_Banking_PluginImpl_Matcher_RegexAnalyser\:\:getTableName\(\) has parameter \$entity with no type specified\.$#' - identifier: missingType.parameter - count: 1 - path: CRM/Banking/PluginImpl/Matcher/RegexAnalyser.php - - - - message: '#^Method CRM_Banking_PluginImpl_Matcher_RegexAnalyser\:\:getValue\(\) has no return type specified\.$#' - identifier: missingType.return - count: 1 - path: CRM/Banking/PluginImpl/Matcher/RegexAnalyser.php - - - - message: '#^Method CRM_Banking_PluginImpl_Matcher_RegexAnalyser\:\:getValue\(\) has parameter \$btx with no type specified\.$#' - identifier: missingType.parameter - count: 1 - path: CRM/Banking/PluginImpl/Matcher/RegexAnalyser.php - - - - message: '#^Method CRM_Banking_PluginImpl_Matcher_RegexAnalyser\:\:getValue\(\) has parameter \$data_parsed with no type specified\.$#' - identifier: missingType.parameter - count: 1 - path: CRM/Banking/PluginImpl/Matcher/RegexAnalyser.php - - - - message: '#^Method CRM_Banking_PluginImpl_Matcher_RegexAnalyser\:\:getValue\(\) has parameter \$key with no type specified\.$#' - identifier: missingType.parameter - count: 1 - path: CRM/Banking/PluginImpl/Matcher/RegexAnalyser.php - - - - message: '#^Method CRM_Banking_PluginImpl_Matcher_RegexAnalyser\:\:getValue\(\) has parameter \$match_data with no type specified\.$#' - identifier: missingType.parameter - count: 1 - path: CRM/Banking/PluginImpl/Matcher/RegexAnalyser.php - - - - message: '#^Method CRM_Banking_PluginImpl_Matcher_RegexAnalyser\:\:getValue\(\) has parameter \$match_index with no type specified\.$#' - identifier: missingType.parameter - count: 1 - path: CRM/Banking/PluginImpl/Matcher/RegexAnalyser.php - - - - message: '#^Method CRM_Banking_PluginImpl_Matcher_RegexAnalyser\:\:processMatch\(\) has no return type specified\.$#' - identifier: missingType.return - count: 1 - path: CRM/Banking/PluginImpl/Matcher/RegexAnalyser.php - - - - message: '#^Method CRM_Banking_PluginImpl_Matcher_RegexAnalyser\:\:processMatch\(\) has parameter \$btx with no type specified\.$#' - identifier: missingType.parameter - count: 1 - path: CRM/Banking/PluginImpl/Matcher/RegexAnalyser.php - - - - message: '#^Method CRM_Banking_PluginImpl_Matcher_RegexAnalyser\:\:processMatch\(\) has parameter \$data_parsed with no type specified\.$#' - identifier: missingType.parameter - count: 1 - path: CRM/Banking/PluginImpl/Matcher/RegexAnalyser.php - - - - message: '#^Method CRM_Banking_PluginImpl_Matcher_RegexAnalyser\:\:processMatch\(\) has parameter \$match_data with no type specified\.$#' - identifier: missingType.parameter - count: 1 - path: CRM/Banking/PluginImpl/Matcher/RegexAnalyser.php - - - - message: '#^Method CRM_Banking_PluginImpl_Matcher_RegexAnalyser\:\:processMatch\(\) has parameter \$match_index with no type specified\.$#' - identifier: missingType.parameter - count: 1 - path: CRM/Banking/PluginImpl/Matcher/RegexAnalyser.php - - - - message: '#^Method CRM_Banking_PluginImpl_Matcher_RegexAnalyser\:\:processMatch\(\) has parameter \$rule with no type specified\.$#' - identifier: missingType.parameter - count: 1 - path: CRM/Banking/PluginImpl/Matcher/RegexAnalyser.php - - - - message: '#^Negated boolean expression is always false\.$#' - identifier: booleanNot.alwaysFalse - count: 1 - path: CRM/Banking/PluginImpl/Matcher/RegexAnalyser.php - - message: '#^Only booleans are allowed in an if condition, int\|false given\.$#' identifier: if.condNotBoolean count: 1 path: CRM/Banking/PluginImpl/Matcher/RegexAnalyser.php - - - message: '#^Variable property access on mixed\.$#' - identifier: property.dynamicName - count: 2 - path: CRM/Banking/PluginImpl/Matcher/RegexAnalyser.php - - message: '#^Call to method getParameter\(\) on an unknown class type\.$#' identifier: class.notFound @@ -9650,12 +9465,6 @@ parameters: count: 2 path: CRM/Banking/PluginModel/Base.php - - - message: '#^Method CRM_Banking_PluginModel_Base\:\:__construct\(\) has parameter \$plugin_dao with no type specified\.$#' - identifier: missingType.parameter - count: 1 - path: CRM/Banking/PluginModel/Base.php - - message: '#^Method CRM_Banking_PluginModel_Base\:\:findContact\(\) has parameter \$attributes with no type specified\.$#' identifier: missingType.parameter @@ -10273,12 +10082,6 @@ parameters: count: 1 path: CRM/Banking/PluginModel/Exporter.php - - - message: '#^Method CRM_Banking_PluginModel_IOPlugin\:\:__construct\(\) has parameter \$plugin_dao with no type specified\.$#' - identifier: missingType.parameter - count: 1 - path: CRM/Banking/PluginModel/IOPlugin.php - - message: '#^Binary operation "\+\=" between int\|string and 1 results in an error\.$#' identifier: assignOp.invalid @@ -10351,12 +10154,6 @@ parameters: count: 7 path: CRM/Banking/PluginModel/Importer.php - - - message: '#^Method CRM_Banking_PluginModel_Importer\:\:__construct\(\) has parameter \$plugin_dao with no type specified\.$#' - identifier: missingType.parameter - count: 1 - path: CRM/Banking/PluginModel/Importer.php - - message: '#^Method CRM_Banking_PluginModel_Importer\:\:_updateTransactionBatchInfo\(\) has no return type specified\.$#' identifier: missingType.return @@ -10657,12 +10454,6 @@ parameters: count: 7 path: CRM/Banking/PluginModel/Matcher.php - - - message: '#^Method CRM_Banking_PluginModel_Matcher\:\:__construct\(\) has parameter \$plugin_dao with no type specified\.$#' - identifier: missingType.parameter - count: 1 - path: CRM/Banking/PluginModel/Matcher.php - - message: '#^Method CRM_Banking_PluginModel_Matcher\:\:addSuggestion\(\) has no return type specified\.$#' identifier: missingType.return @@ -10813,12 +10604,6 @@ parameters: count: 9 path: CRM/Banking/PluginModel/PostProcessor.php - - - message: '#^Method CRM_Banking_PluginModel_PostProcessor\:\:__construct\(\) has parameter \$plugin_dao with no type specified\.$#' - identifier: missingType.parameter - count: 1 - path: CRM/Banking/PluginModel/PostProcessor.php - - message: '#^Method CRM_Banking_PluginModel_PostProcessor\:\:getContributionIDs\(\) return type has no value type specified in iterable type array\.$#' identifier: missingType.iterableValue @@ -11918,6 +11703,54 @@ parameters: count: 1 path: Civi/Banking/DataProcessor/Source/BankAccount.php + - + message: '#^Argument of an invalid type mixed supplied for foreach, only iterables are supported\.$#' + identifier: foreach.nonIterable + count: 1 + path: Civi/Banking/Matcher/RegexAnalyser/ActionHandlers/ApiRegexAnalyserActionHandler.php + + - + message: '#^Cannot access offset string on mixed\.$#' + identifier: offsetAccess.nonOffsetAccessible + count: 2 + path: Civi/Banking/Matcher/RegexAnalyser/ActionHandlers/ApiRegexAnalyserActionHandler.php + + - + message: '#^Cannot cast mixed to string\.$#' + identifier: cast.string + count: 2 + path: Civi/Banking/Matcher/RegexAnalyser/ActionHandlers/ApiRegexAnalyserActionHandler.php + + - + message: '#^Construct empty\(\) is not allowed\. Use more strict comparison\.$#' + identifier: empty.notAllowed + count: 3 + path: Civi/Banking/Matcher/RegexAnalyser/ActionHandlers/ApiRegexAnalyserActionHandler.php + + - + message: '#^Loose comparison via "\=\=" is not allowed\.$#' + identifier: equal.notAllowed + count: 1 + path: Civi/Banking/Matcher/RegexAnalyser/ActionHandlers/ApiRegexAnalyserActionHandler.php + + - + message: '#^Parameter \#1 \$array of function reset expects array\|object, mixed given\.$#' + identifier: argument.type + count: 1 + path: Civi/Banking/Matcher/RegexAnalyser/ActionHandlers/ApiRegexAnalyserActionHandler.php + + - + message: '#^Part \$query\[''limit''\] \(mixed\) of encapsed string cannot be cast to string\.$#' + identifier: encapsedStringPart.nonString + count: 1 + path: Civi/Banking/Matcher/RegexAnalyser/ActionHandlers/ApiRegexAnalyserActionHandler.php + + - + message: '#^Part \$select_clause \(mixed\) of encapsed string cannot be cast to string\.$#' + identifier: encapsedStringPart.nonString + count: 1 + path: Civi/Banking/Matcher/RegexAnalyser/ActionHandlers/ApiRegexAnalyserActionHandler.php + - message: '#^Cannot access offset ''bic'' on mixed\.$#' identifier: offsetAccess.nonOffsetAccessible @@ -12977,12 +12810,6 @@ parameters: count: 1 path: banking.php - - - message: '#^Function banking_civicrm_container\(\) has no return type specified\.$#' - identifier: missingType.return - count: 1 - path: banking.php - - message: '#^Function banking_civicrm_enable\(\) has no return type specified\.$#' identifier: missingType.return @@ -13175,78 +13002,6 @@ parameters: count: 10 path: tests/phpunit/CRM/Banking/Analyser/RegexAnalyserTest.php - - - message: '#^Method CRM_Banking_RegexAnalyserTest\:\:testApiAction\(\) has no return type specified\.$#' - identifier: missingType.return - count: 1 - path: tests/phpunit/CRM/Banking/Analyser/RegexAnalyserTest.php - - - - message: '#^Method CRM_Banking_RegexAnalyserTest\:\:testApiActionSql\(\) has no return type specified\.$#' - identifier: missingType.return - count: 1 - path: tests/phpunit/CRM/Banking/Analyser/RegexAnalyserTest.php - - - - message: '#^Method CRM_Banking_RegexAnalyserTest\:\:testCopyAction\(\) has no return type specified\.$#' - identifier: missingType.return - count: 1 - path: tests/phpunit/CRM/Banking/Analyser/RegexAnalyserTest.php - - - - message: '#^Method CRM_Banking_RegexAnalyserTest\:\:testCopyAppendAction\(\) has no return type specified\.$#' - identifier: missingType.return - count: 1 - path: tests/phpunit/CRM/Banking/Analyser/RegexAnalyserTest.php - - - - message: '#^Method CRM_Banking_RegexAnalyserTest\:\:testLookupAction\(\) has no return type specified\.$#' - identifier: missingType.return - count: 1 - path: tests/phpunit/CRM/Banking/Analyser/RegexAnalyserTest.php - - - - message: '#^Method CRM_Banking_RegexAnalyserTest\:\:testMapAction\(\) has no return type specified\.$#' - identifier: missingType.return - count: 1 - path: tests/phpunit/CRM/Banking/Analyser/RegexAnalyserTest.php - - - - message: '#^Method CRM_Banking_RegexAnalyserTest\:\:testSHA1Action\(\) has no return type specified\.$#' - identifier: missingType.return - count: 1 - path: tests/phpunit/CRM/Banking/Analyser/RegexAnalyserTest.php - - - - message: '#^Method CRM_Banking_RegexAnalyserTest\:\:testSetAction\(\) has no return type specified\.$#' - identifier: missingType.return - count: 1 - path: tests/phpunit/CRM/Banking/Analyser/RegexAnalyserTest.php - - - - message: '#^Method CRM_Banking_RegexAnalyserTest\:\:testSetActionDoesNotMatch\(\) has no return type specified\.$#' - identifier: missingType.return - count: 1 - path: tests/phpunit/CRM/Banking/Analyser/RegexAnalyserTest.php - - - - message: '#^Method CRM_Banking_RegexAnalyserTest\:\:testSetUnsetAction\(\) has no return type specified\.$#' - identifier: missingType.return - count: 1 - path: tests/phpunit/CRM/Banking/Analyser/RegexAnalyserTest.php - - - - message: '#^Method CRM_Banking_RegexAnalyserTest\:\:testSprintfAction\(\) has no return type specified\.$#' - identifier: missingType.return - count: 1 - path: tests/phpunit/CRM/Banking/Analyser/RegexAnalyserTest.php - - - - message: '#^Method CRM_Banking_RegexAnalyserTest\:\:testStrtolowerAction\(\) has no return type specified\.$#' - identifier: missingType.return - count: 1 - path: tests/phpunit/CRM/Banking/Analyser/RegexAnalyserTest.php - - message: '#^Parameter \#2 \$object of static method PHPUnit\\Framework\\Assert\:\:assertObjectNotHasProperty\(\) expects object, mixed given\.$#' identifier: argument.type diff --git a/tests/phpunit/CRM/Banking/Analyser/RegexAnalyserTest.php b/tests/phpunit/CRM/Banking/Analyser/RegexAnalyserTest.php index 5f1046bd..d8342c89 100644 --- a/tests/phpunit/CRM/Banking/Analyser/RegexAnalyserTest.php +++ b/tests/phpunit/CRM/Banking/Analyser/RegexAnalyserTest.php @@ -30,8 +30,10 @@ class CRM_Banking_RegexAnalyserTest extends CRM_Banking_TestBase { /** * Test the 'set' action. + * + * @covers \Civi\Banking\Matcher\RegexAnalyser\ActionHandlers\SetRegexAnalyserActionHandler */ - public function testSetAction() { + public function testSetAction(): void { $transactionId = $this->createTransaction( [ 'purpose' => 'This is a donation', @@ -64,9 +66,11 @@ public function testSetAction() { } /** - * Test that the 'test' action does not set if not matched. + * Test that the 'set' action does not set if not matched. + * + * @covers \Civi\Banking\Matcher\RegexAnalyser\ActionHandlers\SetRegexAnalyserActionHandler */ - public function testSetActionDoesNotMatch() { + public function testSetActionDoesNotMatch(): void { $transactionId = $this->createTransaction( [ 'purpose' => 'This is a nothing', @@ -104,8 +108,10 @@ public function testSetActionDoesNotMatch() { /** * Test the 'map' action. + * + * @covers \Civi\Banking\Matcher\RegexAnalyser\ActionHandlers\MapRegexAnalyserActionHandler */ - public function testMapAction() { + public function testMapAction(): void { $transactionId = $this->createTransaction( [ 'financial_type' => 'CreditCard', @@ -145,8 +151,10 @@ public function testMapAction() { /** * Test the 'copy' action. + * + * @covers \Civi\Banking\Matcher\RegexAnalyser\ActionHandlers\CopyRegexAnalyserActionHandler */ - public function testCopyAction() { + public function testCopyAction(): void { // setup $transaction1_id = $this->createTransaction(['purpose' => "here's your code X92873X2323X, alright!?"]); $transaction2_id = $this->createTransaction(['purpose' => "here's an invalid code X92873Y2323X, alright!?"]); @@ -189,8 +197,10 @@ public function testCopyAction() { /** * Test the 'copy_append' action. + * + * @covers \Civi\Banking\Matcher\RegexAnalyser\ActionHandlers\CopyAppendRegexAnalyserActionHandler */ - public function testCopyAppendAction() { + public function testCopyAppendAction(): void { // setup $transaction_id = $this->createTransaction([ 'purpose' => "here's your code X92873X2323X, alright!?", @@ -226,8 +236,10 @@ public function testCopyAppendAction() { /** * Test the 'copy_ltrim_zeros' action. + * + * @covers \Civi\Banking\Matcher\RegexAnalyser\ActionHandlers\UnsetRegexAnalyserActionHandler */ - public function testSetUnsetAction() { + public function testSetUnsetAction(): void { // setup $transaction_id = $this->createTransaction([ 'field_1' => 'YO', @@ -261,9 +273,11 @@ public function testSetUnsetAction() { } /** - * Test the 'copy_ltrim_zeros' action. + * Test the 'strtolower' action. + * + * @covers \Civi\Banking\Matcher\RegexAnalyser\ActionHandlers\StrtolowerRegexAnalyserActionHandler */ - public function testStrtolowerAction() { + public function testStrtolowerAction(): void { // setup $transaction_id = $this->createTransaction([ 'field_1' => 'YO', @@ -293,8 +307,10 @@ public function testStrtolowerAction() { /** * Test the 'sha1' action. + * + * @covers \Civi\Banking\Matcher\RegexAnalyser\ActionHandlers\Sha1RegexAnalyserActionHandler */ - public function testSHA1Action() { + public function testSHA1Action(): void { // setup $transaction_id = $this->createTransaction([ 'data' => 'Neque porro quisquam est qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit...', @@ -324,8 +340,10 @@ public function testSHA1Action() { /** * Test the 'sprint' action. + * + * @covers \Civi\Banking\Matcher\RegexAnalyser\ActionHandlers\SprintRegexAnalyserActionHandler */ - public function testSprintfAction() { + public function testSprintAction(): void { // setup $transaction_id = $this->createTransaction([ 'data' => '1.234', @@ -357,8 +375,10 @@ public function testSprintfAction() { /** * Test the 'lookup' action. + * + * @covers \Civi\Banking\Matcher\RegexAnalyser\ActionHandlers\LookupRegexAnalyserActionHandler */ - public function testLookupAction() { + public function testLookupAction(): void { // setup $contact_id = $this->createContact(['external_identifier' => 'testLookupAction']); $contact = civicrm_api3('Contact', 'get', ['id' => $contact_id]); @@ -390,8 +410,10 @@ public function testLookupAction() { /** * Test the 'api' action. + * + * @covers \Civi\Banking\Matcher\RegexAnalyser\ActionHandlers\ApiRegexAnalyserActionHandler */ - public function testApiAction() { + public function testApiAction(): void { // setup $contact_id = $this->createContact(['external_identifier' => 'ApiAction', 'first_name' => 'Jenny']); $transaction_id = $this->createTransaction([ @@ -425,8 +447,10 @@ public function testApiAction() { /** * Test the 'api' action via sql + * + * @covers \Civi\Banking\Matcher\RegexAnalyser\ActionHandlers\ApiRegexAnalyserActionHandler */ - public function testApiActionSql() { + public function testApiActionSql(): void { // setup $contact_id = $this->createContact(['external_identifier' => 'ApiAction', 'first_name' => 'Jenny']); $transaction_id = $this->createTransaction([