Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
128 changes: 128 additions & 0 deletions Behaviors/BehaviorDependencies/AbstractBehaviorOrderDependency.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
<?php

namespace exface\Core\Behaviors\BehaviorDependencies;

use exface\Core\Interfaces\Model\BehaviorDependencyInterface;
use exface\Core\Interfaces\Model\BehaviorInterface;
use exface\Core\Interfaces\Model\BehaviorListInterface;

/**
* Base class for managing behavior order dependencies, i.e. making sure a behavior runs "Before" or "After"
* a specified list of behaviors. Note that ordering dependencies are resolved naively: If multiple ordering
* dependencies conflict with one another, the last one to be applied wins out.
*
* // TODO If the dependency pattern proves to be valuable we might upgrade this with proper graph resolution.
*/
abstract class AbstractBehaviorOrderDependency implements BehaviorDependencyInterface
{
private array $orderAgainstBehaviorClasses;

function __construct(array $orderAgainstBehaviorClasses)
{
$this->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;
}
51 changes: 51 additions & 0 deletions Behaviors/BehaviorDependencies/ApplyAfterBehaviors.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
<?php

namespace exface\Core\Behaviors\BehaviorDependencies;

use exface\Core\Interfaces\Events\EventManagerInterface;
use exface\Core\Interfaces\Model\BehaviorInterface;

/**
* Make sure the subject is applied AFTER a given list of behaviors.
*/
class ApplyAfterBehaviors extends AbstractBehaviorOrderDependency
{
/**
* @inheritDoc
*/
protected function comparePriorities(BehaviorInterface $self, BehaviorInterface $other): bool
{
return ($self->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;
}
}
51 changes: 51 additions & 0 deletions Behaviors/BehaviorDependencies/ApplyBeforeBehaviors.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
<?php

namespace exface\Core\Behaviors\BehaviorDependencies;

use exface\Core\Interfaces\Events\EventManagerInterface;
use exface\Core\Interfaces\Model\BehaviorInterface;

/**
* Make sure the subject is applied BEFORE a given list of behaviors.
*/
class ApplyBeforeBehaviors extends AbstractBehaviorOrderDependency
{
/**
* @inheritDoc
*/
protected function comparePriorities(BehaviorInterface $self, BehaviorInterface $other): bool
{
return ($self->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;
}
}
93 changes: 93 additions & 0 deletions Behaviors/BehaviorDependencies/RequireBehaviors.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
<?php

namespace exface\Core\Behaviors\BehaviorDependencies;

use exface\Core\DataTypes\StringDataType;
use exface\Core\Exceptions\Behaviors\BehaviorConfigurationError;
use exface\Core\Interfaces\Model\BehaviorDependencyInterface;
use exface\Core\Interfaces\Model\BehaviorInterface;
use exface\Core\Interfaces\Model\BehaviorListInterface;

/**
* Ensures that a given list of behavior is PRESENT or ABSENT from the metaobject of the subject.
*/
class RequireBehaviors implements BehaviorDependencyInterface
{
private array $requiredBehaviorClasses;
private array $forbiddenBehaviorClasses;

function __construct(array $requiredBehaviorClasses = [], array $forbiddenBehaviorClasses = [])
{
$this->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);
}
}
13 changes: 13 additions & 0 deletions Behaviors/SoftDeleteBehavior.php
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
<?php
namespace exface\Core\Behaviors;

use exface\Core\Behaviors\BehaviorDependencies\ApplyAfterBehaviors;
use exface\Core\CommonLogic\Model\Behaviors\AbstractBehavior;
use exface\Core\Contexts\DebugContext;
use exface\Core\Events\DataSheet\OnBeforeDeleteDataEvent;
Expand Down Expand Up @@ -555,4 +556,16 @@ protected function setBypassBehaviorsOnUpdate(bool $trueOrFalse) : SoftDeleteBeh
$this->bypassBehaviorsOnUpdate = $trueOrFalse;
return $this;
}

/**
* @inheritDoc
*/
protected function getDependencies(): array
{
return array_merge(parent::getDependencies(), [
new ApplyAfterBehaviors([
UndeletableBehavior::class
])
]);
}
}
1 change: 1 addition & 0 deletions Behaviors/UndeletableBehavior.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
*
Expand Down
Loading