Custom CSS Loader: Technical Details
How Custom CSS Loader works: flow with output buffer as a diagram, context detection via SCRIPT_NAME, file checks, classes, auto-update and file structure.
Custom CSS Loader: Technical Details
Flow
graph TD
A["Plugin file is included"] --> B["ob_start with callback"]
C["CustomCssLoaderPlugin.bootstrap()"] --> D{"Option enabled on?"}
D -->|"No"| E["No inclusion prepared"]
D -->|"Yes"| F["CssLoaderOrchestrator.prepare()"]
F --> G{"Context staff or client?"}
G -->|"No"| E
G -->|"Yes"| H["FilesystemCssDiscovery: find files"]
H --> I["HtmlCssRenderer: build link tags"]
I --> J["Page is rendered"]
B --> J
J --> K["Callback: CssLoaderOrchestrator.injectIntoBuffer()"]
K --> L["OutputBufferInjectionStrategy.inject(): tags before the first head end"]
The output buffer is started as soon as the plugin file is included, that is before bootstrap(). The buffer callback returns the page unchanged as long as no link tags were prepared, and catches errors (entry in the PHP error log, page is preserved).
Context detection
ProductionContextDetector decides from constants and $_SERVER['SCRIPT_NAME'], not from the URL path.
| Context | Condition |
|---|---|
| staff | Constant OSTSCPINC defined or SCRIPT_NAME contains /scp/ |
| client | Constant OSTCLIENTINC defined or SCRIPT_NAME contains neither /scp/ nor /api/ and ends in .php |
| none | all other cases, for example /api/ or a call without a script name |
The staff check takes precedence over the client check.
Finding and checking files
FilesystemCssDiscovery reads assets/custom/css/*.css with glob() (alphabetical) and checks every file:
realpath()must resolve, otherwise the file (for example a broken symlink) is skipped.- The real path must lie below the real CSS folder, otherwise the file is blocked and noted in the PHP error log.
- The file name must match
^[a-zA-Z0-9][a-zA-Z0-9_-]*\.css$, otherwise it is blocked and noted. - The patterns
/staff/iand/client/iare checked in this order, the first match wins.
HtmlCssRenderer checks the file name a second time, appends ?v=<modification time> (only when the time is greater than 0), escapes the URL with htmlspecialchars() and produces <link rel="stylesheet" href="...">. The base URL is ROOT_PATH plus assets/custom/css.
Injection
OutputBufferInjectionStrategy looks for the first </head> (case-insensitive) and inserts the block directly before it. If </head> is missing or there are no link tags, the page stays unchanged.
Classes
| Class | Responsibility |
|---|---|
CustomCssLoaderPlugin |
Plugin class: bootstrap(), enable(), version check, folder and demo files |
CustomCssLoaderConfig |
Configuration fields (config.php) |
CssLoaderOrchestrator |
prepare() collects the link tags, injectIntoBuffer() inserts them |
ProductionContextDetector |
Detect staff or client |
FilesystemCssDiscovery |
Find and check files |
HtmlCssRenderer |
Produce link tags |
OutputBufferInjectionStrategy |
Insert tags into the buffer |
There are also test helpers (TestContextDetector) and older methods in the plugin class marked as deprecated, which are kept for the unit tests.
Auto-update and version
On bootstrap() the plugin calls checkVersion(). If installed_version is missing or lower than the version in plugin.php, performUpdate() ensures the CSS folder exists and stores the new version. Demo files are not copied during that.
File structure
custom-css-loader/
βββ plugin.php
βββ class.CustomCssLoaderPlugin.php
βββ config.php
βββ composer.json
βββ src/
β βββ CssLoaderOrchestrator.php
β βββ Context/
β βββ Discovery/
β βββ Rendering/
β βββ Injection/
βββ assets/demo/
βββ vendor/ # only in the release ZIP or after composer install
βββ tests/ # PHPUnit