cubadevops / flexi
The flexible framework that respects standards
Fund package maintenance!
Requires
- php: >=7.4
- ext-json: *
- composer/semver: ^3.4
- cubadevops/upgrader: ^1.6
- flexi/contracts: ^5.0
- guzzlehttp/guzzle: ^7.7
- opis/json-schema: ^2.6
- symfony/error-handler: ^5.4
- vlucas/phpdotenv: ^5.5
Requires (Dev)
- friendsofphp/php-cs-fixer: ^3.57
- phpunit/phpunit: ^9.6
- vimeo/psalm: ^5.22.2
Suggests
None
Provides
None
Conflicts
None
Replaces
None
This package is auto-updated.
Last update: 2026-10-27 23:17:12 UTC
README
Flexi is a modular PHP framework designed to facilitate the development of scalable and maintainable applications. It leverages Dependency Injection (DI), a flexible routing system, CQRS and an event-driven architecture.
Table of Contents
Features
- Modularity & Extensibility: Flexi offers a modular architecture that promotes code organization and reusability. Its extensibility allows developers to customize and extend functionality according to specific project requirements. Each module is a self-contained Composer package with its own dependencies, allowing for easy installation, uninstallation, and version management.
- Modular Architecture: Flexi discovers local and Composer-installed modules. A module declares its configuration files in its Composer
extra.fleximanifest and must be activated before its components are registered. - Dependency Injection: Built-in support for Dependency Injection simplifies management and injection of dependencies, enhancing code maintainability and testability. DI in Flexi is implemented using a container that manages service with lazy loading instantiation.
- Flexible Routing: With a flexible routing system, developers can easily define and manage HTTP request handling, streamlining development of complex routing logic enriched with middleware support.
- Event-Driven Architecture: Flexi dispatches events to registered listeners, helping keep event producers separate from application reactions.
- CQRS: Separate command and query buses let applications organize request handling around explicit command and query handlers.
- Validated JSON Configuration: Services, routes, listeners, commands, and queries use JSON configuration validated against the schemas shipped by
flexi/contracts. That package is the single source of truth for the core and modules.
Structure
- Core: the
src/directory contains the core classes and interfaces that make up the framework. - Contracts: shared interfaces and configuration schemas are provided by the Composer dependency
flexi/contracts; this repository does not maintain a separatecontracts/directory. - Modules: the
modules/directory contains all pieces of code that are not part of the core framework. Each module is a self-contained Composer package with its owncomposer.json, dependencies, controllers, services, and configuration files. Modules are plug-and-play and can be easily installed, uninstalled, or versioned independently. - Config: the
src/Config/andmodules/*/Configdirectories contain configuration files for the framework (routes,commands,queries,event listenersandservices). - Console: the
bin/directory contains the console application for CLI commands.
Installation
The Flexi core repository is a Composer project. Clone the repository and install its dependencies:
git clone https://github.com/Flexi-PHP/flexi-core.git my-app
cd my-app
composer install
This sets up the core project. Flexi does not currently provide a separate application scaffolding command.
Steps after install
To get started with Flexi Framework, ensure you:
- Configure your web server to point to the
publicdirectory in the root of the project. - Deny direct access to any directory that is not the
publicdirectory. - Serve static files directly and route all other requests to the
index.phpfile in thepublicdirectory. - Set up the
.envfile with the necessary environment variables for your application.
After setup, configure and activate a module that provides at least one route before browsing the application. The core repository has no default welcome route; with the default Docker configuration, the application is available at http://localhost:8080.
Module Management
Modules are discovered from the local modules/ directory and Composer's installed packages. Each module declares its identity and configuration files in extra.flexi in its composer.json; only active modules contribute registrations. Before the default container build, Flexi checks declared core compatibility, required active module dependencies, their version constraints, and circular dependencies. See the module manifest and module lifecycle for the current format and behavior.
For example, a module can declare its routes and services like this:
{
"name": "acme/flexi-module-account",
"type": "flexi-module",
"require": {"php": ">=7.4", "flexi/contracts": "^5.0"},
"autoload": {"psr-4": {"Acme\\Account\\": "src/"}},
"extra": {
"flexi": {
"schema-version": 1,
"module-name": "Account",
"compatibility": {"core": "^3.0"},
"config-files": ["Config/services.json", "Config/routes.json"],
"provides": ["account.profile"]
}
}
}
Available Commands
The available module console commands are defined in src/Config/commands.json. Install package dependencies with Composer, then use the registered modules:list, modules:status, modules:activate, modules:deactivate, modules:info, modules:validate, modules:env-update, and modules:publish commands as appropriate. Publishing copies a vendor package into modules/ and switches Composer to a local path repository; this transition is one-way and documented in the module recipes.
For a copyable module structure and operational steps, see the minimal module template and module recipes.
Configuration
Configuration for Flexi is managed through JSON files. These files define services, routes, events, queries, and commands used within your application.
Services
Services are defined in services.json. Module configuration files are selected by the core from the module manifest; file discovery does not use glob entries inside JSON.
Example
{
"services": [
{
"name": "App\\Infrastructure\\Mailer",
"class": {"name": "App\\Infrastructure\\SmtpMailer", "arguments": ["ENV.MAILER_DSN"]}
},
{
"name": "mailer",
"alias": "App\\Infrastructure\\Mailer"
}
]
}
Note:
- Each service has a unique
name; analiasdefinition maps a service ID to another service ID. - Arguments prefixed with
@are references to other services (can be alias as well) - Arguments prefixed with
ENV.are references to environment variables. - Quoted strings without
@orENV.prefixes are treated as string values. - All other values are treated as the standard json_decode values.
- Modules list configuration paths in
extra.flexi.config-files; the core loads files from active modules.
Service decoration
Modules can wrap a service while keeping its original service ID. Give the decorator a unique ID, declare the target with decorates, and pass @.inner exactly once in its constructor or factory arguments. Decoration chains must be explicit: a second decorator targets the first decorator's ID. Duplicate IDs, missing targets, and ambiguous decoration are rejected during container construction.
{
"services": [
{
"name": "App\\Logging\\LoggedMailer",
"decorates": "App\\Contracts\\MailerInterface",
"class": {
"name": "App\\Logging\\LoggedMailer",
"arguments": ["@.inner", "@logger"]
}
}
]
}
The decorated target continues to be resolved through its original ID. The @.inner token is reserved for service decorator arguments.
Routes
Routes are defined in routes.json. Each route specifies a name, path, HTTP method, and controller. Module route files are listed in the module manifest.
Example
{
"routes": [
{
"name": "health",
"path": "/health",
"method": "GET",
"controller": "App\\Http\\HealthController",
"parameters": [],
"middlewares": ["auth.middleware"]
},
{
"name": "account.profile",
"path": "/account/profile",
"method": "GET",
"controller": "App\\Http\\ProfileController",
"parameters": [{"name": "locale", "required": true}]
}
]
}
Note:
- Controllers must implement
Psr\Http\Server\RequestHandlerInterface. Each route middleware ID must resolve from the PSR-11 container and resolve to aPsr\Http\Server\MiddlewareInterfaceinstance. The router applies configured middleware regardless of the controller's base class. - The optional
parameterskey declares required query or form parameters; it does not define path placeholders. - The optional
middlewareskey lists container IDs for middleware to execute before the controller. They run in the listed order, with the first entry outermost. Middleware can short-circuit by returning a response without calling the next handler. - Modules list route configuration files in
extra.flexi.config-files; the core loads files from active modules.
Register each middleware ID in services.json, for example:
{
"services": [
{
"name": "auth.middleware",
"class": {"name": "App\\Http\\Middleware\\AuthMiddleware"}
}
]
}
Routes with duplicate names or METHOD path pairs fail at startup. A module can intentionally replace a route by setting overrides to the existing route name; the replacement must keep that same name. Routes sharing a path with different HTTP methods are supported.
{
"routes": [
{
"name": "account.profile",
"overrides": "account.profile",
"path": "/account/profile",
"method": "GET",
"controller": "App\\Account\\ProfileController"
}
]
}
Events and Listeners
Events and listeners are defined in the listeners.json file. This file maps events to their corresponding listeners.
Example
[
{
"event": "App\\Order\\OrderPlaced",
"listeners": ["App\\Order\\SendConfirmationEmail"]
},
{
"event": "*",
"listeners": ["App\\Observability\\LogEvents"]
}
]
Note:
- The
eventkey can be a specific event name or a wildcard*to listen to all events. - The listener class should implement
Flexi\Contracts\Interfaces\EventListenerInterfaceor extend theFlexi\Contracts\Classes\EventListenerbase class. Itshandle()method receives anEventInterfaceand returnsvoid. - Module listener files are declared in
extra.flexi.config-files; do not add glob objects to listener JSON files.
Queries
Queries are defined in the queries.json file. Each query handler is mapped to a specific DTO and an optional CLI
alias.
Example
{
"handlers": [
{
"id": "App\\Catalog\\GetProductQuery",
"cli_alias": "catalog:product",
"handler": "App\\Catalog\\GetProductHandler"
}
]
}
Note:
- Handlers implement
Flexi\Contracts\Interfaces\HandlerInterface; the query identifier is the DTO class name. Module query files are declared inextra.flexi.config-files.
Commands
Commands are defined similarly to queries in the commands.json file.
Example
{
"handlers": [
{
"id": "App\\Order\\PlaceOrderCommand",
"cli_alias": "orders:place",
"handler": "App\\Order\\PlaceOrderHandler"
}
]
}
Note:
- Handlers implement
Flexi\Contracts\Interfaces\HandlerInterface; the command identifier is the DTO class name. Module command files are declared inextra.flexi.config-files.
Load Order and Priority
The core configuration loads first, followed by active modules sorted by logical name. You can override the module order in the root Composer manifest with extra.flexi.module-load-order; active modules not listed remain alphabetically ordered, while inactive and unknown names are ignored. Definitions retain their JSON array order. Flexi does not use implicit priorities to replace services, routes, handlers, or listeners: conflicts fail unless the service decorator or route override rules explicitly permit replacement. Listener priority is currently fixed; specific-event listeners run before wildcard listeners. See Configuration Load Order and Priority for the complete rules and an example.
Usage
Router
The Router class is responsible for managing routes and dispatching requests to the appropriate controllers.
Example Usage
use Flexi\Infrastructure\Http\Router; function getHomePath(Router $router): string { $route = $router->getByName('home'); return $route->getPath(); }
Event System
The event system in Flexi is based on the EventBus pattern. Events are dispatched to listeners which can handle them accordingly.
Example
use Flexi\Contracts\Interfaces\EventBusInterface; use Flexi\Domain\Events\Event; function dispatchHealthCheck(EventBusInterface $eventBus): void { $event = new Event('health-check', 'health-check endpoint', ['from' => $_SERVER['REMOTE_ADDR'] ?? 'unknown']); $eventBus->dispatch($event); }
CQRS
Flexi implements the CQRS pattern with separate handling for commands and queries.
Command Example
use Flexi\Infrastructure\Bus\CommandBus; // Assume $command is a class that implements the DTOInterface $commandBus->execute($command);
Query Example
use Flexi\Infrastructure\Bus\QueryBus; // Assume $query is a class that implements the DTOInterface $result = $queryBus->execute($query);
Controllers and Responses
Route controllers implement PSR-15 RequestHandlerInterface and return a PSR-7 response. The router resolves the controller and wraps it with the route's declared middleware; this behavior does not depend on extending a Flexi base class. The default ResponseFactoryInterface service uses GuzzleHttp\Psr7\HttpFactory.
namespace App\Http; use Psr\Http\Message\ResponseFactoryInterface; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Server\RequestHandlerInterface; final class GreetingHandler implements RequestHandlerInterface { private ResponseFactoryInterface $responses; public function __construct(ResponseFactoryInterface $responses) { $this->responses = $responses; } public function handle(ServerRequestInterface $request): ResponseInterface { $response = $this->responses->createResponse(200); $response->getBody()->write('Hello'); return $response; } }
Flexi\Contracts\Classes\HttpHandler remains available as a convenience base class for creating PSR-7 responses. The route middleware declared in routes.json is composed by the router independently of that base class. See the minimal module template for a complete controller and middleware example.
Middlewares
Middlewares are classes that can be executed before the controller. They can modify the request, response or stop the execution chain. Middlewares should implement the Psr\Http\Server\MiddlewareInterface interface.
Middleware Implementation Example
namespace Flexi\Infrastructure\Middlewares; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Server\MiddlewareInterface; use Psr\Http\Server\RequestHandlerInterface; class AuthCheckMiddleware implements MiddlewareInterface { public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface { // Perform authentication logic here and stop the execution chain if necessary // or pass the request return $handler->handle($request); } }
Testing
Flexi includes a comprehensive test suite using PHPUnit. Tests use a separate environment configuration to ensure isolation from development and production environments.
Running Tests
# Run all tests ./vendor/bin/phpunit tests/ # Run specific test file ./vendor/bin/phpunit tests/Infrastructure/TestEnvironmentTest.php # Run with coverage ./vendor/bin/phpunit tests/ --coverage-html coverage/
Test Environment
Tests automatically use .env.testing configuration file with test-specific settings:
- Synchronous event dispatch: Events are dispatched synchronously for predictable testing
- In-memory cache: Faster tests without disk I/O
- Separate logs: Test logs go to
var/logs/test.log - Isolated cache: Test cache uses
var/cache/test/directory
To customize test environment:
- Copy
.env.testing.exampleto.env.testing - Modify values as needed
- Tests will automatically use these values
For more details, see tests/README.md.
Writing Tests
<?php namespace Flexi\Test\YourNamespace; use PHPUnit\Framework\TestCase; class YourTest extends TestCase { public function testSomething(): void { // Test environment variables from .env.testing are automatically loaded $this->assertTrue(true); } }
Documentation
The documentation is available online at https://flexi.cubadevops.com (Under construction and not yet available).
Contributing
Contributions are welcome! Please submit a pull request or open an issue to discuss any changes you would like to make.
License
Flexi is open-source software licensed under the MIT license.