<?php
namespace App\AppBundle\Service;
use App\AppBundle\Entity\EstateFlow;
use App\AppBundle\Entity\ModelEntity;
use App\AppBundle\Entity\EstateFlowScenario;
use App\AppBundle\Entity\EstateFlowScenarioTimeline;
use App\AppBundle\Service\BaseEntityManagerHousehold;
use App\AppBundle\Service\EstateFlow\EstateFlowService;
use App\Uplifted\BaseBundle\Exception\BusinessException;
use App\AdminBundle\Service\AwsSesService;
use App\AppBundle\Entity\EstateFlowCache\EstateFlowCache;
use App\AppBundle\Entity\EstateFlowTransition;
use App\AppBundle\Entity\EstateFlowTrigger;
use App\AppBundle\Entity\EstateFlowTriggerEvent;
use App\AppBundle\Service\EstateFlowScenarioManager;
use App\AppBundle\Service\EstateFlowTransitionManager;
use App\AppBundle\Entity\ValuableResource;
class EstateFlowManager extends BaseEntityManagerHousehold
{
protected $estateFlowActionStrategy;
protected $frontendBaseUrl;
protected $twigTemplateEngine;
protected $awsSesService;
protected $estateFlowService;
protected $estateFlowScenarioManager;
protected $estateFlowTransitionManager;
public function setFrontendBaseUrl(string $frontendBaseUrl)
{
$this->frontendBaseUrl = $frontendBaseUrl;
}
public function setTwigTemplateEngine($twigTemplateEngine)
{
if ($this->twigTemplateEngine === null) {
$this->twigTemplateEngine = $twigTemplateEngine;
}
}
public function setAwsSesService(AwsSesService $awsSesService)
{
$this->awsSesService = $awsSesService;
}
public function setEstateFlowService(EstateFlowService $estateFlowService)
{
$this->estateFlowService = $estateFlowService;
}
public function setEstateFlowScenarioManager(EstateFlowScenarioManager $estateFlowScenarioManager)
{
$this->estateFlowScenarioManager = $estateFlowScenarioManager;
}
public function setEstateFlowTransitionManager(EstateFlowTransitionManager $estateFlowTransitionManager)
{
$this->estateFlowTransitionManager = $estateFlowTransitionManager;
}
public function getEntityName()
{
return 'estateFlow';
}
public function getManagedEntityClass()
{
return EstateFlow::class;
}
public function getCreateRequiredFieldsNames(array $values = array())
{
$requiredFields = array('modelEntity', 'sequence', 'result', 'status');
return $requiredFields;
}
protected function setCreateDefaultValues(array $values, array $additionalData = array())
{
// if status is not set, set it to ok
if (!isset($values['status'])){
$values['status'] = EstateFlow::STATUS_OK;
}
return $values;
}
public function validateValues($values, $entity = null)
{
$errors = array();
$this->simpleValidateValues(
$values,
$errors,
array(
'status' => array('choices' => EstateFlow::STATUS_OPTIONS)
)
);
$this->validateEntity($errors, $values, 'modelEntity', ModelEntity::class, true);
$this->validateEntity($errors, $values, 'sequence', EstateFlowScenario::class, true);
$this->validateEntity($errors, $values, 'timeline', EstateFlowScenarioTimeline::class, true);
if (isset($entity) &&
(isset($values['modelEntity']) ||
isset($values['sequence']) ||
isset($values['timeline'])
)){
throw new BusinessException("Cant change identity fields after creation");
}
if (!isset($entity)){
$exist = $this->getEntityRepo()->existEstateFlow($values['modelEntity'], $values['sequence'], $values['timeline']);
if ($exist){
throw new BusinessException("Try to save duplicated flow");
}
}
return $errors;
}
public function setValues(&$entity, $values)
{
$this->simpleSetValues(
$entity,
$values,
array('result' => 'string', 'status' => 'string', 'relatedData' => 'string')
);
$this->setEntityReferenceFieldValue($entity, 'modelEntity', $values, ModelEntity::class);
$this->setEntityReferenceFieldValue($entity, 'sequence', $values, EstateFlowScenario::class);
if (isset($values['timeline'])){
$this->setEntityReferenceFieldValue($entity, 'timeline', $values, EstateFlowScenarioTimeline::class);
}
}
public function sendFlowFailEmail($errorMessage, $householdHashId, $modelEntityId, $sequenceId, $timelineId = null)
{
$flowUrl = $this->frontendBaseUrl .'/household/'. $householdHashId .'/model-entities/estate-report/'.$modelEntityId.'/estate-flow-report?scenarioId='.$sequenceId;
if ($timelineId !== null){
$flowUrl = $flowUrl .'&timelineId='.$timelineId;
}
$parameters = array(
'flowUrl' => $flowUrl,
'errorMessage'=> $errorMessage
);
$html = $this->twigTemplateEngine->render('Emails/flowFailAlert.html.twig',
$parameters);
$this->awsSesService->sendStaffEmail(array(
'from' => 'Aristotle <no-reply@iwplatform.com>',
'subject' => 'Flow fail alert',
'replyTo' => 'support@iwplatform.com',
'body' => $html
));
}
public function saveEstateFlow($values) {
$estateFlow = $this->getEntityRepo()->findEstateFlow($values['modelEntity'],$values['sequence'], $values['timeline']);
if (isset($estateFlow)){
$this->safeUpdate($estateFlow, ['result'=>$values['result']]);
} else {
$this->safeCreate($values);
}
}
public function resolveSequenceEstateFlows($modelEntityId, $sequenceId): array
{
$estateFlows = [];
// Get the sequence
$sequence = (object)$this->estateFlowScenarioManager->find($sequenceId);
if (!$sequence) {
throw new \Exception('No sequence defined');
}
// Resolve default timeline (null timelineId)
$defaultEstateFlow = $this->resolveEstateFlow($modelEntityId, $sequenceId, null);
$estateFlows[] = json_encode($defaultEstateFlow->estateFlow);
// Resolve each timeline in the sequence
foreach ($sequence->getTimelines() as $timeline) {
$timelineEstateFlow = $this->resolveEstateFlow($modelEntityId, $sequenceId, $timeline->getId());
$estateFlows[] = json_encode($timelineEstateFlow->estateFlow);
}
return $estateFlows;
}
public function resolveEstateFlow($modelEntityId, $sequenceId, $timelineId, $saveCache=true, $ignoreCache=false){
$fromCache = 'true';
// Allow cache for the moment
$estateFlow = $this->getEntityRepo()->findEstateFlow($modelEntityId, $sequenceId, $timelineId);
$estateFlowResult = null;
if ($ignoreCache || !isset($estateFlow) || $estateFlow->getStatus() != EstateFlow::STATUS_OK){
$fromCache = 'false';
// Resolve estate flow
$estateFlowResult = $this->estateFlowService->resolveEstateFlow($modelEntityId, $sequenceId, $timelineId);
// Set related data to estateFlow for refrresh indexing
$relatedData = $this->getRelatedData($estateFlowResult);
// Set estateFlow result to json}
//$estateFlowResult = $this->breakCircularReferences($estateFlowResult);
//var_dump('start serialize');
$estateFlowResult = json_encode($estateFlowResult->jsonSerialize(), JSON_UNESCAPED_UNICODE);
//exit('end serialize');
//var_dump('end serialize');
if ($saveCache){
// If estateFlow exists, update it
if (isset($estateFlow)){
$estateFlow = $this->safeUpdate($estateFlow, ['result'=>gzcompress($estateFlowResult, 9), 'relatedData'=>$relatedData, 'status'=>EstateFlow::STATUS_OK]);
} else {
// If estateFlow does not exist, create it
$values = [
'modelEntity'=>$modelEntityId,
'sequence'=>$sequenceId,
'timeline'=>$timelineId,
'result'=>gzcompress($estateFlowResult, 9),
'relatedData'=>$relatedData,
'status'=>EstateFlow::STATUS_OK
];
$estateFlow = $this->safeCreate($values);
}
}
} else {
$estateFlowResult = $estateFlow->getResult();
}
return (object)['estateFlow'=>$estateFlowResult, 'fromCache'=>$fromCache];
}
/**
* Finds and breaks circular references in arrays and objects
*
* @param mixed $data The array or object to process
* @param array $processed Keep track of processed objects (for internal recursion)
* @param int $maxDepth Maximum recursion depth to prevent stack overflow
* @param int $currentDepth Current recursion depth (for internal use)
* @return mixed The processed data with circular references removed
*/
function breakCircularReferences(&$data, array &$processed = [], int $maxDepth = 100, int $currentDepth = 0)
{
// Check for recursion depth
if ($currentDepth >= $maxDepth) {
return null; // Replace with null when max depth reached
}
// If not an array or object, return as is
if (!is_array($data) && !is_object($data)) {
return $data;
}
// Create a unique ID for this array/object to track processed items
$id = is_object($data) ? spl_object_id($data) : md5(serialize($data));
// If we've seen this exact object/array before, we found a circular reference
if (isset($processed[$id])) {
if (is_object($data)) {
// Print object class name for debugging circular references
error_log('Circular reference found in class: ' . get_class($data));
// Check if object has getReferenceVersion method before attempting circular reference handling
debug_print_backtrace();
exit();
if (is_object($data) && method_exists($data, 'getReferenceVersion')) {
return $data->getReferenceVersion();
} else {
// For objects, we could return a placeholder or null depending on needs
return null; // Or create a simple representation like "(circular reference)"
}
} else {
// For arrays, return an empty array or something indicating the circular reference
return []; // Or ['__circular_reference' => true]
}
}
// Mark this array/object as processed
$processed[$id] = true;
// Handle arrays
if (is_array($data)) {
foreach ($data as $key => &$value) {
$value = $this->breakCircularReferences($value, $processed, $maxDepth, $currentDepth + 1);
}
}
// Handle objects
else if (is_object($data)) {
$reflection = new \ReflectionObject($data);
$properties = $reflection->getProperties();
foreach ($properties as $property) {
$property->setAccessible(true);
if ($property->isInitialized($data)) {
$value = $property->getValue($data);
$property->setValue($data, $this->breakCircularReferences($value, $processed, $maxDepth, $currentDepth + 1));
}
}
}
return $data;
}
public function refreshEstateFlow($modelEntityId, $sequenceId, $timelineId){
$estateFlow = $this->estateFlowService->resolveEstateFlow($modelEntityId, $sequenceId, $timelineId);
$estateFlow = json_encode($estateFlow, JSON_UNESCAPED_UNICODE);
$values = [
'modelEntity'=>$modelEntityId,
'sequence'=>$sequenceId,
'timeline'=>$timelineId,
'result'=>$estateFlow,
'status'=>EstateFlow::STATUS_OK
];
return $this->safeCreate($values);
}
public function clearEstateFlow($modelEntityId, $sequenceId, $timelineId){
$estateFlow = $this->getEntityRepo()->findEstateFlow($modelEntityId,$sequenceId, $timelineId);
if ($estateFlow != null){
$this->delete($estateFlow);
}
}
public function refreshOutdatedEstateFlows($householdHashId){
// Refresh outdated estate flows
$outdatedFlow = $this->getEntityRepo()->legacyFindByOne(['status'=>EstateFlow::STATUS_OUTDATED]);
while($outdatedFlow !== null){
try{
// block the flow from being processed by another instance
$values = [
'status'=>EstateFlow::STATUS_PENDING
];
$this->safeUpdate($outdatedFlow, $values);
$this->resolveEstateFlow($outdatedFlow->getModelEntity()->getId(), $outdatedFlow->getSequence()->getId(), $outdatedFlow->getTimeline()?->getId(), true, true);
} catch(\Exception $e){
$values = [
'status'=>EstateFlow::STATUS_FAIL,
'result'=>$e->getMessage()
];
$this->safeUpdate($outdatedFlow, $values);
$this->sendFlowFailEmail($e->getMessage(), $householdHashId, $outdatedFlow->getModelEntity()->getId(), $outdatedFlow->getSequence()->getId(), $outdatedFlow->getTimeline()?->getId());
} finally {
$this->doctrineEntityManager->clear();
gc_collect_cycles();
$outdatedFlow = $this->getEntityRepo()->legacyFindByOne(['status'=>EstateFlow::STATUS_OUTDATED]);
}
}
// Refresh failed estate flows
$failedFlow = $this->getEntityRepo()->legacyFindByOne(['status'=>EstateFlow::STATUS_FAIL]);
while($failedFlow !== null){
try{
$this->resolveEstateFlow($failedFlow->getModelEntity()->getId(), $failedFlow->getSequence()->getId(), $failedFlow->getTimeline()?->getId(), true, true);
} catch(\Exception $e){
throw $e;
// Do not notify for failed flows
} finally {
$this->doctrineEntityManager->clear();
gc_collect_cycles();
$failedFlow = $this->getEntityRepo()->legacyFindByOne(['status'=>EstateFlow::STATUS_FAIL]);
}
}
}
// Defines related data for estetflow, used detect when the estate flow is outdated
public function getRelatedData(EstateFlowCache $estateFlowCache){
//Get all transitions ids from the estate flow
$relatedTransitions = array_map(function($transition) {
return $transition->id;
}, $estateFlowCache->activatedTransitions);
//Get all entities id on actions from applied transitions
$relatedEntities = [];
$relatedActions = [];
foreach ($estateFlowCache->activatedTransitions as $transition) {
foreach ($transition->actions as $action) {
// Get action id
$relatedActions[] = $action->id;
// Get src entity id
$actionEntities[] = $action->src->modelEntity->id;
// Get targets entities ids
foreach ($action->targets as $target) {
$actionEntities[] = $target->modelEntity->id;
}
// Get post auto actions entities ids
foreach ($action->postAutoActions as $postAutoAction) {
$actionEntities[] = $postAutoAction->src->modelEntity->id;
}
// Get pre auto actions entities ids
foreach ($action->preAutoActions as $preAutoAction) {
$actionEntities[] = $preAutoAction->src->modelEntity->id;
}
}
}
// Get all entities id from initial and result of appliedTransitions
foreach ($estateFlowCache->appliedTransitions as $transition) {
// Get initial entities id
foreach ($transition->initial->entities as $entity) {
$relatedEntities[] = $entity->modelEntity->id;
}
// Get result entities id
foreach ($transition->result->entities as $entity) {
$relatedEntities[] = $entity->modelEntity->id;
}
}
$relatedData = [];
// Add entities ids to relatedData with key prefix entity_
foreach ($relatedEntities as $entity) {
$relatedData[] = "e_{$entity}_";
}
// Add transitions ids to relatedData with key prefix transition_
foreach ($relatedTransitions as $transition) {
$relatedData[] = "s_{$transition}_";
}
// Add actions ids to relatedData with key prefix action_
foreach ($relatedActions as $action) {
$relatedData[] = "a_{$action}_";
}
return implode(',', $relatedData);
}
public function newAvailableEstateFlowScenario(EstateFlowTransition $estateFlowScenario){
$estateFlowSequenceList = $this->getSequencesForScenario($estateFlowScenario);
foreach ($estateFlowSequenceList as $estateFlowSequence){
$this->outdatedEstateFlowSequence($estateFlowSequence->getId());
}
}
public function outdatedEstateFlowScenario($scenarioId){
$estateFlowScenario = $this->estateFlowTransitionManager->find($scenarioId);
if ($estateFlowScenario != null){
$this->newAvailableEstateFlowScenario($estateFlowScenario);
}
$estateFlowIdList = $this->getEntityRepo()->findEstateFlowIdListWithScenario($scenarioId);
if ($estateFlowIdList != null){
$this->getEntityRepo()->updateEstateFlowListStatus($estateFlowIdList, EstateFlow::STATUS_OUTDATED);
}
}
public function outdatedEstateFlowAction($actionId){
$estateFlowIdList = $this->getEntityRepo()->findEstateFlowIdListWithAction($actionId);
if ($estateFlowIdList != null){
$this->getEntityRepo()->updateEstateFlowListStatus($estateFlowIdList, EstateFlow::STATUS_OUTDATED);
}
}
public function outdatedEstateFlowEntity($entityId){
$estateFlowIdList = $this->getEntityRepo()->findEstateFlowIdListWithEntity($entityId);
if ($estateFlowIdList != null){
$this->getEntityRepo()->updateEstateFlowListStatus($estateFlowIdList, EstateFlow::STATUS_OUTDATED);
}
}
public function outdatedEstateFlowSequence($sequenceId){
$estateFlowIdList = $this->getEntityRepo()->findEstateFlowIdListWithSequence($sequenceId);
if ($estateFlowIdList != null){
$this->getEntityRepo()->updateEstateFlowListStatus($estateFlowIdList, EstateFlow::STATUS_OUTDATED);
}
}
public function outdatedEstateFlowModelEntity($modelEntityId){
$estateFlowIdList = $this->getEntityRepo()->findEstateFlowIdListWithModelEntity($modelEntityId);
if ($estateFlowIdList != null){
$this->getEntityRepo()->updateEstateFlowListStatus($estateFlowIdList, EstateFlow::STATUS_OUTDATED);
}
}
public function outdatedEstateFlowScenarioTimeline($timelineId){
$estateFlowIdList = $this->getEntityRepo()->findEstateFlowIdListWithTimeline($timelineId);
if ($estateFlowIdList != null){
$this->getEntityRepo()->updateEstateFlowListStatus($estateFlowIdList, EstateFlow::STATUS_OUTDATED);
}
}
public function outdatedValuableResource(ValuableResource $valuableResource){
$outdateFlowIds = [];
foreach ($valuableResource->getActiveOwnerships() as $ownership){
$outdateFlowIds = array_merge($outdateFlowIds, $this->getEntityRepo()->findEstateFlowIdListWithModelEntity($ownership->getModelEntityOwner()->getId()));
}
$this->getEntityRepo()->updateEstateFlowListStatus($outdateFlowIds, EstateFlow::STATUS_OUTDATED);
}
public function getSequencesForScenario(EstateFlowTransition $estateFlowScenario){
$sequences = [];
$allSequences = $this->estateFlowScenarioManager->findAll();
foreach ($allSequences as $sequence){
if ($this->checkIfSequenceActivatesScenario($sequence, $estateFlowScenario)){
$sequences[] = $sequence;
}
}
return $sequences;
}
public function checkIfSequenceActivatesScenario(EstateFlowScenario $estateFlowSequence, EstateFlowTransition $estateFlowScenario){
$triggerList = $estateFlowScenario->getTriggers();
foreach ($triggerList as $trigger){
if ($this->checkTrigger($trigger, $estateFlowSequence->getConfiguration())){
return true;
}
}
return false;
}
private function checkTrigger(
EstateFlowTrigger $trigger,
$eventsLoaded
): bool
{
switch ($trigger->getLogicalOperator()) {
case EstateFlowTrigger::LOGICAL_OPERATOR_AND:
return $this->checkAndTrigger($trigger, $eventsLoaded);
case EstateFlowTrigger::LOGICAL_OPERATOR_OR:
return $this->checkOrTrigger($trigger, $eventsLoaded);
case EstateFlowTrigger::LOGICAL_OPERATOR_BEFORE:
return $this->checkBeforeTrigger($trigger, $eventsLoaded);
case EstateFlowTrigger::LOGICAL_OPERATOR_AFTER:
return $this->checkAfterTrigger($trigger, $eventsLoaded);
default:
throw new Exception("Dev: invalid logical operator on trigger", 1);
}
}
private function checkIfTriggerEventHaveHappened(
EstateFlowTriggerEvent $triggerEvent,
$eventsLoaded
): bool
{
return $this->findTriggerEventPlaceOnLoadedEvents($triggerEvent, $eventsLoaded) !== -1;
}
private function findTriggerEventPlaceOnLoadedEvents(
EstateFlowTriggerEvent $triggerEvent,
$eventsLoaded
): int
{
foreach ($eventsLoaded as $index => $event) {
$event = (object)$event;
if ($event->description === $triggerEvent->getDescription()) {
if ($event->description === EstateFlowTriggerEvent::DESCRIPTION_DEATH) {
if ($event->targetModelEntity == $triggerEvent->getTargetModelEntity()->getId()) {
return $index;
}
} elseif ($event->description === EstateFlowTriggerEvent::DESCRIPTION_DATE) {
if ($event->date && $triggerEvent->getTargetDate() &&
$event->date->format('Y') === $triggerEvent->getTargetDate()->format('Y')) {
return $index;
}
}
}
}
return -1;
}
private function checkAndTrigger(
EstateFlowTrigger $trigger,
$eventsLoaded
): bool
{
$triggerDiscarded = false;
$explicitTriggerEvents = array_filter(
$trigger->getEvents()->toArray(),
fn($event) => in_array(
$event->getDescription(),
EstateFlowTriggerEvent::EVENT_OPTIONS
)
);
foreach ($explicitTriggerEvents as $triggerEvent) {
if (!$this->checkIfTriggerEventHaveHappened($triggerEvent, $eventsLoaded)) {
$triggerDiscarded = true;
break;
}
}
return !$triggerDiscarded;
}
private function checkOrTrigger(
EstateFlowTrigger $trigger,
$eventsLoaded
): bool
{
$triggerActivated = false;
$explicitTriggerEvents = array_filter(
$trigger->getEvents()->toArray(),
fn($event) => in_array(
$event->getDescription(),
EstateFlowTriggerEvent::EVENT_OPTIONS
)
);
foreach ($explicitTriggerEvents as $triggerEvent) {
if ($this->checkIfTriggerEventHaveHappened($triggerEvent, $eventsLoaded)) {
$triggerActivated = true;
break;
}
}
return $triggerActivated;
}
private function checkBeforeTrigger(
EstateFlowTrigger $trigger,
$eventsLoaded
): bool
{
$triggerDiscarded = false;
$eventLoadedIndex = $this->findTriggerEventPlaceOnLoadedEvents($trigger->getEvents()[0], $eventsLoaded);
if ($eventLoadedIndex !== -1) {
// Check if at least one of subsequent trigger events are found in previous loaded events
$previousEventsLoaded = array_slice($eventsLoaded, 0, $eventLoadedIndex);
for ($i = 1; $i < count($trigger->getEvents()) && !empty($previousEventsLoaded) && !$triggerDiscarded; $i++) {
$triggerEvent = $trigger->getEvents()[$i];
$triggerDiscarded = $this->checkIfTriggerEventHaveHappened($triggerEvent, $previousEventsLoaded);
}
} else {
$triggerDiscarded = true;
}
return !$triggerDiscarded;
}
private function checkAfterTrigger(
EstateFlowTrigger $trigger,
$eventsLoaded
): bool
{
$triggerDiscarded = false;
$eventsLoadedPending = array_reverse($eventsLoaded);
$triggerEventsChecked = 0;
for ($i = 0; $i < count($trigger->getEvents()) && !empty($eventsLoadedPending) && !$triggerDiscarded; $i++) {
$triggerEvent = $trigger->getEvents()[$i];
$eventLoadedIndex = $this->findTriggerEventPlaceOnLoadedEvents($triggerEvent, $eventsLoadedPending);
if ($eventLoadedIndex === -1) {
$triggerDiscarded = true;
$eventsLoadedPending = [];
} else {
$eventsLoadedPending = array_slice($eventsLoadedPending, $eventLoadedIndex + 1);
}
$triggerEventsChecked++;
}
return !$triggerDiscarded && $triggerEventsChecked === count($trigger->getEvents());
}
}