AI blog · DotApp PHP Framework 2.0
How URL {not:} selectors work in DotApp PHP Framework
Kernel update 26 August 2026: Router::match_url() understands
{not:mask|mask} on any URL selector.
The exclude runs first, before the positive pattern.
/admin/login against /{path*}{not:/admin*} dies on a prefix test, not on a greedy regex.
The same grammar works on Router::get, Router::before, Router::onPath,
Module::initializeRoutes(), and Listeners::initializeRoutes() — every caller goes through match_url.
Write {not:/admin*}, not CSS :not(). A public catch-all must carry the exclude
on the wake string itself. Cutting /admin only inside initializeCondition is too late:
the optimizer already woke the module.
This article is the exact syntax, a match table, a CMS catch-all, and a wake-map example.
Common mistakes
| Wrong | Right |
|---|---|
Write :not(/admin) or {not(/admin*)} |
{not:/admin*} — brace, colon, then the mask |
Wake on /{path*} and skip /admin only in initializeCondition |
Put {not:/admin*} on the same string the optimizer stores |
{not:/admin/*} and expect exact /admin to be excluded |
/admin/* is prefix /admin/. Exact /admin still matches. Use {not:/admin*} or {not:/admin|/admin/*} |
Router::STATIC_ROUTE on /{path*}{not:/admin*} |
Keep it dynamic. After the exclude is stripped, a static compare would look for the literal text /{path*} |
A selector that is only {not:/admin*} |
After the token is stripped the positive route is empty → no match. Always keep a positive pattern |
Assume /admin* excludes only the admin folder |
It is a starts-with test on /admin, so /administrator is also excluded. For a strict tree use {not:/admin|/admin/*} |
Exclude first, then the positive match
match_url() looks for the substring {not:. If it finds one, it splits every
{not:…} token off the string, then tests the request path against each mask.
The first hit returns false immediately. Only then does the remaining positive route run —
exact compare, /prefix/* starts-with, or the usual {id:i} regex.
- Split
/{path*}{not:/admin*|/api/v1*}→ positive/{path*}, masks/admin*and/api/v1*. - If the URL starts with
/adminor/api/v1, returnfalse. - Otherwise match
/{path*}as usual.
Several {not:} tokens on one selector are collected. Pipes inside one token are the same list:
{not:/admin*}{not:/api/v1*} equals {not:/admin*|/api/v1*}.
Nested {not:} inside a mask is ignored.
Syntax
| Form | What it excludes |
|---|---|
{not:/admin} |
Exact path /admin only |
{not:/admin/*} |
Every path that starts with /admin/. Not exact /admin |
{not:/admin*} |
Every path that starts with /admin, including /admin and /admin/login |
{not:/admin|/admin/*} |
Exact /admin plus the /admin/… tree. Does not hit /administrator |
{not:/admin*|/api/v1*|/assets*} |
Pipe-separated masks. Spaces around | are trimmed |
/{path*}{not:/admin*}{not:/dacore*} |
Two tokens, same result as one pipe list |
A mask that ends in * and has no { : ? is a starts-with test
(the trailing star is stripped). A mask with no special characters is an exact path.
Any other mask is sent back through match_url (for example /item/{id:i}).
Exact outcomes
Selector: /{path*}{not:/admin*|/api/v1*|/assets*}
| URL | Result |
|---|---|
/, /about, /blog/hello |
Match. path is the remainder |
/admin, /admin/, /admin/login |
No match — /admin* |
/api/v1/auth/Shop |
No match — /api/v1* |
/assets/dotapp/dotapp.js |
No match — /assets* |
/administrator |
No match on /admin*. Use {not:/admin|/admin/*} if this URL must stay public |
Public catch-all route
File: app/modules/CMS/module.init.php. Register the catch-all last.
Do not pass Router::STATIC_ROUTE.
Router::get(
'/{path*}{not:/admin*|/api/v1*|/assets*|/dacore*}',
'CMS:Page@show!'
);
/about reaches CMS:Page@show.
/admin/users does not. DACore keeps those URLs.
Read the captured path from $request->matchData()['path'].
Wake maps must carry the same text
php dotapper.php --optimize-modules stores the strings from
initializeRoutes() and later calls match_url on each one.
A public CMS that also owns / must exclude admin on those strings.
After the list changes, re-run the optimizer.
public function initializeRoutes()
{
$this->defaultSettings();
return [
'/',
'/{path*}{not:/admin*|/api/v1*|/assets*|/dacore*}',
'/api/v1/auth/CMS',
'/api/v1/auth/CMS/*',
'/api/v1/noauth/CMS',
'/api/v1/noauth/CMS/*',
];
}
Loyalty that extends Shop still lists Shop prefixes. It does not need {not:}
unless it also uses a catch-all.
A listener that must run everywhere except admin:
public function initializeRoutes()
{
return [
'/{path*}{not:/admin*}',
];
}
That is tighter than ['*']. Prefer known prefixes when you have them.
Independent listener routes.
before hooks and onPath
Router::before binds only when the current request already matches the pattern.
Put {not:} on that pattern so the hook never attaches on admin URLs.
Router::before(
'/{path*}{not:/admin*|/api/v1*}',
'CMS:PublicCache@before!'
);
Router::onPath('/Shop*{not:/Shop/admin*}', function () {
Router::get('/Shop/sale', 'Shop:Sale@index!', Router::STATIC_ROUTE);
});
FAQ
Is this CSS :not()?
No. The token is {not:mask} inside a DotApp URL selector. There is no function-call form.
Why does AIRULES say {not:/admin*} and not {not:/admin/*}?
A fast prefix mask that ends in * and has no braces uses starts-with.
/admin* → prefix /admin → exact /admin and /admin/login both die.
/admin/* → prefix /admin/ → exact /admin still matches the positive route.
Can initializeCondition cut /admin instead?
Not for a public catch-all. The optimizer already decided to construct the module from the wake string.
The exclude must be on that string. initializeCondition can still skip work after a match, but it cannot undo a wrong wake.
Can I mark the catch-all STATIC_ROUTE?
No. After {not:} is stripped, a static compare requires the remaining text to equal the URL.
/{path*} is never equal to /about.
What if I write only {not:/admin*}?
The positive route becomes empty. match_url returns false. Always keep /, /{path*}, or a real prefix in front.
Does {not:} capture a parameter?
No. It only excludes. Captures still come from the positive pattern via $request->matchData().