Zum Inhalt springen

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.

  1. Split /{path*}{not:/admin*|/api/v1*} → positive /{path*}, masks /admin* and /api/v1*.
  2. If the URL starts with /admin or /api/v1, return false.
  3. 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().

See also