Rather than writing a route to match anything other than certain static strings, I find it clearer to write two routes: one route to match certain static strings, and another route to match everything else.
// route that matches forbidden static strings, optionally with a postfix slug
$router->get('/{forbidden}/{optional_path?}', function () {
return response('Not found', 404);
})->where([ 'forbidden' => '(?:string1|string2)', 'optional_path' => '.*' ]);
// route that matches anything else (order of definition matters, must be last)
// might also consider using Route::fallback(), but I prefer to leave that
// alone in case my future self changes this below and opens up a hole
$router->get('/{anything?}', function () {
return response('Found', 200);
})->where([ 'anything' => '.*' ]);
Which results in*:
domain
=> 200 Found
domain/
=> 200 Found
domain/abc
=> 200 Found
domain/string1
=> 404 Not found
domain/string1/
=> 404 Not found
domain/string1/abc
=> 404 Not found
domain/string10
=> 200 Found
domain/string10/
=> 200 Found
domain/string10/abc
=> 200 Found
domain/string2
=> 404 Not found
domain/string2/
=> 404 Not found
domain/string2/abc
=> 404 Not found
domain/string20
=> 200 Found
domain/string20/
=> 200 Found
domain/string20/abc
=> 200 Found
I find this more clear, because I don't have to think in terms of exclusions. Rather, I can think of matching exactly what I want to forbid, then letting Laravel react to everything else (fail open policy). This may not meet your design criteria, but I do believe it results in clearer code.
Also, the code is more performant. ?!
has to backtrack, which is by definition more expensive than forward matching.
I don't have a Laravel environment to hand, but I'll hazard a guess as to why your attempts didn't work. Laravel uses Symfony Router, which does not support lookarounds on slugs. IIRC, when a lookaround's detected, Symfony applies the lookaround to the entire URL, not the slug you've bound the pattern to. This messes with the developer's idea of how anchors (^, $) and greedy (*) meta characters work. This can lead to a bad experience in trying to get it to work, since the developer's operating under one assumption but the underlying libraries operating on another.
* Full disclosure, I wrote this for Lumen then mentally converted it to Laravel format. It's possible there are some translation errors. Here's the original Lumen:
$router->get('/{forbidden:(?:string1|string2)}[/{optional_path:.*}]', function () {
return response('Not found', 404);
});
$router->get('{anything:.*}', function () {
return response('Found', 200);
});