Skip to content

Commit

Permalink
implement basic structure
Browse files Browse the repository at this point in the history
  • Loading branch information
bohdanzhu committed Nov 5, 2024
1 parent d5fb21a commit 38389d4
Show file tree
Hide file tree
Showing 12 changed files with 399 additions and 0 deletions.
26 changes: 26 additions & 0 deletions Civi/Notification/Entity/AbstractEntity.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?php

namespace Civi\Notification\Entity;

abstract class AbstractEntity {

private array $entityValues;

Check failure on line 7 in Civi/Notification/Entity/AbstractEntity.php

View workflow job for this annotation

GitHub Actions / PHPStan with PHP 8.2 prefer-stable

Property Civi\Notification\Entity\AbstractEntity::$entityValues type has no value type specified in iterable type array.

Check failure on line 7 in Civi/Notification/Entity/AbstractEntity.php

View workflow job for this annotation

GitHub Actions / PHPStan with PHP 8.2 prefer-lowest

Property Civi\Notification\Entity\AbstractEntity::$entityValues type has no value type specified in iterable type array.

Check failure on line 7 in Civi/Notification/Entity/AbstractEntity.php

View workflow job for this annotation

GitHub Actions / PHPStan with PHP 8.3 prefer-stable

Property Civi\Notification\Entity\AbstractEntity::$entityValues type has no value type specified in iterable type array.

public function __construct(array $entityValues) {

Check failure on line 9 in Civi/Notification/Entity/AbstractEntity.php

View workflow job for this annotation

GitHub Actions / PHPStan with PHP 8.2 prefer-stable

Method Civi\Notification\Entity\AbstractEntity::__construct() has parameter $entityValues with no value type specified in iterable type array.

Check failure on line 9 in Civi/Notification/Entity/AbstractEntity.php

View workflow job for this annotation

GitHub Actions / PHPStan with PHP 8.2 prefer-lowest

Method Civi\Notification\Entity\AbstractEntity::__construct() has parameter $entityValues with no value type specified in iterable type array.

Check failure on line 9 in Civi/Notification/Entity/AbstractEntity.php

View workflow job for this annotation

GitHub Actions / PHPStan with PHP 8.3 prefer-stable

Method Civi\Notification\Entity\AbstractEntity::__construct() has parameter $entityValues with no value type specified in iterable type array.
$this->entityValues = $entityValues;
}

public function __get($name) {

Check failure on line 13 in Civi/Notification/Entity/AbstractEntity.php

View workflow job for this annotation

GitHub Actions / PHPStan with PHP 8.2 prefer-stable

Method Civi\Notification\Entity\AbstractEntity::__get() has no return type specified.

Check failure on line 13 in Civi/Notification/Entity/AbstractEntity.php

View workflow job for this annotation

GitHub Actions / PHPStan with PHP 8.2 prefer-stable

Method Civi\Notification\Entity\AbstractEntity::__get() has parameter $name with no type specified.

Check failure on line 13 in Civi/Notification/Entity/AbstractEntity.php

View workflow job for this annotation

GitHub Actions / PHPStan with PHP 8.2 prefer-lowest

Method Civi\Notification\Entity\AbstractEntity::__get() has no return type specified.

Check failure on line 13 in Civi/Notification/Entity/AbstractEntity.php

View workflow job for this annotation

GitHub Actions / PHPStan with PHP 8.2 prefer-lowest

Method Civi\Notification\Entity\AbstractEntity::__get() has parameter $name with no type specified.

Check failure on line 13 in Civi/Notification/Entity/AbstractEntity.php

View workflow job for this annotation

GitHub Actions / PHPStan with PHP 8.3 prefer-stable

Method Civi\Notification\Entity\AbstractEntity::__get() has no return type specified.

Check failure on line 13 in Civi/Notification/Entity/AbstractEntity.php

View workflow job for this annotation

GitHub Actions / PHPStan with PHP 8.3 prefer-stable

Method Civi\Notification\Entity\AbstractEntity::__get() has parameter $name with no type specified.

}

public function getEntityValue(string $key) {

Check failure on line 17 in Civi/Notification/Entity/AbstractEntity.php

View workflow job for this annotation

GitHub Actions / PHPStan with PHP 8.2 prefer-stable

Method Civi\Notification\Entity\AbstractEntity::getEntityValue() has no return type specified.

Check failure on line 17 in Civi/Notification/Entity/AbstractEntity.php

View workflow job for this annotation

GitHub Actions / PHPStan with PHP 8.2 prefer-lowest

Method Civi\Notification\Entity\AbstractEntity::getEntityValue() has no return type specified.

Check failure on line 17 in Civi/Notification/Entity/AbstractEntity.php

View workflow job for this annotation

GitHub Actions / PHPStan with PHP 8.3 prefer-stable

Method Civi\Notification\Entity\AbstractEntity::getEntityValue() has no return type specified.
// Check if the value is JSON-encoded and decode if so
if (\CRM_Utils_JSON::isValidJSON($this->entityValues[$key])) {
return json_decode($this->entityValues[$key], true);
}

return $this->entityValues[$key];
}

}
23 changes: 23 additions & 0 deletions Civi/Notification/Entity/ConditionEntity.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?php

namespace Civi\Notification\Entity;

use Civi\Api4\NotificationCondition;

class ConditionEntity extends AbstractEntity {

public static function loadByRuleId(int $ruleId): array {

Check failure on line 9 in Civi/Notification/Entity/ConditionEntity.php

View workflow job for this annotation

GitHub Actions / PHPStan with PHP 8.2 prefer-stable

Method Civi\Notification\Entity\ConditionEntity::loadByRuleId() return type has no value type specified in iterable type array.

Check failure on line 9 in Civi/Notification/Entity/ConditionEntity.php

View workflow job for this annotation

GitHub Actions / PHPStan with PHP 8.2 prefer-lowest

Method Civi\Notification\Entity\ConditionEntity::loadByRuleId() return type has no value type specified in iterable type array.

Check failure on line 9 in Civi/Notification/Entity/ConditionEntity.php

View workflow job for this annotation

GitHub Actions / PHPStan with PHP 8.3 prefer-stable

Method Civi\Notification\Entity\ConditionEntity::loadByRuleId() return type has no value type specified in iterable type array.
$notificationConditions = NotificationCondition::get()
->addWhere('rule_id', '=', $ruleId)
->execute();

// Convert results into array of ConditionEntity objects
$conditions = [];
foreach ($notificationConditions as $condition) {
$conditions[] = new self($condition);

Check failure on line 17 in Civi/Notification/Entity/ConditionEntity.php

View workflow job for this annotation

GitHub Actions / PHPStan with PHP 8.2 prefer-stable

Parameter #1 $entityValues of class Civi\Notification\Entity\ConditionEntity constructor expects array, mixed given.

Check failure on line 17 in Civi/Notification/Entity/ConditionEntity.php

View workflow job for this annotation

GitHub Actions / PHPStan with PHP 8.2 prefer-lowest

Parameter #1 $entityValues of class Civi\Notification\Entity\ConditionEntity constructor expects array, mixed given.

Check failure on line 17 in Civi/Notification/Entity/ConditionEntity.php

View workflow job for this annotation

GitHub Actions / PHPStan with PHP 8.3 prefer-stable

Parameter #1 $entityValues of class Civi\Notification\Entity\ConditionEntity constructor expects array, mixed given.
}

return $conditions;
}

}
22 changes: 22 additions & 0 deletions Civi/Notification/Entity/FieldMonitoringEntity.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?php

namespace Civi\Notification\Entity;

use Civi\Api4\NotificationFieldMonitoring;

class FieldMonitoringEntity extends AbstractEntity {

public static function loadByRuleId(int $ruleId): array {

Check failure on line 9 in Civi/Notification/Entity/FieldMonitoringEntity.php

View workflow job for this annotation

GitHub Actions / PHPStan with PHP 8.2 prefer-stable

Method Civi\Notification\Entity\FieldMonitoringEntity::loadByRuleId() return type has no value type specified in iterable type array.

Check failure on line 9 in Civi/Notification/Entity/FieldMonitoringEntity.php

View workflow job for this annotation

GitHub Actions / PHPStan with PHP 8.2 prefer-lowest

Method Civi\Notification\Entity\FieldMonitoringEntity::loadByRuleId() return type has no value type specified in iterable type array.

Check failure on line 9 in Civi/Notification/Entity/FieldMonitoringEntity.php

View workflow job for this annotation

GitHub Actions / PHPStan with PHP 8.3 prefer-stable

Method Civi\Notification\Entity\FieldMonitoringEntity::loadByRuleId() return type has no value type specified in iterable type array.
$notificationFieldMonitorings = NotificationFieldMonitoring::get()
->addWhere('rule_id', '=', $ruleId)
->execute();

// Convert results into array of FieldMonitoringEntity objects
$fieldMonitorings = [];
foreach ($notificationFieldMonitorings as $fieldMonitoring) {
$fieldMonitorings[] = new self($fieldMonitoring);

Check failure on line 17 in Civi/Notification/Entity/FieldMonitoringEntity.php

View workflow job for this annotation

GitHub Actions / PHPStan with PHP 8.2 prefer-stable

Parameter #1 $entityValues of class Civi\Notification\Entity\FieldMonitoringEntity constructor expects array, mixed given.

Check failure on line 17 in Civi/Notification/Entity/FieldMonitoringEntity.php

View workflow job for this annotation

GitHub Actions / PHPStan with PHP 8.2 prefer-lowest

Parameter #1 $entityValues of class Civi\Notification\Entity\FieldMonitoringEntity constructor expects array, mixed given.

Check failure on line 17 in Civi/Notification/Entity/FieldMonitoringEntity.php

View workflow job for this annotation

GitHub Actions / PHPStan with PHP 8.3 prefer-stable

Parameter #1 $entityValues of class Civi\Notification\Entity\FieldMonitoringEntity constructor expects array, mixed given.
}

return $fieldMonitorings;
}
}
46 changes: 46 additions & 0 deletions Civi/Notification/Entity/RuleEntity.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<?php

namespace Civi\Notification\Entity;

use \Civi\Api4\NotificationRule;

class RuleEntity extends AbstractEntity {

private ?array $fieldMonitorings = NULL;

Check failure on line 9 in Civi/Notification/Entity/RuleEntity.php

View workflow job for this annotation

GitHub Actions / PHPStan with PHP 8.2 prefer-stable

Property Civi\Notification\Entity\RuleEntity::$fieldMonitorings type has no value type specified in iterable type array.

Check failure on line 9 in Civi/Notification/Entity/RuleEntity.php

View workflow job for this annotation

GitHub Actions / PHPStan with PHP 8.2 prefer-lowest

Property Civi\Notification\Entity\RuleEntity::$fieldMonitorings type has no value type specified in iterable type array.

Check failure on line 9 in Civi/Notification/Entity/RuleEntity.php

View workflow job for this annotation

GitHub Actions / PHPStan with PHP 8.3 prefer-stable

Property Civi\Notification\Entity\RuleEntity::$fieldMonitorings type has no value type specified in iterable type array.
private ?array $conditions = NULL;

public function __get($name) {
if (!property_exists($this, $name)) {
throw new \Exception("Property {$name} does not exist.");
}

if (!isset($this->$name)) {
switch ($name) {
case 'fieldMonitorings':
$this->$name = FieldMonitoringEntity::loadByRuleId($this->getEntityValue('id'));
break;
case 'conditions':
$this->$name = ConditionEntity::loadByRuleId($this->getEntityValue('id'));
break;
}
}

return $this->$name;
}

public static function loadByRuleSetId(int $ruleSetId): array {
$notificationRules = NotificationRule::get()
->addWhere('rule_set_id', '=', $ruleSetId)
->addWhere('is_active', '=', TRUE)
->addOrderBy('weight')
->execute();

// Convert results into array of RuleEntity objects
$rules = [];
foreach ($notificationRules as $rule) {
$rules[] = new self($rule);
}

return $rules;
}
}
60 changes: 60 additions & 0 deletions Civi/Notification/Entity/RuleSetEntity.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
<?php

namespace Civi\Notification\Entity;

use Civi\Api4\NotificationRuleSet;

class RuleSetEntity extends AbstractEntity {

private ?array $rules = NULL;

/**
* Magic method to load properties dynamically, ex. rules
*/
public function __get($name) {
if (!property_exists($this, $name)) {
throw new \Exception("Property {$name} does not exist.");
}

if (!isset($this->$name)) {
switch ($name) {
case 'rules':
$this->$name = RuleEntity::loadByRuleSetId($this->getEntityValue('id'));
break;
}
}

return $this->$name;
}

public static function loadByEntityType(string $entityType): array {
$notificationRuleSets = NotificationRuleSet::get()
->addSelect('*')
->addWhere('monitored_entity_type', '=', $entityType)
->addWhere('is_active', '=', 1)
->execute();

$ruleSets = [];

foreach ($notificationRuleSets as $ruleSet) {
$ruleSets[] = new self($ruleSet);
}

return $ruleSets;
}

public static function hasActiveRuleSets(string $entityType): bool {
$result = NotificationRuleSet::get()
->selectRowCount()
->addWhere('monitored_entity_type', '=', $entityType)
->addWhere('is_active', '=', TRUE)
->execute();

return $result->count() > 0;
}

public function isExecuteOnlyFirstRule() {
return $this->getEntityValue('is_execute_only_first_rule');
}

}
79 changes: 79 additions & 0 deletions Civi/Notification/EventSubscriber/NotificationSubscriber.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
<?php

namespace Civi\Notification\EventSubscriber;

use Civi\API\Request;
use Civi\Core\Event\PreEvent;
use Civi\Core\Event\PostEvent;
use Civi\Notification\Entity\RuleSetEntity;
use Civi\Notification\Handlers\RuleSetHandler;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;

final class NotificationSubscriber implements EventSubscriberInterface {

/**
* Store the entity's old state in a cache
*
* @var array
*/
private static array $entityCache = [];

public static function getSubscribedEvents(): array {
return [
'hook_civicrm_pre' => 'onPre',
'hook_civicrm_postCommit' => 'onPostCommit',
'hook_civicrm_postProcess' => 'postProcess',
];
}

public function postProcess() {
// This is temporary for debugging.
// die;
}

public function onPre(PreEvent $event): void {
[ , $entity, $id, ] = $event->getHookValues();

if (RuleSetEntity::hasActiveRuleSets($entity)) {
// Capture old values before the change
$oldValues = $this->loadEntityValues($entity, $id);

// Store the entity's old state in a cache
self::$entityCache[$entity][$id] = $oldValues;
}
}

public function onPostCommit(PostEvent $event): void {
[ , $entity, $id, , ] = $event->getHookValues();

// Check if old values exist for this entity in the cache
if (isset(self::$entityCache[$entity][$id])) {
$oldValues = self::$entityCache[$entity][$id];
// Retrieve new values after the change
$newValues = $this->loadEntityValues($entity, $id);

$ruleSets = RuleSetEntity::loadByEntityType($entity);
$ruleSetHandler = new RuleSetHandler();

foreach ($ruleSets as $ruleSet) {
// Evaluate relevant rule sets
$ruleSetHandler->evaluateRuleSet($ruleSet, $newValues, $oldValues);
}

// Clean up cache after processing
unset(self::$entityCache[$entity][$id]);
}
}

private function loadEntityValues(string $entityType, int $entityID): array {
// Load entity data with API v4
$result = Request::create($entityType, 'get', [
'version' => 4,
'where' => [['id', '=', $entityID]]
])
->execute();

return $result->first() ?? [];
}

}
30 changes: 30 additions & 0 deletions Civi/Notification/Handlers/ConditionHandler.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?php

namespace Civi\Notification\Handlers;

use Civi\Notification\Entity\ConditionEntity;

class ConditionHandler {

public function evaluateCondition(ConditionEntity $condition, array $changeSet): bool {
$operator = $condition->getEntityValue('operator');
$fieldName = $condition->getEntityValue('field_name');
$conditionValue = $condition->getEntityValue('value');
$changeSetValue = $changeSet[$fieldName] ?? NULL; // New value

// Use the comparison method to evaluate condition
return $this->compareValues($conditionValue, $changeSetValue, $operator);
}

// TODO: Move the method to avoid duplication
private function compareValues($value1, $value2, string $operator): bool {
switch ($operator) {
case '=':
return $value1 == $value2;
case 'IN':
return in_array($value2, $value1);
default:
throw new \InvalidArgumentException("Unsupported operator: $operator");
}
}
}
35 changes: 35 additions & 0 deletions Civi/Notification/Handlers/FieldMonitoringHandler.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<?php

namespace Civi\Notification\Handlers;

use Civi\Notification\Entity\FieldMonitoringEntity;

class FieldMonitoringHandler {

public function evaluate(FieldMonitoringEntity $fieldMonitoring, array $oldValues, array $changeSet): bool {
$operatorBefore = $fieldMonitoring->getEntityValue('operator_before');
$operatorAfter = $fieldMonitoring->getEntityValue('operator_after');
$conditionBeforeValue = $fieldMonitoring->getEntityValue('value_before');
$conditionAfterValue = $fieldMonitoring->getEntityValue('value_after');
$fieldName = $fieldMonitoring->getEntityValue('field_name');
$oldValue = $oldValues[$fieldName] ?? NULL; // Old value
$newValue = $changeSet[$fieldName] ?? NULL; // New value

// Use the comparison method to evaluate condition
return
$this->compareValues($conditionBeforeValue, $oldValue, $operatorBefore)
&& $this->compareValues($conditionAfterValue, $newValue, $operatorAfter);
}

// TODO: Move the method to avoid duplication
private function compareValues($value1, $value2, string $operator): bool {
switch ($operator) {
case '=':
return $value1 == $value2;
case 'IN':
return in_array($value2, $value1);
default:
throw new \InvalidArgumentException("Unsupported operator: $operator");
}
}
}
33 changes: 33 additions & 0 deletions Civi/Notification/Handlers/RuleHandler.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?php

namespace Civi\Notification\Handlers;

use Civi\Notification\Entity\RuleEntity;

class RuleHandler {

public function evaluateRule(RuleEntity $rule, array $oldValues, array $changeSet): bool {
// Evaluate additional conditions
foreach ($rule->conditions as $condition) {
$conditionHandler = new ConditionHandler();

// Evaluate condition logic
if (!$conditionHandler->evaluateCondition($condition, $changeSet)) {
return FALSE;
}
}

// Evaluate field monitoring rules
foreach ($rule->fieldMonitorings as $fieldMonitoring) {
$fieldMonitorHandler = new FieldMonitoringHandler();
// Evaluate field monitoring logic
if (!$fieldMonitorHandler->evaluate($fieldMonitoring, $oldValues, $changeSet)) {
return FALSE;
}
}

// TODO: add logic for sending notification

return TRUE;
}
}
22 changes: 22 additions & 0 deletions Civi/Notification/Handlers/RuleSetHandler.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?php

namespace Civi\Notification\Handlers;

use Civi\Notification\Entity\RuleSetEntity;

class RuleSetHandler {

public function evaluateRuleSet(RuleSetEntity $ruleSet, array $entityValues, array $changeSet): void {
foreach ($ruleSet->rules as $rule) {
// TODO: Add logic to check if rule set entity conditions are met (source_entity_type, source_entity_id)

$ruleHandler = new RuleHandler();

if ($ruleHandler->evaluateRule($rule, $entityValues, $changeSet)) {
if ($ruleSet->isExecuteOnlyFirstRule()) {
break;
}
}
}
}
}
Loading

0 comments on commit 38389d4

Please sign in to comment.