Quelltext des aktuellen Standes
<?php
declare(strict_types=1);
use OpenNIT\Locations\LocationScope;
use OpenNIT\Locations\TopicLocations;
/**
* Deckt die Standortbindung der Themengebiete ab.
*
* Ein Themengebiet kann mehreren Standorten zugeordnet sein; ohne eigene
* Zuordnung erbt es die Menge des nächsten übergeordneten, und erbt keiner eine,
* gilt es für alle. Die senkrechte Linie durch einen Standort umfasst dessen
* übergeordnete und untergeordnete Standorte, nicht aber Nebenstandorte.
*/
return static function (): void {
/**
* Datenbank-Doppel mit Standortbaum, Themengebieten und Zuordnungstabelle.
*
* @param array<int,?int> $standorte Standort-ID => übergeordneter Standort
* @param array<int,array{path:string,locs:int[]}> $themen Themengebiet-ID => Pfad und eigene Standorte
* @param bool $mitTabelle false = Zuordnungstabelle fehlt noch (vor der Migration)
*/
$db = function (array $standorte, array $themen, bool $mitTabelle = true): object {
return new class ($standorte, $themen, $mitTabelle) extends \OpenNIT\Core\Database {
public function __construct(
private array $standorte,
private array $themen,
private bool $mitTabelle
) {
// Verbindung bewusst nicht aufbauen – die Abfragen werden bedient.
}
public function fetchAll(string $sql, array $params = []): array
{
if (str_contains($sql, 'FROM ticket_locations')) {
$rows = [];
foreach ($this->standorte as $id => $parent) {
$rows[] = ['id' => $id, 'parent_id' => $parent];
}
return $rows;
}
if (str_contains($sql, 'FROM topic_locations')) {
if (!$this->mitTabelle) {
throw new \RuntimeException('Table topic_locations does not exist');
}
$rows = [];
foreach ($this->themen as $id => $t) {
foreach ($t['locs'] as $loc) {
$rows[] = ['topic_id' => $id, 'location_id' => $loc];
}
}
return $rows;
}
if (str_contains($sql, 'FROM topics')) {
$rows = [];
foreach ($this->themen as $id => $t) {
// Die frühere Einzelzuordnung: der erste Standort.
$rows[] = [
'id' => $id,
'path' => $t['path'],
'location_id' => $t['locs'][0] ?? null,
];
}
return $rows;
}
return [];
}
};
};
// Kunde A (1) mit Niederlassungen Nord (2) und Süd (3); Kunde B (4) daneben.
$standorte = [1 => null, 2 => 1, 3 => 1, 4 => null];
// Themenbaum: Kunde A (10) mit Unterthemen, Kunde B (20), Allgemeines (30)
// mit einer gemeinsamen Anleitung (31) für beide Kunden.
$themen = [
10 => ['path' => '/10/', 'locs' => [1]],
11 => ['path' => '/10/11/', 'locs' => []],
12 => ['path' => '/10/12/', 'locs' => [2]],
20 => ['path' => '/20/', 'locs' => [4]],
30 => ['path' => '/30/', 'locs' => []],
31 => ['path' => '/30/31/', 'locs' => [1, 4]],
];
test('Ein Themengebiet ohne eigene Zuordnung erbt die des übergeordneten', function () use ($db, $standorte, $themen) {
$wirksam = (new TopicLocations($db($standorte, $themen)))->effectiveLocations();
assertSame([1], $wirksam[11], 'Unterthema erbt den Standort von Kunde A');
assertSame([2], $wirksam[12], 'Eigene Zuordnung hat Vorrang vor der geerbten');
assertSame([], $wirksam[30], 'Ohne Zuordnung im ganzen Pfad bleibt es ungebunden');
assertSame([1, 4], $wirksam[31], 'Eine gemeinsame Anleitung trägt mehrere Standorte');
});
test('Ohne zugeordnete Themengebiete gibt es keine Einschränkung', function () use ($db, $standorte) {
$ohne = [10 => ['path' => '/10/', 'locs' => []], 20 => ['path' => '/20/', 'locs' => []]];
$tl = new TopicLocations($db($standorte, $ohne));
assertFalse($tl->inUse());
assertSame(null, $tl->visibleTopicIds([1]), 'null steht für „keine Einschränkung"');
});
test('Der Oberstandort sieht die Themengebiete seiner Unterstandorte', function () use ($db, $standorte, $themen) {
$erlaubt = (new LocationScope($db($standorte, $themen)))->selfAncestorsAndDescendants(1);
$sichtbar = (new TopicLocations($db($standorte, $themen)))->visibleTopicIds($erlaubt);
sort($sichtbar);
assertSame([10, 11, 12, 30, 31], $sichtbar, 'Kunde A samt Unterthemen, das Ungebundene und die gemeinsame Anleitung');
});
test('Die gemeinsame Anleitung erscheint bei beiden Kunden', function () use ($db, $standorte, $themen) {
$scope = new LocationScope($db($standorte, $themen));
$tl = new TopicLocations($db($standorte, $themen));
$beiA = $tl->visibleTopicIds($scope->selfAncestorsAndDescendants(1));
$beiB = (new TopicLocations($db($standorte, $themen)))
->visibleTopicIds($scope->selfAncestorsAndDescendants(4));
assertTrue(in_array(31, $beiA, true), 'Kunde A sieht die gemeinsame Anleitung');
assertTrue(in_array(31, $beiB, true), 'Kunde B ebenfalls');
assertFalse(in_array(10, $beiB, true), 'Kunde B sieht deswegen nicht den Bereich von Kunde A');
});
test('Ein Unterstandort sieht das Kundenweite mit, aber nicht den Nebenstandort', function () use ($db, $standorte, $themen) {
$erlaubt = (new LocationScope($db($standorte, $themen)))->selfAncestorsAndDescendants(2);
sort($erlaubt);
assertSame([1, 2], $erlaubt, 'Standort Nord und dessen Oberstandort – Süd bleibt außen vor');
$sichtbar = (new TopicLocations($db($standorte, $themen)))->visibleTopicIds($erlaubt);
sort($sichtbar);
assertSame([10, 11, 12, 30, 31], $sichtbar);
});
test('Ein fremder Kunde sieht nur sein eigenes und das ungebundene Themengebiet', function () use ($db, $standorte, $themen) {
$erlaubt = (new LocationScope($db($standorte, $themen)))->selfAncestorsAndDescendants(4);
$sichtbar = (new TopicLocations($db($standorte, $themen)))->visibleTopicIds($erlaubt);
sort($sichtbar);
assertSame([20, 30, 31], $sichtbar, 'Der Bereich von Kunde A bleibt verborgen');
});
test('Ohne Standort bleibt nur das Ungebundene sichtbar', function () use ($db, $standorte, $themen) {
$erlaubt = (new LocationScope($db($standorte, $themen)))->selfAncestorsAndDescendants(null);
$sichtbar = (new TopicLocations($db($standorte, $themen)))->visibleTopicIds($erlaubt);
assertSame([], $erlaubt);
assertSame([30], $sichtbar);
});
test('Fehlt die Zuordnungstabelle, gilt die frühere Einzelzuordnung', function () use ($db, $standorte, $themen) {
$wirksam = (new TopicLocations($db($standorte, $themen, false)))->effectiveLocations();
assertSame([1], $wirksam[10], 'Der Wert aus der Spalte zählt');
assertSame([1], $wirksam[31], 'Mehrfachzuordnung gibt es dort noch nicht – der erste Standort gilt');
});
test('Die Sichtbarkeitsbedingung schließt Inhalte ohne Standort ein', function () use ($db, $standorte, $themen) {
[$sql, $params] = (new LocationScope($db($standorte, $themen)))->visibilityCondition(1, 'x.location_id');
assertTrue(str_contains($sql, 'x.location_id IS NULL'), 'Inhalte ohne Standort gelten überall');
sort($params);
assertSame([1, 2, 3], $params, 'Oberstandort samt beider Niederlassungen');
});
test('Ein Zyklus im Standortbaum bricht die Auflösung nicht', function () use ($db, $themen) {
$zyklus = [1 => 2, 2 => 1];
$erlaubt = (new LocationScope($db($zyklus, $themen)))->selfAncestorsAndDescendants(1);
sort($erlaubt);
assertSame([1, 2], $erlaubt);
});
};