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