DynamicalWeb is a PHP framework intended to be used with ncc to create web applications. This framework is designed with simplicity in mind as a way to add-on to PHP's core functionality and to make it easier to create web applications with PHP and deploy them using ncc.
- DynamicalWeb
- Table of Contents
- Installation
- Usage
- Configuration
- DynamicalWeb Execution Flow
- Request Object
- Response Object
- Template Functions
- WebSession
- Routing
- Localization
- Static Resources
- Pre and Post Request Scripts
- WebSocket Support
- XSS Protection
- Configured Response Headers
- Debug Panel
- Built-in Pages
- APCu Caching
- Cookies
- Cookie Sessions
- CSRF Protection
- Request Cache
- Memcached Cache
- Deployment
- License
To use DynamicalWeb in your ncc project you can simply run the command:
ncc project --generate=dynamicalwebWhat this will do is generate files and modify your existing project files to include DynamicalWeb as a dependency and to set up the necessary files for DynamicalWeb to work properly, such as the main web entry point, sample phtml files and a updated project.yml to include the configuration for DynamicalWeb.
To ensure that DynamicalWeb and all other dependencies are met, run the command:
ncc project installDynamicalWeb works by having a configured web server (such as Apache or Nginx) point to a PHP script that executes
the main web entry point of your Application, for instance a index.php located under /var/www/html may look like this:
<?php
require 'ncc';
import('com.example.bootstrap');
(new \DynamicalWeb\DynamicalWeb('com.example.bootstrap'))->handleRequest();Note: It's important that the web server is configured to allow index.php to handle all requests, this is usually done by setting up URL rewriting rules in the web server configuration. Without this, DynamicalWeb will not be able to handle requests properly.
When using ncc project --generate=dynamicalweb these files are already generated for you, the generated Dockerfile
is also configured to set up the web server properly to allow DynamicalWeb to handle requests.
There are two ways to configure DynamicalWeb, both includes pointing DynamicalWeb to a Yaml configuration.
You can add the DynamicalWeb configuration directly to your project.yml file, this is done by adding a web_configuration
property under your build configuration's options property, for example this is how a project.yml file may
look like this:
source: src
default_build: release
web_entry_point: web_entry
assembly:
name: ExampleBootstrap
package: com.example.bootstrap
version: 1.0.0
dependencies:
net.nosial.dynamicalweb: nosial/dynamicalweb@n64
execution_units:
-
name: web_entry
type: php
mode: auto
entry: web_entry
build_configurations:
-
name: debug
output: target/debug/com.example.bootstrap.ncc
type: ncc
definitions:
NCC_DEBUG: true
-
name: release
output: target/release/com.example.bootstrap.ncc
type: ncc
-
name: web_release
output: 'target/web_release/${ASSEMBLY.PACKAGE}.ncc'
type: ncc
definitions:
NCC_DISABLE_LOGGING: '1'
options:
web_configuration:
application:
name: "Example Bootstrap Application"
root: "ExampleBootstrap/WebApplication"
resources: "ExampleBootstrap/WebResources"
default_locale: "en"
report_errors: true
xss_level: 0
debug_panel: true
locales:
en: "ExampleBootstrap/WebLocale/en.yml"
cn: "ExampleBootstrap/WebLocale/cn.yml"
jp: "ExampleBootstrap/WebLocale/jp.yml"
router:
base_path: "/"
response_handlers:
404: "errors/404.phtml"
500: "errors/500.phtml"
routes:
- id: "home"
path: "/"
module: "index.phtml"
locale_id: "home"
allowed_methods: [ "*" ]
- id: "about"
path: "/about"
module: "about.phtml"
locale_id: "about"
allowed_methods: [ GET ]
- id: "error"
path: "/error"
module: "error.phtml"
locale_id: "error"
- id: "contact"
path: "/contact"
module: "contact.phtml"
locale_id: "contact"
allowed_methods: [ GET, POST ]
- id: "examples"
path: "/examples"
module: "examples.phtml"
locale_id: "examples"
allowed_methods: [ GET ]
- id: "redirect_example"
path: "/redirect-example"
module: "redirect-example.phtml"
locale_id: "redirect_example"
allowed_methods: [ GET ]
- id: "stream_example"
path: "/stream-example"
module: "stream-example.phtml"
locale_id: "stream_example"
allowed_methods: [ GET ]
- id: "test_response_types"
path: "/test-response-types"
module: "test-response-types.phtml"
allowed_methods: [ GET ]
- id: "api_hello"
path: "/api/hello"
module: "api/hello.php"
allowed_methods: [ GET ]
- id: "api_user"
path: "/api/users/{id}"
module: "api/user.php"
allowed_methods: [ GET ]
- id: "api_user_foo"
path: "/api/users/{id}/{foo}"
module: "api/user.php"
allowed_methods: [ GET ]
- id: "api_download"
path: "/api/download"
module: "api/download.php"
allowed_methods: [ GET ]
- id: "api_stream"
path: "/api/stream"
module: "api/stream.php"
allowed_methods: [ GET ]
- id: "api_redirect"
path: "/api/redirect"
module: "api/redirect.php"
allowed_methods: [ GET ]
static: trueAlternatively, you can also point DynamicalWeb to a separate Yaml file that contains the configuration, the configuration
format remains the same as the one used in project.yml, the only difference is that the contents of the Yaml file should
be the contents of the web_configuration property in the previous example, for instance
application:
name: "Example Bootstrap Application"
root: "ExampleBootstrap/WebApplication"
resources: "ExampleBootstrap/WebResources"
default_locale: "en"
report_errors: true
xss_level: 0
debug_panel: true
locales:
en: "ExampleBootstrap/WebLocale/en.yml"
cn: "ExampleBootstrap/WebLocale/cn.yml"
jp: "ExampleBootstrap/WebLocale/jp.yml"
router:
base_path: "/"
response_handlers:
404: "errors/404.phtml"
500: "errors/500.phtml"
routes:
- id: "home"
path: "/"
module: "index.phtml"
locale_id: "home"
allowed_methods: [ "*" ]
- id: "about"
path: "/about"
module: "about.phtml"
locale_id: "about"
allowed_methods: [ GET ]
- id: "error"
path: "/error"
module: "error.phtml"
locale_id: "error"
- id: "contact"
path: "/contact"
module: "contact.phtml"
locale_id: "contact"
allowed_methods: [ GET, POST ]
- id: "examples"
path: "/examples"
module: "examples.phtml"
locale_id: "examples"
allowed_methods: [ GET ]
- id: "redirect_example"
path: "/redirect-example"
module: "redirect-example.phtml"
locale_id: "redirect_example"
allowed_methods: [ GET ]
- id: "stream_example"
path: "/stream-example"
module: "stream-example.phtml"
locale_id: "stream_example"
allowed_methods: [ GET ]
- id: "test_response_types"
path: "/test-response-types"
module: "test-response-types.phtml"
allowed_methods: [ GET ]
- id: "api_hello"
path: "/api/hello"
module: "api/hello.php"
allowed_methods: [ GET ]
- id: "api_user"
path: "/api/users/{id}"
module: "api/user.php"
allowed_methods: [ GET ]
- id: "api_user_foo"
path: "/api/users/{id}/{foo}"
module: "api/user.php"
allowed_methods: [ GET ]
- id: "api_download"
path: "/api/download"
module: "api/download.php"
allowed_methods: [ GET ]
- id: "api_stream"
path: "/api/stream"
module: "api/stream.php"
allowed_methods: [ GET ]
- id: "api_redirect"
path: "/api/redirect"
module: "api/redirect.php"
allowed_methods: [ GET ]And this file can be pointed to in the project.yml by using the web_configuration property to point to the file
(relative to the package's path) for instance the above examples would be something like ExampleBootstrap/configuration.yml
The Application section of the configuration is where you can set up the core information about your web application, such as the name and among other configurable properties.
| Name | Required | Example | Type | Description |
|---|---|---|---|---|
name |
Yes | "Web Application" | string |
The name of your web application |
root |
Yes | "ExampleBootstrap/WebApplication" | string |
The root directory where all .phtml files resides |
resources |
No | "ExampleBootstrap/WebResources" | string |
The root directory where all web resources resides (.css, .js, etc...) these files are accessible under the root path of your web application |
default_locale |
No | "en" | string |
The default locale ID of the web application |
report_errors |
No | True | boolean |
When True, any unhandled exceptions will result in DynamicalWeb displaying the exception details. Not recommended for production |
xss_level |
No | 1 | integer (0-3) |
XSS Level protection, when enabled DynamicalWeb will inject xss-protection related headers. 0=Disabled, 1=Low, 2=Medium 3=High |
debug_panel |
No | True | boolean |
When True, a debug iFrame is injected in the resulting HTML responses which contains detailed information about the web environment. Not recommended for production, adds a performance hit when enabled |
pre_request |
No | ['authentication.php', 'foo.php'] |
array |
An array of php scripts (Based from root) to execute in order before processing the http request, entries can be limited to routes, see Limiting Scripts to Routes |
post_request |
No | ['cleanup.php', 'bar.php'] |
array |
An array of php scripts (Based from root) to execute in order after processing the http request, entries can be limited to routes, see Limiting Scripts to Routes |
disable_apcu |
No | True | boolean |
When True, the use of the APCu cache layer is disabled, otherwise DynamicalWeb will use APCu if it's available to cache properties and small resource files when running the WebApplication |
disable_default_headers |
No | True | boolean |
When True, DynamicalWeb omits builtin headers like X-Powered-By and X-Request-ID from being used in the http response |
static_cache_max_age |
No | 3600 | integer |
The max-age value in seconds used in the Cache-Control header when serving static files. Set to 0 to disable cache headers. Defaults to 3600 (1 hour) |
apcu_content_max_size |
No | 262144 | integer |
The maximum file size in bytes for which static file content will be cached in APCu. Files larger than this are streamed from disk. Defaults to 262144 (256 KB) |
apcu_content_ttl |
No | 3600 | integer |
The TTL in seconds for static file content cached in APCu. Defaults to 3600 (1 hour) |
apcu_meta_ttl |
No | 10 | integer |
The TTL in seconds for file metadata (modification time and size) cached in APCu. Defaults to 10 seconds |
apcu_config_ttl |
No | 60 | integer |
The TTL in seconds for the parsed web configuration cached in APCu. Defaults to 60 (1 minute) |
csrf_protection |
No | True | boolean |
When True, POST, PUT and DELETE requests must carry the session's CSRF token, see CSRF Protection. Defaults to false |
headers |
No | { X-Frame-Options: "DENY" } |
object |
Response headers added to every response, such as security headers, see Configured Response Headers. Defaults to none |
This section allows you to configure locales for your web application, the key of each locale is the locale ID and the value is the path to the Yaml file that contains the translations for that locale, for example:
home:
title_banner: "Welcome to the Home Page"
description: "This is the home page of {app_name}"
footer:
copyright: "Copyright © 2024-2026 {company_name}. All rights reserved."A locale configuration may look like this:
locales:
en: "ExampleBootstrap/WebLocale/en.yml"
cn: "ExampleBootstrap/WebLocale/cn.yml"
jp: "ExampleBootstrap/WebLocale/jp.yml"Locales can be configured and changed simply by visiting the path /dynaweb/language/<locale_id> for example, visiting
/dynaweb/language/cn will change the current locale to cn if it's configured properly, otherwise it will return a
404 response. The locale will be stored in a cookie and will persist across requests until it's changed again or the
cookie is cleared.
Sections in DynamicalWeb are reusable .phtml fragments that can be included in any template (e.g., navbars,
headers, footers, sidebars). Sections are not defined in the YAML configuration — they are included directly
in templates by calling Functions::insertSection() with a file path.
A section template file may look like this:
<!-- sections/navbar.phtml -->
<?php \DynamicalWeb\Html\Functions::loadLocalization('navbar'); ?>
<nav>
<a href="/"><?php \DynamicalWeb\Html\Functions::printl('brand'); ?></a>
<a href="/about"><?php \DynamicalWeb\Html\Functions::printl('about_link'); ?></a>
</nav>The router section allows you to define the routing configuration for your web application, this includes the base path, response handlers and the actual routes, for example:
router:
base_path: "/"
response_handlers:
404: "errors/404.phtml"
500: "errors/500.phtml"
routes:
- id: "home"
path: "/"
module: "index.phtml"
locale_id: "home"
allowed_methods: [ "*" ]
- id: "about_user"
path: "/{id}/about"
module: "about.phtml"
locale_id: "about"
allowed_methods: [ GET ]A Router configuration consists of the following properties:
| Name | Required | Example | Type | Description |
|---|---|---|---|---|
base_path |
No | "/" | string |
The base path of the web application, this is used when generating URLs in the application, if not set, it will be automatically detected from the incoming http request, but it's recommended to set it explicitly |
response_handlers |
No | { 404: "errors/404.phtml", 500: "errors/500.phtml" } |
object |
An object where the key is the http status code and the value is the path to the module to handle that response, the module should be a valid .phtml file based from the root directory |
routes |
Yes | See example above | array |
An array of route objects, each route object should have the following properties: id, path, module, allowed_methods and optionally locale_id |
A route object should have the following properties:
| Name | Required | Example | Type | Description |
|---|---|---|---|---|
id |
Yes | "home" | string |
The unique ID of the route, this is used to identify the route and can be used when generating URLs in the application |
path |
Yes | "/users/{id}" | string |
The path of the route, this can include path parameters enclosed in curly braces, for example /users/{id} will match any path that starts with /users/ followed by a value that will be captured as the id parameter |
module |
Yes | "users.phtml" | string |
The path to the module that will handle the route, this should be a valid .phtml or .php file based from the root directory |
allowed_methods |
Yes | [ "GET", "POST" ] | array |
An array of allowed http methods for the route, if the incoming request method is not in this array, a 405 Method Not Allowed response will be returned, the special value * can be used to allow all methods |
locale_id |
No | "home" | string |
The locale ID to use when rendering the module for this route, this should be a valid locale ID that is configured in the locales section, if not set, the default locale will be used |
csrf_exempt |
No | true | bool |
When the application's csrf_protection is enabled, lets this route accept state-changing requests without a CSRF token, for example a webhook or an API authenticated by other means. Defaults to false |
DynamicalWeb follows a specific execution flow when handling an incoming request, this flow can be summarized:
- The main web entry point of the application is executed by the web server, this is usually a
index.phpfile that includes the code to import the web application and use DynamicalWeb to handle the incoming request. If theWSS_ENABLEDenvironment variable is set to1(indicating a WebSocket connection handled by the WebSocket Server), DynamicalWeb detects this and follows the WebSocket execution flow instead, for example:
<?php
require 'ncc';
import('com.example.bootstrap');
(new \DynamicalWeb\DynamicalWeb('com.example.bootstrap'))->handleRequest();
?>-
DynamicalWeb will load your package and parse the configuration file, preparing everything for handling the incoming request including initializing the
WebSession, creating theRequestandResponseobjects, detecting the locale and matching the incoming request to one of the configured routes -
If
pre_requestscripts are configured, they are executed in order before the matched module runs, this allows you to do things like authentication checks, rate limiting, or any other pre-processing logic -
Once a route has been found, the
.phtmlor.phpmodule configured for that route will be executed, during this time DynamicalWeb'sWebSessionclass becomes available to the module allowing the module to access information about the incoming request and to set information for the response, see Request Object and Response Object sections for more details about these objects and how to access them from the module -
If
post_requestscripts are configured, they are executed in order after the matched module finishes, this allows you to do things like cleanup, logging, or any other post-processing logic -
DynamicalWeb sends the response to the client, including all headers, cookies and the response body based on the configured response type (HTML, JSON, File Download, Redirect, Stream, etc.)
-
If the debug panel is enabled, DynamicalWeb will inject a debug iFrame into the resulting HTML response before sending it to the client
-
The
WebSessionis ended and all static state is cleared
The request object can be accessed from anywhere within the module using the \DynamicalWeb\WebSession::getRequest()
method which will return a \DynamicalWeb\Objects\Request object, this object contains all the information about the
incoming http request including parsed information if available
| Method | Return Type | Description |
|---|---|---|
getId() |
string |
Returns the unique ID of the request, this is a randomly generated string that can be used to identify the request in logs and other places |
getMethod() |
RequestMethod (Enum) |
Returns the enum value of the http method of the request, this can be used to determine the method of the incoming request |
getUrl() |
string |
Returns the full URL of the incoming request, this includes the path and query string but not the base URL, for example /users/123?foo=bar |
getPath() |
string |
Returns only the path component of the request URL, for example /users/123 |
getHost() |
string |
Returns the host of the incoming request, for example example.com |
getHttpVersion() |
string |
Returns the HTTP version of the incoming request, for example 1.1 or 2 |
isSecure() |
bool |
Returns True if the request was made over HTTPS |
getHeaders() |
array |
Returns all HTTP headers of the incoming request as an associative array |
getHeader(string $name, ?string $default=null) |
?string |
Returns a specific header value by name (case-insensitive), or the default value if not found |
getQueryParameters() |
array |
Returns the GET query parameters of the incoming request |
getBodyParameters() |
array |
Returns the parsed body parameters, supports application/json and application/x-yaml content types |
getFormParameters() |
array |
Returns the form parameters from a standard POST form submission |
getPathParameters() |
array |
Returns the extracted path parameters from the matched route, for example if the route is /users/{id} and the path is /users/123, this returns ['id' => '123'] |
getPathParameter(string $name) |
?string |
Returns a specific path parameter value by name, or null if not found |
getParameters() |
array |
Returns a merged array of all parameters with priority: form > body > query |
getParameter(string $name) |
?string |
Returns a specific parameter value by name from the merged parameters |
getCookies() |
array |
Returns all cookies from the incoming request |
getCookie(string $name, $default=null) |
mixed |
Returns a specific cookie value by name, or the default value if not found |
getClientIp() |
?string |
Returns the client's IP address, checks Cloudflare headers, X-Forwarded-For and other proxy headers before falling back to REMOTE_ADDR |
getRawBody() |
?string |
Returns the raw request body from php://input |
getDetectedLanguage() |
?string |
Returns the detected ISO 639-1 language code from the Accept-Language header |
getUserAgent() |
?UserAgent |
Returns the parsed User-Agent object, see User Agent Detection for more details |
getUserAgentString() |
?string |
Returns the raw User-Agent header string |
Typed getters read from the merged parameters and return the default when the value is missing or of the wrong type,
so query strings like ?page=abc or ?page[]=1 never reach your code as unexpected values:
| Method | Return Type | Description |
|---|---|---|
getIntParameter(string $name, ?int $default=null, ?int $min=null, ?int $max=null) |
?int |
Returns a whole number, clamped to $min/$max when given |
getBoolParameter(string $name, ?bool $default=null) |
?bool |
Returns a boolean for 1/0, true/false, yes/no and on/off |
getStringParameter(string $name, ?string $default=null, bool $trim=false) |
?string |
Returns a string, ignoring array values, optionally trimmed |
<?php
$page = WebSession::getRequest()->getIntParameter('page', 1, min: 1);
$includeClosed = WebSession::getRequest()->getBoolParameter('closed', false);
?>Here's an example of how to use the request object within a module:
<?php
use DynamicalWeb\WebSession;
use DynamicalWeb\Enums\RequestMethod;
$request = WebSession::getRequest();
if($request->getMethod() === RequestMethod::POST)
{
$name = $request->getParameter('name');
$email = $request->getParameter('email');
// Process the form submission
}
$userId = $request->getPathParameter('id');
$page = $request->getParameter('page') ?? '1';
?>DynamicalWeb parses file uploads into UploadedFile objects which provide a clean interface for working with uploaded
files, the following methods are available on the request object for file uploads:
| Method | Return Type | Description |
|---|---|---|
hasFiles() |
bool |
Returns True if any files were uploaded with the request |
hasFile(string $key) |
bool |
Returns True if a specific file field exists in the upload |
getFile(string $key) |
UploadedFile|array|null |
Returns the uploaded file(s) for a specific field, can return an array for multiple file uploads |
getFiles() |
array |
Returns the raw $_FILES array |
getFileCount() |
int |
Returns the total number of uploaded files |
getValidFiles() |
array |
Returns only the files that were uploaded without errors |
getTotalFileSize() |
int |
Returns the total size of all uploaded files in bytes |
Each UploadedFile object provides the following methods:
| Method | Return Type | Description |
|---|---|---|
getClientFilename() |
string |
Returns the original filename as provided by the client |
getClientExtension() |
string |
Returns the file extension from the original filename |
getTempPath() |
string |
Returns the temporary file path where the uploaded file is stored |
getSize() |
int |
Returns the file size in bytes |
getError() |
int |
Returns the PHP upload error code |
getErrorMessage() |
?string |
Returns a human-readable error message for the upload error code |
isValid() |
bool |
Returns True if the file was uploaded successfully (UPLOAD_ERR_OK) |
getClientMimeType() |
?string |
Returns the MIME type as reported by the client (unreliable, should not be trusted) |
getMimeType() |
?string |
Returns the MIME type detected from the file contents (more reliable than client-provided) |
isMimeType(string|array $type) |
bool |
Checks if the file matches the given MIME type(s), supports wildcards like image/* |
isImage() |
bool |
Returns True if the detected MIME type is image/* |
isSizeWithinLimit(int $maxSize) |
bool |
Returns True if the file size is within the given limit in bytes |
hasAllowedExtension(array $extensions, bool $caseSensitive=false) |
bool |
Returns True if the file extension is in the allowed list |
moveTo(string $destination, bool $overwrite=false) |
bool |
Moves the uploaded file to a permanent location, returns True on success |
isMoved() |
bool |
Returns True if the file has already been moved |
getContents(int $maxSize=10485760) |
?string |
Returns the file contents as a string (max 10MB by default) |
getStream(string $mode='rb') |
resource|false |
Opens and returns a file stream resource |
getHash(string $algo='sha256') |
?string |
Returns the hash of the file contents using the specified algorithm |
Here's an example of handling file uploads in a module:
<?php
use DynamicalWeb\WebSession;
$request = WebSession::getRequest();
if($request->hasFile('avatar'))
{
$file = $request->getFile('avatar');
if($file->isValid() && $file->isSizeWithinLimit(5 * 1024 * 1024))
{
if($file->hasAllowedExtension(['jpg', 'jpeg', 'png', 'webp']))
{
$file->moveTo('/var/uploads/avatars/' . $file->getHash() . '.' . $file->getClientExtension());
}
}
}
?>DynamicalWeb includes a built-in User-Agent parser that can detect browsers, operating systems, device types, device
brands, rendering engines and bots from the incoming request's User-Agent header. The parsed result is accessible via
the getUserAgent() method on the request object which returns a UserAgent object.
| Method | Return Type | Description |
|---|---|---|
getRawUserAgent() |
string |
Returns the raw User-Agent string |
getBrowserName() |
?Browser (Enum) |
Returns the detected browser (Chrome, Firefox, Safari, Edge, Opera, Brave, etc.) |
getBrowserVersion() |
?string |
Returns the detected browser version |
getFullBrowserName() |
?string |
Returns the full browser name with version, for example Chrome 120.0 |
getOsName() |
?OperatingSystem (Enum) |
Returns the detected OS (Windows, macOS, Linux, Android, iOS, etc.) |
getOsVersion() |
?string |
Returns the detected OS version |
getFullOsName() |
?string |
Returns the full OS name with version, for example Windows 11 |
getDeviceType() |
DeviceType (Enum) |
Returns the device type: MOBILE, TABLET, DESKTOP, BOT, or UNKNOWN |
getDeviceBrand() |
?DeviceBrand (Enum) |
Returns the device brand (Apple, Samsung, Google, Xiaomi, Huawei, etc.) |
getDeviceModel() |
?string |
Returns the device model if detectable, for example iPhone or Galaxy S21 |
getFullDeviceName() |
?string |
Returns the full device name with brand and model |
isMobile() |
bool |
Returns True if the device is a mobile phone |
isTablet() |
bool |
Returns True if the device is a tablet |
isDesktop() |
bool |
Returns True if the device is a desktop computer |
isBot() |
bool |
Returns True if the User-Agent is a known bot or crawler |
getBotName() |
?Bot (Enum) |
Returns the detected bot type (Googlebot, Bingbot, Facebook, WhatsApp, Telegram, etc.) |
getEngine() |
?RenderingEngine (Enum) |
Returns the rendering engine (Blink, Gecko, WebKit, Trident, Presto) |
getEngineVersion() |
?string |
Returns the rendering engine version |
getFullEngineName() |
?string |
Returns the full engine name with version |
getPlatform() |
?string |
Returns the platform string |
getPlatformVersion() |
?string |
Returns the platform version |
toArray() |
array |
Returns all parsed data as a nested associative array |
Here's an example of using the User-Agent detection:
<?php
use DynamicalWeb\WebSession;
use DynamicalWeb\Enums\UserAgent\DeviceType;
$request = WebSession::getRequest();
$ua = $request->getUserAgent();
if($ua !== null)
{
if($ua->isBot())
{
// Handle bot request differently
}
if($ua->isMobile())
{
// Serve mobile-optimized content
}
echo $ua->getFullBrowserName(); // "Chrome 120.0"
echo $ua->getFullOsName(); // "Windows 11"
}
?>The response object can be accessed from anywhere within the module using the \DynamicalWeb\WebSession::getResponse()
method which will return a \DynamicalWeb\Objects\Response object, this object allows you to configure the response that
will be sent back to the client. All setter methods on the response object return self for method chaining.
| Method | Return Type | Description |
|---|---|---|
getStatusCode() |
ResponseCode |
Returns the HTTP status code of the response |
setStatusCode(ResponseCode|int $statusCode) |
self |
Sets the HTTP status code of the response |
getHttpVersion() |
string |
Returns the HTTP version of the response (default: 1.1) |
setHttpVersion(string $httpVersion) |
self |
Sets the HTTP version of the response |
getHeaders() |
array |
Returns all response headers |
setHeaders(array $headers) |
self |
Sets all response headers at once |
setHeader(string $name, string $value, bool $replace=true) |
self |
Sets a specific header, if $replace is False the value is appended as an array header |
removeHeader(string $name) |
self |
Removes a specific header from the response |
getBody() |
string |
Returns the response body content |
setBody(string $body) |
self |
Sets the response body content |
getContentType() |
string |
Returns the Content-Type of the response |
setContentType(string|MimeType $contentType) |
self |
Sets the Content-Type, accepts a string or MimeType enum value |
getCharset() |
string |
Returns the character set of the response (default: UTF-8) |
setCharset(string $charset) |
self |
Sets the character set of the response |
getCookies() |
array |
Returns all cookies set for the response |
getCookie(string $name) |
?Cookie |
Returns a specific cookie by name |
setCookie(string $name, string $value, int $expires=0, string $path='/', ...) |
self |
Sets a cookie with the given parameters |
addCookie(Cookie $cookie) |
self |
Adds a Cookie object to the response |
removeCookie(string $name) |
self |
Removes a cookie from the response |
getResponseType() |
ResponseType |
Returns the response type (BASIC, JSON, YAML, FILE_DOWNLOAD, REDIRECT, STREAM) |
setResponseType(ResponseType $responseType) |
self |
Sets the response type |
setJson(mixed $data, int $flags=0, int $depth=512) |
self |
Sets the response as a JSON response, automatically encodes the data |
setYaml(mixed $data, int $inline=2, int $indent=4, int $flags=0) |
self |
Sets the response as a YAML response, automatically encodes the data |
setFileDownload(string $filePath, ?string $filename=null) |
self |
Sets the response as a file download, optionally with a custom filename |
setRedirect(string $url, ResponseCode $statusCode=null) |
self |
Sets the response as a redirect, defaults to 302 Found |
setStream(callable $callback) |
self |
Sets the response as a streaming response with a callback function |
DynamicalWeb supports six response types, each serving a different purpose. The response type determines how DynamicalWeb processes and sends the response to the client.
The default response type is BASIC, this is a standard HTML/text response where the body content is sent directly
to the client. When using .phtml files, the output of the file is captured using output buffering and set as the
body content.
<?php
use DynamicalWeb\WebSession;
use DynamicalWeb\Enums\ResponseCode;
WebSession::getResponse()
->setStatusCode(ResponseCode::OK)
->setContentType('text/plain')
->setBody('Hello, World!');
?>For .phtml files, you don't need to explicitly set the body since the output buffer captures everything:
<html>
<body>
<h1>Hello, World!</h1>
<p>The current request method is: <?php \DynamicalWeb\Html\Functions::print(WebSession::getRequest()->getMethod()); ?></p>
</body>
</html>JSON responses are useful for building API endpoints, the setJson() method will automatically set the content type
to application/json and encode the given data as JSON:
<?php
use DynamicalWeb\WebSession;
use DynamicalWeb\Enums\ResponseCode;
$data = [
'message' => 'Hello, World!',
'timestamp' => time(),
'user' => [
'id' => WebSession::getRequest()->getPathParameter('id'),
'name' => 'John Doe',
]
];
WebSession::getResponse()
->setStatusCode(ResponseCode::OK)
->setJson($data);
?>Similar to JSON responses, the setYaml() method will automatically set the content type to application/x-yaml
and encode the given data as YAML using Symfony's Yaml component:
<?php
use DynamicalWeb\WebSession;
WebSession::getResponse()->setYaml([
'status' => 'ok',
'services' => ['web', 'database', 'cache']
]);
?>File download responses allow you to serve files to the client with the appropriate Content-Disposition header
set to trigger a download in the browser:
<?php
use DynamicalWeb\WebSession;
WebSession::getResponse()->setFileDownload('/path/to/report.pdf', 'monthly-report.pdf');
?>The first argument is the path to the file on the server, and the second optional argument is the filename that the client will see when downloading the file, if not provided the original filename will be used.
Redirect responses allow you to redirect the client to a different URL, the setRedirect() method accepts the
target URL and an optional status code (defaults to 302 Found):
<?php
use DynamicalWeb\WebSession;
use DynamicalWeb\Enums\ResponseCode;
// Temporary redirect (302)
WebSession::getResponse()->setRedirect('/dashboard');
// Permanent redirect (301)
WebSession::getResponse()->setRedirect('/new-location', ResponseCode::MOVED_PERMANENTLY);
// See Other (303) - useful after POST requests
WebSession::getResponse()->setRedirect('/success', ResponseCode::SEE_OTHER);
?>Streaming responses allow you to send data to the client in real-time as it becomes available, without buffering
the entire response in memory. This is useful for long-running processes, real-time updates, or large data exports.
The setStream() method accepts a callable that will be invoked to produce the stream output:
<?php
use DynamicalWeb\WebSession;
WebSession::getResponse()->setStream(function() {
for($i = 0; $i < 100; $i++)
{
echo "Update #$i at " . date('H:i:s') . "\n";
flush();
sleep(1);
}
});
?>On the client side, you can consume a streaming response using the Fetch API:
const response = await fetch('/api/stream');
const reader = response.body.getReader();
const decoder = new TextDecoder();
while(true) {
const { done, value } = await reader.read();
if(done) break;
console.log(decoder.decode(value));
}DynamicalWeb provides a set of helper functions through the \DynamicalWeb\Html\Functions class that are designed
to be used within .phtml template files to make rendering content easier and safer.
The Functions::print() method prints text to the output with HTML escaping enabled by default, this is important
for preventing XSS attacks when displaying user-provided content:
<?php use DynamicalWeb\Html\Functions; ?>
<!-- Escaped output (safe) -->
<p><?php Functions::print($userInput); ?></p>
<!-- Unescaped output (only use for trusted HTML) -->
<div><?php Functions::print($trustedHtml, false); ?></div>This method also supports objects that implement the StringInterface, allowing you to print enum values and other
objects that define a toString() method directly.
The Functions::printl() method prints a localized string from the current locale, it resolves the active locale
scope from the current route's locale_id, or from the localization context set by insertSection() or
loadLocalization() when called from within a section:
<?php use DynamicalWeb\Html\Functions; ?>
<!-- Simple locale string -->
<h1><?php Functions::printl('page_title'); ?></h1>
<!-- Locale string with placeholder replacements -->
<p><?php Functions::printl('welcome_message', ['name' => $userName]); ?></p>Given a locale file like this:
home:
page_title: "Welcome Home"
welcome_message: "Hello {name}, welcome back!"The printl() method will look up the string using the active locale section (e.g., home) and the given key
(e.g., page_title), and replace any {placeholder} tokens with the values from the provided array.
To use a localized string in PHP code rather than print it, for example for a page title, a flash message or a JSON
response, use Functions::getl(). It resolves the key exactly like printl() and returns the unescaped string:
<?php
use DynamicalWeb\Html\Functions;
$title = Functions::getl('page_title');
$message = Functions::getl('welcome_message', ['name' => $userName]);
// Like printl(), a missing key throws; pass a default to get that instead
$label = Functions::getl($statusKey, default: $statusKey);
if (Functions::hasl($statusKey))
{
// The key is defined for the active section (or the global section)
}
?>Functions::getActiveLocaleId() returns the locale section printl()/getl() currently resolve from.
The Functions::printRoute() method generates and prints a fully-qualified URL for a named route, this is useful
for creating links between pages without hardcoding paths:
<?php use DynamicalWeb\Html\Functions; ?>
<!-- Simple route URL -->
<a href="<?php Functions::printRoute('home'); ?>">Home</a>
<!-- Route URL with path variables -->
<a href="<?php Functions::printRoute('api_user', ['id' => '123']); ?>">User Profile</a>
<!-- Route URL with path variables and query parameters -->
<a href="<?php Functions::printRoute('api_user', ['id' => '123'], ['tab' => 'settings']); ?>">User Settings</a>This method resolves the route by its ID, builds the URL from the automatically detected base URL (scheme and host
from the current HTTP request) and base_path, substitutes
any {variable} placeholders in the route path with the provided values, and appends optional GET query parameters.
Functions::getRouteUrl() returns the same root-relative URL instead of printing it. When a full address is needed,
such as a canonical link, an Open Graph tag or a link in an e-mail, Functions::getAbsoluteRouteUrl() takes the same
arguments and prefixes the scheme and host of the current request:
<link rel="canonical" href="<?php Functions::print(Functions::getAbsoluteRouteUrl('report', ['id' => $id])); ?>">
<!-- https://example.com/reports/123 -->The Functions::insertSection() method renders a .phtml section file and outputs its content inline. It accepts
a file path (absolute or relative to the calling file) and an optional $localizationId parameter:
\DynamicalWeb\Html\Functions::insertSection(string $path, ?string $localizationId = null): voidWhen a $localizationId is provided, DynamicalWeb switches the active locale context to that ID for the duration
of the section's rendering, so printl() calls inside the section resolve strings from the section's own locale
scope. The previous context is restored automatically when the section finishes.
Sections can also set their own locale scope from within the template file by calling loadLocalization():
\DynamicalWeb\Html\Functions::loadLocalization(string $name): voidExample usage in a parent template:
<?php use DynamicalWeb\Html\Functions; ?>
<html>
<body>
<?php Functions::insertSection('sections/navbar.phtml', 'navbar'); ?>
<main>
<h1><?php Functions::printl('page_title'); ?></h1>
<p>Page content here...</p>
</main>
<?php Functions::insertSection('sections/footer.phtml'); ?>
</body>
</html>Example section template (sections/navbar.phtml):
<?php \DynamicalWeb\Html\Functions::loadLocalization('navbar'); ?>
<nav>
<a href="/"><?php \DynamicalWeb\Html\Functions::printl('brand'); ?></a>
<a href="/about"><?php \DynamicalWeb\Html\Functions::printl('about_link'); ?></a>
</nav>Relative paths are resolved against the directory of the file that calls insertSection(). Absolute paths
(starting with /) are used as-is.
The WebSession class is a static class that acts as the central access point during a request's lifecycle,
it holds references to the current DynamicalWeb instance, the request, the response, the current route, the
loaded locale and any custom variables you want to store.
| Method | Return Type | Description |
|---|---|---|
getInstance() |
?DynamicalWeb |
Returns the current DynamicalWeb instance |
getRequest() |
?Request |
Returns the current request object |
getResponse() |
?Response |
Returns the current response object |
getModule() |
?string |
Returns the path to the currently executing module |
getCurrentRoute() |
?Route |
Returns the matched route object for the current request |
getLocale() |
?Locale |
Returns the loaded locale object for the current request |
getException() |
?Throwable |
Returns the current exception if one has been set |
setException(?Throwable $exception) |
void |
Sets an exception on the session |
The WebSession class also provides a simple key-value store for storing custom variables during the request's lifecycle,
this can be useful for passing data between pre-request scripts, modules, sections and post-request scripts:
| Method | Return Type | Description |
|---|---|---|
set(string $key, mixed $value) |
void |
Stores a custom variable in the session |
get(string $key) |
mixed |
Retrieves a custom variable from the session |
exists(string $key) |
bool |
Checks if a custom variable exists in the session |
unset(string $key) |
void |
Removes a custom variable from the session |
Note: These variables are only stored in memory during the request and are not persisted across requests, they are ideal for sharing data between different parts of the request handling process without using global variables or other less clean methods.
Here's an example of using session variables to pass data from a pre-request script to a module:
// pre_request/auth.php
<?php
use DynamicalWeb\WebSession;
$token = WebSession::getRequest()->getHeader('Authorization');
if($token !== null)
{
$user = validateToken($token);
WebSession::set('authenticated_user', $user);
}
?>// dashboard.phtml
<?php
use DynamicalWeb\WebSession;
$user = WebSession::get('authenticated_user');
if($user === null)
{
WebSession::getResponse()->setRedirect('/login');
return;
}
?>
<h1>Welcome, <?php \DynamicalWeb\Html\Functions::print($user->getName()); ?></h1>These methods prepare the response, send it and end the request immediately, so no code after them runs:
| Method | Description |
|---|---|
redirectTo(string $url, ?ResponseCode $statusCode = null) |
Redirects to a URL (302 by default) |
redirectToRoute(string $id, array $pathVariables = [], array $queryParams = [], ?ResponseCode $statusCode = null) |
Redirects to a named route |
respondJson(mixed $data, ResponseCode|int $statusCode = ResponseCode::OK) |
Sends a JSON response |
abort(ResponseCode|int $statusCode, ?string $message = null) |
Sends an error response: the router's response_handlers page for the code when configured, otherwise (or with a message) plain text |
<?php
use DynamicalWeb\Enums\ResponseCode;
use DynamicalWeb\WebSession;
if (!$canEdit)
{
WebSession::abort(ResponseCode::FORBIDDEN);
}
if ($request->getParameter('format') === 'json')
{
WebSession::respondJson(['report' => $report->toArray()]);
}
$client->closeReport($reportId);
WebSession::flash('success', 'report_closed');
WebSession::redirectToRoute('report_detail', ['id' => $reportId]);
?>DynamicalWeb::renderResponseHandler(ResponseCode $code) renders a configured response handler into the current
response without ending the request, and returns false when none is configured.
Flash values are kept in the cookie session until they are read once, which makes them suitable for showing a message on the page a redirect leads to without putting it in the URL. They require Cookie Sessions:
| Method | Return Type | Description |
|---|---|---|
flash(string $key, mixed $value, ?string $cookieName = null) |
bool |
Stores a value, creating the cookie session if needed; false when sessions are disabled |
getFlash(string $key, mixed $default = null, ?string $cookieName = null) |
mixed |
Returns the value and removes it, or the default when it is not set |
hasFlash(string $key, ?string $cookieName = null) |
bool |
Checks if a value is waiting, without removing it |
<?php if (WebSession::hasFlash('success')): ?>
<div class="alert"><?php Functions::printl(WebSession::getFlash('success')); ?></div>
<?php endif; ?>DynamicalWeb uses a configuration-based routing system where routes are defined in the Yaml configuration file, the router matches incoming requests to configured routes based on the request path and method.
Routes can include dynamic path parameters enclosed in curly braces, these parameters are extracted from the actual request path and made available through the request object:
routes:
- id: "user_profile"
path: "/users/{id}"
module: "user-profile.phtml"
allowed_methods: [ GET ]
- id: "user_post"
path: "/users/{userId}/posts/{postId}"
module: "user-post.phtml"
allowed_methods: [ GET ]Within the module you can access these parameters using the request object:
<?php
use DynamicalWeb\WebSession;
$request = WebSession::getRequest();
// For /users/123
$userId = $request->getPathParameter('id'); // "123"
// For /users/456/posts/789
$userId = $request->getPathParameter('userId'); // "456"
$postId = $request->getPathParameter('postId'); // "789"
// Or get all path parameters at once
$params = $request->getPathParameters(); // ['userId' => '456', 'postId' => '789']
?>DynamicalWeb supports parameter constraints that restrict what values a path parameter can match, constraints are specified after the parameter name separated by a colon:
routes:
- id: "user_by_id"
path: "/users/{id:int}"
module: "user.php"
allowed_methods: [ GET ]
- id: "article_by_slug"
path: "/articles/{slug:slug}"
module: "article.phtml"
allowed_methods: [ GET ]
- id: "resource_by_uuid"
path: "/resources/{uuid:uuid}"
module: "resource.phtml"
allowed_methods: [ GET ]The following built-in constraint shortcuts are available:
| Constraint | Pattern | Description |
|---|---|---|
int, integer, num, number |
\d+ |
Matches one or more digits |
alpha |
[a-zA-Z]+ |
Matches one or more alphabetic characters |
alnum, alphanumeric |
[a-zA-Z0-9]+ |
Matches one or more alphanumeric characters |
uuid |
[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12} |
Matches a standard UUID format |
slug |
[a-z0-9]+(?:-[a-z0-9]+)* |
Matches a URL-friendly slug |
You can also use a custom regex pattern as the constraint:
routes:
- id: "year_archive"
path: "/archive/{year:[0-9]{4}}"
module: "archive.phtml"
allowed_methods: [ GET ]If no constraint is specified, the parameter will match any value that doesn't contain a forward slash ([^/]+).
Each route must specify which HTTP methods it accepts, if the incoming request method is not in the route's
allowed_methods array, DynamicalWeb will return a 405 Method Not Allowed response. The special value * can
be used to allow all HTTP methods:
routes:
# Allow all methods
- id: "home"
path: "/"
module: "index.phtml"
allowed_methods: [ "*" ]
# Allow only GET
- id: "about"
path: "/about"
module: "about.phtml"
allowed_methods: [ GET ]
# Allow GET and POST
- id: "contact"
path: "/contact"
module: "contact.phtml"
allowed_methods: [ GET, POST ]The supported HTTP methods are: GET, HEAD, POST, PUT, DELETE, CONNECT, OPTIONS, TRACE, WEBSOCKET
The WEBSOCKET method is a non-standard method used internally to designate routes that handle WebSocket connections.
These routes are matched when the request originates from the WebSocket Server (see WebSocket Support).
Response handlers allow you to define custom error pages for specific HTTP status codes, these are .phtml files
that will be rendered when DynamicalWeb encounters the corresponding error:
router:
response_handlers:
404: "errors/404.phtml"
500: "errors/500.phtml"When a 404 Not Found error occurs (no route matches the request), DynamicalWeb will render the configured 404
handler module. When an unhandled exception occurs during module execution, DynamicalWeb will render the configured
500 handler module. Within these error handler modules, the WebSession is still available so you can access
the request information and the exception details:
<!-- errors/404.phtml -->
<?php use DynamicalWeb\WebSession; ?>
<h1>Page Not Found</h1>
<p>The requested path <code><?php echo htmlspecialchars(WebSession::getRequest()->getPath()); ?></code> was not found.</p>
<a href="/">Go to Homepage</a><!-- errors/500.phtml -->
<?php use DynamicalWeb\WebSession; ?>
<h1>Internal Server Error</h1>
<p>Something went wrong while processing your request.</p>
<a href="/">Go to Homepage</a>DynamicalWeb has built-in support for multi-language web applications through its localization system, this system
allows you to define translations in Yaml files and reference them in your templates using the Functions::printl()
method.
Locale files are Yaml files that contain key-value pairs organized by section, the section names correspond to
the locale_id values configured on routes, or the $localizationId parameter passed to insertSection() /
loadLocalization():
# en.yml
home:
page_title: "Welcome Home"
jumbotron_text: "Build web applications with ease"
learn_more_button: "Learn More"
about:
page_title: "About Us"
heading: "About Our Application"
feature_routing_title: "Routing"
feature_routing_desc: "Define routes with parameters and constraints"
navbar:
brand: "My Application"
home_link: "Home"
about_link: "About"
contact_link: "Contact"
footer:
copyright: "Copyright © 2024-2026 {company}. All rights reserved."# jp.yml
home:
page_title: "ホームへようこそ"
jumbotron_text: "簡単にウェブアプリケーションを構築"
learn_more_button: "もっと詳しく"
about:
page_title: "私たちについて"
heading: "アプリケーションについて"
feature_routing_title: "ルーティング"
feature_routing_desc: "パラメータと制約付きのルートを定義"
navbar:
brand: "マイアプリケーション"
home_link: "ホーム"
about_link: "アバウト"
contact_link: "コンタクト"
footer:
copyright: "著作権 © 2024-2026 {company}. 全著作権所有。"When a request comes in, DynamicalWeb determines which locale to use based on the following priority:
- Locale Cookie — If the user has previously selected a locale by visiting
/dynaweb/language/{locale_id}, the selected locale is stored in a cookie and will be used for subsequent requests - Accept-Language Header — If no cookie is set, DynamicalWeb will try to detect the user's preferred language
from the
Accept-Languageheader and match it against the configured locales - Default Locale — If no match is found from the header, the configured
default_localefrom the application configuration is used - First Available — If no default locale is configured, the first locale defined in the configuration is used
Locale strings are accessed in templates using the Functions::printl() method, this method uses the active
locale context (determined by the route's locale_id, the $localizationId passed to insertSection(), or
loadLocalization() within a section) to look up the translation key:
<?php use DynamicalWeb\Html\Functions; ?>
<h1><?php Functions::printl('page_title'); ?></h1>
<p><?php Functions::printl('welcome_message', ['name' => 'John']); ?></p>The placeholder replacement uses curly braces {key} syntax in the locale strings, for example a locale string
"Hello {name}, you have {count} messages" with replacements ['name' => 'John', 'count' => 5] will produce
"Hello John, you have 5 messages".
DynamicalWeb supports a special global locale section that makes its labels available across all other sections.
When a label is not found in the active section, the system automatically falls back to the global section.
This allows you to define shared labels (e.g., site name, common buttons, footer text) once and use them from
any locale context without duplication:
# en.yml
global:
site_name: "My Application"
page_title: "My App"
learn_more: "Learn More"
copyright: "Copyright © {year} {company}. All rights reserved."
home:
page_title: "Welcome Home"
jumbotron_text: "Build web applications with ease"
about:
page_title: "About Us"
heading: "About Our Application"In the above example, site_name and learn_more are defined only in global but can be used from any section.
The page_title key is defined in both global and the individual sections — the section-specific value takes
priority, so when the home section is active, page_title resolves to "Welcome Home", not "My App".
<!-- Works from any section context -->
<?php Functions::printl('site_name'); ?>
<?php Functions::printl('copyright', ['year' => '2026', 'company' => 'Nosial']); ?>You can also access the locale object directly from the WebSession for more advanced use cases:
<?php
use DynamicalWeb\WebSession;
$locale = WebSession::getLocale();
// Check if a locale section exists
if($locale->hasLocaleId('dashboard'))
{
// Check if a specific key exists
if($locale->hasKey('dashboard', 'welcome'))
{
$string = $locale->getString('dashboard', 'welcome', ['name' => 'John']);
}
}
// Get all section IDs
$sections = $locale->getLocaleIds();
// Get the current locale code
$code = $locale->getLocaleCode(); // "en"
?>To allow users to switch locales, you can create links to the built-in language endpoint:
<a href="/dynaweb/language/en">English</a>
<a href="/dynaweb/language/cn">中文</a>
<a href="/dynaweb/language/jp">日本語</a>DynamicalWeb can serve static resource files such as CSS, JavaScript, images and fonts directly without needing to
configure individual routes for them. Static resources are stored in the directory configured by the resources
property in the application configuration.
For example, if your resources directory is set to ExampleBootstrap/WebResources and it contains the following
files:
WebResources/
├── css/
│ ├── bootstrap.min.css
│ └── style.css
└── js/
├── bootstrap.bundle.min.js
└── jquery.min.js
These files will be automatically accessible under the root path of your web application:
<link rel="stylesheet" href="/css/bootstrap.min.css">
<link rel="stylesheet" href="/css/style.css">
<script src="/js/bootstrap.bundle.min.js"></script>
<script src="/js/jquery.min.js"></script>DynamicalWeb will serve these files with appropriate Content-Type headers based on the file extension and will
also set caching headers like Last-Modified, ETag and Cache-Control to enable browser caching. If the APCu
extension is available, small resource files (up to 256KB) will be cached in APCu to further improve performance.
DynamicalWeb also protects against directory traversal attacks by sanitizing the requested path, any attempts to
access files outside the configured resources directory using ../ or similar patterns will be blocked.
Pre-request and post-request scripts allow you to execute PHP code before and after the matched module runs, this is useful for implementing cross-cutting concerns like authentication, rate limiting, logging, and cleanup.
application:
name: "My Application"
root: "MyApp/WebApplication"
pre_request:
- "middleware/auth.php"
- "middleware/rate-limit.php"
post_request:
- "middleware/logging.php"
- "middleware/cleanup.php"The paths are relative to the root directory, and the scripts are executed in the order they are defined. During
execution, the WebSession is fully initialized so you have access to the request, response, and all other
session data.
Here's an example of a pre-request authentication script:
// middleware/auth.php
<?php
use DynamicalWeb\WebSession;
use DynamicalWeb\Enums\ResponseCode;
$publicRoutes = ['home', 'login', 'register'];
$currentRoute = WebSession::getCurrentRoute();
if($currentRoute !== null && !in_array($currentRoute->getId(), $publicRoutes))
{
$token = WebSession::getRequest()->getHeader('Authorization');
if($token === null)
{
WebSession::getResponse()
->setStatusCode(ResponseCode::UNAUTHORIZED)
->setJson(['error' => 'Authentication required']);
return;
}
WebSession::set('user', validateToken($token));
}
?>An entry can also be a mapping, which limits the script to some routes instead of checking the path inside the script. Plain strings and mappings can be mixed:
application:
pre_request:
- "middleware/headers.php" # every route
- module: "middleware/auth.php"
except: [ "login", "logout", "error" ] # route IDs to skip
websocket: false # skip WebSocket requests
- module: "middleware/admin.php"
only: [ "operators", "operator_detail" ] # only these route IDs| Name | Required | Type | Description |
|---|---|---|---|
module |
Yes | string |
The script to run, based from root |
only |
No | array/string |
Route IDs the script runs for; when set, every other route skips it |
except |
No | array/string |
Route IDs the script is skipped for |
websocket |
No | bool |
Set to false to skip the script for WebSocket requests |
ApplicationConfiguration::getPreRequest() and getPostRequest() keep returning the script paths, and
getPreRequestHooks() / getPostRequestHooks() return the entries with their filters.
DynamicalWeb has native support for handling WebSocket connections through integration with the
WebsocketServer — a Rust-based WebSocket-to-TCP bridge. This allows you
to write WebSocket handlers as standard .phtml files using the same routing system as regular HTTP requests, with
full access to the WebSession, Request, localization, and pre/post-request pipeline.
Client (Browser) ←→ Nginx (port 8080) ←→ PHP-FPM (HTTP requests)
←→ WebsocketServer (port 9001) ←→ PHP CLI (WebSocket connections)
-
Nginx listens on a single port for both HTTP and WebSocket connections. When a request includes the
Upgrade: websocketheader, Nginx proxies it to the WebsocketServer instead of PHP-FPM. -
WebsocketServer (a Rust binary managed by Supervisor) accepts the WebSocket handshake, upgrades the connection, and spawns a PHP CLI process for each connected client.
-
The PHP process runs your application's entry point (
index.php), with theWSS_ENABLED=1environment variable set. DynamicalWeb detects this and handles the request through its WebSocket execution flow. -
Communication between the PHP process and the WebsocketServer happens over a local TCP bridge. The WebsocketServer handles all WebSocket frame encoding/decoding — PHP reads and writes raw binary data over the TCP connection.
When WSS_ENABLED=1 is detected, DynamicalWeb::handleRequest() follows this flow instead of the standard HTTP flow:
WebSessionis initialized — theRequestobject is populated fromWSS_*environment variables (path, headers, cookies, client IP, query string) instead of$_SERVER, and the request method is set toWEBSOCKET- A
WebSocketobject is created, which opens a TCP connection to the bridge and identifies itself with the connection ID from the environment - If configured, pre-request PHP scripts are executed (same as HTTP flow)
- The router matches the incoming
WSS_REQUEST_PATHagainst configured routes that haveallowed_methods: ['WEBSOCKET'] - If a route matches, the configured
.phtmlhandler is executed as a long-running PHP script (no output buffering) — this handler manages the WebSocket connection using theWebSocketAPI - When the handler finishes (connection closes or handler exits), post-request scripts are executed
WebSession::endSession()closes the WebSocket TCP connection and clears all static state- No HTTP response is sent — all communication happens over the WebSocket
WebSocket routes are defined in the same YAML configuration as regular routes, using WEBSOCKET as the allowed
method:
router:
base_path: "/"
routes:
- id: "ws_chat"
path: "/chat"
module: "websocket/chat.phtml"
allowed_methods: [ WEBSOCKET ]
- id: "ws_notifications"
path: "/notifications"
module: "websocket/notifications.phtml"
allowed_methods: [ WEBSOCKET ]Routes with allowed_methods: [ WEBSOCKET ] will only match when the request originates from the WebSocket Server.
The path must correspond to the URL path that the client used when establishing the WebSocket connection (e.g.,
new WebSocket('wss://example.com/chat')).
Note: The
*wildcard does not matchWEBSOCKETrequests. You must explicitly setallowed_methods: [ WEBSOCKET ]on WebSocket handler routes.
A WebSocket handler is a standard .phtml file that uses the WebSocket API to communicate with the connected
client. The handler runs as a long-lived process — it stays alive as long as the WebSocket connection is open.
<?php
// websocket/chat.phtml
use DynamicalWeb\WebSession;
$ws = WebSession::getWebSocket();
// Send a welcome message
$ws->send(json_encode(['type' => 'connected', 'message' => 'Welcome to chat!']));
// Message loop — runs until the client disconnects
while ($ws->isConnected())
{
$data = $ws->read();
if ($data === null)
{
break; // Connection closed
}
$message = json_decode($data, true);
if (isset($message['type']) && $message['type'] === 'ping')
{
$ws->send(json_encode(['type' => 'pong']));
continue;
}
// Broadcast or respond
$ws->send(json_encode([
'type' => 'message',
'content' => $message['content'] ?? '',
'timestamp' => time(),
]));
}
?>Accessing the connection metadata within the handler:
<?php
use DynamicalWeb\WebSession;
$ws = WebSession::getWebSocket();
$conn = $ws->getConnection(); // DynamicalWeb\WebSocket\Connection
$clientIp = $conn->getClientIp();
$clientPort = $conn->getClientPort();
$path = $conn->getRequestPath();
$headers = $conn->getRequestHeaders();
$origin = $conn->getOrigin();
$userAgent = $conn->getUserAgent();
$protocol = $conn->getProtocol();
?>The WebSocket object is accessible via WebSession::getWebSocket() and provides the following API for
communicating over the WebSocket connection. All data is transmitted as raw binary over the TCP bridge — the
WebsocketServer handles encoding/decoding WebSocket frames.
| Method | Return Type | Description |
|---|---|---|
getConnection() |
?Connection |
Returns the connection metadata object populated from WSS_* environment variables |
getMetadata() |
array |
Returns the connection metadata as an associative array |
send(string $data, int $chunkSize=65536) |
bool |
Sends raw data to the WebSocket client. Returns false if the connection is closed or the payload exceeds maxPayloadSize |
read(int $length=8192) |
?string |
Reads raw data from the WebSocket client. Returns null on connection close or timeout |
readAll(int $chunkSize=65536, int $maxLength=0, float $idleTimeout=0) |
?string |
Reads all available data until connection closes, max length reached, or idle timeout expires |
sendAndReceive(string $data, float $timeout=5.0, int $chunkSize=8192) |
?string |
Sends data and waits for a response using kernel-level polling (stream_select) |
readLine() |
?string |
Reads a line of data (terminated by \r\n, \n, or \r) from the socket |
setTimeout(float $seconds) |
void |
Sets the read timeout on the underlying socket |
setMaxPayloadSize(int $bytes) |
void |
Limits the maximum payload size for send() and read() operations |
isConnected() |
bool |
Returns true if the connection is still alive and not timed out |
getSocket() |
resource|null |
Returns the raw PHP socket resource for advanced use |
getState() |
array |
Returns diagnostic state: connected, closed, timed_out, bytes_sent, bytes_received |
getBytesSent() |
int |
Returns the total number of bytes sent over this connection |
getBytesReceived() |
int |
Returns the total number of bytes received over this connection |
close() |
void |
Closes the connection |
The Connection object provides the following accessors for the connection metadata:
| Method | Return Type | Description |
|---|---|---|
getConnectionId() |
string |
Unique connection ID assigned by the WebsocketServer |
getClientIp() |
string |
Client IP address |
getClientPort() |
int |
Client port |
getServerHost() |
?string |
Server host or IP |
getServerPort() |
?int |
Server port |
getRequestUri() |
string |
Full request URI from the original HTTP upgrade |
getRequestPath() |
?string |
Path component of the request URI |
getRequestQuery() |
?string |
Query string component of the request URI |
getRequestHeaders() |
array |
All HTTP headers from the original upgrade request |
getProtocol() |
?string |
Sec-WebSocket-Protocol header value |
getVersion() |
?string |
Sec-WebSocket-Version header value |
getOrigin() |
?string |
Origin header |
getUserAgent() |
?string |
User-Agent header |
getHost() |
?string |
Host header |
getXForwardedFor() |
?string |
X-Forwarded-For header |
getXRealIp() |
?string |
X-Real-IP header |
getTcpHost() |
string |
TCP bridge host (for diagnostics) |
getTcpPort() |
int |
TCP bridge port (for diagnostics) |
To use WebSocket support, your deployment must include the WebsocketServer binary alongside PHP-FPM and Nginx.
The generated Dockerfile and supervisord.conf already include this setup.
Key configuration files:
Nginx (nginx.conf):
# Map Upgrade header to connection upgrade
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
# WebSocketServer backend
upstream websocket_backend {
server 127.0.0.1:9001;
}
# Intercept .php requests with WebSocket Upgrade header
location ~ \.php$ {
if ($http_upgrade != '') {
rewrite ^ /_websocket last;
}
# ... standard PHP-FPM config ...
}
# Internal WebSocket proxy
location = /_websocket {
proxy_pass http://websocket_backend;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_set_header Host $host;
proxy_read_timeout 86400s;
}Supervisor (supervisord.conf):
[program:wss]
command=/usr/bin/wss --ws-host 127.0.0.1 --ws-port 9001 \
--php-executable /usr/local/bin/php \
--script /var/www/html/index.php
autostart=true
autorestart=trueThe WebsocketServer requires the following arguments:
--ws-host/--ws-port— The address the WebsocketServer listens on (Nginx proxies to this)--php-executable— Path to the PHP CLI binary--script— Path to your application's entry point (the sameindex.phpused for HTTP)--tcp-bind/--tcp-port— Internal TCP bridge address (defaults to127.0.0.1:8081)
For additional WebsocketServer options (TLS, connection limits, timeouts), refer to the WebsocketServer documentation.
DynamicalWeb includes built-in XSS (Cross-Site Scripting) protection that can be configured through the xss_level
property in the application configuration. There are four levels of protection available:
| Level | Name | Value | Headers Applied |
|---|---|---|---|
| 0 | DISABLED |
0 | No XSS protection headers are applied |
| 1 | LOW |
1 | X-XSS-Protection: 1; mode=block |
| 2 | MEDIUM |
2 | Content-Security-Policy: default-src 'self'; report-uri /csp-report |
| 3 | HIGH |
3 | Content-Security-Policy: default-src 'self'; script-src 'self' 'nonce-{nonce}'; report-uri /csp-report |
At level 3 (HIGH), DynamicalWeb generates a unique nonce for each request that can be used in your script tags to allow only explicitly authorized scripts to execute.
Additionally, the Functions::print() method HTML-escapes output by default using htmlspecialchars() with
ENT_QUOTES | ENT_SUBSTITUTE flags and UTF-8 encoding, which provides protection against XSS attacks at the
template level.
Headers listed under headers in the application configuration are added to every response, which is the place
for security headers that would otherwise be set by a pre-request script:
application:
headers:
X-Frame-Options: "DENY"
X-Content-Type-Options: "nosniff"
Referrer-Policy: "same-origin"
Content-Security-Policy: "frame-ancestors 'none'; base-uri 'self'; object-src 'none'"They are set before pre-request scripts run, so a pre-request script or module can still override or remove one for
a particular response with WebSession::getResponse()->setHeader() or removeHeader().
The debug panel is a development tool that, when enabled, injects an iFrame into the bottom of every HTML response
containing detailed information about the current request and the web application's state. To enable it, set
debug_panel: true in the application configuration.
Note: The debug panel adds a performance overhead and exposes internal application details, it should never be enabled in production environments.
The debug panel includes the following tabs of information:
| Tab | Description |
|---|---|
| App | Application name, version, package, configuration details |
| Request | Request method, URL, path, host, HTTP version, headers, query/body/form/path parameters |
| Response | Response status code, content type, headers, cookies |
| Cookies | All cookies from the request with their values |
| PHP | PHP version, SAPI, memory usage, loaded configuration |
| Extensions | List of all loaded PHP extensions |
| Server | Server variables ($_SERVER superglobal) |
| Constants | PHP constants and their values |
| Session | PHP session data |
| Routes | All configured routes with their paths, modules and allowed methods |
| Sections | All configured sections and their execution details |
| INI | PHP INI directives and their values |
| APCu | APCu cache information including memory usage and cache entries |
| Locale | Current locale code, available locales, and a locale switcher |
| Profiler | File execution tracking showing which modules/sections were executed and their execution times |
| OPcache | OPcache status and configuration information |
DynamicalWeb also exposes a debug stats API endpoint at /dynaweb/debug/stats when the debug panel is enabled, this
returns a JSON object containing profiling data about the current application state.
DynamicalWeb comes with several built-in pages that are served under the /dynaweb/ path prefix, these pages
provide framework-level functionality and don't need to be configured in your application's routes:
| Path | Description |
|---|---|
/dynaweb |
An about page that displays information about the DynamicalWeb framework |
/dynaweb/health |
A health check endpoint that returns the application status, useful for load balancers and monitoring systems |
/dynaweb/language/{id} |
The locale switcher endpoint, sets a cookie with the selected locale and redirects back |
/dynaweb/debug/stats |
Debug statistics API endpoint (only available when debug_panel is enabled) |
DynamicalWeb also comes with default 404 and 500 error pages that will be used if you don't configure custom response handlers in your router configuration.
DynamicalWeb uses the APCu (APC User Cache) extension when available to cache various data to improve performance
across requests. If APCu is not installed or is disabled, DynamicalWeb will work without it using in-process caches
that only persist for the duration of a single request. You can also explicitly disable APCu by setting
disable_apcu: true in the application configuration.
The following data is cached in APCu when available:
| Cached Data | Cache Key Pattern | TTL | Description |
|---|---|---|---|
| Web Configuration | dw_webcfg_{md5} |
60s | Parsed Yaml configuration to avoid re-parsing on every request |
| Locale Files | dw_locale_{md5} |
300s | Parsed locale Yaml files |
| Language Detection | dw_lang_{accept_lang_md5}_{locales_md5} |
3600s | Accept-Language header detection results |
| Route Resolution | dw_rtres_{app_md5}_{method}_{path_md5} |
default | Matched route results for specific request paths |
| Route Regex Patterns | dw_route_regex_{route_md5} |
default | Compiled regex patterns for route matching |
| Static Resource Existence | dw_static_res_{path_md5} |
60s | Whether a static resource file exists on disk |
| Built-in Resource Existence | dw_builtin_res_{path_md5} |
60s | Whether a built-in resource file exists on disk |
| File Metadata | dw_filemeta_{file_md5} |
10s | File modification time and size |
| File Content | dw_filecontent_{file_md5} |
3600s | Content of small static files (≤ 256KB) |
In addition to APCu, DynamicalWeb also maintains in-process static caches for route regex patterns and resource existence checks that persist for the duration of a single request.
DynamicalWeb provides a Cookie object for managing cookies in responses, you can set cookies on the response object
using either the convenience setCookie() method or by creating a Cookie object and adding it:
<?php
use DynamicalWeb\WebSession;
use DynamicalWeb\Objects\Cookie;
// Using the convenience method
WebSession::getResponse()->setCookie(
name: 'session_id',
value: 'abc123',
expires: time() + 3600,
path: '/',
domain: '',
secure: true,
httpOnly: true
);
// Using a Cookie object
$cookie = new Cookie('preferences', json_encode(['theme' => 'dark']));
$cookie->setExpires(time() + 86400 * 30);
$cookie->setSecure(true);
$cookie->setHttpOnly(true);
WebSession::getResponse()->addCookie($cookie);
?>The Cookie object has the following properties:
| Property | Type | Default | Description |
|---|---|---|---|
name |
string |
- | The name of the cookie |
value |
string |
- | The value of the cookie |
expires |
int |
0 |
Expiration time as a Unix timestamp, 0 means session cookie (expires when browser closes) |
path |
string |
"/" |
The path on the server where the cookie will be available |
domain |
string |
"" |
The domain that the cookie is available to, empty string means current domain |
secure |
bool |
false |
When True, the cookie will only be transmitted over HTTPS connections |
httpOnly |
bool |
false |
When True, the cookie is not accessible via JavaScript (document.cookie) |
Cookie Sessions provide server-side session storage using Memcached, allowing you to persist arbitrary data across requests securely. The session data is stored on the server while only a session identifier is sent to the client via a cookie. This prevents client-side tampering and enables secure storage of sensitive data like authentication status and user identifiers.
Cookie Sessions are disabled by default. To enable them, set the MEMCACHED_ENABLED environment variable to 1
and ensure Memcached is running.
Cookie Sessions are configured exclusively through environment variables:
| Variable | Default | Description |
|---|---|---|
MEMCACHED_ENABLED |
— | Set to 1, true, yes, or on to enable |
MEMCACHED_HOST |
127.0.0.1 |
Memcached server host |
MEMCACHED_PORT |
11211 |
Memcached server port |
MEMCACHED_SESSION_TTL |
3600 |
Session time-to-live in seconds |
MEMCACHED_SESSION_SECRET |
(internal default) | HMAC secret key used for fingerprint verification |
MEMCACHED_SESSION_SLIDING |
0 |
Set to 1 to renew the session cookie's expiry whenever the session is read, so the TTL becomes an idle timeout |
MEMCACHED_SESSION_BIND_IP |
1 |
Set to 0 to leave the client IP out of the session fingerprint (the User-Agent is still checked) |
The WebSession class provides convenience static methods for interacting with cookie sessions:
<?php
use DynamicalWeb\WebSession;
// Check if the manager is available (Memcached enabled and connected)
$manager = WebSession::getCookieSessionManager();
if ($manager === null)
{
// Memcached is not available
return;
}
// Check if a session already exists (from the request cookie)
if (WebSession::hasCookieSession('session'))
{
$session = WebSession::getCookieSession('session');
$userId = $session->get('user_id');
$authenticated = $session->get('authenticated', false);
}
else
{
// Create a new session with initial data
$session = WebSession::createCookieSession([
'authenticated' => false,
'user_id' => 0,
'created_at' => time(),
]);
// The session cookie is automatically set in the response
}
?>You can modify session data using the CookieSession object and persist changes with saveCookieSession():
<?php
use DynamicalWeb\WebSession;
$session = WebSession::getCookieSession();
if ($session !== null)
{
// Update values
$session->set('authenticated', true);
$session->set('user_id', 42);
$session->set('last_activity', time());
// Remove a value
$session->remove('temporary_data');
// Check if a key exists
if ($session->has('user_role'))
{
$role = $session->get('user_role');
}
// Persist changes to Memcached
WebSession::saveCookieSession($session);
}
?>To log a user out or clear their session:
<?php
use DynamicalWeb\WebSession;
WebSession::destroyCookieSession();
// The session is removed from Memcached and the cookie is expired
?>When creating or destroying a session, you can control the cookie attributes via optional parameters. All parameters have sensible defaults, so you only need to specify what you want to override:
| Parameter | Type | Default | Description |
|---|---|---|---|
$cookieName |
?string |
null |
Cookie name (falls back to web_session) |
$path |
string |
"/" |
Cookie path |
$domain |
string |
"" |
Cookie domain (empty = current domain) |
$secure |
?bool |
null |
HTTPS-only flag (null = auto-detect from request) |
$httpOnly |
bool |
true |
HTTP-only flag (prevents JavaScript access) |
$sameSite |
string |
"Lax" |
SameSite attribute (None, Lax, or Strict) |
For destroyCookieSession() only $cookieName, $path and $domain are available, as these must match
the values used when the cookie was originally set to properly expire it.
<?php
use DynamicalWeb\WebSession;
// Create a session scoped to /admin/ with Strict SameSite
$session = WebSession::createCookieSession(
data: ['role' => 'admin'],
cookieName: 'ADMIN_SESSION',
path: '/admin',
secure: true,
sameSite: 'Strict'
);
// Destroy it using matching cookie parameters
WebSession::destroyCookieSession(
cookieName: 'ADMIN_SESSION',
path: '/admin'
);
?>You can manage multiple independent session cookies by passing a $cookieName argument to the session methods.
This is useful when you need separate sessions for different concerns, such as authentication and shopping cart:
<?php
use DynamicalWeb\WebSession;
// Authentication session
if (!WebSession::hasCookieSession('AUTH_SESSION'))
{
WebSession::createCookieSession(
['authenticated' => false],
'AUTH_SESSION'
);
}
$authSession = WebSession::getCookieSession('AUTH_SESSION');
// Shopping cart session
if (!WebSession::hasCookieSession('CART_SESSION'))
{
WebSession::createCookieSession(
['items' => [], 'total' => 0],
'CART_SESSION'
);
}
$cartSession = WebSession::getCookieSession('CART_SESSION');
// Each session stores data independently in Memcached
$authSession->set('authenticated', true);
$authSession->set('user_id', 42);
WebSession::saveCookieSession($authSession);
$cartSession->set('items', ['product_1', 'product_2']);
WebSession::saveCookieSession($cartSession);
// Destroy only the auth session
WebSession::destroyCookieSession('AUTH_SESSION');
?>The CookieSession object provides the following methods:
| Method | Return Type | Description |
|---|---|---|
getSessionId() |
string |
Returns the unique session identifier |
getData() |
array |
Returns all session data as an associative array |
getExpires() |
int |
Returns the Unix timestamp when the session expires |
set(string $key, mixed $value) |
void |
Sets a value in the session data |
get(string $key, mixed $default) |
mixed |
Gets a value from the session data, or the default if not found |
has(string $key) |
bool |
Checks if a key exists in the session data |
remove(string $key) |
void |
Removes a key from the session data |
| Method | Return Type | Description |
|---|---|---|
getCookieSessionManager() |
?CookieSessionManager |
Returns the manager if Memcached is enabled and connected |
hasCookieSession(?string $cookieName = null) |
bool |
Returns true if a valid session exists for the current request |
getCookieSession(?string $cookieName = null) |
?CookieSession |
Retrieves the current session (with fingerprint verification) |
createCookieSession(array $data, ?string $cookieName, string $path, string $domain, ?bool $secure, bool $httpOnly, string $sameSite) |
?CookieSession |
Creates a new session and sets the session cookie |
saveCookieSession(CookieSession $session) |
bool |
Persists session changes to Memcached |
destroyCookieSession(?string $cookieName, string $path, string $domain) |
bool |
Deletes the session from Memcached and expires the cookie |
Cookie Sessions include built-in protection against session hijacking:
- Session ID Generation — Session IDs are generated using
random_bytes(32), producing 64-character hexadecimal strings that are cryptographically secure - Fingerprint Verification — Each session stores an HMAC-SHA256 fingerprint computed from the client's IP
address and User-Agent string. On every
getSession()call, the fingerprint is verified against the current request. If the fingerprint does not match, the session is immediately deleted from Memcached and the cookie is expired, mitigating session hijacking attempts - HttpOnly Cookies — The session cookie is set with the
HttpOnlyflag, preventing client-side JavaScript from accessing the session identifier - Secure Cookies — The session cookie is marked
Securewhen the request is made over HTTPS - TTL Refresh — The session TTL is refreshed on every access via
Memcached::touch(), keeping active sessions alive while expired sessions are automatically cleaned up by Memcached. The browser cookie keeps the expiry it was created with unlessMEMCACHED_SESSION_SLIDING=1, which renews it with the attributes (path, domain,SameSite, …) the session was created with - IP Binding — By default the fingerprint includes the client IP, so a session ends when the user's address
changes (for example a phone switching networks).
MEMCACHED_SESSION_BIND_IP=0keeps sessions across address changes; sessions created while IP-bound are moved to the new fingerprint on their next request instead of ending - WebSocket Requests — WebSocket requests arrive through the local bridge with a different address, so a fingerprint mismatch on a WebSocket request returns no session but leaves the browser's session and cookie intact
- One Instance per Request —
WebSession::getCookieSession()returns the sameCookieSessionobject for every call within a request, so a change saved by one part of the page is never overwritten by another part saving an older copy
For advanced use cases, you can interact with the CookieSessionManager directly:
<?php
use DynamicalWeb\WebSession;
$manager = WebSession::getCookieSessionManager();
if ($manager !== null)
{
// Access configuration
$cookieName = $manager->getCookieName();
$ttl = $manager->getSessionTtl();
// Check session cookie presence without loading the session
if ($manager->hasSessionCookie())
{
$sessionId = $manager->getSessionIdFromCookie();
}
// Use a custom cookie name for a specific session
if ($manager->hasSessionCookie('CART_SESSION'))
{
$cartSession = $manager->getSession('CART_SESSION');
}
// Check if a session exists in Memcached without loading it
if ($manager->sessionExists())
{
// Session exists in the cache
}
}
?>The CookieSessionManager provides the following methods:
| Method | Return Type | Description |
|---|---|---|
isEnabled() |
bool |
Returns true if Memcached is available and connected |
getCookieName() |
string |
Returns the configured session cookie name |
getSessionTtl() |
int |
Returns the configured session TTL in seconds |
hasSessionCookie(?string $cookieName = null) |
bool |
Checks if the session cookie exists in the current request |
getSessionIdFromCookie(?string $cookieName = null) |
?string |
Returns the session ID from the request cookie, or null |
getSession(?string $cookieName = null) |
?CookieSession |
Retrieves the session from Memcached with fingerprint verification |
sessionExists(?string $cookieName = null) |
bool |
Checks if the session exists in Memcached without loading it |
createSession(array $data, ?string $cookieName, string $path, string $domain, ?bool $secure, bool $httpOnly, string $sameSite) |
?CookieSession |
Creates a new session and sets the cookie in the response |
saveSession(CookieSession $session) |
bool |
Persists the session data to Memcached |
destroySession(?string $cookieName, string $path, string $domain) |
bool |
Deletes the session and expires the cookie |
deleteSession(string $sessionId) |
bool |
Deletes a specific session by its ID |
When using the provided Docker image, Memcached is already installed and configured to start automatically via
Supervisor. The MEMCACHED_* environment variables can be set in your docker-compose.yml or container
environment to configure session behaviour:
# docker-compose.yml
services:
app:
image: your-app:latest
ports:
- "8080:8080"
environment:
- MEMCACHED_ENABLED=1
- MEMCACHED_SESSION_TTL=7200DynamicalWeb can protect forms and scripts against cross-site request forgery using a random token kept in the cookie session. It requires Cookie Sessions.
Put the token in every form and, for scripts, in the page head:
<head>
<!-- meta tag only, or with true: also sends the token with same-origin XHR/fetch requests -->
<?php Functions::csrfMeta(true); ?>
</head>
<form method="post">
<?php Functions::csrfField(); ?>
...
</form>Then enable checking in the application configuration:
application:
csrf_protection: true
router:
routes:
- id: "webhook"
path: "/webhook"
module: "webhook.php"
csrf_exempt: true # authenticated by other meansWith csrf_protection enabled, every POST, PUT and DELETE request to a route that is not csrf_exempt must
send the token in the csrf_token field or the X-CSRF-Token header. Otherwise the request is answered with
403 before pre-request scripts and the module run: JSON {"error": "csrf_failed"} for script requests (those
sending the header or accepting application/json), otherwise the router's 403 response handler when configured,
or plain text.
The token can also be used directly, for example to check it yourself instead of enabling csrf_protection:
| Method | Return Type | Description |
|---|---|---|
WebSession::getCsrfToken(?string $cookieName = null) |
?string |
Returns the session's token, creating the session and token on first use; null when sessions are disabled |
WebSession::verifyCsrfToken(?string $token = null, ?string $cookieName = null) |
bool |
Checks a token, by default the one in the request's csrf_token field or X-CSRF-Token header |
Functions::csrfField(?string $cookieName = null) |
void |
Prints a hidden csrf_token input |
Functions::csrfMeta(bool $attachToRequests = false, ?string $cookieName = null) |
void |
Prints a csrf-token meta tag, and optionally the script that adds the header to requests |
DynamicalWeb\Classes\RequestCache is an in-process cache that lasts for one request, for lookups a page repeats
while it renders, such as the same record resolved by several sections. It is cleared when the request starts and
ends. It has the same remember() shape as Memcache, so moving a value to a cache that lasts
across requests is a one-word change:
<?php
use DynamicalWeb\Classes\Memcache;
use DynamicalWeb\Classes\RequestCache;
// Once per request
$operator = RequestCache::remember('operator:' . $uuid, fn() => $client->getOperator($uuid));
// Once per minute across all requests and workers
$serverInfo = Memcache::remember('server_info', 60, fn() => $client->getServerInformation());
?>| Method | Return Type | Description |
|---|---|---|
fetch(string $key, mixed &$success = false) |
mixed |
Returns the cached value, or null on a miss |
store(string $key, mixed $value) |
void |
Stores a value |
exists(string $key) |
bool |
Returns true if the key is cached, even when its value is null |
delete(string $key) |
bool |
Removes an entry |
remember(string $key, callable $callback) |
mixed |
Returns the cached value or computes, stores and returns it; a callback that throws is not cached |
clear() |
void |
Removes every entry |
count() |
int |
Returns the number of entries |
The Memcached instance used for Cookie Sessions is also exposed to web applications as a general-purpose shared cache
through the DynamicalWeb\Classes\Memcache class. This lets applications cache expensive data (such as a compiled
application kernel) across requests and worker processes without setting up an additional caching service like Redis.
The cache uses the same MEMCACHED_ENABLED, MEMCACHED_HOST and MEMCACHED_PORT environment variables described in
Cookie Sessions. Every key is transparently prefixed so application entries never collide with
DynamicalWeb's internal session entries:
| Variable | Default | Description |
|---|---|---|
MEMCACHED_KEY_PREFIX |
dw_app_ |
Prefix applied to every key stored by Memcache |
All methods silently no-op when Memcached is unavailable or disabled, so applications keep working without it.
<?php
use DynamicalWeb\Classes\Memcache;
// Compute once, then serve from Memcached for 10 minutes
$kernel = Memcache::remember('kernel', 600, function() {
return buildKernel();
});
// Basic operations
Memcache::store('greeting', 'hello', 60);
$value = Memcache::fetch('greeting', $success);
if ($success)
{
// Cache hit
}
// Counters
$views = Memcache::increment('page_views');| Method | Return Type | Description |
|---|---|---|
isExtensionAvailable() |
bool |
Returns true if the Memcached extension is loaded |
isAvailable() |
bool |
Returns true if Memcached is enabled and the client was initialized |
getClient() |
?Memcached |
Returns the underlying client for advanced usage (keys are still prefixed) |
getKeyPrefix() |
string |
Returns the configured key prefix |
fetch(string $key, mixed &$success = false) |
mixed |
Returns the cached value, or false on a miss |
store(string $key, mixed $value, int $ttl = 0) |
bool |
Stores a value, overwriting any existing one |
add(string $key, mixed $value, int $ttl = 0) |
bool |
Stores a value only if the key does not exist |
delete(string $key) |
bool |
Deletes an entry |
exists(string $key) |
bool |
Returns true if the key exists |
fetchMultiple(array $keys) |
array |
Returns key => value for every key found |
storeMultiple(array $items, int $ttl = 0) |
bool |
Stores multiple key => value pairs |
increment(string $key, int $offset = 1, int $initial = 0, int $ttl = 0) |
int|false |
Increments a counter, initializing it to $initial if missing |
decrement(string $key, int $offset = 1, int $initial = 0, int $ttl = 0) |
int|false |
Decrements a counter (never below zero), initializing it if missing |
touch(string $key, int $ttl) |
bool |
Updates the TTL of an existing entry |
remember(string $key, int $ttl, callable $callback) |
mixed |
Returns the cached value or computes, stores and returns it |
stats() |
array|false |
Returns Memcached server statistics |
DynamicalWeb applications can be deployed using Docker, when you generate a project using ncc project --generate=dynamicalweb
a Dockerfile, docker-compose.yml, nginx.conf and supervisord.conf are generated for you. The Docker setup uses a
multi-stage build where the first stage compiles the ncc package and the second stage sets up a production environment
with PHP-FPM and Nginx managed by Supervisor.
# docker-compose.yml
services:
app:
build:
context: .
dockerfile: Dockerfile
image: com.example.bootstrap:1.0.0
ports:
- "8080:8080"
environment:
- PHP_MEMORY_LIMIT=256M
- PHP_MAX_EXECUTION_TIME=60
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/dynaweb/health"]
interval: 30s
timeout: 10s
retries: 3The health check uses the built-in /dynaweb/health endpoint to verify the application is running and responsive.
DynamicalWeb requires all requests to be routed through a single entry point (index.php), this is achieved using
URL rewriting in the web server configuration. Here's an example Nginx configuration:
server {
listen 8080;
server_name _;
root /var/www/html;
index index.php;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location ~ \.php$ {
fastcgi_pass 127.0.0.1:9000;
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
}
# Deny access to hidden files
location ~ /\. {
deny all;
}
}The key part is the try_files directive which routes all requests that don't match an existing file to
index.php, this allows DynamicalWeb's router to handle all incoming requests.
DynamicalWeb is licensed under the MIT License, see LICENSE for more information. Multiple licenses for the open-source components used in this project can be found at LICENSE