diff --git a/Behaviors/BehaviorDependencies/AbstractBehaviorOrderDependency.php b/Behaviors/BehaviorDependencies/AbstractBehaviorOrderDependency.php new file mode 100644 index 000000000..70416e181 --- /dev/null +++ b/Behaviors/BehaviorDependencies/AbstractBehaviorOrderDependency.php @@ -0,0 +1,128 @@ +orderAgainstBehaviorClasses = $orderAgainstBehaviorClasses; + } + + /** + * @inheritDoc + */ + public function resolve( + BehaviorInterface $subjectBehavior, + BehaviorListInterface $otherBehaviors, + array $behaviorClasses + ): void + { + $targetPriority = $currentPriority = $subjectBehavior->getPriority() ?? 0; + + // Find the desired priority for the subject + foreach ($otherBehaviors as $key => $behavior) { + if(in_array($behaviorClasses[$key], $this->orderAgainstBehaviorClasses)) { + $targetPriority = $this->comparePriorities($subjectBehavior, $behavior) ? + $targetPriority : + $behavior->getPriority() ?? 0; + } + } + + if($this->isInOrder($currentPriority, $targetPriority)) { + return; + } + + // Shift the priorities of all other behaviors to maintain pre-existing dependencies. + $otherBehaviors->sort(function ($self, $other) { return !$this->comparePriorities($self, $other);} ); + $currentPriority = $targetPriority; + $shiftedIndices = []; + + foreach ($otherBehaviors as $key => $behavior) { + if($behavior === $subjectBehavior) { + $shiftedIndices[$key] = $targetPriority; + continue; + } + + $priority = $behavior->getPriority() ?? 0; + $nextPriority = $this->getNextPriority($currentPriority); + + switch (true) { + // Shift from current to next. + case $priority === $currentPriority: + break; + // Move on to nex index, then shift from there. + case $priority === $nextPriority: + $currentPriority = $nextPriority; + $nextPriority = $this->getNextPriority($currentPriority); + break; + // If we have not shifted any indices yet, we keep searching. + case empty($shiftedIndices): + continue 2; + // If we did not create any new overlaps, we are done. + default: + break 2; + } + + $shiftedIndices[$key] = $nextPriority; + } + + // Now we have to ensure all indices remain within the boundaries of EventManagerInterface. + $delta = $this->getShiftDelta($shiftedIndices); + foreach ($shiftedIndices as $key => $index) { + $behavior = $otherBehaviors->get($key); + $enable = !$behavior->isDisabled(); + + // We have to toggle the behaviors to ensure that the shifted priorities are applied + // to all event delegates. + $behavior->disable(); + + $behavior->setPriority($index + $delta); + + if($enable) { + $behavior->enable(); + } + } + } + + /** + * @param BehaviorInterface $self + * @param BehaviorInterface $other + * @return bool + */ + protected abstract function comparePriorities(BehaviorInterface $self, BehaviorInterface $other) : bool; + + /** + * Returns TRUE if `$currentPriority` already fulfills the desired conditions. + * + * @param int $currentPriority + * @param int $targetPriority + * @return bool + */ + protected abstract function isInOrder(int $currentPriority, int $targetPriority) : bool; + + /** + * @param int $priority + * @return int + */ + protected abstract function getNextPriority(int $priority) : int; + + /** + * @param array $shiftedIndices + * @return int + */ + protected abstract function getShiftDelta(array $shiftedIndices) : int; +} \ No newline at end of file diff --git a/Behaviors/BehaviorDependencies/ApplyAfterBehaviors.php b/Behaviors/BehaviorDependencies/ApplyAfterBehaviors.php new file mode 100644 index 000000000..7a38614c7 --- /dev/null +++ b/Behaviors/BehaviorDependencies/ApplyAfterBehaviors.php @@ -0,0 +1,51 @@ +getPriority() ?? 0) < ($other->getPriority() ?? 0); + } + + /** + * @inheritDoc + */ + protected function isInOrder(int $currentPriority, int $targetPriority): bool + { + if($targetPriority > EventManagerInterface::PRIORITY_MIN) { + if($currentPriority <= $targetPriority) { + return true; + } + } + + return false; + } + + /** + * @inheritDoc + */ + protected function getNextPriority(int $priority): int + { + return $priority + 1; + } + + /** + * @inheritDoc + */ + protected function getShiftDelta(array $shiftedIndices) : int + { + $maxPriority = max($shiftedIndices); + return EventManagerInterface::PRIORITY_MAX - $maxPriority; + } +} \ No newline at end of file diff --git a/Behaviors/BehaviorDependencies/ApplyBeforeBehaviors.php b/Behaviors/BehaviorDependencies/ApplyBeforeBehaviors.php new file mode 100644 index 000000000..9c7420f05 --- /dev/null +++ b/Behaviors/BehaviorDependencies/ApplyBeforeBehaviors.php @@ -0,0 +1,51 @@ +getPriority() ?? 0) > ($other->getPriority() ?? 0); + } + + /** + * @inheritDoc + */ + protected function isInOrder(int $currentPriority, int $targetPriority) : bool + { + if($targetPriority < EventManagerInterface::PRIORITY_MAX) { + if($currentPriority >= $targetPriority) { + return true; + } + } + + return false; + } + + /** + * @inheritDoc + */ + protected function getNextPriority(int $priority): int + { + return $priority - 1; + } + + /** + * @inheritDoc + */ + protected function getShiftDelta(array $shiftedIndices) : int + { + $minPriority = min($shiftedIndices); + return EventManagerInterface::PRIORITY_MIN - $minPriority; + } +} \ No newline at end of file diff --git a/Behaviors/BehaviorDependencies/RequireBehaviors.php b/Behaviors/BehaviorDependencies/RequireBehaviors.php new file mode 100644 index 000000000..d350b406e --- /dev/null +++ b/Behaviors/BehaviorDependencies/RequireBehaviors.php @@ -0,0 +1,93 @@ +requiredBehaviorClasses = $requiredBehaviorClasses; + + $this->forbiddenBehaviorClasses = []; + foreach ($forbiddenBehaviorClasses as $class) { + if(!in_array($class, $requiredBehaviorClasses)) { + $this->forbiddenBehaviorClasses[] = $class; + } + } + } + + /** + * @inheritDoc + */ + public function resolve( + BehaviorInterface $subjectBehavior, + BehaviorListInterface $otherBehaviors, + array $behaviorClasses + ) : void + { + $selfClass = get_class($subjectBehavior); + $missingBehaviors = []; + $conflictingBehaviors = []; + + foreach ($this->requiredBehaviorClasses as $requiredBehaviorClass) { + if($requiredBehaviorClass === $selfClass) { + continue; + } + + if(!in_array($requiredBehaviorClass, $behaviorClasses)) { + $missingBehaviors[] = StringDataType::substringAfter( + $requiredBehaviorClass, + '\\', + false, + false, + true + ); + } + } + + foreach ($this->forbiddenBehaviorClasses as $forbiddenBehaviorClass) { + if($forbiddenBehaviorClass === $selfClass) { + continue; + } + + if(in_array($forbiddenBehaviorClass, $behaviorClasses)) { + $conflictingBehaviors[] = StringDataType::substringAfter( + $forbiddenBehaviorClass, + '\\', + false, + false, + true + ); + } + } + + if(empty($missingBehaviors) && empty($conflictingBehaviors)) { + return; + } + + $msg = 'Could not register behavior "' . $subjectBehavior->getAliasWithNamespace() . '" on object "' . + $subjectBehavior->getObject()->getAliasWithNamespace() . '".'; + + if(!empty($missingBehaviors)) { + $msg .= ' Missing REQUIRED behaviors: ' . json_encode($missingBehaviors) . '.'; + } + + if(!empty($conflictingBehaviors)) { + $msg .= ' Detected CONFLICTING behaviors: ' . json_encode($conflictingBehaviors) . '.'; + } + + throw new BehaviorConfigurationError($subjectBehavior, $msg); + } +} \ No newline at end of file diff --git a/Behaviors/SoftDeleteBehavior.php b/Behaviors/SoftDeleteBehavior.php index 06b1f951d..0a80e5e42 100644 --- a/Behaviors/SoftDeleteBehavior.php +++ b/Behaviors/SoftDeleteBehavior.php @@ -1,6 +1,7 @@ bypassBehaviorsOnUpdate = $trueOrFalse; return $this; } + + /** + * @inheritDoc + */ + protected function getDependencies(): array + { + return array_merge(parent::getDependencies(), [ + new ApplyAfterBehaviors([ + UndeletableBehavior::class + ]) + ]); + } } \ No newline at end of file diff --git a/Behaviors/UndeletableBehavior.php b/Behaviors/UndeletableBehavior.php index 757af17ca..286e8fa05 100644 --- a/Behaviors/UndeletableBehavior.php +++ b/Behaviors/UndeletableBehavior.php @@ -16,6 +16,7 @@ /** * Prevents the deletion of data if it matches the provided conditions. + * You must provide at least one condition for this behavior to function. * * ## Examples * diff --git a/CommonLogic/Model/Behaviors/AbstractBehavior.php b/CommonLogic/Model/Behaviors/AbstractBehavior.php index 437dd3477..0c8a7e32a 100644 --- a/CommonLogic/Model/Behaviors/AbstractBehavior.php +++ b/CommonLogic/Model/Behaviors/AbstractBehavior.php @@ -46,7 +46,7 @@ abstract class AbstractBehavior implements BehaviorInterface private $name = null; protected bool $isInProgress = false; - + /** * Disable this behavior type for the specified object. * @@ -247,6 +247,23 @@ public static function getUxonSchemaClass() : ?string */ public function register() : BehaviorInterface { + $behaviorList = $this->getObject()->getBehaviors(); + $classList = []; + foreach ($behaviorList as $key => $behavior) { + if($behavior->isDisabled()) { + $behaviorList->remove($behavior); + continue; + } + + $classList[$key] = get_class($behavior); + } + + // TODO Doing this here ensures that dependencies are always resolved and up-to-date. However, whenever a + // TODO new behavior is registered, all dependencies must be re-resolved, which might be expensive. + foreach ($behaviorList as $behavior) { + $behavior->resolveDependencies($behaviorList, $classList); + } + $this->registerEventListeners(); $this->setRegistered(true); return $this; @@ -430,4 +447,29 @@ protected function setName(string $name) : BehaviorInterface $this->name = $name; return $this; } + + /** + * Returns an array with dependencies to be resolved. + * + * Override this method (and merge results with the parent) to + * add new dependencies. + * + * @return array + */ + protected function getDependencies() : array + { + return []; + } + + /** + * @param BehaviorListInterface $behaviorList + * @param array $classList + * @return void + */ + protected function resolveDependencies(BehaviorListInterface $behaviorList, array $classList) : void + { + foreach ($this->getDependencies() as $dependency) { + $dependency->resolve($this, $behaviorList, $classList); + } + } } \ No newline at end of file diff --git a/Interfaces/Model/BehaviorDependencyInterface.php b/Interfaces/Model/BehaviorDependencyInterface.php new file mode 100644 index 000000000..15a4d0a35 --- /dev/null +++ b/Interfaces/Model/BehaviorDependencyInterface.php @@ -0,0 +1,26 @@ +