'submit', self::Click => 'link', self::Match => 'next', self::AddToCart => 'add_to_cart' }; } /** * Every act a tree offers, once each, in this enum's order. * * A Results screen offers Match regardless of whether contact is required * before it. Without Results, a button offers Submit or Click; navigation * buttons are not acts. * * The walk covers every step and every pane, through * {@see TemplateTree::childrenOf()}, because a `split`'s far pane is * exactly where a second converting act would hide from a reader. * * @param mixed $tree A template's `{steps: [...]}`, validated or not. * @return list */ public static function offeredIn($tree): array { $tree = is_array($tree) ? $tree : []; $resultAt = array_search('result', array_column(is_array($tree['steps'] ?? null) ? $tree['steps'] : [], 'kind'), true); if ($resultAt !== false) { return [self::Match]; } $found = []; foreach (is_array($tree['steps'] ?? null) ? $tree['steps'] : [] as $step) { self::collect($step, $found); } return array_values(array_filter(self::cases(), static fn (self $act): bool => in_array($act, $found, true))); } /** * @param mixed $node * @param list $found */ private static function collect($node, array &$found): void { if (!is_array($node)) { return; } if (($node['type'] ?? null) === 'products') $found[] = ($node['action'] ?? 'link') === 'add_to_cart' ? self::AddToCart : self::Click; if (($node['type'] ?? null) === 'button' && in_array($node['action'] ?? null, ['submit', 'link'], true)) { $act = ($node['action'] ?? null) === self::Click->action() ? self::Click : self::Submit; if (!in_array($act, $found, true)) { $found[] = $act; } } foreach (TemplateTree::childrenOf($node) as $child) { self::collect($child, $found); } } }