Advent of code 2019/14
Ajax Direct

Answer 14ms

Part 1 : 397771 Part 2 : 3126714
// Parse reactions recipes
$recipes = $input->lines->mapAssoc(function ($i, $line) {
    [$res, $comp] = explode(" => ", $line);
    [$qt, $mat]   = explode(" ", $comp);
    $res = set(explode(", ", $res))->mapAssoc(function ($i, $c) {
        $c = explode(" ", $c);
        return [$c[1] => (int) $c[0]];
    });
    return [$mat => [(int) $qt, $res]];
});

// Decompose a reciepe to have the number of ores and all the leftovers materials
function decompose($recipe, $recipes) {
    $leftovers = [];
    while (true) {
        $decomp = [];
        foreach ($recipe as $comp=>$qty) {
            if ($comp == "ORE") {
                $decomp["ORE"] ??= 0;
                $decomp["ORE"] += $qty;
                continue;
            }

            // Take from leftovers first
            if (!empty($leftovers[$comp])) {
                $available = min($leftovers[$comp], $qty);
                $qty -= $available;
                $leftovers[$comp] -= $available;
            }
            if (!$qty) continue;

            // Decompose item
            $times = ceil($qty / $recipes[$comp][0]);
            $build = $recipes[$comp][1]->map(fn ($r) => $r * $times);

            // Add build surpulus to leftovers
            $leftovers[$comp] ??= 0;
            $leftovers[$comp] += $recipes[$comp][0] * $times - $qty;

            // Replace item with its composition
            foreach ($build as $bc=>$bq) {
                $decomp[$bc] ??= 0;
                $decomp[$bc] += $bq;
            }
        }
        if (count(array_keys($decomp)) == 1) return [$decomp["ORE"], $leftovers];
        $recipe = $decomp;
    }
}

// ==================================================
// > PART 1 : Decompose FUEL recipe
// ==================================================
[$solution_1, $_] = decompose($recipes["FUEL"][1], $recipes);

// ==================================================
// > PART 2 : Binary search the maximum number of fuel that decompose to the closest of 1_000_000_000_000 ORES
// ==================================================
$lb = floor(1_000_000_000_000 / $solution_1);
$ub = $lb * 2;

$solution_2 = binary_search($lb, $ub, function ($value) use ($recipes, $solution_1) {
    $ores = decompose(["FUEL" => $value], $recipes)[0];
    if (1_000_000_000_000 - $solution_1 < $ores && $ores < 1_000_000_000_000) {
        return 0;
    }
    return 1_000_000_000_000 <=> $ores;
});