Advent of code 2020/21
Ajax Direct

Answer

Part 1 :
Part 2 :
$aler = set();
$ingr = set();

foreach ($input->lines as $line) {
    [$_, $i, $a] = $line->match("/^(.+) \(contains (.*)\)$/");

    $i = explode(" ", $i);
    $ingr = $ingr->merge($i);

    foreach (explode(", ", $a) as $al) {
        $aler[$al] = isset($aler[$al]) ? $aler[$al]->keep($i) : set($i);
    }
}

// ==================================================
// > PART 1
// ==================================================
$solution_1 = $ingr->remove($aler->merge()->unique())->count();

// ==================================================
// > PART 2
// ==================================================
while (true) {
    foreach ($aler as $i=>$a) {
        if (!is_string($a) && $a->count() == 1) {
            $aler[$i] = $al = $a->values()[0];
            foreach ($aler as $j=>$b) if (!is_string($aler[$j])) $aler[$j] = $aler[$j]->remove([$al]);
            continue 2;
        }
    }
    break;
}

$solution_2 = $aler->sortKeys()->join(",");