diff --git a/components/ILIAS/Component/src/Dependencies/In.php b/components/ILIAS/Component/src/Dependencies/In.php index 73a461e42662..e65744452233 100644 --- a/components/ILIAS/Component/src/Dependencies/In.php +++ b/components/ILIAS/Component/src/Dependencies/In.php @@ -75,4 +75,9 @@ public function getResolvedBy(): array { return $this->resolved_by; } + + public function isResolved(): bool + { + return count($this->resolved_by) > 0; + } } diff --git a/components/ILIAS/Component/src/Dependencies/ResolutionDirective.php b/components/ILIAS/Component/src/Dependencies/ResolutionDirective.php new file mode 100644 index 000000000000..34a958dd9326 --- /dev/null +++ b/components/ILIAS/Component/src/Dependencies/ResolutionDirective.php @@ -0,0 +1,33 @@ +x; + } + + public function getY(): string + { + return $this->y; + } + + public function getSpecificity(): int + { + return 1; + } +} diff --git a/components/ILIAS/Component/src/Dependencies/ResolutionDirective/InComponent.php b/components/ILIAS/Component/src/Dependencies/ResolutionDirective/InComponent.php new file mode 100644 index 000000000000..2d544b735a08 --- /dev/null +++ b/components/ILIAS/Component/src/Dependencies/ResolutionDirective/InComponent.php @@ -0,0 +1,58 @@ + $l->getSpecificity() <=> $r->getSpecificity()); + $this->directives = $directives; + $this->specificity = max(array_map(fn($d) => $d->getSpecificity(), $directives)) + 1; + } + + public function getComponentName(): string + { + return $this->component_name; + } + + public function getDirectives(): array + { + return $this->directives; + } + + public function getSpecificity(): int + { + return $this->specificity; + } +} diff --git a/components/ILIAS/Component/src/Dependencies/ResolutionDirective/WhenPulling.php b/components/ILIAS/Component/src/Dependencies/ResolutionDirective/WhenPulling.php new file mode 100644 index 000000000000..12950c18bdba --- /dev/null +++ b/components/ILIAS/Component/src/Dependencies/ResolutionDirective/WhenPulling.php @@ -0,0 +1,47 @@ +directives = $directives; + $this->specificity = max(array_map(fn($d) => $d->getSpecificity(), $directives)) + 1; + } + + public function getSpecificity(): int + { + return $this->specificity; + } +} diff --git a/components/ILIAS/Component/src/Dependencies/Resolver.php b/components/ILIAS/Component/src/Dependencies/Resolver.php index 9a38a4f8c362..4cc002aa0504 100644 --- a/components/ILIAS/Component/src/Dependencies/Resolver.php +++ b/components/ILIAS/Component/src/Dependencies/Resolver.php @@ -20,55 +20,37 @@ namespace ILIAS\Component\Dependencies; +use ILIAS\Component\Dependencies\ResolutionDirective as RD; + class Resolver { + // Dependencies are resolved recursively, we capture where we are currently + // at. Will contain pairs (OfComponent, In-Dependency) + protected array $stack; + + // Components to be resolved. + protected array $components; + + // Directives to resolve dependencies. + protected array $directives; + /** - * Resolves dependencies of all components. This is unambigous for all types of - * dependencies but the use/implement-pair. If there would be ambiguities, these - * can be disambiguated by the first argument. - * - * The structure of the first argument is as such: keys are components that use - * services ("dependant") that need disambiguation, value for each dependant is - * an array where the key is the definition ("dependency") and the value is the - * implementation ("implementation") to be used. - * - * The entry "*" for the dependant will define fallbacks to be used for all - * components that have no explicit disambiguation. - * - * So, the array might look as such: + * Resolves dependencies of all components. Use ResolutionDirective to dis- + * ambiguate use/implement-pairs or force special resolutions for certain + * circumstances. * - * [ - * "*" => [ - * "ILIAS\Logger\Logger" => ILIAS\Logger\DBLogger - * ], - * "ILIAS\Database\DB" => [ - * "ILIAS\Logger\Logger" => ILIAS\Logger\StdErrLogger - * ] - * ] - * - * @param array> $disambiguation + * @param array $directives * @param OfComponent[] * @return OfComponent[] */ - public function resolveDependencies(array $disambiguation, OfComponent ...$components): array + public function resolveDependencies(array $directives, OfComponent ...$components): array { - foreach ($components as $component) { - foreach ($component->getInDependencies() as $d) { - switch ($d->getType()) { - case InType::PULL: - $this->resolvePull($d, $components); - break; - case InType::SEEK: - $this->resolveSeek($d, $components); - break; - case InType::USE: - $this->resolveUse($component, $disambiguation, $d, $components); - break; - } - } - } + usort($directives, fn($l, $r) => $l->getSpecificity() <=> $r->getSpecificity()); - $cycles = iterator_to_array($this->findCycles(...$components)); + $this->directives = $directives; + $this->components = $components; + + $cycles = iterator_to_array($this->resolveDependenciesSeed()); if (!empty($cycles)) { throw new \LogicException( "Detected Cycles in Dependency Tree: " . @@ -85,14 +67,57 @@ public function resolveDependencies(array $disambiguation, OfComponent ...$compo ); } + // TODO: Make this return void, modifies components in place. return $components; } - protected function resolvePull(In $in, array &$others): void + /** + * @return Generator> cycles in the dependency graph + */ + protected function resolveDependenciesSeed(): \Generator + { + foreach ($this->components as $component) { + foreach ($component->getInDependencies() as $in) { + yield from $this->resolveDependency([], $component, $in); + } + } + } + + /** + * @return Generator> cycles in the dependency graph + */ + protected function resolveDependency(array $visited, OfComponent $component, In $in): \Generator + { + // Since SEEK-dependencies might in fact have no resolution and this + // is also fine, this would lead to these dependencies be checked + // again, even if that is not necessary. We take this potential + // trade off for some simplicity. + if ($in->isResolved()) { + return; + } + + // This is a cycle, we arrived where we started. + if (!empty($visited) && $visited[0][0] === $component && $visited[0][1] == $in) { + yield $visited; + return; + } + + array_push($visited, [$component, $in]); + yield from match ($in->getType()) { + InType::PULL => $this->resolvePull($visited, $component, $in), + InType::SEEK => $this->resolveSeek($visited, $component, $in), + InType::USE => $this->resolveUse($visited, $component, $in), + default => throw new \LogicException("Unknown type: {$in->getType()}") + }; + array_pop($visited); + } + + + protected function resolvePull(array $visited, OfComponent $component, In $in): \Generator { $candidate = null; - foreach ($others as $other) { + foreach ($this->components as $other) { if ($other->offsetExists("PROVIDE: " . $in->getName())) { if (!is_null($candidate)) { throw new \LogicException( @@ -108,26 +133,28 @@ protected function resolvePull(In $in, array &$others): void throw new \LogicException("Could not resolve dependency for: " . (string) $in); } + yield from $this->resolveTransitiveDependencies($visited, $candidate); $in->addResolution($candidate); } - protected function resolveSeek(In $in, array &$others): void + protected function resolveSeek(array $visited, OfComponent $component, In $in): \Generator { - foreach ($others as $other) { + foreach ($this->components as $other) { if ($other->offsetExists("CONTRIBUTE: " . $in->getName())) { // For CONTRIBUTEd, we just use all contributions. foreach ($other["CONTRIBUTE: " . $in->getName()] as $o) { + yield from $this->resolveTransitiveDependencies($visited, $o); $in->addResolution($o); } } } } - protected function resolveUse(OfComponent $component, array &$disambiguation, In $in, array &$others): void + protected function resolveUse(array $visited, OfComponent $component, In $in): \Generator { $candidates = []; - foreach ($others as $other) { + foreach ($this->components as $other) { if ($other->offsetExists("IMPLEMENT: " . $in->getName())) { // For IMPLEMENTed dependencies, we need to make choice. $candidates[] = $other["IMPLEMENT: " . $in->getName()]; @@ -141,19 +168,21 @@ protected function resolveUse(OfComponent $component, array &$disambiguation, In } if (count($candidates) === 1) { + yield from $this->resolveTransitiveDependencies($visited, $candidates[0]); $in->addResolution($candidates[0]); return; } - $preferred_class = $this->disambiguate($component, $disambiguation, $in); + $preferred_class = $this->disambiguateUse($component, $this->directives, $in); if (is_null($preferred_class)) { throw new \LogicException( "Dependency {$in->getName()} is provided (at least) twice, " . - "no disambiguation for {$component->getComponentName()}." + "no directives for {$component->getComponentName()}." ); } foreach ($candidates as $candidate) { if ($candidate->aux["class"] === $preferred_class) { + yield from $this->resolveTransitiveDependencies($visited, $candidates[0]); $in->addResolution($candidate); return; } @@ -164,47 +193,26 @@ protected function resolveUse(OfComponent $component, array &$disambiguation, In ); } - protected function disambiguate(OfComponent $component, array &$disambiguation, In $in): ?string + protected function resolveTransitiveDependencies(array $visited, Out $out): \Generator { - $service_name = (string) $in->getName(); - foreach ([$component->getComponentName(), "*"] as $c) { - if (isset($disambiguation[$c]) && isset($disambiguation[$c][$service_name])) { - return $disambiguation[$c][$service_name]; - } + $component = $out->getComponent(); + array_push($visited, [$component, $out]); + foreach ($out->getDependencies() as $dep) { + yield from $this->resolveDependency($visited, $component, $dep); } - return null; + array_pop($visited); } - /** - * @var Generator> - */ - protected function findCycles(OfComponent ...$components): \Generator + protected function disambiguateUse(OfComponent $component, array $directives, In $in): ?string { - foreach ($components as $component) { - foreach ($component->getInDependencies() as $in) { - foreach ($this->findCyclesWith([], $component, $in) as $cycle) { - yield $cycle; - } + foreach ($directives as $d) { + if ($d instanceof RD\InComponent && $d->getComponentName() === $component->getComponentName()) { + return $this->disambiguateUse($component, $d->getDirectives(), $in); } - } - } - - protected function findCyclesWith(array $visited, OfComponent $component, In $in): \Generator - { - if (!empty($visited) && $visited[0][0] === $component && $visited[0][1] == $in) { - yield $visited; - return; - } - - array_push($visited, [$component, $in]); - foreach ($in->getResolvedBy() as $out) { - $other = $out->getComponent(); - array_push($visited, [$component, $out]); - foreach ($out->getDependencies() as $next) { - yield from $this->findCyclesWith($visited, $out->getComponent(), $next); + if ($d instanceof RD\ForXUseY && $d->getX() == (string) $in->getName()) { + return $d->getY(); } - array_pop($visited); } - array_pop($visited); + return null; } } diff --git a/components/ILIAS/Component/tests/Dependencies/ResolverTest.php b/components/ILIAS/Component/tests/Dependencies/ResolverTest.php index be44a8a658ad..99882ba51146 100644 --- a/components/ILIAS/Component/tests/Dependencies/ResolverTest.php +++ b/components/ILIAS/Component/tests/Dependencies/ResolverTest.php @@ -23,8 +23,25 @@ use PHPUnit\Framework\TestCase; use ILIAS\Component\Dependencies\Resolver; use ILIAS\Component\Dependencies as D; +use ILIAS\Component\Dependencies\ResolutionDirective as RD; use ILIAS\Component\Component; +interface Component1 extends Component +{ +} + +interface Component2 extends Component +{ +} + +interface Component3 extends Component +{ +} + +interface Component4 extends Component +{ +} + class ResolverTest extends TestCase { protected Resolver $resolver; @@ -216,13 +233,14 @@ public function testUseDisambiguateDuplicateSpecific(): void $c2 = new D\OfComponent($component, $implement1); $c3 = new D\OfComponent($component, $implement2); - $disambiguation = [ - get_class($component) => [ - TestInterface::class => "Some\\OtherClass" - ] + $directives = [ + new RD\InComponent( + get_class($component), + new RD\ForXUseY(TestInterface::class, "Some\\OtherClass") + ) ]; - $result = $this->resolver->resolveDependencies($disambiguation, $c1, $c2, $c3); + $result = $this->resolver->resolveDependencies($directives, $c1, $c2, $c3); $use = new D\In(D\InType::USE, $name); $implement1 = new D\Out(D\OutType::IMPLEMENT, $name, ["class" => "Some\\Class"], []); @@ -250,13 +268,11 @@ public function testUseDisambiguateDuplicateGeneric(): void $c2 = new D\OfComponent($component, $implement1); $c3 = new D\OfComponent($component, $implement2); - $disambiguation = [ - "*" => [ - TestInterface::class => "Some\\OtherClass" - ] + $directives = [ + new RD\ForXUseY(TestInterface::class, "Some\\OtherClass") ]; - $result = $this->resolver->resolveDependencies($disambiguation, $c1, $c2, $c3); + $result = $this->resolver->resolveDependencies($directives, $c1, $c2, $c3); $use = new D\In(D\InType::USE, $name); $implement1 = new D\Out(D\OutType::IMPLEMENT, $name, ["class" => "Some\\Class"], []); @@ -271,6 +287,68 @@ public function testUseDisambiguateDuplicateGeneric(): void $this->assertEquals([$c1, $c2, $c3], $result); } + public function testDisambiguateTransitive(): void + { + $component1 = $this->createMock(Component1::class); + $component2 = $this->createMock(Component2::class); + $component3 = $this->createMock(Component3::class); + $component4 = $this->createMock(Component4::class); + + $name = TestInterface::class; + $name2 = TestInterface2::class; + + $pull1 = new D\In(D\InType::PULL, $name); + $pull2 = new D\In(D\InType::PULL, $name); + $use = new D\In(D\InType::USE, $name2); + $provide = new D\Out(D\OutType::PROVIDE, $name, ["class" => "Some\\Class"], [$use]); + $implement1 = new D\Out(D\OutType::IMPLEMENT, $name2, ["class" => "Some\\OtherClass"], []); + $implement2 = new D\Out(D\OutType::IMPLEMENT, $name2, ["class" => "Some\\DifferentClass"], []); + + $c1 = new D\OfComponent($component1, $pull1); + $c2 = new D\OfComponent($component2, $pull2); + $c3 = new D\OfComponent($component3, $provide, $use); + $c4 = new D\OfComponent($component4, $implement1, $implement2); + + $directives = [ + new RD\InComponent( + get_class($component1), + new RD\WhenPulling( + $name, + new RD\ForXUseY($name2, "Some\\OtherClass") + ) + ), + new RD\InComponent( + get_class($component2), + new RD\WhenPulling( + $name, + new RD\ForXUseY($name2, "Some\\DifferentClass") + ) + ) + ]; + + $result = $this->resolver->resolveDependencies($directives, $c1, $c2, $c3, $c4); + + $pull1 = new D\In(D\InType::PULL, $name); + $pull2 = new D\In(D\InType::PULL, $name); + $use1 = new D\In(D\InType::USE, $name2); + $use2 = new D\In(D\InType::USE, $name2); + $provide1 = new D\Out(D\OutType::PROVIDE, $name, ["class" => "Some\\Class"], [$use1]); + $provide2 = new D\Out(D\OutType::PROVIDE, $name, ["class" => "Some\\Class"], [$use2]); + $implement1 = new D\Out(D\OutType::IMPLEMENT, $name2, ["class" => "Some\\OtherClass"], []); + $implement2 = new D\Out(D\OutType::IMPLEMENT, $name2, ["class" => "Some\\DifferentClass"], []); + + $use1->addResolution($implement1); + $use2->addResolution($implement2); + $pull1->addResolution($provide1); + $pull1->addResolution($provide2); + + $c1 = new D\OfComponent($component, $use); + $c2 = new D\OfComponent($component, $implement1); + $c3 = new D\OfComponent($component, $implement2); + + $this->assertEquals([$c1, $c2, $c3], $result); + } + public function testFindSimpleCycle(): void { $this->expectException(\LogicException::class);