Skip to content
PHP microframework for robust API development with CQRS
Branch: master
Clone or download

Latest commit

Fetching latest commit…
Cannot retrieve the latest commit at this time.


Type Name Latest commit message Commit time
Failed to load latest commit information.
app backoffice refacktoring Apr 4, 2020
config refactoring in oauth Apr 2, 2020
helpers various fixes Mar 24, 2020
public changed namespace for system files Mar 20, 2020
resources fix Apr 4, 2020
storage poprawki i ulepszenia Feb 24, 2019
.ackrc various fixes Nov 9, 2019
.editorconfig initial commit Feb 23, 2019
.env_example error reporting done Apr 26, 2019
.gitignore fixy pod deployment na mydevil Jun 3, 2019 small fix in readme Mar 22, 2020
apidoc.json apidoc May 29, 2019
cli-config.php translations introduced Jan 6, 2020
composer.json updated composer Mar 22, 2020
composer.lock fix in templates Mar 22, 2020
phpinsights.php code refacktoring in Users module Nov 16, 2019
phpstan.neon fixes after static code analysis Mar 7, 2020
queue-worker changed namespace for system files Mar 20, 2020


Cordo is a microframework designed for efficient development of REST APIs using principles such as:

  • Layered architecture
  • CQRS (Command Query Responsibility Segregation)
  • Event Dispatcher
  • Repository pattern
  • Queues (Redis, RabbitMQ, Amazon SQS)
  • OAuth2
  • UUIDs
  • Package by feature
  • Minimal config approach

It's compliant with PSRs: PSR-1, PSR-2, PSR-3, PSR-4, PSR-7, PSR-11, PSR-15, PSR-18.

Main goal to create this framework was to have an efficient tool to build API based applications with good architectural foundations and as little "magic" and configuration as possible.

Note: Cordo is still in development. Some of the basic features are implemented but tests are still missing. Please keep that in mind.


  • PHP 7.4.0 or newer
  • Apache/Nginx
  • MySql 5.7
  • PHP Redis extension (default driver for queues)


If you'd like to utilize of the Queues functionality make sure that you have Redis server installed on your machine by typing in your shell:

$ redis-cli ping

If Redis is properly installed it replies with PONG. Otherwise you should install it from PECL repository:

$ sudo pecl install redis
$ sudo service php7.4-fpm restart

Next clone this repository into your new project folder.

$ git clone .


$ composer install

Next copy .env_example file and rename it to .env. Then complete it with your configuration data.

Final step is running console command:

$ php cordo system:init

It will create:

  • DB schema based on Doctrine XML metadata files
  • All the neccessary DB tables for OAuth2
  • Uuid DB helper functions

After that you are good to go with Authentication, Acl and basic CRUD operations in Backoffice\Users module.

Still missing

  • Caching

How things work

Cordo does not reinvent the wheel. It is basically a set of popular PHP libraries put together and configured in order to create a simple framework that is in compliance with good programming practices for modern PHP.

Some of the used libraries:

This documentation does not focus on describing in detail how to deal with Routes, Command Bus, DI Container, querying DB, etc., for that use the documentation of the relevant library.

You are also encouraged to find for yourself how things work under the hood, check the cordo-core library where abstract classes and interfaces are located.

Cordo is shipped with one previously prepared module: Backoffice\Users. It presents how the code can be organized within all the layers and utilizes of Events and Queues.

Entry points

Entry points to the application:


Entry point for HTTP requests is public/index.php. Your apache/nginx configuration should point to the public folder.


Command-line commands are handled with use of Symfony Console component.

You can fire your commands through command-line with:

php cordo [command_name]

List currently registered commands:

php cordo list

Global commands should be registered in ./cordo file by adding them to the application object:

$application->add(new SecurityCheckerCommand(new SecurityChecker()));

Feature commands should be registered in app/[Context]/[PackageName]/UI/Console/commands.php file.

return [

Registering new package

This framework uses package by feature approach. It means that you organize your code in packages placed in app/ folder.

Just add your package folder name to the app/Register.php:

protected static $register = [
    // add you packages here

Once you package is registered, framework will have access to defined routes, DI container definitions, configs, commands, etc.

Package structure

Users package is shipped by default with implemented basic CRUD actions.

Here's how the code is organised:

├── Application
│   ├── Acl
│   │   └── UsersAcl.php
│   ├── Command
│   │   ├── CreateNewUser.php
│   │   ├── DeleteUser.php
│   │   ├── Handler
│   │   │   ├── CreateNewUserHandler.php
│   │   │   ├── DeleteUserHandler.php
│   │   │   ├── SendUserWelcomeMessageHandler.php
│   │   │   └── UpdateUserHandler.php
│   │   ├── SendUserWelcomeMessage.php
│   │   └── UpdateUser.php
│   ├── Event
│   │   ├── Listener
│   │   │   └── UserCreatedListener.php
│   │   └── Register
│   │       └── UsersListeners.php
│   ├── Query
│   │   ├── UserFilter.php
│   │   ├── UserQuery.php
│   │   └── UserView.php
│   ├── Service
│   │   └── UserQueryService.php
│   ├── config
│   │   └── users.php
│   ├── definitions.php
│   └── handlers.php
├── Domain
│   ├── Event
│   │   └── UserCreated.php
│   ├── User.php
│   ├── UserActive.php
│   ├── UserEmail.php
│   ├── UserId.php
│   ├── UserPassword.php
│   ├── UserPasswordHash.php
│   └── UserRepository.php
├── Infrastructure
│   └── Persistance
│       └── Doctrine
│           ├── ORM
│           │   ├── Metadata
│           │   │   └── App.Backoffice.Users.Domain.User.dcm.xml
│           │   └── UserDoctrineRepository.php
│           └── Query
│               ├── UserDoctrineFilter.php
│               └── UserDoctrineQuery.php
├── UI
│   ├── Console
│   │   ├── Command
│   │   │   └── CreateNewUserConsoleCommand.php
│   │   └── commands.php
│   ├── Http
│   │   ├── Auth
│   │   │   └── OAuth2UserCredentials.php
│   │   ├── Controller
│   │   │   ├── UserCommandsController.php
│   │   │   └── UserQueriesController.php
│   │   ├── Middleware
│   │   │   └── OAuthMiddleware.php
│   │   └── Route
│   │       ├── OAuthRoutes.php
│   │       └── UsersRoutes.php
│   ├── Transformer
│   │   └── UserTransformer.php
│   ├── Validator
│   │   ├── EmailExistsValidation.php
│   │   ├── NewUserValidator.php
│   │   └── UpdateUserValidator.php
│   ├── trans
│   │   ├── mail.en.yaml
│   │   ├──
│   │   └──
│   └── views
│       └── mail
│           └── new-user-welcome.php
└── UsersInit.php

This structure consists of layers: User Interface, Application, Domain and Infrastructure.


If you want to quickly boilerplate your new module there's a command for that:

php cordo system:module-builder <context> <module_name> [module_archive_file]

you can find pre-prepared archive in app/resources/module.

That can save you a lot of time as this will generate the whole structure of folders and files for typycial CRUD.


Route definitons should be located at app/[Context]/[PackageName]/UI/Http/Route/[PackageName]Routes.php file. Routes loader class should inherit from abstract class Cordo\Core\Application\Service\Register\RoutesRegister.

Routing is done with use of FastRoute but modified allowing to use per route Middlewares.

Perferable way to generate API documentation is ApiDoc but that can be changed according to individual preferences.

Dependency Injection Container

DI Conteriner definitions should be placed in app/[Context]/[PackageName]/Application/definitions.php.

Cordo uses PHP-ID for DI Container, if you need to find out more check the documentation.


Global config files should be located at config/ dir, while feature configs location should be: app/[Context]/[PackageName]/Application/config/

Config files should return PHP associative arrays. Multidimensional arrays are supproted.


$limit = $config->get('users.limit');

where "users" is the name of the config file and the following segments are array associative keys.


Database configuration is located at bootstrap/db.php file. Framework uses Doctrine for database storage and object mapping.

According to the CQRS approach preferable way is to use Doctrine ORM for storing and Doctrine DBAL for querying.

Doctine is preconfigured to support UUID.

XML Mapping also is supported so you can map your Domain Models directly with database tables. You should place your mappings in app/[Context]/[PackageName]/Infractructure/Doctrine/ORM/Metadata/.

When you have your mappings ready you can create/update/drop schema directly from composer:

composer schema-create
composer schema-update
composer schema-drop


For mail Laminas Mail is used. Currently there are 2 drivers for mail transport log (for development) and smtp. You can define your mail driver and credentials in config/mail.php file.

In order to add/change drivers you can just replace MailerFactory class with your own in bootstrap/app.php.


Translations are handled with Symfony Translations. Default file format for translations is Yaml.

You should place your translation files in app/[Context]/[PackageName]/UI/trans/ folder or subfolders. Naming convention is: [domain].[locale].yaml, for example: mail.en.yaml.


$translator->trans('hello', [], 'mail', 'en')

You can set per request locale by adding &lang parameter to request uri or by adding --lang flag while running console command.

Config file for translations is located at config/trans.php.


Plates is used as a template engine. Place your view files in app/[Context]/[PackageName]/UI/views/ folder or subfolders.


$templates->render('[context].[module]::[subfolder]/[template_name]', [
    // data you want to pass to the template file

Command bus

Cordo uses Tactician command bus for implementing Command Pattern.

Your Command -> Handler mappings should be placed in: app/[Context]/[PackageName]/Application/handlers.php file.

Command bus is configured to lock each handler in seperate transaction, it also supports events, queues, command logging. Check bootstrap/command_bus.php and Tactician documentation for details.


In contrast to the Command -> Handler mapping where for one Command there can be one and only one Handler you can have several listeners for a single emmited event.

Your listeners definitions should be located at: app/[Context]/[PackageName]/Event/Loader/[PackageName]Listeners.php. Events loaders class should extend Cordo\Core\Application\Service\Register\ListenersRegister.

Here is how you can emit an event:

 * @var League\Event\EmitterInterface
$emitter->emit('users.created', new UserCreated($command->email()));

Define your listeners in app/[Context]/[PackageName]/Application/events.php file (see example in Users module):

    static function ($event, UserCreated $userCreated) use ($container) {
        $listener = new UserCreatedListener($container);


For background processing Bernard library is used.

Bernard supports several different drivers:

  • Google AppEngine
  • Doctrine DBAL
  • Flatfile
  • IronMQ
  • MongoDB
  • Pheanstalk
  • PhpAmqp / RabbitMQ
  • Redis Extension
  • Predis
  • Amazon SQS
  • Queue Interop

This framework is configured with Redis Extention driver by default. Driver declaration is placed in bootstrap/queue_factory.php and can be changed there.

If you want to make your Command to be queued just make it implementing Cordo\Core\Application\Queue\MessageInterface interface or just extend Cordo\Core\Application\Queue\AbstractMessage class.

To launch background process that will process queued commands run in the console:

php queue-worker &

To better understand how to deal with events check Users module how welcome message is being sent for newly created users.


OAuth2 authorization is shipped with Users module and is ready to use. Endpoints for token generation and token refresh are located at app/Backoffice/Users/UI/Http/Route/OAuthRoutes.php. Default Client ID is Cordo.

General OAuth2 configuration is performed during Users initialization at: app/Backoffice/Users/UsersInit.php.

If you'd like to change default credentials check behavior you can do it here: app/Backoffice/Users/UI/Http/Auth/OAuth2UserCredentials.php.


For the purpose of Authorization Zend ACL has been used. ACL roles, resources, permissions cen be defined seperately in each package in app/[Context]/[PackageName]/Application/Acl/[PackageName]Acl.php which should extend Cordo\Core\Application\Service\Register\AclRegister.

In Auth package that is shipped with this framework there are CRUD actions prepared for users ACL rules.

There is also a Middeware for authorizing access to API endpoints in system/Module/Auth/UI/Http/Middleware/AclMiddleware.php. You can pass privilage name in constructor or leave empty then it will simply map http request method (GET, POST, PUT, DELETE) as acl privilage.


By default all the errors are logged to the storage/logs/error.log file.

Additionally in dev environment errors will be prompth to the screen in pretty format using Whoops. Errors in console are also pretty formated. In production environment errors stack traces will be emailed to the addresses defined in config/error.php.

If you'd like to change any of that bevavior you can to it in: bootstrap/error.php file.



The MIT License (MIT). Please see License File for more information.

You can’t perform that action at this time.