packages/sql-fixture/src/Fixture/Choice/CaseSelection.php
1<?php
2
3declare(strict_types=1);
4
5namespace SqlFixture\Fixture\Choice;
6
7use Faker\Generator;
8use SqlFixture\Plan\Choice\ChoiceCase;
9use SqlFixture\Plan\Relation;
10use SqlFixture\Plan\RelationChoice;
11
12/**
13 * Chooses a branch from explicit values or compatible inverse relations.
14 * @visibility root
15 */
16final class CaseSelection
17{
18 /**
19 * @template TValue
20 * @param array<TValue> $values
21 * @throws ChoiceValueException
22 */
23 public function select(RelationChoice $choice, array $values, ?Relation $arrival, Generator $faker): ChoiceCase
24 {
25 $column = $choice->discriminator->columns[0];
26 if (array_key_exists($column, $values)) {
27 foreach ($choice->cases as $case) {
28 if ($values[$column] === $case->value) {
29 return $this->checkArrival($choice, $case, $arrival);
30 }
31 }
32 if ($choice->fallback !== null) {
33 return $this->checkArrival($choice, $choice->fallback, $arrival);
34 }
35 throw new ChoiceValueException($choice->discriminator, 'The supplied value does not match any case. Define otherwise() to accept other values.');
36 }
37 $cases = $choice->cases;
38 if ($arrival !== null && in_array($arrival, $choice->relations(), true)) {
39 $cases = array_values(array_filter($cases, fn (ChoiceCase $case): bool => $this->matchingArrival($case, $arrival) !== null));
40 }
41 if ($cases === []) {
42 throw new ChoiceValueException($choice->discriminator, 'An inverse fallback relation requires an explicit discriminator value.');
43 }
44 return $cases[$faker->numberBetween(0, count($cases) - 1)];
45 }
46
47 /**
48 * Rejects an explicit case incompatible with the relation already walked.
49 * @throws ChoiceValueException
50 */
51 public function checkArrival(RelationChoice $choice, ChoiceCase $case, ?Relation $arrival): ChoiceCase
52 {
53 if ($arrival !== null && in_array($arrival, $choice->relations(), true) && $this->matchingArrival($case, $arrival) === null) {
54 throw new ChoiceValueException($choice->discriminator, 'The supplied value conflicts with the relation used to reach this row.');
55 }
56 return $case;
57 }
58
59 /**
60 * Finds the selected equivalent of an inverse candidate relation.
61 */
62 public function matchingArrival(ChoiceCase $case, Relation $arrival): ?Relation
63 {
64 foreach ($case->relations as $relation) {
65 if ($relation->parent()->equals($arrival->parent()) && $relation->child()->equals($arrival->child()) && $relation->kind === $arrival->kind) {
66 return $relation;
67 }
68 }
69 return null;
70 }
71}
72