Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Sukarix is an open-source modern PHP framework built to empower developers by facilitating rapid development of web and CLI applications with a focus on simplicity and efficiency. Born out of the need for a streamlined, performance-oriented framework, Sukarix builds on top of the Fat-Free Framework to offer a solid foundation that is both lightweight and feature-rich.

Introduction

Philosophy

Sukarix Framework embraces a philosophy built on principles designed to enhance your development process and deliver robust, scalable applications. Here’s what defines our approach:

  1. Lean yet Robust

    • Powered with F3 (Fat-Free Framework): While F3 provides a lightweight core, Sukarix builds on this foundation, adding just the right amount of structure and modern features to deliver a robust, feature-rich experience without overwhelming complexity.
  2. Proven Expertise

    • Real world experience: Sukarix has run in production since 2016, covering a wide range of applications including CLI tools, web apps, React applications, JSON APIs, middleware, and reverse proxy apps. That testing under diverse conditions is what shaped the framework before its first public release in 2024, and it continues to drive every release since.
  3. Stability Over PHP Versions

    • Minimalistic upgrades: Sukarix is designed to ensure stability across different PHP versions with minimal changes required. The framework is built in such a way that upgrading your PHP version typically requires no adjustments to the framework itself, simplifying maintenance and ensuring long-term compatibility.
  4. Rich in features

    • Best-in-Class Libraries: Sukarix incorporates the best PHP libraries available from the open-source community, offering enterprise-level features that meet the demands of complex business applications.
  5. Efficiency first

    • Simplicity and Performance: When multiple approaches exist for a feature, we prioritise using the Fat-Free Framework’s methods for their proven efficiency. Features are designed and continually refined to ensure the most performant outcomes while maintaining simplicity.
  6. Flexibility in approach

    • Multiple ways to succeed: While Sukarix standardises the use of the Fat-Free Framework (F3), it remains flexible, allowing developers to utilize F3’s core features, its plugins, and a range of open-source libraries to craft alternative solutions. This approach provides developers the freedom to choose or create the method that best fits the specific requirements of their projects.
  7. Smart defaults

    • Convention Over Configuration: Sukarix is built on the principle of “It just works!” By promoting conventions over configurations, the framework provides smart defaults that streamline development tasks, enabling developers to focus more on innovation and less on setup.
  8. Testability

    • Comprehensive Testing: Sukarix supports both CLI and browser-based unit testing, ensuring the reliability and performance of applications through thorough testing. Statera is the Sukarix testing kit built on top of the Fat-Free Framework’s Test class. It is a standalone, optional package — the framework itself uses it via a require-dev dependency for its own test suite. Applications can choose to use Statera or integrate any other testing framework (such as PHPUnit) as needed.

Features

  • Intuitive Routing System: Define your routes intuitively, ensuring that your application logic is separated from the routing layer and stay human-readable. See Routing Engine & ACL.
  • Powerful Templating Engine: Comes with a powerful templating engine that facilitates creating dynamic content in a clean and manageable way.
  • Built-in Caching Solutions: Reduce load times and enhance application performance with advanced caching techniques.
  • ORM Layer: Simplify database interactions with a robust ORM that supports a variety of database systems. See Databases & Cortex.
  • JSON APIs: A stateless ApiAction base class, CORS, health probes and capability tokens for machine callers. See Building JSON APIs.
  • Security Building Blocks: Deny-by-default ACL, CSRF protection, a pluggable session contract and secrets at rest on libsodium.
  • Observability: Request-id correlation and structured JSON logging on Monolog. See Logging.
  • Queues & Messaging: A Redis-backed FIFO queue and an AMQP command publisher for pipelines and service-to-service work.
  • Testing: Statera, a scenario-based testing kit with CLI and browser runners and code coverage.

Getting Started

Sukarix requires PHP 8.4 or later. Our documentation is designed to guide you through setting up your development environment, creating your first project, and exploring Sukarix’s capabilities. Whether you’re building a small website or a complex application, Sukarix provides all the tools you need to succeed.

Contributing

Sukarix is an open-source project and thrives on your contributions. Whether it’s fixing bugs, adding features, or improving documentation, your help is always welcome. Join our community and help shape the future of Sukarix!

The Sukarix Framework

What is Sukarix?

Sukarix is an open-source PHP framework built on top of the Fat-Free Framework (F3) designed to create enterprise-grade web and server/CLI applications. The name “Sukarix” is derived from the Arabic word “سُكَّر” (sukkar), which means sugar, symbolizing the framework’s goal to add sweetness and efficiency to the robust and lightweight Fat-Free Framework.

Purpose of Sukarix

The primary purpose of Sukarix is to provide developers with a powerful and efficient framework for building enterprise-level PHP applications. Whether you are developing web applications or server-side/command-line interface ( CLI) applications, Sukarix aims to streamline the development process by leveraging the strengths of Fat-Free Framework and enhancing it with additional features and tools for modern development needs. Sukarix seeks to combine the best available PHP libraries to build the most robust and usable PHP framework.

Why Choose Sukarix?

Stability Across PHP Upgrades

One of the key strengths of Sukarix, inherited from Fat-Free Framework, is its stability across PHP upgrades. The framework is designed to be robust and reliable, ensuring that your code remains functional and intact even as PHP evolves. This minimizes the need for extensive refactoring and reduces the risk of breaking changes during upgrades.

Robustness

Sukarix offers a robust foundation for building applications. It is designed to handle various use cases and edge scenarios gracefully, ensuring that your application remains stable and secure under different conditions. This robustness comes from years of development and real-world use, making it a trustworthy choice for enterprise applications.

Resource Efficiency

Sukarix is built to be resource-efficient. It has a minimal footprint and is optimized for performance, ensuring that it consumes fewer resources compared to heavier frameworks. This efficiency translates to faster response times and lower operational costs, making it ideal for high-performance applications.

Usability and Practicality

The Fat-Free Framework, which forms the backbone of Sukarix, boasts a well-established history of stability and performance. Over the years, it has been employed in numerous projects, showcasing its reliability and versatility. Sukarix capitalizes on this robust foundation, enhancing it with additional features to meet modern development needs.

Proven Track Record

Fat-Free Framework, the foundation of Sukarix, has a proven track record of stability and performance. It has been used in numerous projects over the years, demonstrating its reliability and versatility. Sukarix builds on this solid foundation, offering additional features and enhancements that cater to modern development needs.

Features

Introduction

Sukarix builds upon the robust and lightweight foundation of the Fat-Free Framework (F3), a micro-framework known for its efficiency and simplicity. We have retained the best features of F3 where it excels, ensuring a solid, stable, and high-performance core. At the same time, we have integrated or replaced certain components with more powerful and feature-rich libraries to make Sukarix enterprise-ready. These enhancements provide additional functionality, improve scalability, and offer a more comprehensive development experience for building complex, modern PHP applications.

Feature List

Feature NameF3 NativeSukarix Replacement (Library)
Fast and clean template engineYes-
Unit testing toolkit (Web UI & CLI)YesStatera
Database-managed sessionsYes-
Markdown-to-HTML converterYes-
Atom/RSS feed readerYes-
Image processorYes-
Geodata handlerYes-
On-the-fly Javascript/CSS compressorYesMatthias Mullie Minify
OpenID (consumer)Yes-
Custom loggerYesMonolog
Basket/Shopping cartYes-
Pingback server/consumerYes-
Unicode-aware string functionsYes-
SMTP over SSL/TLSYesF3 Mailer
Tools for communicating with other serversYes-
Data ValidationYesSukarix extends F3
ORM-F3 Cortex
ACL-F3 Access
Multi-language-F3 Multilang
Event Dispatching-F3 Events
Messages to chat platforms-Sukarix Notifier (Zulip)
Database Migrations-Phinx
Debugging & Global Event Catching-Tracy
Cron job scheduler-peppeocchi/php-cron-scheduler
Queues-Sukarix (Redis)
AMQP messaging-Sukarix (php-amqplib)
Pluggable session-Sukarix
Dependency Injection-Sukarix
Inversion of Control-Sukarix
JSON APIs (CORS, health probes)-Sukarix
Capability tokens-Sukarix (HMAC)
Secrets at rest-Sukarix (libsodium)
Request correlation & JSON logs-Sukarix + Monolog
Environment-variable overrides-Sukarix
Privilege discovery-Sukarix
Bash toolbox-Sukarix CLI

Description of Features

  • Fast and clean template engine: Provides a lightweight and efficient templating system that allows for easy creation of dynamic web pages.
  • Unit testing toolkit (Web UI & CLI): Integrated tools for unit testing both web interfaces and command-line interfaces, ensuring comprehensive code coverage and reliability. Statera is the Sukarix testing kit built on top of the Fat-Free Framework’s Test class. It is an optional, standalone package — the framework itself uses it via require-dev for its own test suite.
  • Database-managed sessions: Manages user sessions through the database, enhancing security and scalability.
  • Markdown-to-HTML converter: Converts Markdown syntax into HTML, facilitating easy content creation and management.
  • Atom/RSS feed reader: Parses and reads Atom and RSS feeds, allowing for integration with external content sources.
  • Image processor: Handles image manipulation tasks such as resizing, cropping, and format conversion.
  • Geodata handler: Manages geographical data, enabling features such as location-based services and mapping.
  • On-the-fly Javascript/CSS compressor: Minifies JavaScript and CSS files on the fly to improve page load times and performance.
  • OpenID (consumer): Supports OpenID authentication, allowing users to log in using their existing OpenID accounts.
  • Custom logger: Enhanced logging capabilities using Monolog, providing flexible and powerful logging options.
  • Basket/Shopping cart: Manages e-commerce functionalities such as shopping carts and order processing.
  • Pingback server/consumer: Implements pingback functionality for communication between blogs and websites.
  • Unicode-aware string functions: Handles string operations with full Unicode support, ensuring compatibility with multiple languages and character sets.
  • SMTP over SSL/TLS: Securely sends emails using SMTP with SSL/TLS encryption, enhancing email security.
  • Tools for communicating with other servers: Provides utilities for making HTTP requests and interacting with external APIs.
  • Data Validation: Sukarix extends F3 to validate input data, ensuring it meets specified rules and constraints before processing.
  • ORM: Uses F3 Cortex for object-relational mapping, simplifying database interactions.
  • ACL: Manages access control using F3 Access, enforcing permissions and roles.
  • Multi-language: Supports multiple languages using F3 Multilang, allowing for easy localisation of content and routes.
  • Event Dispatching: Handles event management using F3 Events, allowing for flexible and decoupled event-driven architecture.
  • Messages to chat platforms: Sends notifications to Zulip chat streams using the Sukarix Notifier, which uses F3’s Web class for HTTP requests — no external notification library required.
  • Database Migrations: Manages database schema changes using Phinx, ensuring smooth and controlled migrations.
  • Debugging & Global Event Catching: Utilizes Tracy for debugging and catching global events, providing detailed error reports and insights.
  • Cron job scheduler: Schedules and manages cron jobs using peppeocchi/php-cron-scheduler, automating repetitive tasks.
  • Queues: A Redis-backed FIFO queue service with deduplication and per-item attempt tracking, for passing work between the stages of a pipeline.
  • Pluggable session: The framework depends on a small session contract rather than on the database-backed implementation, so an application can supply its own — a stateless JWT session for an API, for instance — and register it under the session alias.
  • Dependency Injection: Facilitates dependency management using Sukarix’s own DI container, promoting loose coupling and testability.
  • Inversion of Control: Implements IoC using Sukarix, ensuring modular and maintainable code.
  • AMQP messaging: A CommandPublisher contract with a RabbitMQ implementation and an in-memory recorder for tests, for telling another service to do something. See Messaging.
  • JSON APIs: A stateless ApiAction base class with JSON error envelopes, CORS handling and liveness/readiness probes. See Building JSON APIs.
  • Capability tokens: HMAC-signed, short-lived Bearer tokens carrying iss/aud/exp claims, for machine callers that hold no session.
  • Secrets at rest: SecretBox, a libsodium wrapper for encrypting credentials before they are stored. See Encrypting Secrets at Rest.
  • Request correlation & JSON logs: Every request is stamped with an X-Request-Id that flows into each log line; log.json switches the logger to structured JSON output. See Logging.
  • Environment-variable overrides: The [environment] configuration section maps a variable to the setting it stands in for, so secrets stay out of committed files. See Configuration.
  • Privilege discovery: The privileges an application declares are read from its action classes, so a newly added action never silently disappears from the role matrix. See Authorisation.
  • Bash toolbox: The sukarix command deploys, restarts, tests and cleans a Sukarix application from the shell. See CLI toolbox.

Understanding Fat-Free Framework

Introduction

Fat-Free Framework (F3) is a powerful yet lightweight PHP framework designed to help developers build dynamic web applications quickly and efficiently. One of the core concepts in F3 is the “Hive,” which is a centralized place where the framework stores and manages all application data.

The Hive and F3 Class

The Hive

The Hive is a key-value store that acts as the central repository for all configuration settings, routing information, session data, and more. It is accessible throughout the application, making it easy to manage and retrieve necessary data from anywhere in your code.

The F3 Class

The F3 class is a singleton, meaning there is only one instance of this class throughout the application’s lifecycle. This singleton instance is accessible from anywhere in your application, providing a consistent and centralized way to interact with the framework’s core functionalities.

How It Works

  1. Initialization: When the application starts, the F3 singleton is created and the Hive is initialized with system variables and configurations.
  2. Routing: F3 uses the Hive to manage routes, mapping URLs to specific functions or controllers.
  3. Session Management: The framework uses the Hive to store and manage session data, ensuring user states are maintained across different requests.
  4. Configuration: All configuration settings are stored in the Hive, making it easy to adjust and retrieve settings as needed.
  5. Template Rendering: F3’s template engine uses the Hive to access variables and directives needed to render views dynamically.

More Resources

To dive deeper into how Fat-Free Framework works and to explore its extensive features, refer to the following resources:

User Guide

For a comprehensive understanding of F3, including installation, basic usage, and advanced features, consult the User Guide.

API Reference

The API Reference provides detailed information on all the classes, methods, and properties available in F3, making it an essential resource for developers looking to utilize the framework to its fullest potential.

System Variables

System variables are predefined variables that F3 uses to manage various aspects of the application. For a quick reference on these variables, check out the System Variables page.

Template Directives

F3’s template engine comes with a set of powerful directives that make it easy to render dynamic content. You can find a list of these directives and how to use them on the Template Directives page.

By leveraging these resources, you can gain a thorough understanding of the Fat-Free Framework and harness its full potential to build efficient, scalable, and robust PHP applications.

Instructions for a new Sukarix application

Introduction

To set up your Sukarix application with Vagrant follow these steps:

Step 1: Create a new Sukarix project

Run the following command to create a new Sukarix project using Composer:

composer create-project sukarix/application

Tip

Using development version

It is also possible to create a new Sukarix project using the current development version of the framework by adding --stability=dev to the command.

composer create-project --stability=dev sukarix/application

Step 2: Navigate to the project directory

Once the project is created, navigate to the project directory:

cd application

Step 3: Initialize vagrant

The Sukarix project includes a Vagrant setup to help streamline the development environment. Initialize Vagrant by running:

vagrant up

This command will set up the Vagrant environment and automatically install PostgreSQL and Redis.

Step 4: Access the Application

After the Vagrant setup is complete, the application will be accessible via:

http://sukarix.test

Configuration Details

  • PostgreSQL and Redis are installed by default in the current version of the Sukarix project.
  • Ensure your local environment is configured to handle .test domains (you might need to adjust your hosts file if necessary).

Accessing the Application

After running vagrant up, you can access your application at http://sukarix.test. If you encounter issues accessing the site, ensure your system’s hosts file includes the correct entry, typically something like:

192.168.56.100  sukarix.test

Configuration

Configuration Files

Sukarix is configured via .ini files by default, allowing for clear and manageable settings. The configuration load order ensures that settings can be overridden based on the environment.

Configuration Load Order

  1. Base Configuration:

    • config/classes.ini
    • config/default.ini
  2. Additional Configurations:

    • Files listed in the CONFIGS variable, e.g., smtp, notifications, upload. No need to add .ini extension.
    • config/validation.ini (loaded only when a data validation request is initiated).
  3. Environment-Specific Configuration:

    • config/config-<environment>.ini
  4. Routing and Access Control:

    • config/routes.ini
    • config/routes-<environment>.ini
    • config/access.ini or config/access-cli.ini (based on environment).

Note

Dynamic reconfiguration

Configuration changes are automatically reloaded, meaning there is no need to restart any service.

Tip

Automatic .ini Extension Handling

When specifying additional configuration files in the CONFIGS variable, you do not need to include the .ini extension. Sukarix will automatically append .ini to the file name during the loading process. For example, if you set CONFIGS=notifications,smtp,upload, Sukarix will load config/notifications.ini, config/smtp.ini, and config/upload.ini.

Environment Overrides

Any configuration setting can be overridden in the environment-specific .ini file (config/config-<environment>.ini). This allows for tailored configurations depending on whether the application is in development, testing, staging or production.

Environment Variables

A setting that differs per host — and especially a secret — does not belong in a file that is committed. The [environment] section declares which variable stands in for which setting, and the framework applies them after the configuration is loaded:

[environment]
APP_SMTP_HOST     = mailer.smtp.host
APP_SMTP_USERNAME = mailer.smtp.user
APP_SMTP_PASSWORD = mailer.smtp.pw
APP_DB_DSN        = db.dsn

A variable that is unset or empty leaves the configured value alone, so a deployment overrides only what it needs to and the .ini files keep working as defaults.

Tip

Where to declare the section

Put it in the file that owns the settings it maps — mail variables in smtp.ini, database variables in default.ini. The section is read from the merged configuration, so several files may each contribute one.

Default Configuration Example

Here is an overview of the default settings found in default.ini:

  • Global Settings:

    • DEBUG: Stack trace verbosity.
    • PACKAGE: Display name in requests.
    • LOGS: Custom log location.
    • TEMP: Temporary folder for cache and compiled templates.
    • UPLOADS: Directory for file uploads.
    • UI: Paths for user interface files.
    • LOCALES: Location of language dictionaries.
    • ENCODING: Default character encoding.
    • LANGUAGE: Active language.
    • FALLBACK: Fallback language.
  • Cache and Minification:

    • CACHE: Cache configuration.
    • SEED: Cache seed.
    • MINIFY_JS and MINIFY_CSS: Toggle minification.
  • Timezone and Environment:

    • TZ: Timezone setting.
    • application.environment: Current environment.
    • log.session: Log session queries.
    • session.table: Table for session storage.
    • pagination.limit: Default pagination limit.
    • template.default: Default view to render.
    • log.keep: Log retention period.
    • server.host: Host for command-line actions.
    • error.channel: Error notification channel.
  • Security:

    • SECURITY.csrf.enabled: Enable or disable CSRF protection.
    • SECURITY.csrf.expiry: CSRF token expiry time in seconds.
    • SECURITY.stateless.prefixes: Route prefixes served without a session, /api by default. See Session.
    • api.cors.origins: Origins allowed to call a JSON API endpoint. See Building JSON APIs.
  • Logging:

    • log.json: Emit structured JSON log lines to stdout instead of the line-formatted log file. See Logging.
  • Mail:

    • mailer.smtp.timeout: Seconds to wait for the SMTP server, 2 by default.
    • mailer.debugger_name: Name exception reports are sent under. See Emails.
  • ORM:

    • orm.mask_deprecations: Keeps a deprecation in Cortex from ending every request on PHP 8.5. Enabled by default; set to false once Cortex is fixed.

Directory Structure

Structure

Understanding the directory structure of a Sukarix application is crucial for effective development and maintenance. Below is an overview of the typical directory layout and the purpose of each directory and file.

app
├── config
│   ├── access.ini
│   ├── access-cli.ini
│   ├── classes.ini
│   ├── config-development.ini
│   ├── config-test.ini
│   ├── default.ini
│   ├── notifications.ini
│   ├── routes.ini
│   ├── routes-cli.ini
│   ├── routes-test.ini
│   ├── smtp.ini
│   ├── upload.ini
│   ├── validation.ini
├── i18n
├── src
├── templates
data
db
├── data
├── migrations
├── seeds
logs
public
├── css
├── js
├── index.php
├── minified
uploads
tmp
├── cache
├── mail
tests
tools
vendor
.php-cs-fixer.php
phinx.json

Directory and File Descriptions

app

This is the main application directory containing configuration files, source code, templates, and localization files.

config

Contains various configuration files for the application.

  • access.ini: Defines access control rules for web applications.
  • access-cli.ini: Defines access control rules for CLI applications.
  • classes.ini: Configuration for class mappings and dependencies.
  • config-development.ini: Configuration specific to the development environment.
  • config-test.ini: Configuration specific to the test environment.
  • default.ini: Default configuration settings.
  • notifications.ini: Settings for notifications (e.g., email, chat integrations).
  • routes.ini: Defines routes for web applications.
  • routes-cli.ini: Defines routes for CLI applications.
  • routes-test.ini: Defines routes for test environments.
  • smtp.ini: Configuration for SMTP settings.
  • upload.ini: Settings for file uploads.
  • validation.ini: Configuration for data validation.

i18n

Directory for internationalization (i18n) files, containing localization strings for different languages.

src

Contains the source code for the application, including controllers, models, and other classes.

templates

Directory for template files used by the application. These are typically .phtml files that define the HTML structure and content.

data

Directory for storing the SQLite database files.

db

Contains database-related files and directories.

  • data: Directory for storing the SQLite database files.
  • migrations: Directory for database migration scripts.
  • seeds: Directory for database seed files.

logs

Directory for log files generated by the application.

public

The web root directory containing publicly accessible files.

  • css: Directory for CSS files.
  • js: Directory for JavaScript files.
  • index.php: The entry point for the application.
  • minified: Directory for minified JavaScript and CSS files.

uploads

Directory for user-uploaded files.

tmp

Directory for temporary files.

  • mail: Directory for emails sent in development and test environments, stored as files.
  • cache: Directory for cache files.

tests

Directory for unit tests and testing-related files.

tools

The Statera test runner and the shell toolbox.

vendor

Directory for Composer dependencies.

.php-cs-fixer.php

Configuration file for PHP-CS-Fixer, a tool for automatically fixing PHP coding standards issues.

phinx.json

Configuration file for Phinx, a PHP database migration tool.

This directory structure helps to keep your application organized and maintainable, ensuring that related files and configurations are logically grouped together.

Terminology

Hive

A central place where Fat-Free Framework (F3) stores and manages application data, including configuration settings, routing, and session information. The Hive acts as a global registry, making data accessible throughout the application.

Configuration File

Refers to any ini configuration file used to define application settings and dependencies. This file helps in managing the application’s configuration in a structured and centralized manner.

Action

Refers to action-wise micro-controllers inspired by ForkCMS. These are responsible for handling specific tasks or requests within the application, typically mapped to routes and executed to perform a particular function.

Bootstrapping

The application initialization phase where the framework, akin to the kernel in other frameworks, loads configurations, sets up the environment, and prepares the application for handling requests.

Environment

The automatically detected application environment, which can be test, development, staging or production. This setting can be overridden to suit specific needs during development or deployment.

Injector

A component responsible for injecting and initializing Inversion of Control (IoC) classes defined in the configuration file. It manages dependencies and ensures that required services are available throughout the application.

Processor

A component responsible for applying behaviors or traits to classes. It ensures that specific functionalities or attributes are consistently applied across relevant classes.

Session

The class responsible for managing user sessions, including session creation, data storage, and session termination.

ErrorChannel

The global exception handling component used to manage and route errors. It supports multiple channels like Zulip and email for notifying about exceptions.

User Role

Defines the various roles within the application, such as visitor, admin, customer, or api user. These roles determine the level of access and permissions for different parts of the application.

Assets

The class responsible for injecting JavaScript and CSS assets into views, either from the action or directly within the view itself.

View

Refers to the F3 template view files, typically ending with .phtml, used to render the user interface.

Flash

Contains notifications to be displayed in the frontend. It manages temporary messages that inform users about the status of their actions.

I18n

The class that contains different types of messages depending on the localization settings. It supports internationalization and helps manage translations.

MailSender

The class responsible for sending emails within the application. It handles email composition, configuration, and delivery.

Model

A Cortex class with additional behaviors, used to represent and manage the application’s data layer. It includes ORM features for interacting with the database.

Notifier

The component responsible for sending notifications to third parties, such as Zulip, Slack, or email, ensuring that important events are communicated effectively.

Constraints

The class responsible for validating data inputs within the application. It ensures that data conforms to specified rules and constraints before processing.

Tailored

The Tailored class is a singleton that extends F3’s \Prefab, ensuring only one instance of the class exists. It has the ability to glue trait behaviour to a class while maintaining its singleton nature and integrating it into the dependency injection registry.

Template Directive

Directives used within F3 templates to insert dynamic content, control flow, and manage template inheritance. These directives allow developers to create flexible and reusable templates for rendering views. For a detailed list and explanation of template directives, refer to the F3 Quick Reference.

Application Lifecycle

Entry-point

The entry point for a Sukarix application is public/index.php.

This is where initial detection happens to determine if unit tests need to be run in the browser, a process that will be covered in detail later.

Once the Application is instantiated, the following steps occur:

1. Detect if the Application is Running in CLI Mode

The application first checks if it is being executed from the command line (CLI) or through a web server. This allows for conditional loading of CLI-specific configurations and behaviors.

2. Create an Instance of the Fat-Free Framework (F3) Singleton Class

An instance of the F3 singleton class is created. This instance acts as the central hub for managing application configuration, routing, and other core functionalities.

3. Set Up PHP Variables

The application sets up necessary PHP variables, including error reporting levels, timezone settings, and memory limits, to ensure a consistent execution environment. Up to the developer to override it.

4. Automatically Detect the Application Environment

The application detects the current environment (test, development, staging or production) from APP_ENV, or from the hostname when it is unset. This setting can be overridden if needed, and it dictates how the application behaves, including error reporting and debugging levels.

5. Load the Configuration Files

The application loads its configuration settings from ini configuration files. These files define various parameters and settings required for the application’s operation.

6. Set Up Logging

Logging is set up to capture and record important events, errors, and performance metrics. This is crucial for monitoring the application and diagnosing issues.

7. Add the Global Exception Handler

A global exception handler is added to catch and manage any unhandled exceptions, ensuring that errors are logged and appropriate responses are provided without crashing the application.

8. Connect to the Database

The application establishes a connection to the database using the settings defined in the configuration files. This connection is essential for data storage and retrieval.

9. Prepare the Session

The session handler is initialized to manage user sessions, including creating, storing, and terminating sessions. This is important for maintaining user state across requests.

10. Load Additional Application Settings

Additional settings and configurations specific to the application are loaded. These can include custom parameters, feature toggles, and other application-specific settings.

11. Load Additional CLI Configuration if CLI Mode is Detected

If the application is running in CLI mode, additional configurations specific to the command-line environment are loaded. This ensures that CLI commands have the necessary settings and resources.

12. Load Routing and Access Control Lists (ACL)

The routing configuration and ACLs are loaded to define how incoming requests are handled and to enforce access permissions. This ensures that routes are correctly mapped to actions and that users have appropriate access rights.

13. Execute the Start Function

The start function of the application is executed, which in turn calls the F3 run function. This kicks off the application, allowing it to handle incoming requests or execute CLI commands.

14. Log Performance Metrics and Finish

Finally, the application logs performance metrics for the entire lifecycle process. This includes timing information and resource usage statistics, which are useful for performance monitoring and optimization.

This comprehensive sequence ensures that the Sukarix application is thoroughly initialized, configured, and prepared to handle both web requests and CLI commands efficiently and reliably.

Dependency Injection

Overview

Dependency Injection in Sukarix is facilitated through the singleton class Injector::instance()->get('access'). This method allows for easy and efficient management of dependencies within your application.

Example Usage

In this example, access is the default ACL Middleware tied to f3-access:

$access = Injector::instance()->get('access');

Additionally, the Injector can find an object from its class directly:

$multiLang = Injector::instance()->get(\Multilang::class);

Configuration & Default Dependencies

These default classes can be easily changed or overridden by updating the classes.ini file. The default configuration is in the table below:

KeyClass
mailerSukarix\Mail\MailSender
notifierSukarix\Notification\Notifier
sessionSukarix\Core\Session
i18nSukarix\Helpers\I18n
assetsSukarix\Helpers\Assets
access\Access
eventsSugar\Event
template\Template
htmlSukarix\Helpers\HTML
multi_language\Multilang
receiverSukarix\Helpers\Receiver

Creating Injectable Singletons

For classes that should be singletons with behaviors, they must extend the Sukarix\Core\Tailored class and call Processor::instance()->initialize($this) in their constructor. The Helper base class already does this, so classes extending Helper only need to call parent::__construct(). More details on these behaviors can be found in the Behaviors section of the documentation.

Note

Instantiating an injectable singleton object

There is no need to manually instantiate the injected object, Sukarix will do it for you.

Storing DI Classes

The F3 \Registry class is used to store these dependency-injected classes, ensuring they are easily accessible throughout your application. However, it is recommended to use the Injector class that already wraps this functionality, as it provides additional features and should be preferred over direct usage of \Registry.

Routing and Access Control

Introduction

Sukarix leverages the powerful routing and access control capabilities of the Fat-Free Framework (F3) to manage web and CLI application routes. The configuration for routing and access control is handled through routes.ini and access.ini for web applications, and routes-cli.ini and access-cli.ini for CLI applications. Additionally, multi-language routing is supported via f3-multilang.

Routing

Concepts

Routing in Sukarix allows you to map URLs to specific actions in your application. This is managed through configuration files where you define patterns for web and CLI routes. Key concepts include:

  • HTTP Methods: Define the HTTP methods (GET, POST, PUT, DELETE, etc.) that the route will respond to.
  • Route Aliases: Provide named routes for easier reference.
  • Tokens: Use tokens to capture parts of the URL (e.g., /users/@id).
  • Modifiers: Use [ajax] for AJAX requests and [cli] for CLI routes.
  • Asterisk (*): Use wildcards to match any subpath.
  • Named Routes: Assign names to routes for easy referencing.
  • Rerouting: Redirect requests to different routes or actions.
  • ReST: Implement RESTful APIs using standard HTTP methods.
  • Multi-language Routing: Support multiple languages using f3-multilang.

Web Application Routing (routes.ini)

In Sukarix, routes for web applications are defined in the routes.ini file.

Example Configuration

[routes]
; default route
GET      @home        : /                          = Actions\Core\Main->execute
GET      @docs        : /docs/*                    = Actions\Core\Docs->execute
POST|PUT @set_locale  : /set-locale/@locale [ajax] = Actions\Account\SetLocale->execute
DELETE   @user_delete : /users/@id                 = Actions\Users\Delete->execute

Explanation

  • GET @home : / = Actions\Core\Main->execute

    • GET: The HTTP method for the route.
    • @home: The route alias.
    • /: The URL pattern for the home page.
    • = Actions\Core\Main->execute: The action and method that handle the request.
  • GET @docs : /docs/* = Actions\Core\Docs->execute

    • GET: The HTTP method for the route.
    • @docs: The route alias.
    • /docs/*: The URL pattern with a wildcard to match any subpath.
    • = Actions\Core\Docs->execute: The action and method that handle the request.
  • POST|PUT @set_locale : /set-locale/@locale [ajax] = Actions\Account\SetLocale->execute

    • POST|PUT: The HTTP methods for the route.
    • @set_locale: The route alias.
    • /set-locale/@locale: The URL pattern with a token (@locale).
    • [ajax]: Modifier indicating the route should handle AJAX requests.
    • = Actions\Account\SetLocale->execute: The action and method that handle the request.
  • DELETE @user_delete : /users/@id = Actions\Users\Delete->execute

    • DELETE: The HTTP method for the route.
    • @user_delete: The route alias.
    • /users/@id: The URL pattern with a token (@id).
    • = Actions\Users\Delete->execute: The action and method that handle the request.

CLI Application Routing (routes-cli.ini)

In Sukarix, routes for CLI applications are defined in the routes-cli.ini file.

Example Configuration

[routes]
; route to the main task scheduler
GET @run_job : /cli/jobs/run       [cli] = Actions\Jobs\TaskScheduler->execute
GET @logs_clean : /cli/logs/clean     [cli] = Sukarix\Actions\Logs\Clean->execute
GET @sessions_clean : /cli/sessions/clean [cli] = Sukarix\Actions\Core\SessionsClean->execute

Explanation

  • GET @run_job : /cli/jobs/run [cli] = Actions\Jobs\TaskScheduler->execute

    • GET: The HTTP method for the route.
    • @run_job: The route alias.
    • /cli/jobs/run: The URL pattern for running jobs.
    • [cli]: Modifier indicating the route is for CLI mode.
    • = Actions\Jobs\TaskScheduler->execute: The action and method that handle the request.
  • GET @logs_clean : /cli/logs/clean [cli] = Sukarix\Actions\Logs\Clean->execute

    • GET: The HTTP method for the route.
    • @logs_clean: The route alias.
    • /cli/logs/clean: The URL pattern for cleaning logs.
    • [cli]: Modifier indicating the route is for CLI mode.
    • = Sukarix\Actions\Logs\Clean->execute: The action and method that handle the request.
  • GET @sessions_clean : /cli/sessions/clean [cli] = Sukarix\Actions\Core\SessionsClean->execute

    • GET: The HTTP method for the route.
    • @sessions_clean: The route alias.
    • /cli/sessions/clean: The URL pattern for cleaning sessions.
    • [cli]: Modifier indicating the route is for CLI mode.
    • = Sukarix\Actions\Core\SessionsClean->execute: The action and method that handle the request.

Multi-language Routing

Sukarix supports multi-language routing using f3-multilang. This allows you to define routes that adapt to different languages.

Configuration Example

[MULTILANG.languages]
en = en-GB
fr = fr-FR
ar = ar-AR
es = es-ES

[MULTILANG]
global = locale,/cli
  • [MULTILANG.languages]: Defines the languages and their respective locale codes.
  • [MULTILANG]: Specifies global aliases that are not localized by default.

Access Control

Access control in Sukarix is managed through access.ini and access-cli.ini files, defining policies and rules for both web and CLI applications.

Web Application Access Control (access.ini)

Sukarix uses the access.ini file to manage access control for web applications. The configuration defines policies and rules for access control using the F3-Access component.

Example Configuration

[ACCESS]
; deny all routes by default
policy = deny

[ACCESS.rules]
; Routes allowed to all type of users
deny  *      /* = *
allow GET    @home = *
allow GET    @docs = admin,customer
allow        @set_locale = *
allow DELETE @user_delete = admin

Explanation

  • policy = deny

    • Sets the default policy to deny access to all routes.
  • deny * /* = *

    • Denies access to all users for all routes by default.
  • allow GET @home = *

    • Allows access to the home route (@home) for all users.
  • allow GET @docs = admin,customer

    • Allows access to the documentation route (@docs) only for admin and customer roles.
  • allow @set_locale = *

    • Allows access to the set locale route (@set_locale) for all users.
  • allow DELETE @user_delete = admin

    • Allows access to delete user route (@user_delete) only for admin role.

CLI Application Access Control (access-cli.ini)

Sukarix uses the access-cli.ini file to manage access control for CLI applications.

Example Configuration

[ACCESS]
; allow all routes by default
policy = allow

Explanation

  • policy = allow
    • Sets the default policy to allow access to all CLI routes.

References

For more detailed information on F3’s routing and access control mechanisms, refer to the following resources:

Framework Variables

Fat Free Framework Variables

Detailed information on Fat-Free Framework variables can be found here.

Globals

Global variables and their usage are explained here.

Naming Rules

Naming conventions are outlined here. Sukarix follows these rules, using capital letters when possible. If F3 extensions deviate from this rule, they are used as is.

Sukarix Framework Variables

Fat-Free Framework system variables are listed here.

Example Sukarix framework variables:

MINIFY_JS

boolean     Default: TRUE in production configuration

Turns JavaScript minification on or off. Should be environment dependant.

MINIFY_CSS

boolean     Default in template application: TRUE in production configuration

Turns CSS minification on or off. Should be environment dependant.

CONFIGS

string     Default in template application: smtp,notifications,upload

List of configuration files to load.

application.environment

string

Sets the application environment. Recognised values are test, development, staging and production (Sukarix\Configuration\Environment).

Note

Sukarix Convention: Environment auto-detection

If the server hostname ends with .test the application.environment variable will automatically be set to development.

template.default

string     Default in template application: website

Default view to render.

log.keep

integer     Default in template application: 14

Days to keep logs before cleanup.

server.host

string     Default: NULL

Server host for command line actions.

error.channel

string     Default: NULL

Error notification channel (“email” or “zulip”).

Databases & Cortex

Database Configuration

For configuring database access, the framework follows the guidelines outlined by the Fat-Free Framework. This includes setting up database connections and handling queries efficiently.

Cortex ORM

The framework utilizes Cortex ORM for object-relational mapping, simplifying interactions with the database. For detailed usage and configuration instructions, refer to the Cortex documentation.

Database Migrations

Database migrations are managed using the Phinx library, which is included in the template application with a phinx.json file. This file can be customised, although Phinx is the recommended tool. Comprehensive documentation for Phinx is available here.

Model Class

The Model class provides a structured way to access data using Cortex ORM. It includes methods for pagination, data conversion, and change detection, ensuring robust and efficient data handling.

Note

Sukarix Convention: Model timestamps

The Model class follows conventions for managing timestamps:

  • created_on: Automatically set when a record is created.
  • updated_on: Automatically updated when a record is modified.

Reading a Record Back After an Insert

Model::insert() reads the record back when the insert left it blank, so the values the database assigned — the identifier, the defaults, the timestamps — are in memory when save() returns.

This is not cosmetic. PostgreSQL identity columns carry no nextval default, so Fat-Free does not recognise them as auto increment. The reload Cortex runs itself then filters on the identifier the record held before the insert, matches nothing, and leaves the whole record blank. Without the read-back, every model is empty immediately after being saved.

Note

Sukarix Convention: Identity columns

A schema created for PostgreSQL 10 or later normally uses GENERATED BY DEFAULT AS IDENTITY rather than a serial column, and is therefore affected. No application code is required — Model::insert() handles it.

Uniqueness Checks

Model::excludeId() adds an identifier exclusion to a filter, and drops the clause when there is no identifier to exclude:

// While creating a record, $id is null and the filter is left alone.
$taken = $this->load($this->excludeId(['lower(name) = ?', $name], $id));

Writing the clause by hand is a common and silent mistake:

// Wrong: matches nothing while creating a record.
$this->load(['lower(name) = ? and id != ?', $name, $id]);

Comparing against a null identifier yields NULL in SQL, not true, and NULL makes the whole condition unsatisfiable. The uniqueness check then passes for every record, and duplicates are accepted.

Caution

Uniqueness checks and null identifiers

Always build the exclusion with excludeId(). A hand-written and id != ? disables the check it is part of whenever the identifier is null.

Query Return Type

Cortex documents find() as returning an array of records; it returns a DB\CortexCollection. Model::find() declares the real type, so castAll() and the other collection methods resolve for static analysis and in an editor.

File Upload

Introduction

Sukarix\Helpers\Receiver handles file uploads: validating what the browser sent, checking the real MIME type and size, and moving the file into the uploads directory under a generated name. It covers both multipart form uploads and base64 data URIs (an image cropped in the browser, for instance).

Configuration

The helper is resolved through the Injector under the receiver alias, declared in classes.ini:

[classes]
receiver = Sukarix\Helpers\Receiver

Limits and allowed types live in upload.ini, keyed by MIME type:

[UPLOAD.allowed.mimes]
images[png]  = image/png
images[jpg]  = image/jpeg
images[webp] = image/webp

[UPLOAD.maxsize.image]
value    = 5
exponent = MB

The array key (png, jpg, …) becomes the extension of the stored file, so the extension always matches the detected content rather than whatever name the client chose. UPLOADS (see Configuration) is the directory every stored path is relative to.

Uploading a Base64 Image

uploadImageBase64() accepts a data URI or bare base64 string, decodes it, sniffs the MIME type of the decoded bytes and stores the file under UPLOADS/<sub directory>/<field name><hash>.<ext>:

/** @var Receiver $receiver */
$receiver = Injector::instance()->get('receiver');

$result = $receiver->uploadImageBase64($this->getDecodedBody()['avatar'], 'avatars/', 'avatar');
if (null !== $result['error']) {
    $this->error($this->i18n->err($result['error']), 422);
}

$path = $receiver->uploadedFiles()['avatar']; // 'avatars/avatar1cfmd20afirq4.png'

Every outcome is reported in the returned array rather than thrown. error is null on success, otherwise one of:

KeyMeaning
upload.invalid_formatNot valid base64, or the MIME type is not in the allow list
upload.exceeded_file_sizeLarger than UPLOAD.maxsize.image
upload.failed_to_moveThe file could not be written

The keys are translation keys by design: add them to your dictionaries under i18n.error and pass them straight to the user.

Uploading a Form File

upload() is the equivalent for a $_FILES entry. It is protected, so expose it from your own receiver with the directory, size limit and allow list your application wants:

namespace Helpers;

class Receiver extends \Sukarix\Helpers\Receiver
{
    public function storeDocument(array $file): array
    {
        return $this->upload(
            $file,
            $this->f3->get('UPLOADS') . 'documents',
            10 * 1024 * 1024,
            ['pdf' => 'application/pdf'],
            'document'
        );
    }
}

Register that subclass under the receiver alias instead of the framework’s. upload() rejects corrupt $_FILES entries (upload.invalid_parameters), maps PHP’s own upload errors (upload.no_file, upload.exceeded_file_size, upload.unknown), then applies the same MIME and size checks as above. Pass null for the size limit or the allow list to skip that check. Under Environment::isTest() the file is moved with rename() instead of move_uploaded_file(), so a test can hand it any temporary file.

uploadedFiles() returns every file stored by the receiver during the request, keyed by form field name, as paths relative to UPLOADS — ready to persist on a model.

Data Validation

Human-readable data validation syntax

Form validation is a crucial part of building robust web applications. Sukarix simplifies this process by allowing you to declare your validation rules in an easy-to-read configuration file. This page provides an overview of how to use Sukarix’s data validation capabilities.

Configuration

Validation rules are declared in an INI file. Here’s an example:

[CONSTRAINTS.users.edit]
email = email
username = length:4,32
role = in:Enum\UserRole
status = in:Sukarix\Enum\UserStatus

Using the Validator

To validate data, use the Constraints class in your controller or model. Below are various examples demonstrating different usages.

Example: Basic Usage

public function save($f3, $params): void
{
    $form = $this->getDecodedBody();

    Constraints::instance()->verify($form, 'users.edit');

    // Process the validated data
}

getDecodedBody() accepts a JSON body, a form-encoded body, or no body at all (falling back to POST), so the same action validates the same way whether it is called from an HTML form, a JSON API client, or a test.

Example: Multiple Rulesets

You can validate against multiple rulesets by separating them with a pipe (|).

public function save($f3, $params): void
{
    $form = $this->getDecodedBody();

    Constraints::instance()->verify($form, 'settings.save.default|users.edit');

    // Process the validated data
}

You can also pass ruleset names as an array.

public function save($f3, $params): void
{
    $form = $this->getDecodedBody();

    Constraints::instance()->verify($form, ['settings.save.default', 'users.edit']);

    // Process the validated data
}

Example: Individual Checks

You can also perform individual checks using the check method.

$filePath = '/path/to/file.css';
Constraints::instance()->check($this->f3->get('ROOT') . $filePath, 'file', true, 'css_file');

How Validation Works

  1. Audit Class:

    • If available, validation rules are first checked in the Audit class.
  2. Constraints Class:

    • If the rule is not found in the Audit class, it falls back to the Constraints class.
  3. Custom Validator:

    • If the rule is not found in the Constraints class, it checks for a user-defined validator class specified in the VALIDATOR configuration property.

Warning

Validation rule names are case insensitive

Validator names are case insensitive, meaning notempty, notEmpty, NotEmpty, and NOTEMPTY are treated the same.

Available Validation Rules

Here is a list of available validation rules that you can use in your INI configuration file or directly in your code:

  • email: Validates that the input is a valid email address.
  • length:min,max: Validates that the input length is between min and max.
  • in:Enum\ClassName: Validates that the input is one of the values in the specified enumeration.
  • notEmpty: Validates that the input is not empty.
  • file: Validates that the input is a valid file path.

Example: Custom Validator

You can define custom validation rules in your own validator class and configure Sukarix to use it.

class CustomValidator
{
    public static function isCustom($value)
    {
        // Custom validation logic
        return $value === 'custom';
    }
}

// Usage
$customRules = [
    'custom_field' => 'custom'
];

Constraints::instance()->verifyRules($form, $customRules, true);

Error Handling

When validation fails, the verify method throws an exception if the throwOnFail parameter is set to true.

try {
    Constraints::instance()->verify($form, 'users.edit', true);
} catch (\RuntimeException $e) {
    echo "Validation failed: " . $e->getMessage();
}

To get all validation errors, use the getErrors method.

$errors = Constraints::instance()->getErrors();
print_r($errors);

Combining Validators

You can combine multiple validation rules using the pipe (|) separator.

[CONSTRAINTS.users.edit]
email = email
username = length:4,32|notEmpty
role = in:Enum\UserRole|notEmpty
status = in:Sukarix\Enum\UserStatus|notEmpty
public function save($f3, $params): void
{
    $form = $this->getDecodedBody();

    Constraints::instance()->verify($form, 'users.edit');

    // Process the validated data
}

Session

Session Management

Session handler documentation can be found here.

By default, sessions are stored in the database but can be configured to use the cache system. This allows session data to be debuggable in non-production environments, while in production, storing sessions in cache enhances performance.

Caution

Issues with storing sessions in transactional database

Storing a session in the database can be problematic when using transactions, as the database connection is tied to that session. Rolling back or failing a transaction may result in failed updates to the session data. It’s important to carefully consider this when designing your application’s session management strategy.

Session Functions

The session management system includes the following functions:

  • authorizeUser: Authorizes a user and initiates their session.
  • revokeUser: Revokes a user’s session and logs them out.
  • getRole: Retrieves the role of the current user.
  • isRole: Checks if the current user has a specific role.

Supplying Your Own Session

Sukarix\Core\SessionInterface declares what the framework itself requires of a session. An application that needs a different implementation — a stateless JWT session for an API, for instance — implements the interface and registers it under the session alias in config/classes.ini:

[classes]
session = App\Core\Session

The contract is deliberately small. It covers only the methods Sukarix calls on $this->session, so an implementation is free to omit the database-backed extras such as isRole() or generateToken():

MethodPurpose
cleanupOldSessions()Discard sessions that are no longer current
set($key, $value)Store a runtime value
get($key)Read a runtime value
isLoggedIn()Whether a user is authenticated
getRole()Role name of the authenticated user, or the guest role
validateToken()Whether the request carries a valid anti-forgery token

A session that does not use anti-forgery tokens — because the caller authenticates with a bearer token that a browser will not send on its own — returns true from validateToken().

Note

Sukarix Convention: the contract is the framework’s requirement

SessionInterface is not the full surface of Sukarix\Core\Session; it is what the framework needs in order to run. The library’s own test suite asserts that every method Sukarix calls on a session is declared in the interface, so the two cannot drift apart.

Session-Free Routes

Routes under SECURITY.stateless.prefixes are served without opening a session. The default is /api, on the assumption that an API authenticates each request by its own means.

; Serve /api and /hooks without a session.
SECURITY.stateless.prefixes = /api,/hooks

An application whose API does rely on the session — one authenticating with a JWT session, for example — turns the behaviour off by declaring the setting empty:

; Every route opens a session.
SECURITY.stateless.prefixes =

Caution

Declaring the setting empty is not the same as omitting it

Leaving the key out entirely keeps the /api default. Writing it with nothing after the equals sign is how an application says that no route is session free.

Logging

Introduction

The framework uses Monolog by default for logging. To enable logging, use the LogWriter behavior trait in your class. The trait provides a $logger property — a Monolog logger accepting a message and a context array:

use Sukarix\Behaviours\LogWriter;

class MyService
{
    use LogWriter;

    public function process(string $id): void
    {
        $this->logger->info('Processing started', ['id' => $id]);
    }
}

Note

Initializing LogWriter

If the class does not extend the Tailored singleton class, ensure to call the initLogWriter method in the constructor.

Log Level

The minimum level is set with log.level (defaults to info):

[globals]
log.level = debug

Request Correlation

Every request is stamped with a request id: Boot forwards an incoming X-Request-Id header when the caller already sent one (useful behind a reverse proxy or an upstream service), or mints one otherwise, and echoes it back on the response. The id is available in the hive as application.request_id and is attached to every log line automatically via Sukarix\Observability\RequestIdProcessor:

[203.0.113.7] [2026-09-14 10:15:03.120] [POST] app.INFO: Processing started {"id":"42"} {"request_id":"a1b2c3d4e5f6a7b8"} []

Forward the same header when your application calls another service so a single request can be traced across all of them.

Structured (JSON) Logging

By default LogWriter writes the line-formatted log described above. Set log.json (or the LOG_JSON environment variable) to emit JSON lines to stdout instead — the shape a log collector (Loki, CloudWatch, an ELK stack) expects:

[globals]
log.json = true
{"message":"Processing started","context":{"id":"42"},"level":200,"level_name":"INFO","channel":"MyService","datetime":"2026-09-14T10:15:03.120000+00:00","extra":{"request_id":"a1b2c3d4e5f6a7b8"}}

All channels share one underlying Monolog logger (Sukarix\Observability\LoggerFactory), so every class using LogWriter writes to the same handler with a consistent format.

Console Output

CLI applications can mirror the log to standard output by enabling log.console. This has no effect when log.json is on — JSON mode already writes to stdout, so there is nothing left for the console handler to add.

[globals]
log.console = true

When the optional bramus/monolog-colored-line-formatter package is installed, console output is coloured per level; otherwise a plain stream is used. The file handler stays active either way, so enabling this adds console output rather than replacing the log file.

Logging Naming Conventions

Logs follow these naming patterns:

  • Application Logs: app-yyyy-mm-dd.log
  • Application Error Logs: app-error-yyyy-mm-dd.log
  • CLI Logs: cli-yyyy-mm-dd.log
  • CLI Error Logs: cli-error-yyyy-mm-dd.log
  • Exception Logs: exception.log

Log Rotation

Logs are kept for 14 days by default, configurable via the log.keep setting (an integer in days). The Sukarix\Actions\Logs\Clean action is responsible for rotating and cleaning logs.

Debugging

Introduction

Sukarix uses the Tracy library for debugging. Tracy offers powerful tools to help you debug your applications efficiently.

Basic Usage

To use Tracy, simply call the following functions in your code:

  • Dumping Variables:
    • dump($variable) or dumpe($variable)
    • Debugger::dump($variable)
    • Debugger::barDump($variable)

For more detailed usage and advanced features, refer to the Tracy documentation.

Emails

MailSender Class

The MailSender class in Sukarix is used to send emails with a predefined template. The primary method for sending emails is send(), which has the following signature:

send($template, $vars, $to, $title, $subject): bool

Method Parameters

  • $template: The name of the email template, which must be located in the /mail folder.
  • $vars: An array of variables to be used within the template.
  • $to: The recipient’s email address.
  • $title: The title of the email.
  • $subject: The subject line of the email.

Usage Example

Here’s how you might use the send() method to send an email:

/**
 * @var MailSender $mailSender
 */
$mailSender = Injector::instance()->get('mailer');
$template = 'welcome';
$vars = ['name' => 'John Doe', 'link' => 'https://example.com'];
$to = 'johndoe@example.com';
$title = 'Welcome to Our Service';
$subject = 'Getting Started with Our Service';

$mailSender->send($template, $vars, $to, $title, $subject);

This example sends a welcome email using the welcome template, passing in the recipient’s name and a link to be used within the email body.

Sender Identity

Three settings decide who the mail comes from, and they are deliberately separate:

SettingMeaning
mailer.smtp.userThe account the application signs in to the server with
mailer.from_mailThe address the mail comes from
mailer.from_nameThe name shown beside that address

A relay such as Postal authenticates as one identity and sends as another, so the account is not the address. The sender name is put on the message itself; without it the mail arrives showing a bare address.

[mailer]
smtp.host = postal.example.org
smtp.port = 587
smtp.user = rooms-app-7f3c
smtp.pw   = …
from_mail = notifications@rooms.example.org
from_name = Example Rooms

Unreachable Servers

The SMTP transport aborts the whole request when the server cannot be reached — not an exception the application can catch, the request simply dies. MailSender therefore checks that the server accepts a connection before handing the message over, logs the failure and returns false, leaving the caller to report it:

if (!$mailSender->send($template, $vars, $to, $title, $subject)) {
    // The message was not sent; the reason is in the log.
}

The wait is two seconds by default and is configurable with mailer.smtp.timeout.

Delivery & Tracking Hooks

MailSender sits on ikkez/f3-mailer, which calls back into the application on three events — wired in smtp.ini:

[mailer]
on.failure = \Sukarix\Mail\Track::logError
on.ping    = \Sukarix\Mail\Track::traceMail
on.jump    = \Sukarix\Mail\Track::traceClick

Sukarix\Mail\Track is the default implementation, logging all three to the mail channel. An application that wants its own behaviour points these at its own class instead — the callback just needs the same three static methods.

  • on.failure fires when the SMTP transport reports a failed send; the callback receives the Mailer instance and the SMTP transcript.
  • on.ping fires when a tracking pixel embedded in a sent mail is loaded; the callback receives the tracking hash.
  • on.jump fires when a tracked link in a sent mail is followed, just before the browser is redirected to the real target; the callback receives the target URL.

fatfree-core’s SMTP client echoes the AUTH LOGIN/AUTH PLAIN exchange into the transcript as bare base64 lines — the account’s username and password, not encrypted, only encoded. Track::logError() strips those lines before writing the transcript to the log, so a relay account’s credentials never end up there even when a send fails.

Exception Reports

When error.channel is set to email, an uncaught exception is reported to debug.email. Repeats of the same exception are snoozed for a day so a failing deployment does not flood the inbox. The report is sent under mailer.debugger_name, which defaults to Application Debugger:

[mailer]
debugger_name = Example Rooms Debugger

Naming it after the application makes the report recognisable in an inbox that receives several.

Notifications

Notifier Class

The Notifier class in Sukarix is designed to send notifications about exceptions that occur in the application. This class extends the Tailored class and uses the HasF3 and LogWriter traits.

Methods

notifyException

The notifyException method is responsible for sending an exception notification. It generates a unique error ID, creates a notification message, and sends it to a specified Zulip stream.

Method Signature:

public function notifyException($exception): void

Parameters:

  • $exception: An instance of \Exception containing the exception details to be notified.

Usage

Here is an example of how to use the notifyException method:

/**
 * @var Notifier $notifier
 */
$notifier = Injector::instance()->get('notifier');
try {
    // Code that may throw an exception
} catch (\Exception $e) {
    $notifier->notifyException($e);
}

Configuration

Ensure the following configuration settings in your .ini file:

[globals]
; error notification channel "email" or "zulip"
error.channel = zulip

[NOTIFICATIONS.zulip]
token = "your-zulip-token"
mail = "your-zulip-email"
uri = "your-zulip-uri"
stream = "your-zulip-stream"
topic = "your-zulip-topic"

Translation / i18n

Localisation Helper Class

The I18n class in Sukarix provides methods to retrieve localized strings from different translation tables. There are four types of translation tables:

  1. Labels (i18n.label)
  2. Messages (i18n.message)
  3. Errors (i18n.error)
  4. Lists (i18n.list)

Methods

lbl

Fetches a localized label.

public function lbl($key): string

msg

Fetches a localized message.

public function msg($key): string

err

Fetches a localized error message.

public function err($key): string

lst

Fetches a localized list.

public function lst($key): array

negotiateLocale

Picks the best supported locale for an Accept-Language header, for a first visit before the user has chosen one:

public static function negotiateLocale(array $languages, string $acceptLanguageHeader, string $fallback): string

$languages is the MULTILANG.languages map (short code => full locale) and $fallback a short code or full locale from it. Candidates are tried by descending quality — an exact tag (fr-FR) first, then the language alone (fr → fr-FR); a q=0 candidate is treated as explicitly refused. When nothing matches, the fallback is resolved to its full locale:

$locale = I18n::negotiateLocale(
    $f3->get('MULTILANG.languages'),
    (string) $f3->get('HEADERS.Accept-Language'),
    $f3->get('FALLBACK')
);

Example Usage

$i18n = Injector::instance()->get('i18n');
echo $i18n->lbl('welcome'); // Fetches the 'welcome' label
echo $i18n->msg('greeting'); // Fetches the 'greeting' message
echo $i18n->err('not_found'); // Fetches the 'not_found' error message
print_r($i18n->lst('countries')); // Fetches the 'countries' list

locales.js

The locales.js file handles loading and switching of localisation data in JSON format.

Functions

  • loadLocale: Loads the locale data from a JSON file.
  • init: Initializes the locale settings and language menu.
  • initLanguageMenu: Sets up the language selection menu.
  • setLocale: Sets the current locale.
  • switchLocale: Switches to the selected locale and updates the displayed strings.
  • translaterStrings: Updates all strings in the document with localized content.
  • get: Retrieves a localized string based on type, module, and key.
  • err: Retrieves a localized error message.
  • lbl: Retrieves a localized label.
  • loc: Retrieves a localized string.
  • msg: Retrieves a localized message.
  • lst: Retrieves a localized list item.

Example Usage in locales.js

Locale.init(); // Initializes the locale settings
Locale.setLocale('en-GB'); // Sets the current locale to 'en-GB'
Locale.switchLocale('fr-FR'); // Switches to 'fr-FR' and updates the displayed strings

Routing for JSON localisation

The routing for generating JSON localisation files is defined as follows:

GET @locale: /locale/json/@locale.json = Sukarix\Actions\Core\GetLocale->execute

Caching

JSON locale data is cached whenever the translation file is updated to improve performance and reduce load times.

Behaviours

Introduction

Behaviours in Sukarix are traits that can be plugged into classes to extend their functionality. By convention, an init<TraitName> method should be implemented and is called immediately after the injector creates an instance of the class using the trait.

Available Behaviours

TraitPropertyVisibilityDescription
LogWriter$loggerprotectedA Monolog logger writing to the application log
HasF3$f3protectedThe Fat-Free Framework instance
HasSession$sessionprotectedThe session service
HasCache$cacheprotectedThe cache service, with remember() and forget() helpers
HasI18n$i18nprotectedThe translation service
HasMessages$messagesprotectedFlash messages
HasAssets$assetsprotectedAsset management
HasQueue$queueServiceprotectedThe queue service; requires a registered queue service
HasAccess$accessprivateThe ACL service
HasEvents$eventsprivateThe event dispatcher

The init<TraitName> method of each trait is invoked by the Processor, so a class only needs to use the trait to receive its property:

class MyClass {
    use LogWriter;
    use HasF3;
}

Warning

HasAccess and HasEvents are private

These two traits declare their property private. A private trait property is usable inside the class that declares the use, but is not visible to its subclasses. If you need the property in a class hierarchy, apply the trait in each class that reads it rather than only in the base class.

Singleton Classes

Any singleton class inheriting from Tailored must call Processor::instance()->initialize($this); in its constructor to ensure that the init<TraitName> methods are called. The Helper base class already does this, so classes extending Helper (directly or indirectly) only need to call parent::__construct().

Non-Singleton Classes

If a class does not inherit from Tailored, it must call Processor::instance()->initialize($this); in the constructor to ensure that the init<TraitName> methods are called.

Example Usage

Here’s how you might use these behaviours in a class:

class MyService {
    use LogWriter;
    use HasF3;

    public function __construct() {
        Processor::instance()->initialize($this); // Initialise traits manually
    }

    public function logSomething() {
        $this->logger->info('Logging something!', ['context' => 'value']);
    }

    public function getFrameworkInstance() {
        return $this->f3;
    }
}

In this example, MyService uses both the LogWriter and HasF3 traits. Since it does not extend Tailored, it explicitly calls Processor::instance()->initialize($this); in the constructor to ensure that the traits are initialised.

Queues

Introduction

Sukarix\Queue\QueueService is a Redis-backed FIFO queue for passing work between the stages of a pipeline — typically CLI workers scheduled by TaskScheduler.

It provides three things on top of a plain Redis list:

  • FIFO delivery — items are pushed onto the head and popped from the tail.
  • Deduplication — a companion Redis set tracks membership, so pushing a payload that is already queued is a no-op instead of a duplicate.
  • Attempt tracking — a per-queue hash counts failed processing attempts per item, so a stage can decide when an item has exhausted its retries.

Note

Retry policy stays in your application

The queue counts attempts but never decides what to do with them. Whether a failing item is retried, parked or dead-lettered is a business decision and belongs in your own stage code.

Requirements

The queue requires the redis PHP extension and a reachable Redis server.

Configuration

Connection settings are read from the hive:

[globals]
redis.host = 127.0.0.1
redis.port = 6379
; connection timeout in seconds, keep it bounded so workers fail fast
redis.timeout = 2

The constructor throws \RedisException when the connection cannot be established, so a misconfigured worker fails immediately instead of part-way through a run.

Registering the service

Register the queue once during boot so it can be resolved through the Injector:

protected function setupQueueService(): void
{
    \Registry::set('queue', \Sukarix\Queue\QueueService::instance());
}

Using the queue in a service

Add the HasQueue behaviour to get a $queueService property. See Behaviours for how init<TraitName> methods are invoked.

use Sukarix\Behaviours\HasQueue;
use Sukarix\Core\Processor;

class MyWorker
{
    use HasQueue;

    public function __construct()
    {
        Processor::instance()->initialize($this);
    }

    public function run(): void
    {
        while (null !== $item = $this->queueService->popFromQueue('queue.work.pending')) {
            // process $item
        }
    }
}

Warning

Register before constructing

HasQueue throws a \LogicException if no queue service is registered when the consuming class is constructed. This is deliberate: it surfaces the misconfiguration at boot instead of failing later with a null member access.

Queue operations

MethodDescription
pushToQueue(string $queue, mixed $data): voidEnqueue a payload, skipping it if already queued
popFromQueue(string $queue): mixedDequeue the oldest payload, or null when empty
peek(string $queue): mixedRead the next payload without removing it
getQueueSize(string $queue): intNumber of queued items
isEmpty(string $queue): boolWhether the queue holds no items
existsInQueue(string $queue, mixed $value): boolWhether a payload is already tracked
getQueueItems(string $queue, ?int $limit = null): arraySnapshot of queued items, oldest first
clearQueue(string $queue): voidDelete the list, membership set and attempt counters

Payloads are JSON-encoded, so any JSON-serialisable value works — scalars, arrays and nested structures all survive the round trip:

$queue = \Sukarix\Queue\QueueService::instance();

$queue->pushToQueue('queue.work.pending', ['order_id' => 'A-1', 'total' => 42]);
$queue->pushToQueue('queue.work.pending', ['order_id' => 'A-1', 'total' => 42]); // skipped

$queue->getQueueSize('queue.work.pending'); // 1
$item = $queue->popFromQueue('queue.work.pending'); // ['order_id' => 'A-1', 'total' => 42]

Note

getQueueItems is a snapshot

getQueueItems() is an observational read for reporting and administration. Concurrent producers and consumers may change the queue before you act on the result, so never treat it as a lock.

Tracking failed attempts

Attempt counters are keyed by the payload, independently of the queue contents:

try {
    $this->process($item);
    // Success clears the history so a future failure starts from a clean count
    $this->queueService->clearAttempts('queue.work.failed', $item);
} catch (\Throwable $e) {
    $attempts = $this->queueService->incrementAttempts('queue.work.failed', $item);

    if ($attempts >= $this->f3->get('pipeline.max_retries')) {
        // Your policy: park it, alert, or leave it for manual analysis
        $this->logger->critical('Item exhausted its retries', ['attempts' => $attempts]);
    }
}

getAttempts() returns 0 for an item that has never failed, so a fresh item is always below any positive limit.

Consistency guarantees

Enqueue and dequeue are executed as Redis Lua scripts, which Redis runs atomically. A worker killed mid-operation can therefore never leave the list and the membership set disagreeing — an item is either fully queued and tracked, or neither.

Warning

clearQueue is an administrative operation

clearQueue() removes three keys and is not atomic with respect to a running pipeline. Do not call it while producers or consumers are active.

Queue logging

Sukarix\Queue\QueueProgressLogger emits structured queue events with a distinctive prefix and a context array, so the output is parsable by log aggregators rather than only human-readable.

$logger = \Sukarix\Queue\QueueProgressLogger::instance();

$logger->logQueueStart('queue.work.pending', 'PROCESS', ['max_items' => 100]);
$logger->logQueueProcess('queue.work.pending', 40, 2, 58, 100);
$logger->logQueueComplete('queue.work.pending', 'PROCESS', 98, 2);

Available methods: logQueueStart(), logQueueComplete(), logQueueStatus(), logQueueFill(), logQueueProcess(), logBatchProgress(), logQueueThroughput(), logItemAdded(), logItemRemoved(), logQueueWarning() and logQueueError().

The queue service logs enqueue and dequeue events through this logger. Item previews are deliberately generic — an array is reported as array[3], never with its values — so payload contents never leak into logs.

Overriding the logger

Register your own subclass under the queue.progress_logger alias to route queue telemetry to a different sink:

\Registry::set('queue.progress_logger', new MetricsQueueProgressLogger());

The queue validates the alias and throws \UnexpectedValueException if the registered service does not extend QueueProgressLogger. When the alias is absent, the framework default is used.

Messaging

Introduction

Sukarix\Messaging\CommandPublisher is a small interface for publishing JSON commands onto a message bus — telling another service to do something, as opposed to the request/response of a web action or the work-queue polling of Queues:

interface CommandPublisher
{
    public function publish(string $routingKey, array $payload): void;
}

Two implementations ship with the framework:

  • Sukarix\Messaging\AmqpPublisher — publishes onto a direct-exchange RabbitMQ bus.
  • Sukarix\Messaging\RecordingPublisher — records published messages in memory instead of sending them, for tests.

Publishing to RabbitMQ

use Sukarix\Messaging\AmqpPublisher;

$publisher = new AmqpPublisher($dsn, $exchange);
$publisher->publish('user.created', ['id' => $user->id]);

Both constructor arguments are plain values — a DSN (amqp://user:pass@host:port) and an exchange name — so wiring one up through the Injector under your own alias is a one-line factory. AmqpPublisher opens a connection, declares the exchange, publishes, and closes the connection on every call; it does not hold a connection open between calls, so it suits code that publishes occasionally rather than a high-throughput producer.

Testing Code That Publishes

Depend on CommandPublisher, not AmqpPublisher, in the code under test, and swap in RecordingPublisher to assert on what was sent without a broker:

use Sukarix\Messaging\RecordingPublisher;

$publisher = new RecordingPublisher();
$service = new OrderService($publisher);

$service->confirm($order);

$test->expect('order.confirmed' === $publisher->messages[0]['routing_key'], 'the confirmation event was published');

$messages is a plain array of ['routing_key' => ..., 'payload' => ...] entries, in publish order.

CRON / Task Scheduling

TaskScheduler

TaskScheduler is an Action in Sukarix designed for task scheduling. The CLI toolbox installs it as a cron job by default, utilizing the GO\Scheduler syntax.

Default Configuration

The routes-cli.ini file contains the default route for task scheduling:

[routes]
; route to the main task scheduler
GET @run_job : /cli/jobs/run [cli] = Actions\Jobs\TaskScheduler->execute

Setting Up Cron

The default cron job should contain:

* * * * * /usr/bin/php /var/www/sukarix/public/index.php "/cli/jobs/run"

Defining Jobs

Each job must be defined in routes-cli.ini. For example, the default log cleaning job is implemented as:

GET @logs_clean : /cli/logs/clean [cli] = Sukarix\Actions\Logs\Clean->execute

Implementing the Execute Function

The execute function should schedule tasks using the scheduler:

public function execute() {
    // Clean old sessions every 8 hours at 10 minutes after the hour
    $this->scheduler->php($this->documentRoot, null, ['/cli/sessions/clean' => ''], 'sessions-clean')
                    ->onlyOne()
                    ->at('20 */8 * * *');
}

This example schedules a session cleaning task to run every 8 hours at 10 minutes past the hour, ensuring only one instance runs at a time.

Events

Introduction

In Sukarix, events are managed using the Sukarix\Behaviours\HasEvents trait. This allows any class to emit and handle events throughout the application using the Sugar Events system integrated with the PHP Fat-Free Framework.

Key Features of Sugar Events

  • Emit events from any point in your application
  • Attach multiple listeners to an event with priority
  • Local events on specific objects
  • Send payload and context data with events
  • Support for sub-events and event propagation
  • Stop the event chain
  • Bundled with Sukarix through ikkez/f3-events, so it follows the framework’s PHP 8.4+ baseline

Example Usage

Initializing Events

To initialize the event system and get the global Event instance:

$events = \Sugar\Event::instance();

Defining a Listener

You can define a listener (or hook) to an event using a typical F3 callstring, callback, or callable:

$events->on('user_login', 'Notification->user_login');
$events->on('user_login', function() {
  // ...
});
$events->on('user_login', [$this, 'method']);

Emitting an Event

To emit an event and notify all listeners:

$events->emit('user_login');

Sending Payload with Event

You can send additional data (payload) with an event:

$events->on('user_login', function($username) {
  \Logger::log($username . ' logged in');
});
$events->emit('user_login', 'freakazoid');

Multiple Listeners with Prioritization

Listeners can be prioritized; higher numbers are called first:

$events->on('user_login', function($username) {
  \Logger::log($username . ' logged in');
}, 10);
$events->on('user_login', function() {
  \Flash::addMessage('You have logged in successfully');
}, 20);
$events->emit('user_login', 'freakazoid');

Stopping the Event Chain

A listener can stop further listeners from being called:

$events->on('user_login', function($username) {
  \Logger::log($username . ' logged in');
});
$events->on('user_login', function() {
  \Flash::addMessage('You have logged in successfully');
  return false;
}, 20);
$events->emit('user_login', 'freakazoid');

Additional Event Context Data

Send additional context data with an event:

$events->on('user_login', function($username, $context) {
  if ($context['lang'] == 'en')
    \Flash::addMessage('You have logged in successfully');
  elseif ($context['lang'] == 'de')
    \Flash::addMessage('Du hast dich erfolgreich angemeldet');
});
$events->emit('user_login', 'freakazoid', array('lang' => 'en'));

Additional Listener Options

Listeners can have additional options:

$events->on('user_login', function($username, $context, $event) {
  \Flash::addMessage('You have logged in successfully', $event['options']['type']);
}, 20, array('type' => 'success'));

Filtering Payload

Listeners can modify and return the event payload:

$events->on('get_total', function($basket) {
  $sum = 0;
  foreach ($basket as $prod) {
    $sum += $prod;
  }
  return $sum;
});

$products = array('a' => 2, 'b' => 8, 'c' => 15);
$sum = $events->emit('get_total', $products);
echo $sum; // 25

Adding Sub-Events

Sub-events are called after the parent event:

$events->on('get_total.tax', function($sum) {
  return $sum + ($sum * 0.2);
});
$events->on('get_total.shipping', function($sum) {
  return $sum + 5;
});
$sum = $events->emit('get_total', $products);
echo $sum; // 35

Removing Hooks

Remove listeners for specific events:

$events->off('get_total.tax');
$sum = $events->emit('get_total', $products);
echo $sum; // 30

Local Events for Mappers

Support for local events on specific objects:

$user = new \Model\User();
$events->watch($user)->on('update.email', '\Mailer->sendEmailActivationLink');

Error Handling

Introduction

In Sukarix, error handling is managed by a global error handler that can be overridden in the Application class. The default implementation uses Tracy\Debugger for managing errors and handling fatal exceptions. The global exception handling is only active in the production environment.

Default Implementation

The Application class includes a protected method handleException() that sets up the error handler. The default implementation is capable of sending email or Zulip notifications based on the configuration of error.channel. Before sending the email, it stores the exception stack trace in HTML format. It’s important to note that the error stack might contain sensitive information, so sending it by default is not enabled.

Additionally, notifications for the same exception hash are sent only once every 24 hours.

Customizing Error Handling

To customize the error handling, you can override the handleException() method in your Application class:

protected function handleException(): void {
    if (Debugger::$productionMode) {
        Debugger::$onFatalError[] = function($exception) {
            // Custom fatal error handling logic
        };
    }
}

Building JSON APIs

Overview

Sukarix includes a small set of building blocks for machine-facing JSON endpoints, as opposed to the session-backed, form-submitting web actions covered elsewhere in these docs:

  • Stateless routing — opt a path prefix out of session creation entirely.
  • Sukarix\Actions\ApiAction — a CSRF-exempt base action answering every error path with a JSON envelope.
  • Sukarix\Http\Cors — origin allow-listing and preflight handling.
  • Sukarix\Actions\Health\Probe — liveness/readiness endpoints for orchestrators and load balancers.
  • Sukarix\Security\CapabilityToken — HMAC-signed, short-lived tokens for machine-to-machine calls.

None of this replaces session-based authentication for browser-facing routes; it is meant for endpoints called by other services.

Stateless Routes

By default every request boots a session. ApiAction is normally paired with a route under SECURITY.stateless.prefixes (/api by default), which skips that step entirely — see Session-Free Routes for how to configure it, including how to turn it off for an API that authenticates through the session itself.

Note

$this->session becomes nullable on a stateless route

Boot::$session is null for the lifetime of a stateless request. Action::beforeroute() and getRole() already guard every access, but any code you write that reaches for $this->session on such a route must check for null first.

The ApiAction Base Class

Sukarix\Actions\ApiAction extends Action with defaults suited to a JSON API:

use Sukarix\Actions\ApiAction;

class Users extends ApiAction
{
    public function show($f3, $params): void
    {
        // ...
        $this->json(['id' => $params['id'], 'name' => $user->name]);
    }
}
  • $csrfExempt is true by default — a machine caller holds no session token to send.
  • onAccessAuthorizeDeny() answers a denied route with a JSON 403 instead of the HTML flow Action uses.
  • onError() is a static handler you can register as ONERROR for API routes; it turns any uncaught error into the same JSON envelope, using ERROR.code and ERROR.text from the hive:
{ "success": false, "message": "Not Found", "status": 404 }

Wire it up for your API prefix from your own Bootstrap, for example by checking PATH before calling Debugger::enable() in handleException().

CORS

Sukarix\Http\Cors::handle() answers a preflight OPTIONS request for an allow-listed origin and tags every response Vary: Origin:

[api.cors]
origins[] = https://app.example.com
origins[] = https://admin.example.com

ApiAction::options() calls it automatically whenever api.cors.origins is configured, and falls back to a plain 204 otherwise:

Cors::handle(
    $f3,
    $allowedOrigins,
    allowedMethods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
    allowedHeaders: ['Content-Type', 'Authorization']
);

An origin not on the allow list is simply ignored — no CORS headers are sent, and the browser enforces the same-origin policy as usual.

Health Probes

Sukarix\Actions\Health\Probe exposes liveness and readiness checks, both CSRF-exempt and, when reached under a stateless prefix, session-free:

[routes]
GET @health_live  : /api/health/live  = Sukarix\Actions\Health\Probe->liveness
GET @health_ready : /api/health/ready = Sukarix\Actions\Health\Probe->readiness
  • liveness() answers 200 with { "status": "ok", "request_id": "..." } — use it to know the process is up.
  • readiness() additionally pings Redis when CACHE is Redis-backed (redis=...), answering 503 with "status": "degraded" when a dependency is unreachable.

Capability Tokens

Sukarix\Security\CapabilityToken issues and verifies compact, HMAC-signed tokens for a machine caller to present as a Bearer token — no session, no database round trip to verify:

use Sukarix\Security\CapabilityToken;

// Issuing side
$token = CapabilityToken::issue($secret, ['iss' => 'billing', 'aud' => 'jobs'], ttl: 3600);

// Verifying side
$claims = CapabilityToken::verify($secret, $token, audience: 'jobs');
if (null === $claims) {
    $this->error('Unauthorized', 401);
}

A token is a base64url payload and signature joined by a dot (payload.signature), signed with HMAC-SHA256. verify() returns null — never throws — when the signature does not match, the token is malformed, it has expired (exp claim), or it was issued for a different audience.

Warning

The signing secret is the only thing standing between “verified” and “forged”

Keep it out of version control (an untracked .ini file, per Configuration) and rotate it if it ever leaks. CapabilityToken does not manage secret storage or rotation for you.

Bearer Authentication

Action::parseHeaderAuthorization() strips both the Basic and Bearer schemes from the Authorization header. isApiUserVerified() still assumes Basic-encoded user:password credentials (see Authentication); for a Bearer-scheme token, read HEADERS.Authorization directly in your action and hand the token to CapabilityToken::verify().

Request Bodies

Action::getDecodedBody() accepts a JSON body, a form-encoded body, or nothing at all — a form-encoded body is detected by inspecting the first byte, and an empty body falls back to POST. This keeps data validation endpoints working the same way whether the caller is a browser form, a JSON API client, or a Statera test.

Testing JSON Responses

Response::json() calls exit after writing the body, which is normally what you want — nothing accidentally runs after a response has already been sent. Under Environment::isTest() that exit is skipped, so a Statera test can call an action and make assertions afterward.

When an action needs to compose the body itself — wrapping it, streaming it, or handing it to something else — Response::renderJson() behaves like json() but returns the encoded string instead of echoing and exiting:

$body = $this->response->renderJson(['status' => 'ok']);

Request Correlation

Every request is stamped with a request id that flows into your logs; see Logging for how to read and forward it.

Template Engine

Introduction

The template engine in Sukarix is built on top of the Fat-Free Framework’s (F3) template engine. It provides a flexible and efficient way to render views, leveraging F3’s powerful features while adding enhancements specific to Sukarix.

Template Loading

In Sukarix, templates are loaded into views using a specific pattern. By default, the view files are stored in the templates directory and have a .phtml extension.

Mapping Actions to Templates

Sukarix maps actions to templates by following a convention-based approach. When an action calls $this->render();, it maps to the template based on the namespace and class name, converted to snake_case. For example:

  • Action Class: Actions\Core\Main
  • Mapped Template: templates/actions/core/main.phtml

Note

Default Template Extension

The default template extension in Sukarix is .phtml and is automatically appended.

Example Code

$this->render('commons/api/response');

This method call will automatically render the templates/commons/api/response.phtml when called from any action.

JavaScript and CSS Template Directives

Sukarix adds custom template directives for including JavaScript and CSS files in your views. These directives simplify the process of managing assets.

  • JavaScript Directive:

    <js src="path/to/script.js" type="module"></js>
    

    This directive is used to include JavaScript files in your view. You can pass an optional type attribute, such as "module". If the type attribute is not provided, "text/javascript" will be chosen by default.

  • CSS Directive:

    <css href="path/to/style.css"></css>
    

    This directive is used to include CSS files in your view.

Tip

File Validation

The existence of the specified file is validated when the directive attempts to load it. If the file is not found, an exception will be thrown. This ensures that developers can handle missing files during the development phase before deploying to production.

By utilizing these custom directives, Sukarix ensures that assets are managed efficiently and that any issues with missing files are caught early in the development cycle.

F3 Template Directives

F3 provides a set of powerful template directives that make it easy to render dynamic content, control flow, and manage template inheritance. Some of the key directives include:

  • Include Directive:

    <include href="partial.phtml" />
    

    This directive includes another template file within the current template.

  • Repeat Directive:

    <repeat group="{{ @items }}" value="{{ @item }}">
      <!-- Content to repeat -->
    </repeat>
    

    This directive repeats a block of content for each item in a collection.

  • Conditional Directive:

    <check if="{{ @condition }}">
      <!-- Content to show if condition is true -->
    </check>
    

    This directive conditionally renders content based on a boolean expression.

  • Set Directive:

    <set name="variable" value="value" />
    

    This directive sets a variable to a specified value within the template.

  • Session Directive:

    <session name="variable" />
    

    This directive retrieves a value from the session.

  • Cookie Directive:

    <cookie name="variable" />
    

    This directive retrieves a value from cookies.

  • Locale Directive:

    <locale name="variable" />
    

    This directive retrieves a localized value.

Additional Resources

For more detailed information on F3’s template engine and directives, you can refer to the F3 Views and Templates Documentation.

Assets

Introduction

Sukarix uses the Assets class to manage and render CSS and JavaScript files efficiently. By default, it looks for CSS files under public/css and JavaScript files under public/js.

Default Storage Locations

  • CSS Files: public/css
  • JavaScript Files: public/js
  • Minified Files: public/minified

The Assets class automatically handles the minification of CSS and JS files and places the minified versions in the public/minified directory.

Tip

Default Storage Locations

By default, CSS files are stored in public/css, JavaScript files in public/js, and minified files in public/minified.

Rendering Groups

Sukarix provides two rendering groups named head and footer to organize asset rendering. To render these groups in your template, use:

{{ \Sukarix\Helpers\Assets::instance()->renderGroup('footer') }}

JavaScript Initialization

The Assets class can also call JavaScript functions and initialize them. Example:


<script>
    jQuery(document).ready(function () {
        {{ \Sukarix\Helpers\Assets::instance()->currentJsLocale() }}
        {{ \Sukarix\Helpers\Assets::instance()->setUserRole() }}
        {{ \Sukarix\Helpers\Assets::instance()->initJsClasses() }}
    });
</script>

Example Usage

Adding CSS and JS Files in a View

In your action, you can add CSS and JS files like this:

$this->assets->addJs('core.js');
$this->assets->addJs('core/servers.js');
$this->assets->addJs('vendors/datatables.min.js');
$this->assets->addCss('datatables.min.css');
$f3->push('init.js', 'Servers');

Rendering Assets

To render assets in the head group:

{{ \Sukarix\Helpers\Assets::instance()->renderGroup('head') }}

To render assets in the footer group:

{{ \Sukarix\Helpers\Assets::instance()->renderGroup('footer') }}

By using these functionalities, Sukarix ensures efficient management and rendering of assets, enhancing the performance and maintainability of your web application.

JavaScript

Introduction

The default Sukarix application template includes locale.js, which downloads the session locale using AJAX and provides functions to access localized strings. Additionally, Common.userRole is accessible via JavaScript and is set from the session.

locale.js

locale.js handles the localization of strings in your application. It downloads the session locale and provides easy access to localized strings. This ensures that your application can support multiple languages and dynamically switch between them based on the user’s session.

Common.userRole

The Common.userRole variable is set from the session and is accessible via JavaScript. This allows you to manage user roles on the client side and adjust your application’s behavior based on the user’s role.

Minimalistic Implementation

Sukarix provides this minimalistic implementation, giving developers the flexibility to use modern frontend frameworks as needed. You can integrate libraries and frameworks such as React, Vue, or Angular to build more complex and interactive user interfaces.

Example Usage

Accessing Localized Strings

var welcomeMessage = Locale.msg('welcome');
console.log(welcomeMessage);

Using Common.userRole

if (Common.userRole === 'admin') {
    console.log('User is an admin');
} else {
    console.log('User is not an admin');
}

Users

Introduction

The User Model class in Sukarix is the central class for managing users. It includes user status, roles, and authentication mechanisms.

User Status

User status can be either active or inactive, managed using the Sukarix\Enum\UserStatus enum.

User Roles

Sukarix provides four default user roles:

  • visitor: Default role, represented as * in the routing ACL.
  • admin
  • customer
  • api: A user that can authenticate through HTTP basic authentication.

These roles impact the access control list (ACL) in the configuration and can be extended by developers as needed.

Extending User Roles

Developers can extend user roles by adding new roles to the UserRole enum and updating the ACL configuration accordingly. This allows for greater flexibility and customization to fit the needs of your application.

Authentication

Introduction

Sukarix provides a straightforward method for authenticating users using models and session management. Below are examples and explanations on how to implement authentication and handle user verification in Sukarix.

Authenticating a User

To authenticate a user, retrieve the user model by their email, check their status and role, and verify their password. If all conditions are met, authorize the user in the session.

Example Code

$user = new UserModel();
$user = $user->getByEmail($email);
if (UserStatus::ACTIVE === $user->status && UserRole::API !== $user->role && $user->verifyPassword($password)) {
    $this->session->authorizeUser($user);
}

Authorizing a User

The authorizeUser method is used to authorize a user in the session. This method sets the necessary session variables to mark the user as authenticated.

Verifying API Users

In the Action class, the isApiUserVerified() method checks if a user has authenticated via the HTTP Basic Auth header. This is useful for API endpoints where HTTP Basic Authentication is preferred.

Example Usage

if ($this->isApiUserVerified()) {
    // API user is authenticated
}

Bearer Tokens

The Authorization header parser also recognises the Bearer scheme alongside Basic. isApiUserVerified() itself still expects Basic-encoded user:password credentials; a Bearer-scheme caller — typically one presenting a Sukarix\Security\CapabilityToken — is verified by reading HEADERS.Authorization in your action instead. See Building JSON APIs for capability tokens and other machine-to-machine building blocks.

Authorisation System

Introduction

The authorisation system in Sukarix is managed through the f3-access library. The documentation for f3-access is available in the f3-access repository.

Access Instance

The access instance is available in classes using the HasAccess behaviour trait. This trait integrates the access control mechanisms provided by f3-access into your Sukarix application.

Default Authorization System

The beforeroute method of the Action class implements the default authorization system. This method checks the user’s permissions before allowing access to specific routes or actions.

Security Best Practices

By default, everything is set to deny in the template application for security reasons. This default setting ensures that only explicitly allowed actions are accessible, enhancing the overall security of your application.

Caution

Recommended Authorization Policy

For security, it is recommended to maintain the default deny setting and explicitly allow access to necessary routes and actions.

Declared Privileges

Sukarix\Utils\PrivilegeUtils reads the privileges an application declares from the action classes that carry them, which is what a role matrix is built from:

$privileges = PrivilegeUtils::listSystemPrivileges(
    __DIR__ . '/Actions',
    'Actions',
    'Actions\RequirePrivilegeTrait'
);
// ['rooms' => ['add', 'delete', 'index'], 'users' => ['add', 'index'], …]

A privilege is the first two namespace segments below the root: Actions\Rooms\Index declares rooms.index. Classes nested deeper share the privilege of the group above them, so Actions\Rooms\Presentations\{Index,Add,Delete} is the single privilege rooms.presentations rather than three. Classes sitting directly under the root are the shared base classes and declare nothing.

Note

Why the directory rather than the class map

The composer class map is the obvious source, but an action added after the autoloader was dumped is missing from it, and its privilege then silently disappears from the role matrix — leaving a route nobody can be granted. The files on disk are the truth.

CSRF Protection

Overview

Sukarix provides built-in CSRF (Cross-Site Request Forgery) protection mechanisms to ensure the security of your forms and requests. By enabling CSRF protection, you can safeguard your application from unauthorized actions performed by malicious users.

Protection is enforced automatically: every action extending Sukarix\Actions\Action validates the token in beforeroute() before your handler runs, so you never have to call the validation yourself.

Configuration

CSRF protection is configured in default.ini or the environment-specific config-<environment>.ini. The following default settings enable CSRF protection and set the token expiry time:

[SECURITY]
csrf.enabled = true
csrf.expiry = 3600
  • csrf.enabled: Enables or disables CSRF protection.
  • csrf.expiry: Sets the expiry time for the CSRF token in seconds (default is 3600 seconds or 1 hour).

Protected Verbs

The token is required on every state changing request, that is POST, PUT, DELETE and PATCH. GET, HEAD and OPTIONS requests are never validated.

When validation fails, the request is not passed to your action: it is rerouted to the current path, and form_errors.csrf_token is set in the session so the form can report the problem. A csrf_bounce session message is also set, with a message suitable for display directly to the user (Your form session expired. Please try again.). The rejection is also logged at critical level with the route alias, the verb and the client IP:

Invalid CSRF token {"alias":"login","verb":"POST","ip":"203.0.113.7"}

Tip

A form that silently reloads instead of submitting

Because a rejected request is rerouted rather than answered with an error page, a missing token looks like a form that reloads and does nothing. If a form appears inert, grep your logs for Invalid CSRF token before looking anywhere else.

Adding the Token to a Form

To include a hidden CSRF token input in your forms, use the <csrf/> template directive. This directive generates a hidden input field named csrf_token, ensuring that each form submission includes a valid token.

Example Usage

<form method="post" action="{{ @ALIASES.login }}">
    <csrf/>
    <!-- other form fields -->
    <input type="submit" value="Submit">
</form>

Add the directive to every form that is not a GET form. A form without it cannot be submitted while CSRF protection is enabled.

Sending the Token with AJAX Requests

A hidden field only covers form encoded submissions. Two common cases cannot use one:

  • Requests sending a JSON body, because PHP only populates $_POST for form encoded bodies.
  • PUT, DELETE and PATCH requests, because Fat-Free only maps GET, POST and COOKIE into the hive, so a token sent as a PUT parameter is never readable.

Those requests must send the token in the X-Csrf-Token header instead. Expose the token in your layout:

<meta name="csrf-token" content="{{ @SESSION.csrf_token }}"/>

Then attach it to every state changing request. With jQuery, a single global hook covers the whole application:

$(document).ajaxSend(function (event, jqXHR, settings) {
    if (/^(GET|HEAD|OPTIONS)$/i.test(settings.type)) {
        return;
    }

    // Never disclose the token to another origin
    if (/^(https?:)?\/\//i.test(settings.url) && settings.url.indexOf(window.location.origin) !== 0) {
        return;
    }

    let token = $('meta[name="csrf-token"]').attr('content');
    if (token) {
        jqXHR.setRequestHeader('X-CSRF-Token', token);
    }
});

Header names are case insensitive over HTTP, and Fat-Free normalises them, so the framework always reads the value from HEADERS.X-Csrf-Token. Both names are available as constants:

Sukarix\Core\Session::CSRF_HEADER; // 'X-Csrf-Token'
Sukarix\Core\Session::CSRF_FIELD;  // 'csrf_token'

The header takes precedence over the request parameter, so a request may safely send both.

Token Lifetime

Each session holds a single token, which stays valid until it expires. It is not single use: the same token validates any number of requests within its expiry window. This matters in two everyday cases:

  • A page holding several forms — all of them carry the same valid token.
  • A page performing successive writes without reloading, as an AJAX driven table does.

Within a single request the token is stable, so rendering <csrf/> more than once always yields the same value. Once the token expires, the next request issues a new one.

Note

CSRF Token Expiry

The CSRF token has an expiry time configured in default.ini or the environment-specific config-<environment>.ini. If the token has expired, it will be considered invalid. Ensure your application handles token expiry by prompting the user to refresh the form or session.

Exempting an Endpoint

Endpoints called by external systems — webhooks, telephony gateways, payment callbacks — hold no session and cannot carry a token. Set the $csrfExempt property on those actions:

class Callback extends BaseAction
{
    protected bool $csrfExempt = true;

    public function execute($f3, $params): void
    {
        // ...
    }
}

Warning

An exempt endpoint must authenticate its caller

Exempting an action removes its only CSRF defence. Such an endpoint has to identify the caller by its own means, for instance a shared secret, a request signature, or matching the request against a known host. Never exempt an action that acts on the current user’s session.

Validating the Token Manually

Automatic enforcement is usually enough. When you need the outcome inside a handler, use the isCsrfValid() method from the session. This method reports whether the token submitted with the request matched the token stored in the user’s session.

Example Usage

if ($this->session->isCsrfValid()) {
    // Token is valid, proceed with form processing
} else {
    // Token is invalid, handle the error
}

This code snippet shows how to check the CSRF token when processing a form submission. If the token is valid, you can proceed with the form processing. If the token is invalid, handle the error appropriately.

Encrypting Secrets at Rest

Overview

Sukarix\Security\SecretBox is a thin wrapper around libsodium’s secretbox (XSalsa20-Poly1305) for encrypting credentials before they are stored — a third-party API key or OAuth token sitting in a database column, for example. It is authenticated encryption: a tampered or foreign-key blob fails to decrypt rather than silently returning garbage.

A fresh nonce is generated for every call and prefixed to the ciphertext, so encrypting the same plaintext twice never produces the same blob — a database dump reveals nothing by comparison.

Provisioning a Key

Generate a key once per deployment and keep it out of version control, the same way as any other secret (see Environment Variables):

use Sukarix\Security\SecretBox;

echo SecretBox::generateKey(); // 64 hex characters — store this as an env var

Encrypting and Decrypting

$box = SecretBox::fromKeyMaterial(getenv('SECRETS_KEY'));

$blob = $box->encrypt('sk_live_...');       // store $blob
$plaintext = $box->decrypt($blob);          // 'sk_live_...'

fromKeyMaterial() accepts the key as 64 hex characters, base64, or 32 raw bytes, and throws \RuntimeException for anything else. Structured data — several credentials belonging to one integration, say — can be encrypted as a whole with encryptArray()/decryptArray(), which JSON-encode the value before sealing it:

$blob = $box->encryptArray(['client_id' => '...', 'client_secret' => '...']);
$credentials = $box->decryptArray($blob); // ['client_id' => '...', 'client_secret' => '...']

Warning

Losing the key means losing every secret sealed with it

There is no recovery path built in. Back the key up the same way you would a database encryption key, and rotate it by decrypting with the old key and re-encrypting with the new one — SecretBox does not do this for you.

Failure Modes

decrypt() and decryptArray() throw \RuntimeException in two cases: the stored value is not a valid sealed blob (truncated, not base64, or shorter than a nonce), or it was sealed with a different key. Both cases look identical from the caller’s side — decryption either succeeds with the original plaintext or throws, never a partial or corrupted result.

Statera: The Testing Package

Overview

Statera is a testing package designed to work with Sukarix, offering an alternative to PHPUnit. It supports generating coverage reports in formats like Clover, HTML, and Text. Statera operates in both Web and CLI modes, providing flexible testing options.

Statera is an optional, standalone package — applications can choose whether or not to use it for unit testing. The Sukarix framework itself uses Statera for its own test suite via a require-dev dependency.

Key Concepts

TestGroup

A TestGroup is used to group multiple TestScenario instances. It helps organize related test scenarios into logical groups, making it easier to manage and run tests collectively.

TestScenario

A TestScenario represents a specific test case or scenario. It encompasses multiple TestCase instances that define the individual tests to be run as part of the scenario.

TestCase

A TestCase is an individual test case created inside a TestScenario. It extends the Fat-Free Framework’s Test class and provides the expect() method for assertions to verify the expected behavior of the code under test.

Running Tests

Statera can run tests in both Web and CLI modes, providing detailed coverage reports.

Sukarix Shell Toolbox

Introduction

The Sukarix Shell Toolbox is a Bash utility for configuring, developing, monitoring and administering a Sukarix application on its server. It lives in its own repository, sukarix/cli, and ships with the template application under tools/sukarix.sh. It targets Ubuntu 22.04 and later.

Install it once so the sukarix command is available from anywhere:

tools/sukarix.sh --selfinstall

Environment

The toolbox follows the same rule as the framework’s Boot::detectEnvironment(): APP_ENV wins when it is one of development, staging or production; otherwise a hostname ending in .test means development and anything else means production. Commands that only make sense on a server (--pull, --deploy) refuse to run outside production.

Functionality

Configuration

  • --version: Display the application, Nginx, PHP, Redis and PostgreSQL versions.
  • --selfinstall: Make sukarix runnable from anywhere.

Development

  • --enabletests: Enable running unit tests from the browser.
  • --test [-c] [name]: Run the unit tests, optionally a single group; -c adds code coverage.
  • --fix: Fix PHP code style.
  • --migrate: Run database migrations.
  • --metrics: Generate code metrics.

Monitoring

  • --check: Check configuration files and processes for problems.
  • --status: Display the running status of components.

Administration

  • --pull: Pull source code from its repository.
  • --deploy: Deploy the application on a production server.
  • --jobs: Install the cron jobs.
  • --start, --stop, --restart: Control the Sukarix stack (PostgreSQL, Redis, Nginx, PHP-FPM).
  • --clean: Clean the cache and log files.
  • --cleansessions: Clean sessions through the framework’s Sukarix\Actions\Core\SessionsClean CLI action.
  • --zip: Zip up log files for reporting an error.

Example Usage

# Display the installed versions
sukarix --version

# Enable running unit tests
sukarix --enabletests

# Run one test group with coverage
sukarix --test -c core

# Deploy on a production server
APP_ENV=production sukarix --deploy

Data & Filesystem Helpers

Introduction

Sukarix\Utils provides small, dependency-free static helpers shared by every Sukarix application, so common array/data manipulation, filesystem checks and git introspection are not reimplemented per project.

DataUtils

  • keepIntegerInArray(?array &$array): Filters an array in place, keeping only digit-string values.
  • unsetByValue(array &$array, mixed $value): Removes the first occurrence of a value from an array.
  • getArrayFromField(array $array, int|string $key): Extracts a column from a multidimensional array (array_column-like, null for missing keys).
  • toJsonArray(array $array, bool $wrapStrings = false): Joins an array into a comma-separated string, optionally quoting each value.

Application-specific data helpers should extend Sukarix\Utils\DataUtils rather than duplicating these methods.

FileSystemUtils

  • createDirectory($path) / ensureDirectoryExists(string $directory): Create a directory recursively, throwing \RuntimeException on failure.
  • isDirectoryEmpty(string $dir): Returns false for a missing/unreadable directory as well as a non-empty one.
  • scanDirectory(string $directory, string $pattern): Recursively lists files whose name ends with $pattern.

CommandExecutor

  • gitBranch() / gitVersion(): Shell out to git (via mikehaertl/php-shellcommand) to report the checked-out branch and tag/commit, typically surfaced on a diagnostics or “about” page.

Example Usage

use Sukarix\Utils\DataUtils;
use Sukarix\Utils\FileSystemUtils;
use Sukarix\Utils\CommandExecutor;

DataUtils::toJsonArray(['a', 'b'], true); // "'a','b'"

FileSystemUtils::ensureDirectoryExists($f3->get('TEMP') . 'uploads');

$version = CommandExecutor::gitVersion();

Testing with Sukarix

The Sukarix Shell Toolbox provides several commands to facilitate testing, ensuring your application maintains high quality and reliability. Here’s how you can leverage these testing commands:

Enabling and Running Tests

Enable Tests

To enable the testing environment, use:

sukarix --enabletests

Running Unit Tests

Run unit tests with an optional coverage report using:

sukarix --test <-c> <name>
  • -c: Generates a coverage report.
  • <name>: Specify the name of the test to run.

Statera Test Runner

Sukarix uses the Statera test runner, which is built on top of the Fat-Free Framework’s Test class. Tests are organised into groups and scenarios.

Test Structure

  • TestGroup: A collection of test scenario classes. Extends Sukarix\TestGroup.
  • TestScenario: A class containing test methods (methods starting with test). Extends Sukarix\TestScenario.
  • TestCase: Individual test assertions. Extends Sukarix\TestCase.

Registering Test Groups

Create a Statera class in your application’s tests/src/Core/ directory:

namespace Core;

use Sukarix\Statera as SukarixStatera;
use Suite\ConfigurationTest;
use Suite\ModelTest;

class Statera extends SukarixStatera
{
    public static function registerGroups(): void
    {
        self::setGroups([
            ConfigurationTest::class,
            ModelTest::class,
        ]);
    }
}

Writing a Test Suite

A test suite groups related scenarios:

namespace Suite;

use Test\TestGroup;

final class ConfigurationTest extends TestGroup
{
    protected $classes = [
        \Core\ConfigurationTest::class,
    ];
}

Writing a Test Scenario

namespace Core;

use Test\Scenario;

final class ConfigurationTest extends Scenario
{
    protected $group = 'Framework Configuration';

    public function testDefaultConfiguration($f3)
    {
        $test = $this->newTest();
        $test->expect('UTC' === date_default_timezone_get(), 'Timezone set to UTC');
        $test->expect('UTF-8' === \ini_get('default_charset'), 'Default charset is UTF-8');

        return $test->results();
    }
}

Running Tests via CLI

Create a tools/statera.php entry point in your application:

use Application\Application;
use Core\Statera;

// load composer autoload
require_once __DIR__ . '/../vendor/autoload.php';

// Change to application directory to execute the code
chdir(realpath(__DIR__ . '/../app'));

$GLOBALS['test_cli'] = PHP_SAPI === 'cli';

// Activate test environment so detectEnvironment() loads config-test.ini
$f3 = Base::instance();
if (!$f3->exists('GET.statera')) {
    $f3->set('GET.statera', 'all');
}

Statera::registerGroups();
Statera::startCoverage('Application Bootstrapping');
$app = new Application();
Statera::stopCoverage();
$app->start();

Run with: php tools/statera.php

For coverage reports, pass the query string:

php tools/statera.php "?statera=withCoverage"

Test Environment

The test environment is activated by setting GET.statera in the F3 hive. When detected, Sukarix loads config-test.ini and uses the test database. The config-test.ini should use a separate database and can use file-based cache to avoid Redis dependency during testing.

Deployment

Introduction

Deploying a Sukarix application involves several steps to ensure your code is properly set up on the production server.

Sukarix Deployment Basics

  1. Upload Your Code:

    • Transfer your application’s codebase to the production server using your preferred method (e.g., FTP, SCP, or Git).
  2. Ensure sukarix.sh is in the tools Directory:

    • Make sure the sukarix.sh script is located in the tools directory of your application.
  3. Run the Deployment Script:

    • Execute the deployment script with the -d option:
      ./tools/sukarix.sh -d
      
    • This command performs the following actions:
      • Pulls the latest code from the Git repository.
      • Runs Composer to install dependencies.
      • Applies default permissions to the www-data user.
      • Runs database migrations.
      • Clears the application cache.

Deploying Documentation

The Sukarix documentation is built with mdBook and deployed automatically to GitHub Pages via GitHub Actions — at no cost.

How It Works

  1. A GitHub Actions workflow (.github/workflows/deploy-docs.yml) triggers on every push to the main branch.
  2. It installs the Rust toolchain, then builds mdBook along with the mdbook-toc preprocessor.
  3. mdbook build generates the static site in the book/ output directory.
  4. The output is published to GitHub Pages, served at docs.sukarix.com.

Prerequisites

  • The docs repository must be public (GitHub Pages is free for public repos).
  • The custom domain docs.sukarix.com is set via the cname field in book.toml and configured in GitHub Pages settings.
  • GitHub Pages must be enabled in the repository settings (see steps below).

One-Time Setup

  1. Enable GitHub Pages:

    • Go to the repository on GitHub → Settings → Pages.
    • Under Source, select GitHub Actions.
    • Save.
  2. Configure DNS:

    • In your domain registrar, add a CNAME record:
      CNAME  docs  <your-github-org-or-username>.github.io.
      
    • Wait for DNS propagation (can take up to a few minutes to a few hours).
    • GitHub will automatically provision an HTTPS certificate once DNS is verified.

Manual Deployment

To trigger a deployment outside of a push, go to the Actions tab in the repository, select the Deploy Docs to GitHub Pages workflow, and click Run workflow.

Contribution Guide

How to Contribute

We welcome contributions from the community! Here’s how you can help.

  1. Fork the Repository: Create a fork of the Sukarix repository.
  2. Create a Branch: Make a new branch for your feature or bugfix.
  3. Commit Changes: Commit your changes with clear commit messages.
  4. Create a Pull Request: Submit a pull request to the main repository.

Reporting Issues

If you find any bugs or have feature requests, please report them through GitHub Issues.

Community Help

We encourage everyone to contribute to Sukarix and be part of our vibrant community. Whether you have ideas for new features, need support with testing, or want to discuss CLI tools, your input is valuable. Join the conversations and help us improve Sukarix together:

Your contributions make a difference!

Code of Conduct

We strive to maintain a welcoming and inclusive community. Please read our Code of Conduct before contributing.

Thank you for your contributions!

Code of Conduct

Our Pledge

In the interest of fostering an open and welcoming environment, we pledge to make participation in our project and community a harassment-free experience for everyone.

Our Standards

Examples of behavior that contributes to creating a positive environment include:

  • Using welcoming and inclusive language
  • Being respectful of differing viewpoints and experiences
  • Gracefully accepting constructive criticism
  • Focusing on what is best for the community
  • Showing empathy towards other community members

Examples of unacceptable behavior include:

  • The use of inappropriate or unwelcome language or imagery
  • Trolling, insulting/derogatory comments, and personal or political attacks
  • Public or private harassment
  • Publishing others’ private information, such as a physical or electronic address, without explicit permission
  • Other conduct which could reasonably be considered inappropriate in a professional setting

Our Responsibilities

Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.

Scope

This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community.

Enforcement

Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at [contact@riadvice.com]. All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances.

Attribution

This Code of Conduct is adapted from the Contributor Covenant, version 1.4, available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html.

Release Notes

Support Policy

Sukarix is currently supported for PHP 8.4+.

Released versions

Unreleased

Nothing yet.

v0.5.0

🚀 Features & Improvements

  • Version Number: 0.5.0
  • Release Date: 2026-09-14
  • General Overview: Adds JSON API building blocks (stateless routes, ApiAction, CORS, health probes, capability tokens), request correlation with structured JSON logging, encryption at rest for stored secrets, AMQP messaging, a session contract applications can implement themselves, environment-variable configuration overrides, and privilege discovery from the action directory — alongside CSRF, database, mail and PHP 8.5 compatibility fixes. The release is additive — no existing API changed, so upgrading from v0.4.0 requires no application changes.

🐛 Bug Fixes

  • CSRF Validation: Could not succeed for PUT, DELETE and PATCH requests, nor for any request sending a JSON body. See CSRF.
  • Records blank after an insert (sukarix): Model::insert() now reads the record back when the insert left it blank. PostgreSQL identity columns carry no nextval default, so Fat-Free does not recognise them as auto increment; the reload Cortex runs itself then filters on the identifier the record held before the insert, matches nothing, and leaves the whole record empty in memory — defaults and timestamps included. Any schema created for PostgreSQL 10 or later is affected. See Databases & Cortex.
  • smtpSend() return type (sukarix): The method declared bool and returned the message id on success, which under strict_types throws a TypeError in any caller that reaches a successful send. It returns a boolean, as send() already promised. The framework’s own suite never caught it because no test reached the success path; one was added.
  • Stateless prefixes could not be turned off (sukarix): SECURITY.stateless.prefixes fell back to the /api default whenever it was declared empty, because an .ini entry with nothing after the equals sign reads as null and the lookup used ?:. An application whose API relies on the session — one authenticating with a JWT session, for instance — had no way to say so. A declared key now wins even when empty. See Session.
  • Test suite aborted part-way (sukarix): The test bootstrap registered no injector aliases, so the first scenario constructing an Action died resolving access and took the rest of the run with it. Two of the three test groups never executed: the suite reported 71 passing tests where 165 existed.
  • MailSender date format (sukarix): The date template variable used strftime()-style specifiers, which date() doesn’t support, so emails rendered literal %A/%d/%B instead of a date. Fixed.
  • Pluggable session at boot (sukarix): Boot::$session was typed against the concrete Session class, so an application registering its own SessionInterface implementation failed with a TypeError before the first request. It is typed against the interface now.
  • Error pages (sukarix): The default ONERROR handler included templates/error/*.phtml relative to the working directory; it now resolves them through the UI search paths, so error pages render whatever directory the process was started from.

Changes

  • CSRF Token Transport (sukarix): The token is now read from the X-Csrf-Token header, then from REQUEST, and compared with hash_equals(). It was read from the verb hive, which Fat-Free populates for GET, POST and COOKIE only, so the other verbs Action::beforeroute() enforces were always rejected.
  • CSRF Token Lifetime (sukarix): The token stays valid until csrf.expiry instead of being consumed on first use, and generateToken() reuses a live one, so pages holding several forms or issuing successive writes keep working. The csrf_used session key is gone.
  • CSRF Exemptions (sukarix): New $csrfExempt property on Sukarix\Actions\Action for endpoints reached by external systems, which hold no session and must authenticate their caller by their own means.
  • Data & Filesystem Helpers (sukarix): New Sukarix\Utils\DataUtils, Sukarix\Utils\FileSystemUtils and Sukarix\Utils\CommandExecutor, extracted from duplicated code in bbb-lb and BBBAnalytiX. See Data & Filesystem Helpers.
  • Session Contract (sukarix): New Sukarix\Core\SessionInterface, declaring what the framework itself requires of a session — cleanupOldSessions(), set(), get(), isLoggedIn(), getRole() and validateToken(). Sukarix\Core\Session implements it and HasSession types its property against it, so an application can register its own implementation under the session alias without inheriting the database-backed one, and without its own static analysis reporting every call as undefined. The contract is deliberately the framework’s requirement rather than the full surface of the shipped session; a test asserts that every method Sukarix calls on a session is declared in it, so the two cannot drift. See Session.
  • Uniqueness Checks (sukarix): New Model::excludeId(), adding an identifier exclusion to a filter and dropping the clause when there is no identifier to exclude. Written by hand, ... and id != ? with a null identifier evaluates to NULL in SQL, which makes the whole condition unsatisfiable — so the uniqueness check passes for every record while one is being created, and duplicates are accepted. See Databases & Cortex.
  • Query Return Type (sukarix): Model::find() declares DB\CortexCollection|false. Cortex documents an array, so castAll() and the other collection methods read as errors in static analysis and in an editor.
  • Sender Identity (sukarix): The configured mailer.from_name is now put on the message. A relay such as Postal authenticates as one identity and sends as another, so the account, the address and the name beside it are three separate settings; without the name the mail arrived showing a bare address. See Emails.
  • Unreachable SMTP Servers (sukarix): MailSender checks that the server accepts a connection before handing the message to the transport, logs the failure and returns false. The transport aborts the whole request when the server cannot be reached — not an exception the application can catch — so a mail server outage took the request with it. The wait is configurable with mailer.smtp.timeout, two seconds by default.
  • Exception Reports (sukarix): Reports are sent under mailer.debugger_name, which defaults to Application Debugger, so an inbox receiving reports from several applications can tell them apart. The snooze marker moved from md5 to xxh128 and no longer relies on suppressed filesystem calls.
  • Environment Variables (sukarix): New [environment] configuration section mapping a variable name to the setting it stands in for, applied after the configuration is loaded. A setting that differs per host — and especially a secret — no longer has to live in a committed file. A variable that is unset or empty leaves the configured value alone. See Configuration.
  • Declared Privileges (sukarix): New Sukarix\Utils\PrivilegeUtils, reading the privileges an application declares from the action classes that carry them. A privilege is the first two namespace segments below the root, so classes nested deeper share the privilege of the group above them. It reads the directory rather than the composer class map, where an action added after the autoloader was dumped is missing — and its privilege then silently disappears from the role matrix, leaving a route nobody can be granted. See Authorisation System.
  • ORM Deprecations (sukarix): ikkez/f3-cortex still calls ReflectionProperty::setAccessible(), which PHP 8.5 deprecates and Tracy escalates into a fatal error on every ORM query. The bootstrap now drops E_DEPRECATED from the forced error level after Fat-Free and Tracy have set theirs. Set orm.mask_deprecations = false once Cortex is fixed.
  • Staging Environment (sukarix): Added Environment::STAGING, recognised alongside development and production. See Framework Variables.
  • Request Correlation & JSON Logging (sukarix): Boot mints or forwards an X-Request-Id header and stamps every log line with it. log.json switches LogWriter to structured JSON output. See Logging.
  • Building JSON APIs (sukarix): New ApiAction base class, Cors helper and Health\Probe endpoints, plus Bearer auth and form-encoded body support on Action. See Building JSON APIs.
  • Capability Tokens (sukarix): New Sukarix\Security\CapabilityToken::issue()/verify() for HMAC-signed, short-lived Bearer tokens. See Building JSON APIs.
  • CSRF Bounce Message (sukarix): A failed CSRF check now also sets a csrf_bounce session message. See CSRF.
  • Secrets at Rest (sukarix): New Sukarix\Security\SecretBox, a libsodium secretbox wrapper for encrypting credentials before they’re stored. See Encrypting Secrets at Rest.
  • Mail Tracking Hooks (sukarix): New Sukarix\Mail\Track, the on.failure/on.ping/on.jump callbacks smtp.ini wires into delivery-failure and open/click tracking. See Emails.
  • AMQP Messaging (sukarix): New Sukarix\Messaging\CommandPublisher interface with an AmqpPublisher (RabbitMQ) and a RecordingPublisher for tests. See Messaging.
  • Uploads & Locale Negotiation (sukarix): New Receiver::uploadImageBase64() and Receiver::upload() with MIME sniffing and size limits, and I18n::negotiateLocale() for Accept-Language headers. See File Upload and Translation / i18n.
  • Tooling (sukarix, statera): PHPStan 2, Rector 2 and PHPInsights 2.14 with a clean composer check; Statera runs on php-code-coverage 14 and declares it as a runtime requirement, so composer coverage works out of the box. The unused respect/validation and the retired phpdd are gone.
  • CLI Toolbox (cli): sukarix.sh follows APP_ENV and staging, detects the installed PHP-FPM service, and cleans sessions through the framework’s SessionsClean action. See CLI toolbox.
  • Skeleton Application (application): The landing page shows the 0.5.0 features, gradient titles and a comparison with Fat-Free, Slim, Laravel and Symfony, in all 16 languages.
  • Documentation: Rewrote the CSRF page. Added Data & Filesystem Helpers, Building JSON APIs, Encrypting Secrets at Rest and Messaging. Expanded Databases & Cortex with the insert read-back, uniqueness checks and the query return type; Session with the session contract and session-free routes; Emails with sender identity, unreachable servers and exception reports; Configuration with the [environment] section and the new settings; Authorisation System with privilege discovery; Logging with request correlation and structured JSON logging; File Upload with the new receiver methods; and Translation / i18n with locale negotiation. Refreshed the feature list, the CLI toolbox page and the stale claims a release audit turned up.

v0.4.0

🚀 Features & Improvements

  • Version Number: 0.4.0
  • Release Date: 2026-08-16
  • General Overview: Adds a Redis-backed queue service for pipeline workloads, a HasQueue behaviour to consume it, and optional coloured console logging. The release is additive — no existing API changed, so upgrading from v0.3.2 requires no application changes.

Changes

  • Queue Service (sukarix): New Sukarix\Queue namespace providing QueueService, a Redis-backed FIFO queue for passing work between pipeline stages. It adds membership-based deduplication (pushing an already-queued payload is a no-op) and per-item attempt counters on top of a Redis list. Enqueue and dequeue are executed as Redis Lua scripts, so a worker terminated mid-operation can never leave the list and the membership set disagreeing — an item is either fully queued and tracked, or neither. The queue counts attempts but intentionally owns no retry policy: deciding when a failing item is retried or parked stays with the application. See Queues.
  • Queue Logging (sukarix): New QueueProgressLogger emitting structured queue events (start, progress, completion, throughput, batch progress, per-item add/remove, warnings and errors) with a machine-parsable context array. Item previews are generic by design — an array is reported as array[3], never with its values — so payload contents never leak into logs. Applications can route queue telemetry elsewhere by registering a subclass under the queue.progress_logger Injector alias; a service that does not extend QueueProgressLogger is rejected with an \UnexpectedValueException.
  • HasQueue Behaviour (sukarix): New behaviour trait exposing the resolved queue service as a typed $queueService property. Resolution is strict by design: constructing a consumer without a registered queue service throws a \LogicException naming the class, and a service that does not extend QueueService is rejected with an \UnexpectedValueException. This surfaces a misconfiguration at boot rather than at the first queue call, where a null member access would point at the wrong place.
  • Console Logging (sukarix): LogWriter gained an optional standard-output handler enabled with log.console = true, intended for CLI applications. When bramus/monolog-colored-line-formatter is installed the output is coloured per level, otherwise a plain stream is used. The file handler remains active, so console output is added rather than substituted. See Logging.
  • Redis Connection Timeout (sukarix): The queue honours redis.timeout (default 2 seconds) and throws \RedisException when the connection cannot be established, so a misconfigured worker fails immediately instead of part-way through a run.
  • Documentation: Added a Queues page. Rewrote the Behaviours page to list every available trait with the property it injects and its visibility, documenting that HasAccess and HasEvents expose a private property that subclasses cannot read. Corrected its example, which called a non-existent $this->log->write() instead of the $this->logger Monolog instance the LogWriter trait actually provides. Expanded Logging with the logger API, log.level and the new log.console setting. Added Queues to the feature list.

v0.3.2

🐛 Bug Fixes

  • Version Number: 0.3.2
  • Release Date: 2026-08-04
  • General Overview: Bug-fix release addressing cache, JSON encoding, and documentation accuracy issues found via static analysis.

Changes

  • HasCache::remember() (sukarix): Cache::get() returns false both on a cache miss and when the stored value is legitimately false, so remember() could never recognise a cached falsy value as a hit and recomputed it on every call. Switched to Cache::exists() to distinguish a real hit from a miss. The accompanying ResponseTest was rewritten to exercise the trait directly (the previous test asserted hardcoded literals against themselves) and a regression test for the falsy-value case was added.
  • Model::__construct() PHPDoc (sukarix): Corrected the @param tags, which inaccurately claimed $db, $table and $fluid must always be null. They mirror Cortex’s constructor and accept object, string and bool respectively; the wrong annotations caused PHPStan to flag the subsequent null-check as dead code.
  • TestScenario::postJsonData() (statera): The method declared a string return type but json_encode() can return false on failure, which under strict_types would throw a TypeError instead of a clear JSON error. Now passes JSON_THROW_ON_ERROR, matching the convention already used elsewhere in the class.
  • TestScenario::run() cleanup (statera): Removed an orphaned @var CodeCoverage $coverage docblock and its accompanying use statement left over from a prior refactor; they no longer corresponded to any variable in the method.

v0.3.1

🚀 Features & Improvements

  • Version Number: 0.3.1
  • Release Date: 2026-07-18
  • General Overview: Statera migration, trait initialization refactor, cursor-based pagination, cache helpers, and open-source readiness.

Changes

  • PHPUnit Removal: Dropped phpunit/phpunit and phpunit/php-code-coverage from require-dev and removed phpunit.xml.dist. The framework’s InjectorTest was converted from a PHPUnit TestCase to a Statera TestScenario using expect() assertions. Code coverage is now provided transitively by Statera’s own phpunit/php-code-coverage dependency.
  • Statera Dependency: Added sukarix/statera to require-dev so the framework uses its own testing kit for its test suite. Statera remains an optional, standalone package for applications.
  • Test Runner: Added tools/statera.php and test infrastructure (tests/src/Test/, tests/src/Suite/) to run the framework’s tests via Statera.
  • Statera Package: Declared the previously-undeclared sukarix/sukarix runtime dependency in sukarix/statera’s composer.json (Statera uses Sukarix\Utils\CliUtils and Sukarix\Utils\Time).
  • Trait Initialization: Moved Processor::initialize() from Tailored::instance() to class constructors. Each Tailored subclass must now call Processor::instance()->initialize($this) in its constructor. The Helper base class does this automatically.
  • LogWriter: Added initLogger() backward-compatible alias for initLogWriter().
  • TestCase: Removed final keyword to allow subclassing in application test suites.
  • Action: Added Processor import. Made $view, $argv, $headerAuthorization, and $templatesDir nullable to prevent uninitialized typed property errors.
  • MailSender: smtpSend() and generateId() changed from private to protected for extensibility.
  • Assets: Constructor now calls parent::__construct() for proper trait initialization.
  • TestScenario: loadResult() handles missing template files gracefully.
  • Cursor Pagination: Added Response::cursor() for cursor-based API pagination — stable under concurrent inserts, efficient on large datasets. Returns { field, limit, has_more, next } structure.
  • Cache Remember: Added HasCache::remember() for get-or-set cache pattern and HasCache::forget() for invalidation.
  • Paginate Fix: Response::paginate() now casts pages to int (was float from ceil()).
  • Open Source: Added AGENTS.md with contribution guidelines and AI usage transparency across all repositories.

v0.2.0

🚀 Features

  • Version Number: 0.2.0
  • Release Date: 2025-01-20
  • General Overview: Added CSRF protection, enhanced session management, and improved validation.

v0.1.0

🚀 Introduction

  • Version Number: 0.1.0
  • Release Date: 2024-06-14
  • General Overview: First version of the Sukarix Framework.