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:
-
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.
-
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.
-
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.
-
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.
-
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.
-
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.
-
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.
-
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
Testclass. It is a standalone, optional package — the framework itself uses it via arequire-devdependency for its own test suite. Applications can choose to use Statera or integrate any other testing framework (such as PHPUnit) as needed.
- 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
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
ApiActionbase 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 Name | F3 Native | Sukarix Replacement (Library) |
|---|---|---|
| Fast and clean template engine | Yes | - |
| Unit testing toolkit (Web UI & CLI) | Yes | Statera |
| Database-managed sessions | Yes | - |
| Markdown-to-HTML converter | Yes | - |
| Atom/RSS feed reader | Yes | - |
| Image processor | Yes | - |
| Geodata handler | Yes | - |
| On-the-fly Javascript/CSS compressor | Yes | Matthias Mullie Minify |
| OpenID (consumer) | Yes | - |
| Custom logger | Yes | Monolog |
| Basket/Shopping cart | Yes | - |
| Pingback server/consumer | Yes | - |
| Unicode-aware string functions | Yes | - |
| SMTP over SSL/TLS | Yes | F3 Mailer |
| Tools for communicating with other servers | Yes | - |
| Data Validation | Yes | Sukarix 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
Testclass. It is an optional, standalone package — the framework itself uses it viarequire-devfor 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
Webclass 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
sessionalias. - 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
CommandPublishercontract with a RabbitMQ implementation and an in-memory recorder for tests, for telling another service to do something. See Messaging. - JSON APIs: A stateless
ApiActionbase 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/expclaims, 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-Idthat flows into each log line;log.jsonswitches 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
sukarixcommand 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
- Initialization: When the application starts, the F3 singleton is created and the Hive is initialized with system variables and configurations.
- Routing: F3 uses the Hive to manage routes, mapping URLs to specific functions or controllers.
- Session Management: The framework uses the Hive to store and manage session data, ensuring user states are maintained across different requests.
- Configuration: All configuration settings are stored in the Hive, making it easy to adjust and retrieve settings as needed.
- 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
- Step 1: Create a new Sukarix project
- Step 2: Navigate to the project directory
- Step 3: Initialize vagrant
- Step 4: Access the Application
- Configuration Details
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=devto 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
.testdomains (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
- Configuration Load Order
- Environment Overrides
- Environment Variables
- Default Configuration Example
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
-
Base Configuration:
config/classes.iniconfig/default.ini
-
Additional Configurations:
- Files listed in the
CONFIGSvariable, e.g.,smtp,notifications,upload. No need to add.iniextension. config/validation.ini(loaded only when a data validation request is initiated).
- Files listed in the
-
Environment-Specific Configuration:
config/config-<environment>.ini
-
Routing and Access Control:
config/routes.iniconfig/routes-<environment>.iniconfig/access.iniorconfig/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
CONFIGSvariable, you do not need to include the.iniextension. Sukarix will automatically append.inito the file name during the loading process. For example, if you setCONFIGS=notifications,smtp,upload, Sukarix will loadconfig/notifications.ini,config/smtp.ini, andconfig/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 indefault.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_JSandMINIFY_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,/apiby 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 tofalseonce 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
- Configuration File
- Action
- Bootstrapping
- Environment
- Injector
- Processor
- Session
- ErrorChannel
- User Role
- Assets
- View
- Flash
- I18n
- MailSender
- Model
- Notifier
- Constraints
- Tailored
- Template Directive
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
- 1. Detect if the Application is Running in CLI Mode
- 2. Create an Instance of the Fat-Free Framework (F3) Singleton Class
- 3. Set Up PHP Variables
- 4. Automatically Detect the Application Environment
- 5. Load the Configuration Files
- 6. Set Up Logging
- 7. Add the Global Exception Handler
- 8. Connect to the Database
- 9. Prepare the Session
- 10. Load Additional Application Settings
- 11. Load Additional CLI Configuration if CLI Mode is Detected
- 12. Load Routing and Access Control Lists (ACL)
- 13. Execute the Start Function
- 14. Log Performance Metrics and Finish
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
- Example Usage
- Configuration & Default Dependencies
- Creating Injectable Singletons
- Storing DI Classes
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:
| Key | Class |
|---|---|
mailer | Sukarix\Mail\MailSender |
notifier | Sukarix\Notification\Notifier |
session | Sukarix\Core\Session |
i18n | Sukarix\Helpers\I18n |
assets | Sukarix\Helpers\Assets |
access | \Access |
events | Sugar\Event |
template | \Template |
html | Sukarix\Helpers\HTML |
multi_language | \Multilang |
receiver | Sukarix\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.
- Allows access to the home route (
-
allow GET @docs = admin,customer- Allows access to the documentation route (
@docs) only foradminandcustomerroles.
- Allows access to the documentation route (
-
allow @set_locale = *- Allows access to the set locale route (
@set_locale) for all users.
- Allows access to the set locale route (
-
allow DELETE @user_delete = admin- Allows access to delete user route (
@user_delete) only foradminrole.
- Allows access to delete user route (
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
.testtheapplication.environmentvariable will automatically be set todevelopment.
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
- Cortex ORM
- Database Migrations
- Model Class
- Reading a Record Back After an Insert
- Uniqueness Checks
- Query Return Type
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
Modelclass 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 IDENTITYrather than aserialcolumn, 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-writtenand 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:
| Key | Meaning |
|---|---|
upload.invalid_format | Not valid base64, or the MIME type is not in the allow list |
upload.exceeded_file_size | Larger than UPLOAD.maxsize.image |
upload.failed_to_move | The 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
- Configuration
- Using the Validator
- How Validation Works
- Available Validation Rules
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
-
Audit Class:
- If available, validation rules are first checked in the
Auditclass.
- If available, validation rules are first checked in the
-
Constraints Class:
- If the rule is not found in the
Auditclass, it falls back to theConstraintsclass.
- If the rule is not found in the
-
Custom Validator:
- If the rule is not found in the
Constraintsclass, it checks for a user-defined validator class specified in theVALIDATORconfiguration property.
- If the rule is not found in the
Warning
Validation rule names are case insensitive
Validator names are case insensitive, meaning
notempty,notEmpty,NotEmpty, andNOTEMPTYare 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 betweenminandmax.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():
| Method | Purpose |
|---|---|
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
SessionInterfaceis not the full surface ofSukarix\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
/apidefault. Writing it with nothing after the equals sign is how an application says that no route is session free.
Logging
- Introduction
- Log Level
- Request Correlation
- Structured (JSON) Logging
- Console Output
- Logging Naming Conventions
- Log Rotation
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
Tailoredsingleton class, ensure to call theinitLogWritermethod 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)ordumpe($variable)Debugger::dump($variable)Debugger::barDump($variable)
For more detailed usage and advanced features, refer to the Tracy documentation.
Emails
- MailSender Class
- Method Parameters
- Usage Example
- Sender Identity
- Unreachable Servers
- Delivery & Tracking Hooks
- Exception Reports
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
/mailfolder. - $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:
| Setting | Meaning |
|---|---|
mailer.smtp.user | The account the application signs in to the server with |
mailer.from_mail | The address the mail comes from |
mailer.from_name | The 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
Mailerinstance 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
\Exceptioncontaining 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
- Methods
- Example Usage
- locales.js
- Example Usage in locales.js
- Routing for JSON localisation
- Caching
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:
- Labels (
i18n.label) - Messages (
i18n.message) - Errors (
i18n.error) - 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
| Trait | Property | Visibility | Description |
|---|---|---|---|
LogWriter | $logger | protected | A Monolog logger writing to the application log |
HasF3 | $f3 | protected | The Fat-Free Framework instance |
HasSession | $session | protected | The session service |
HasCache | $cache | protected | The cache service, with remember() and forget() helpers |
HasI18n | $i18n | protected | The translation service |
HasMessages | $messages | protected | Flash messages |
HasAssets | $assets | protected | Asset management |
HasQueue | $queueService | protected | The queue service; requires a registered queue service |
HasAccess | $access | private | The ACL service |
HasEvents | $events | private | The 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 theuse, 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
- Requirements
- Configuration
- Registering the service
- Using the queue in a service
- Queue operations
- Tracking failed attempts
- Consistency guarantees
- Queue logging
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
HasQueuethrows a\LogicExceptionif noqueueservice 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
| Method | Description |
|---|---|
pushToQueue(string $queue, mixed $data): void | Enqueue a payload, skipping it if already queued |
popFromQueue(string $queue): mixed | Dequeue the oldest payload, or null when empty |
peek(string $queue): mixed | Read the next payload without removing it |
getQueueSize(string $queue): int | Number of queued items |
isEmpty(string $queue): bool | Whether the queue holds no items |
existsInQueue(string $queue, mixed $value): bool | Whether a payload is already tracked |
getQueueItems(string $queue, ?int $limit = null): array | Snapshot of queued items, oldest first |
clearQueue(string $queue): void | Delete 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
- Stateless Routes
- The
ApiActionBase Class - CORS
- Health Probes
- Capability Tokens
- Bearer Authentication
- Request Bodies
- Testing JSON Responses
- Request Correlation
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->sessionbecomes nullable on a stateless route
Boot::$sessionisnullfor the lifetime of a stateless request.Action::beforeroute()andgetRole()already guard every access, but any code you write that reaches for$this->sessionon such a route must check fornullfirst.
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]);
}
}
$csrfExemptistrueby default — a machine caller holds no session token to send.onAccessAuthorizeDeny()answers a denied route with a JSON403instead of the HTML flowActionuses.onError()is a static handler you can register asONERRORfor API routes; it turns any uncaught error into the same JSON envelope, usingERROR.codeandERROR.textfrom 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()answers200with{ "status": "ok", "request_id": "..." }— use it to know the process is up.readiness()additionally pings Redis whenCACHEis Redis-backed (redis=...), answering503with"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
.inifile, per Configuration) and rotate it if it ever leaks.CapabilityTokendoes 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
.phtmland 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
typeattribute, such as"module". If thetypeattribute 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 inpublic/js, and minified files inpublic/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
- Access Instance
- Default Authorization System
- Security Best Practices
- Declared Privileges
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
denysetting 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
- Configuration
- Protected Verbs
- Adding the Token to a Form
- Sending the Token with AJAX Requests
- Token Lifetime
- Exempting an Endpoint
- Validating the Token Manually
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 tokenbefore 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
$_POSTfor form encoded bodies. PUT,DELETEandPATCHrequests, because Fat-Free only mapsGET,POSTandCOOKIEinto the hive, so a token sent as aPUTparameter 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.inior the environment-specificconfig-<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 —
SecretBoxdoes 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: Makesukarixrunnable from anywhere.
Development
--enabletests: Enable running unit tests from the browser.--test [-c] [name]: Run the unit tests, optionally a single group;-cadds 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’sSukarix\Actions\Core\SessionsCleanCLI 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,nullfor 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
\RuntimeExceptionon failure. - isDirectoryEmpty(string $dir): Returns
falsefor 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(viamikehaertl/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). ExtendsSukarix\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
-
Upload Your Code:
- Transfer your application’s codebase to the production server using your preferred method (e.g., FTP, SCP, or Git).
-
Ensure
sukarix.shis in thetoolsDirectory:- Make sure the
sukarix.shscript is located in thetoolsdirectory of your application.
- Make sure the
-
Run the Deployment Script:
- Execute the deployment script with the
-doption:./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-datauser. - Runs database migrations.
- Clears the application cache.
- Execute the deployment script with the
Deploying Documentation
The Sukarix documentation is built with mdBook and deployed automatically to GitHub Pages via GitHub Actions — at no cost.
How It Works
- A GitHub Actions workflow (
.github/workflows/deploy-docs.yml) triggers on every push to themainbranch. - It installs the Rust toolchain, then builds mdBook along with the
mdbook-tocpreprocessor. mdbook buildgenerates the static site in thebook/output directory.- 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.comis set via thecnamefield inbook.tomland configured in GitHub Pages settings. - GitHub Pages must be enabled in the repository settings (see steps below).
One-Time Setup
-
Enable GitHub Pages:
- Go to the repository on GitHub → Settings → Pages.
- Under Source, select GitHub Actions.
- Save.
-
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.
- In your domain registrar, add a CNAME record:
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.
- Fork the Repository: Create a fork of the Sukarix repository.
- Create a Branch: Make a new branch for your feature or bugfix.
- Commit Changes: Commit your changes with clear commit messages.
- 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
- v0.5.0 - 2026-09-14
- v0.4.0 - 2026-08-16
- v0.3.2 - 2026-08-04
- v0.3.1 - 2026-07-18
- v0.2.0 - 2025-01-20
- v0.1.0 - 2024-06-14
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,DELETEandPATCHrequests, 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 nonextvaldefault, 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 declaredbooland returned the message id on success, which understrict_typesthrows aTypeErrorin any caller that reaches a successful send. It returns a boolean, assend()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.prefixesfell back to the/apidefault whenever it was declared empty, because an.inientry with nothing after the equals sign reads asnulland 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 anActiondied resolvingaccessand 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): Thedatetemplate variable usedstrftime()-style specifiers, whichdate()doesn’t support, so emails rendered literal%A/%d/%Binstead of a date. Fixed. - Pluggable session at boot (
sukarix):Boot::$sessionwas typed against the concreteSessionclass, so an application registering its ownSessionInterfaceimplementation failed with aTypeErrorbefore the first request. It is typed against the interface now. - Error pages (
sukarix): The defaultONERRORhandler includedtemplates/error/*.phtmlrelative to the working directory; it now resolves them through theUIsearch paths, so error pages render whatever directory the process was started from.
Changes
- CSRF Token Transport (
sukarix): The token is now read from theX-Csrf-Tokenheader, then fromREQUEST, and compared withhash_equals(). It was read from the verb hive, which Fat-Free populates forGET,POSTandCOOKIEonly, so the other verbsAction::beforeroute()enforces were always rejected. - CSRF Token Lifetime (
sukarix): The token stays valid untilcsrf.expiryinstead of being consumed on first use, andgenerateToken()reuses a live one, so pages holding several forms or issuing successive writes keep working. Thecsrf_usedsession key is gone. - CSRF Exemptions (
sukarix): New$csrfExemptproperty onSukarix\Actions\Actionfor endpoints reached by external systems, which hold no session and must authenticate their caller by their own means. - Data & Filesystem Helpers (
sukarix): NewSukarix\Utils\DataUtils,Sukarix\Utils\FileSystemUtilsandSukarix\Utils\CommandExecutor, extracted from duplicated code in bbb-lb and BBBAnalytiX. See Data & Filesystem Helpers. - Session Contract (
sukarix): NewSukarix\Core\SessionInterface, declaring what the framework itself requires of a session —cleanupOldSessions(),set(),get(),isLoggedIn(),getRole()andvalidateToken().Sukarix\Core\Sessionimplements it andHasSessiontypes its property against it, so an application can register its own implementation under thesessionalias 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): NewModel::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 toNULLin 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()declaresDB\CortexCollection|false. Cortex documents an array, socastAll()and the other collection methods read as errors in static analysis and in an editor. - Sender Identity (
sukarix): The configuredmailer.from_nameis 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):MailSenderchecks that the server accepts a connection before handing the message to the transport, logs the failure and returnsfalse. 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 withmailer.smtp.timeout, two seconds by default. - Exception Reports (
sukarix): Reports are sent undermailer.debugger_name, which defaults toApplication Debugger, so an inbox receiving reports from several applications can tell them apart. The snooze marker moved frommd5toxxh128and 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): NewSukarix\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-cortexstill callsReflectionProperty::setAccessible(), which PHP 8.5 deprecates and Tracy escalates into a fatal error on every ORM query. The bootstrap now dropsE_DEPRECATEDfrom the forced error level after Fat-Free and Tracy have set theirs. Setorm.mask_deprecations = falseonce Cortex is fixed. - Staging Environment (
sukarix): AddedEnvironment::STAGING, recognised alongsidedevelopmentandproduction. See Framework Variables. - Request Correlation & JSON Logging (
sukarix):Bootmints or forwards anX-Request-Idheader and stamps every log line with it.log.jsonswitchesLogWriterto structured JSON output. See Logging. - Building JSON APIs (
sukarix): NewApiActionbase class,Corshelper andHealth\Probeendpoints, plus Bearer auth and form-encoded body support onAction. See Building JSON APIs. - Capability Tokens (
sukarix): NewSukarix\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 acsrf_bouncesession message. See CSRF. - Secrets at Rest (
sukarix): NewSukarix\Security\SecretBox, a libsodium secretbox wrapper for encrypting credentials before they’re stored. See Encrypting Secrets at Rest. - Mail Tracking Hooks (
sukarix): NewSukarix\Mail\Track, theon.failure/on.ping/on.jumpcallbackssmtp.iniwires into delivery-failure and open/click tracking. See Emails. - AMQP Messaging (
sukarix): NewSukarix\Messaging\CommandPublisherinterface with anAmqpPublisher(RabbitMQ) and aRecordingPublisherfor tests. See Messaging. - Uploads & Locale Negotiation (
sukarix): NewReceiver::uploadImageBase64()andReceiver::upload()with MIME sniffing and size limits, andI18n::negotiateLocale()forAccept-Languageheaders. See File Upload and Translation / i18n. - Tooling (
sukarix,statera): PHPStan 2, Rector 2 and PHPInsights 2.14 with a cleancomposer check; Statera runs on php-code-coverage 14 and declares it as a runtime requirement, socomposer coverageworks out of the box. The unusedrespect/validationand the retiredphpddare gone. - CLI Toolbox (
cli):sukarix.shfollowsAPP_ENVandstaging, detects the installed PHP-FPM service, and cleans sessions through the framework’sSessionsCleanaction. 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
HasQueuebehaviour 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): NewSukarix\Queuenamespace providingQueueService, 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): NewQueueProgressLoggeremitting 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 asarray[3], never with its values — so payload contents never leak into logs. Applications can route queue telemetry elsewhere by registering a subclass under thequeue.progress_loggerInjector alias; a service that does not extendQueueProgressLoggeris rejected with an\UnexpectedValueException. - HasQueue Behaviour (
sukarix): New behaviour trait exposing the resolved queue service as a typed$queueServiceproperty. Resolution is strict by design: constructing a consumer without a registeredqueueservice throws a\LogicExceptionnaming the class, and a service that does not extendQueueServiceis 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):LogWritergained an optional standard-output handler enabled withlog.console = true, intended for CLI applications. Whenbramus/monolog-colored-line-formatteris 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 honoursredis.timeout(default 2 seconds) and throws\RedisExceptionwhen 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
HasAccessandHasEventsexpose aprivateproperty that subclasses cannot read. Corrected its example, which called a non-existent$this->log->write()instead of the$this->loggerMonolog instance theLogWritertrait actually provides. Expanded Logging with the logger API,log.leveland the newlog.consolesetting. 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()returnsfalseboth on a cache miss and when the stored value is legitimatelyfalse, soremember()could never recognise a cached falsy value as a hit and recomputed it on every call. Switched toCache::exists()to distinguish a real hit from a miss. The accompanyingResponseTestwas 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@paramtags, which inaccurately claimed$db,$tableand$fluidmust always benull. They mirror Cortex’s constructor and acceptobject,stringandboolrespectively; the wrong annotations caused PHPStan to flag the subsequent null-check as dead code. - TestScenario::postJsonData() (
statera): The method declared astringreturn type butjson_encode()can returnfalseon failure, which understrict_typeswould throw aTypeErrorinstead of a clear JSON error. Now passesJSON_THROW_ON_ERROR, matching the convention already used elsewhere in the class. - TestScenario::run() cleanup (
statera): Removed an orphaned@var CodeCoverage $coveragedocblock and its accompanyingusestatement 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/phpunitandphpunit/php-code-coveragefromrequire-devand removedphpunit.xml.dist. The framework’sInjectorTestwas converted from a PHPUnitTestCaseto a StateraTestScenariousingexpect()assertions. Code coverage is now provided transitively by Statera’s ownphpunit/php-code-coveragedependency. - Statera Dependency: Added
sukarix/stateratorequire-devso the framework uses its own testing kit for its test suite. Statera remains an optional, standalone package for applications. - Test Runner: Added
tools/statera.phpand test infrastructure (tests/src/Test/,tests/src/Suite/) to run the framework’s tests via Statera. - Statera Package: Declared the previously-undeclared
sukarix/sukarixruntime dependency insukarix/statera’scomposer.json(Statera usesSukarix\Utils\CliUtilsandSukarix\Utils\Time). - Trait Initialization: Moved
Processor::initialize()fromTailored::instance()to class constructors. EachTailoredsubclass must now callProcessor::instance()->initialize($this)in its constructor. TheHelperbase class does this automatically. - LogWriter: Added
initLogger()backward-compatible alias forinitLogWriter(). - TestCase: Removed
finalkeyword to allow subclassing in application test suites. - Action: Added
Processorimport. Made$view,$argv,$headerAuthorization, and$templatesDirnullable to prevent uninitialized typed property errors. - MailSender:
smtpSend()andgenerateId()changed fromprivatetoprotectedfor 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 andHasCache::forget()for invalidation. - Paginate Fix:
Response::paginate()now castspagestoint(wasfloatfromceil()). - Open Source: Added
AGENTS.mdwith 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.