PHP bridge

A stylesheet can reach php — but only through what the application explicitly handed it. There is no implicit access to any php function, to superglobals, to the filesystem or to the database: the registry is the trust boundary.

PHP application ├── chooses the context values → context::name ├── chooses the php functions → php::name() ▼ SCS compiler → parse → evaluate → call authorized functions → css

Authorizing a function

use MMO\client\SCSCompiler; use MMO\client\scs\EvaluationContext; SCSCompiler::Shared() //a value function only sees its scs arguments ->allowFunction('php::round', 'round') ->allowFunction('php::spacing', \App\Css\Functions::spacing(...)) //a context function also receives the compilation context and the resolved selector ->allowContextFunction('php::cardPadding', function (EvaluationContext $context) { $density = $context->compilation()->get('density') ?? 1; return ($context->selector() === '.card'? 24 * $density : 8 * $density) . 'px'; });

This page ran exactly that, for php::round, php::strtolower, php::spacing, php::palette and php::cardPadding. Every example below is compiled live against that registry.

example.scs
@let $base = 8; @let $name = php::strtolower("BUTTON"); .${$name} { width: ${php::round($base * 1.5)}px; padding: php::spacing(4); }
generated css
.button { width: 12px; padding: 16px; }

Anything else is refused

A function that was never registered is a compile error, not a call — and the error says which name was refused.

example.scs
.a { content: php::file_get_contents("/etc/passwd"); }
compilation error
Error: php function [php::file_get_contents()] is not authorized. at example.scs:2:12 2 | content: php::file_get_contents("/etc/passwd"); | ^^^

php:: and context:: are reserved: any other namespace is a file of the stylesheet (the language, section 11), so a php function cannot be reached by inventing one.

example.scs
.a {width: shell::exec("ls");}
compilation error
Error: unknown namespace [shell::]. No imported file declares it. at example.scs:1:12 1 | .a {width: shell::exec("ls");} | ^^^^^

Values in, values out

Arguments reach php as plain php (4, '16px', 'x', true, [1, 2]), and what php returns becomes an scs value — a string that looks like a length becomes one, an array becomes a list, an associative array becomes a map. A php object with no adapter is refused rather than emitted as garbage.

example.scs
@let $palette = php::palette(); .button { background: ${$palette.primary}; border-color: ${$palette.danger}; }
generated css
.button { background: #6366f1; border-color: #ef4444; }

The compilation context

The application injects values for a compilation; the stylesheet reads them with context::name, dotted for nested ones. The context is read only: scs can never write back into the application.

example.scs
@let $density = context::density; @let $padding = php::round(8 * $density); button { padding: ${$padding}px; font-family: context::design.font; }
generated css
button { padding: 16px; font-family: Inter; }

A value that was never injected is an error, never an empty declaration:

example.scs
.a {padding: context::unknown;}
compilation error
Error: undefined context value [context::unknown]. at example.scs:1:14 1 | .a {padding: context::unknown;} | ^^^^^^^

Context functions see the resolved selector

A context function receives an EvaluationContext: the compilation context, the current scope, the source position and the resolved selector — the one that will really be emitted, after nesting and component resolution. It is evaluated once per generated rule.

example.scs
.card { padding: php::cardPadding(); .title {padding: php::cardPadding();} } .panel {padding: php::cardPadding();}
generated css
.card { padding: 48px; } .card .title { padding: 16px; } .panel { padding: 16px; }

Sharing a calculation with the application

The same expression semantics and the same registry are available without generating css, so application code and stylesheet can share one calculation.

//the same calculation the stylesheet uses, without generating any css $value = SCSCompiler::Shared()->evaluate('php::cardPadding()', ['density' => 2], '.card'); $value->render(); // the css text $value->toPhp(); // plain php
Called on this pageAnswered
evaluate('php::cardPadding()', ['density' => 2], '.card')48px
evaluate('php::cardPadding()', ['density' => 2], '.panel')16px
evaluate('8px * 3')24px
evaluate('php::spacing(4)')16px

Reading a stylesheet back

$result = SCSCompiler::Shared()->compileText($source, ['density' => 2]); $result->css(); $result->phpValue('spacing'); // a compile-time value, not a custom property $result->themes(); // the design tokens the stylesheet declared

Compiling @let $spacing = php::spacing(context::density) with ['density' => 3] gives the css below, and $result->phpValue('spacing') returns '12px' — note that no --spacing custom property was emitted for it.

.a { padding: 12px; }

Keeping the boundary

See the reference for the full contract, and tests/scs/PhpBridgeTest.php for the tests that hold it in place.

Tags

<#simple css> <#scs> <#php> <#security>