4 * interface/super/rules/library/RuleManager.php
7 * @link https://www.open-emr.org
8 * @author Aron Racho <aron@mi-squared.com>
9 * @author Brady Miller <brady.g.miller@gmail.com>
10 * @copyright Copyright (c) 2010-2011 Aron Racho <aron@mi-squared.com>
11 * @copyright Copyright (c) 2019 Brady Miller <brady.g.miller@gmail.com>
12 * @license https://github.com/openemr/openemr/blob/master/LICENSE GNU General Public License 3
15 require_once(src_dir() . "/clinical_rules.php");
16 require_once(library_src('RuleCriteriaFilterFactory.php'));
17 require_once(library_src('RuleCriteriaTargetFactory.php'));
20 * Responsible for handling the persistence (CRU operations, deletes are
21 * not currently supported).
22 * This class should be kept synchronized with clinical_rules.php
27 const SQL_RULE_DETAIL
=
28 "SELECT lo.title as title, cr.*
29 FROM clinical_rules cr
31 ON (cr.id = lo.option_id AND lo.list_id = 'clinical_rules' AND lo.activity = 1)";
33 const SQL_RULE_REMINDER_INTERVAL
=
41 const SQL_RULE_FILTER
=
42 "SELECT SHA1(CONCAT( id, include_flag, required_flag, method, method_detail, value )) AS guid, rule_filter.*
43 FROM rule_filter WHERE id = ?";
45 const SQL_RULE_TARGET
=
46 "SELECT SHA1(CONCAT( id, group_id, include_flag, required_flag, method, value, rule_target.interval )) AS guid, rule_target.*
47 FROM rule_target WHERE id = ?";
49 const SQL_RULE_FILTER_BY_GUID
=
50 "SELECT * FROM rule_filter
51 WHERE SHA1(CONCAT( id, include_flag, required_flag, method, method_detail, value )) = ?";
53 const SQL_RULE_TARGET_BY_GUID
=
54 "SELECT * FROM rule_target
55 WHERE SHA1(CONCAT( id, group_id, include_flag, required_flag, method, value, rule_target.interval )) = ?";
57 const SQL_RULE_TARGET_BY_ID_GROUP_ID
=
58 "SELECT SHA1(CONCAT( id, group_id, include_flag, required_flag, method, value, rule_target.interval )) AS guid, rule_target.*
59 FROM rule_target WHERE id = ? AND group_id = ?";
61 const SQL_RULE_ACTIONS
=
62 "SELECT SHA1( CONCAT(id, category, item, group_id) ) AS guid, rule_action.* FROM rule_action
65 const SQL_RULE_ACTION_BY_GUID
=
66 "SELECT rule_action.*,
67 rule_action_item.clin_rem_link,
68 rule_action_item.reminder_message,
69 rule_action_item.custom_flag
70 FROM rule_action JOIN rule_action_item ON (rule_action_item.category = rule_action.category AND rule_action_item.item = rule_action.item )
71 WHERE SHA1( CONCAT(rule_action.id, rule_action.category, rule_action.item, rule_action.group_id ) ) = ?";
73 const SQL_UPDATE_FLAGS
=
74 "UPDATE clinical_rules
75 SET active_alert_flag = ?,
76 passive_alert_flag = ?,
79 patient_reminder_flag = ?,
84 bibliographic_citation = ?,
85 linked_referential_cds = ?
86 WHERE id = ? AND pid = 0";
88 const SQL_UPDATE_TITLE
=
91 WHERE list_id = 'clinical_rules' AND option_id = ?";
93 const SQL_REMOVE_INTERVALS
=
94 "DELETE FROM rule_reminder
97 const SQL_INSERT_INTERVALS
=
98 "INSERT INTO rule_reminder
99 (id, method, method_detail, value)
100 VALUES ( ?, ?, ?, ?)";
102 const SQL_UPDATE_FILTER
=
103 "UPDATE rule_filter SET include_flag = ?, required_flag = ?, method = ?, method_detail = ?, value = ?
104 WHERE SHA1(CONCAT( id, include_flag, required_flag, method, method_detail, value )) = ?";
106 const SQL_INSERT_FILTER
=
107 "INSERT INTO rule_filter (id, include_flag, required_flag, method, method_detail, value )
108 VALUES ( ?, ?, ?, ?, ?, ? )";
110 const SQL_UPDATE_TARGET
=
111 "UPDATE rule_target SET include_flag = ?, required_flag = ?, method = ?, value = ?
112 WHERE SHA1(CONCAT( id, group_id, include_flag, required_flag, method, value, rule_target.interval )) = ?";
114 const SQL_INSERT_TARGET
=
115 "INSERT INTO rule_target ( id, include_flag, required_flag, method, value, group_id )
116 VALUES ( ?, ?, ?, ?, ?, ? )";
118 var $filterCriteriaFactory;
119 var $targetCriteriaFactory;
121 function __construct()
123 $this->filterCriteriaFactory
= new RuleCriteriaFilterFactory();
124 $this->targetCriteriaFactory
= new RuleCriteriaTargetFactory();
128 * Returns a Rule object if the supplied rule id matches a record in
129 * clinical_rules. An optional patient id parameter allows you to get the
130 * rules specific to the patient.
132 * Returns null if no rule is found matching the id or patient.
137 function getRule($id, $pid = 0)
139 $ruleResult = sqlQuery(
140 self
::SQL_RULE_DETAIL
. " WHERE id = ? AND pid = ?",
148 $rule = new Rule($id, $ruleResult['title']);
150 $rule->setBibliographicCitation($ruleResult['bibliographic_citation']);
151 $rule->setDeveloper($ruleResult['developer']);
152 $rule->setFunding($ruleResult['funding_source']);
153 $rule->setRelease($ruleResult['release_version']);
154 $rule->setWeb_ref($ruleResult['web_reference']);
155 $rule->setLinkedReferentialCds($ruleResult['linked_referential_cds']);
157 $this->fillRuleTypes($rule, $ruleResult);
158 $this->fillRuleReminderIntervals($rule);
159 $this->fillRuleFilterCriteria($rule);
160 $this->fillRuleTargetActionGroups($rule);
172 * Adds a RuleType to the given rule based on the sql result row
173 * passed to it, evaluating the *_flag columns.
176 private function fillRuleTypes($rule, $ruleResult)
178 if ($ruleResult['active_alert_flag'] == 1) {
179 $rule->addRuleType(RuleType
::from(RuleType
::ActiveAlert
));
182 if ($ruleResult['passive_alert_flag'] == 1) {
183 $rule->addRuleType(RuleType
::from(RuleType
::PassiveAlert
));
187 if ($ruleResult['cqm_flag'] == 1) {
188 $rule->addRuleType(RuleType
::from(RuleType
::CQM
));
191 if ($ruleResult['amc_flag'] == 1) {
192 $rule->addRuleType(RuleType
::from(RuleType
::AMC
));
195 if ($ruleResult['patient_reminder_flag'] == 1) {
196 $rule->addRuleType(RuleType
::from(RuleType
::PatientReminder
));
201 * Fills the given rule with criteria derived from the rule_filter
202 * table. Relies on the RuleCriteriaFilterFactory for the parsing of
203 * rows in this table into concrete subtypes of RuleCriteria.
206 private function fillRuleFilterCriteria($rule)
208 $stmt = sqlStatement(self
::SQL_RULE_FILTER
, array( $rule->id
));
209 $criterion = $this->gatherCriteria($rule, $stmt, $this->filterCriteriaFactory
);
210 $ruleFilters = new RuleFilters();
211 $rule->setRuleFilters($ruleFilters);
212 if (sizeof($criterion) > 0) {
213 foreach ($criterion as $criteria) {
214 $ruleFilters->add($criteria);
219 private function fillRuleTargetActionGroups($rule)
221 $stmt = sqlStatement(self
::SQL_RULE_TARGET
, array( $rule->id
));
222 $criterion = $this->gatherCriteria($rule, $stmt, $this->targetCriteriaFactory
);
224 $ruleTargetGroups = $this->fetchRuleTargetCriteria($rule);
225 $ruleActionGroups = $this->fetchRuleActions($rule);
227 $groupCount = max(end(array_keys($ruleTargetGroups)), end(array_keys($ruleActionGroups)));
228 for ($groupId = 0; $groupId <= $groupCount; $groupId++
) {
229 $group = new RuleTargetActionGroup($groupId);
231 if (isset($ruleTargetGroups[$groupId])) {
232 $group->setRuleTargets($ruleTargetGroups[$groupId]);
236 if (isset($ruleActionGroups[$groupId])) {
237 $group->setRuleActions($ruleActionGroups[$groupId]);
241 if ($addGroup == true) {
242 $groups[$groupId] = $group;
246 $rule->setGroups($groups);
252 private function fetchRuleTargetCriteria($rule)
254 $stmt = sqlStatement(self
::SQL_RULE_TARGET
, array( $rule->id
));
255 $criterion = $this->gatherCriteria(
258 $this->targetCriteriaFactory
260 $ruleTargetGroups = array();
261 if (sizeof($criterion) > 0) {
262 foreach ($criterion as $criteria) {
263 if (!isset($ruleTargetGroups[$criteria->groupId
])) {
264 $ruleTargetGroups[$criteria->groupId
] = new RuleTargets();
267 $ruleTargetGroups[$criteria->groupId
]->add($criteria);
271 ksort($ruleTargetGroups);
272 return $ruleTargetGroups;
278 private function fetchRuleActions($rule)
280 $stmt = sqlStatement(self
::SQL_RULE_ACTIONS
, array( $rule->id
));
281 $ruleActionGroups = array();
282 for ($iter = 0; $row = sqlFetchArray($stmt); $iter++
) {
283 $action = new RuleAction();
284 $action->category
= $row['category'];
285 $action->item
= $row['item'];
286 $action->guid
= $row['guid'];
287 $action->groupId
= $row['group_id'];
288 if (!isset($ruleActionGroups[$action->groupId
])) {
289 $ruleActionGroups[$action->groupId
] = new RuleActions();
292 $ruleActionGroups[$action->groupId
]->add($action);
295 ksort($ruleActionGroups);
296 return $ruleActionGroups;
300 * @param string $guid
301 * @return RuleCriteria
303 function getRuleFilterCriteria($rule, $guid)
305 $stmt = sqlStatement(self
::SQL_RULE_FILTER_BY_GUID
, array( $guid ));
306 $criterion = $this->gatherCriteria(
309 $this->filterCriteriaFactory
311 if (sizeof($criterion) > 0) {
312 $criteria = $criterion[0];
313 $criteria->guid
= $guid;
314 return $criterion[0];
321 * @param string $guid
322 * @return array of RuleTargetActionGroup
324 function getRuleTargetActionGroups($rule)
326 $criterion = $this->getRuleTargetCriteria($rule);
327 $actions = $this->getRuleAction($rule);
328 if (sizeof($criterion) > 0) {
329 $criteria = $criterion[0];
330 $criteria->guid
= $guid;
331 return $criterion[0];
338 * @param string $guid
339 * @return RuleCriteria
341 function getRuleTargetCriteria($rule, $guid)
343 $stmt = sqlStatement(self
::SQL_RULE_TARGET_BY_GUID
, array( $guid ));
344 $criterion = $this->gatherCriteria(
347 $this->targetCriteriaFactory
349 if (sizeof($criterion) > 0) {
350 $criteria = $criterion[0];
351 $criteria->guid
= $guid;
359 * @param string $guid
360 * @return RuleCriteria
362 function getRuleTargetCriteriaByGroupId($rule, $groupId)
364 $stmt = sqlStatement(self
::SQL_RULE_TARGET_BY_ID_GROUP_ID
, array( $rule->id
, $groupId ));
365 $criterion = $this->gatherCriteria(
368 $this->targetCriteriaFactory
370 if (sizeof($criterion) > 0) {
371 $criteria = $criterion[0];
372 return $criterion[0];
379 * Given a sql source for gathering rule criteria (target or filter), this
380 * method relies on its supplied subtype of RuleCriteriaFactory to parse out
381 * instances of RuleCriteria from the sql source (typically rule_filter or
384 * Returns an array of RuleCriteria subtypes, if they were parsable from the
385 * supplied sql source.
387 * @param RuleCriteriaFactory $factory
389 private function gatherCriteria($rule, $stmt, $factory)
391 $criterion = array();
392 for ($iter = 0; $row = sqlFetchArray($stmt); $iter++
) {
393 $guid = $row['guid'];
394 $method = $row['method'];
395 $methodDetail = $row['method_detail'];
396 $value = $row['value'];
397 $inclusion = $row['include_flag'] == 1;
398 $optional = $row['required_flag'] == 1;
399 $groupId = $row['group_id'];
402 $criteria = $factory->build(
412 if (is_null($criteria)) {
413 // unrecognized critera
417 if (!is_null($groupId)) {
418 $criteria->groupId
= $groupId;
422 array_push($criterion, $criteria);
429 * Creates a ReminderIntervals object from rows in the rule_reminder table,
430 * and sets it in the supplied Rule.
433 private function fillRuleReminderIntervals($rule)
435 $stmt = sqlStatement(self
::SQL_RULE_REMINDER_INTERVAL
, array( $rule->id
));
436 $reminderInterval = new ReminderIntervals();
438 for ($iter = 0; $row = sqlFetchArray($stmt); $iter++
) {
439 $amount = $row['value'];
440 $unit = TimeUnit
::from($row['method_detail']);
441 $methodParts = explode('_', $row['method']);
442 $type = ReminderIntervalType
::from($methodParts[0]);
443 $range = ReminderIntervalRange
::from($methodParts[2]);
444 if (!is_null($type) && !is_null($range) && !is_null($unit)) {
445 $detail = new ReminderIntervalDetail($type, $range, $amount, $unit);
446 $reminderInterval->addDetail($detail);
450 $rule->setReminderIntervals($reminderInterval);
457 function getRuleAction($rule, $guid)
459 $result = sqlQuery(self
::SQL_RULE_ACTION_BY_GUID
, array($guid));
465 $action = new RuleAction();
466 $action->guid
= $guid;
467 $action->id
= $result['id'];
468 $action->category
= $result['category'];
469 $action->item
= $result['item'];
470 $action->reminderLink
= $result['clin_rem_link'];
471 $action->reminderMessage
= $result['reminder_message'];
472 $action->customRulesInput
= $result['custom_flag'] == 1;
473 $action->groupId
= $result['group_id'];
475 $target = $this->getRuleTargetCriteriaByGroupId($rule, $action->groupId
);
477 $action->targetCriteria
= $target;
482 function deleteRuleAction($rule, $guid)
484 sqlStatement("DELETE FROM rule_action WHERE SHA1( CONCAT(id, category, item, group_id) ) = ?", [$guid]);
487 function deleteRuleTarget($rule, $guid)
489 sqlStatement("DELETE FROM rule_target WHERE SHA1(CONCAT( id, group_id, include_flag, required_flag, method, value, rule_target.interval )) = ?", [$guid]);
492 function deleteRuleFilter($rule, $guid)
494 sqlStatement("DELETE FROM rule_filter WHERE SHA1(CONCAT( id, include_flag, required_flag, method, method_detail, value )) = ?", [$guid]);
497 function updateSummary($ruleId, $types, $title, $developer, $funding, $release, $web_ref, $bibliographic_citation, $linked_referential_cds)
499 $rule = $this->getRule($ruleId);
501 if (is_null($rule)) {
503 $result = sqlQuery("select count(*)+1 AS id from clinical_rules");
504 $ruleId = "rule_" . $result['id'];
506 "INSERT INTO clinical_rules (id, pid, active_alert_flag, passive_alert_flag, cqm_flag, amc_flag, patient_reminder_flag, developer, funding_source, release_version, web_reference, bibliographic_citation, linked_referential_cds ) " .
507 "VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?) ",
511 in_array(RuleType
::ActiveAlert
, $types) ?
1 : 0,
512 in_array(RuleType
::PassiveAlert
, $types) ?
1 : 0,
513 in_array(RuleType
::CQM
, $types) ?
1 : 0,
514 in_array(RuleType
::AMC
, $types) ?
1 : 0,
515 in_array(RuleType
::PatientReminder
, $types) ?
1 : 0,
520 $bibliographic_citation,
521 $linked_referential_cds
526 $this->doRuleLabel(false, "clinical_rules", $ruleId, $title);
531 sqlStatement(self
::SQL_UPDATE_FLAGS
, array(
532 in_array(RuleType
::ActiveAlert
, $types) ?
1 : 0,
533 in_array(RuleType
::PassiveAlert
, $types) ?
1 : 0,
534 in_array(RuleType
::CQM
, $types) ?
1 : 0,
535 in_array(RuleType
::AMC
, $types) ?
1 : 0,
536 in_array(RuleType
::PatientReminder
, $types) ?
1 : 0,
541 $bibliographic_citation,
542 $linked_referential_cds,
546 sqlStatement(self
::SQL_UPDATE_TITLE
, array( $title,
555 * @param ReminderIntervals $intervals
557 function updateIntervals($rule, $intervals)
559 // remove old intervals
560 sqlStatement(self
::SQL_REMOVE_INTERVALS
, array( $rule->id
));
562 // insert new intervals
563 foreach ($intervals->getTypes() as $type) {
564 $typeDetails = $intervals->getDetailFor($type);
565 foreach ($typeDetails as $detail) {
566 sqlStatement(self
::SQL_INSERT_INTERVALS
, array(
568 $type->code
. "_reminder_" . $detail->intervalRange
->code
, // method
569 $detail->timeUnit
->code
, // method_detail
570 $detail->amount
// value
579 * @param RuleCriteria $criteria
581 function updateFilterCriteria($rule, $criteria)
583 $dbView = $criteria->getDbView();
584 $method = "filt_" . $dbView->method
;
586 $guid = $criteria->guid
;
587 if (is_null($guid)) {
589 sqlStatement(self
::SQL_INSERT_FILTER
, array(
591 $dbView->inclusion ?
1 : 0,
592 $dbView->optional ?
1 : 0,
593 $dbView->method
= $method,
594 $dbView->methodDetail
= $dbView->methodDetail
,
595 $dbView->value
= $dbView->value
));
598 sqlStatement(self
::SQL_UPDATE_FILTER
, array(
599 $dbView->inclusion ?
1 : 0,
600 $dbView->optional ?
1 : 0,
601 $dbView->method
= $method,
602 $dbView->methodDetail
= $dbView->methodDetail
,
603 $dbView->value
= $dbView->value
,
611 * @param RuleCriteria $criteria
613 function updateTargetCriteria($rule, $criteria)
615 $dbView = $criteria->getDbView();
616 $method = "target_" . $dbView->method
;
618 $guid = $criteria->guid
;
619 $group_id = $criteria->groupId
;
621 if (is_null($guid)) {
624 $result = sqlQuery("SELECT max(group_id) AS group_id FROM rule_target WHERE id = ?", array($rule->id
));
627 $group_id = $result['group_id'] ?
$result['group_id'] +
1 : 1;
631 sqlStatement(self
::SQL_INSERT_TARGET
, array(
633 $dbView->inclusion ?
1 : 0,
634 $dbView->optional ?
1 : 0,
635 $dbView->method
= $method,
636 $dbView->value
= $dbView->value
,
640 sqlStatement(self
::SQL_UPDATE_TARGET
, array(
641 $dbView->inclusion ?
1 : 0,
642 $dbView->optional ?
1 : 0,
643 $dbView->method
= $method,
644 $dbView->value
= $dbView->value
,
650 "SELECT COUNT(*) AS interval_count FROM rule_target WHERE rule_target.id = ? AND rule_target.method = ?",
651 array($rule->id
, 'target_interval')
653 if ($result && $result['interval_count'] > 0) {
657 SET rule_target.value = ?, rule_target.interval = ?, rule_target.include_flag = '1', rule_target.required_flag = '1'
658 WHERE rule_target.method = ?
659 AND rule_target.id = ?";
661 sqlStatement($intervalSql, array(
662 $dbView->intervalType
,
668 sqlStatement("INSERT INTO rule_target ( rule_target.value, rule_target.interval, rule_target.method, rule_target.id, rule_target.include_flag, rule_target.required_flag ) "
669 . "VALUES ( ?, ?, ?, ?, '1', '1' ) ", array(
670 $dbView->intervalType
,
677 function getAllowedFilterCriteriaTypes()
680 foreach (RuleCriteriaType
::values() as $type) {
681 $criteria = RuleCriteriaType
::from($type);
682 array_push($allowed, $criteria);
688 function getAllowedTargetCriteriaTypes()
691 array_push($allowed, RuleCriteriaType
::from(RuleCriteriaType
::lifestyle
));
692 array_push($allowed, RuleCriteriaType
::from(RuleCriteriaType
::custom
));
693 array_push($allowed, RuleCriteriaType
::from(RuleCriteriaType
::custom_bucket
));
700 * @param RuleCriteriaType $criteriaType
701 * @return RuleCriteria
703 function createFilterRuleCriteria($rule, $criteriaType)
705 return $this->filterCriteriaFactory
->buildNewInstance($rule->id
, $criteriaType);
711 * @param RuleCriteriaType $criteriaType
712 * @return RuleCriteria
714 function createTargetRuleCriteria($rule, $criteriaType)
716 return $this->targetCriteriaFactory
->buildNewInstance($rule->id
, $criteriaType);
722 * @param RuleAction $action
724 function updateRuleAction($action)
726 $ruleId = $action->id
;
727 $rule = $this->getRule($ruleId);
728 $groupId = $action->groupId
;
729 $guid = $action->guid
;
731 $category = $action->category
;
732 $categoryLbl = $action->categoryLbl
;
733 $item = $action->item
;
734 $itemLbl = $action->itemLbl
;
735 $link = $action->reminderLink
;
736 $message = $action->reminderMessage
;
737 $customOption = $action->customRulesInput
;
739 // do labels -- if new category or item, insert them
740 $exists = $this->labelExists('rule_action_category', $category, $categoryLbl);
742 $category = 'act_cat_' . $categoryLbl;
745 $this->doRuleLabel($exists, 'rule_action_category', $category, $categoryLbl);
747 $exists = $this->labelExists('rule_action', $item, $itemLbl);
749 $item = 'act_' . $itemLbl;
752 $this->doRuleLabel($exists, 'rule_action', $item, $itemLbl);
754 // persist action itself
756 // its a brand new action
758 "INSERT INTO rule_action (id, group_id, category, item ) VALUES (?,?,?,?)",
759 array( $ruleId, $groupId, $category, $item )
762 // its an action edit
763 if (!is_null($groupId)) {
765 "UPDATE rule_action SET group_id = ?, category = ?, item = ? " .
766 "WHERE SHA1( CONCAT(rule_action.id, rule_action.category, rule_action.item, rule_action.group_id ) ) = ? ",
767 array( $groupId, $category, $item, $guid )
772 // handle rule action_item
773 $result = sqlQuery("SELECT * FROM rule_action_item WHERE category = ? AND item = ?", array($category, $item));
775 sqlStatement("UPDATE rule_action_item SET clin_rem_link = ?, reminder_message = ?, custom_flag = ? "
776 . "WHERE category = ? AND item = ?", array(
783 sqlStatement("INSERT INTO rule_action_item (clin_rem_link, reminder_message, custom_flag, category, item) "
784 . "VALUES (?,?,?,?,?)", array(
793 private function doRuleLabel($exists, $listId, $optionId, $title)
797 sqlStatement("UPDATE list_options SET title = ? WHERE list_id = ? AND option_id = ?", array(
803 $result = sqlQuery("select max(seq)+10 AS seq from list_options where list_id = ? AND activity = 1", array($listId));
804 $seq = $result['seq'];
805 sqlStatement("INSERT INTO list_options (list_id,option_id,title,seq) VALUES ( ?, ?, ?, ? )", array(
813 private function labelExists($listId, $optionId, $title)
815 $result = sqlQuery("SELECT COUNT(*) AS CT FROM list_options WHERE list_id = ? AND option_id = ? AND title = ? AND activity = 1", array($listId, $optionId, $title));
816 if ($result && $result['CT'] > 0) {