busarm / armie
An elegant PHP framework designed to provide high performance with optimal developer experience
Requires
- php: ^8.1
- guzzlehttp/guzzle: ^7.5
- laravel/serializable-closure: ^1.3
- nyholm/psr7: ^1.5
- psr/cache: ^3.0
- psr/container: ^2.0
- psr/http-message: ^1.0
- psr/http-server-middleware: ^1.0
- psr/log: ^2.0
- symfony/console: ^6.0
- symfony/process: ^6.0
- workerman/workerman: ^4.1
Requires (Dev)
- fakerphp/faker: ^1.20
- middlewares/firewall: ^2.0
- phpstan/phpstan: ^1.8
- phpunit/phpunit: ^9.5
- squizlabs/php_codesniffer: 3.*
README
Table of Contents
- Introduction
- Installation
- Usage
- Configs
- Route
- Providers
- Middleware
- Bindings
- Views
- Database
- Tests
- License
Introduction
Armie is an expressive and extendable lightweight PHP framework designed to provide high performance with all the essential features needed for quick application development.
It is more suited for small applications or microservices but can easily handle the development of large-scale applications with minimum extension or abstraction.
It includes support for different design paradigms and architectural patterns:
- Model-View-Controller (MVC)
- Service-oriented
- Microservices
- Event Driven
- Asynchronous Queuing
Installation
composer require busarm/armie
Usage
Traditional HTTP Server
Traditional HTTP server using PHP-FPM and NGINX or Apache.
Single Application
Run a single application
# ../myapp/public/index.php define('APP_START_TIME', floor(microtime(true) * 1000)); require __DIR__ . '/../vendor/autoload.php'; $config = (new Config()) ->setAppPath(dirname(__DIR__)) ->setConfigPath('Configs') ->setViewPath('Views'); $app = new App($config); $app->get('/product/{id}')->to(ProductController::class, 'get'); $app->run()->send();
Multi Tenant Application
Host multiple applications or modules. Supports path and domain routing
# ../index.php require __DIR__ . '/../vendor/autoload.php'; $server = (new Server()) // Use `myapp` for requests with path `v1/....` ->addRoutePath('v1', __DIR__ . '/myapp/public') // Use `mydevapp` for requests with domain name `dev.myapp.com` ->addDomainPath('dev.myapp.com', __DIR__ . '/mydevapp/public'); $server->run()->send(); # ../myapp/public/index.php /** * @var \Psr\Http\Message\ServerRequestInterface|null $request Capture Server request * @var \Armie\Interfaces\ServiceDiscoveryInterface|null $discovery Capture Service discovery */ require __DIR__ . '/../vendor/autoload.php'; $config = (new Config()) ->setAppPath(dirname(__DIR__)) ->setConfigPath('Configs') ->setViewPath('Views'); $app = new App($config); $app->setServiceDiscovery($discovery ?? new LocalServiceDiscovery([])); $app->get('/product/{id}')->to(ProductController::class, 'get'); return $app->run(Request::capture($request ?? null, $config));
Asynchronous HTTP Server (powered by workerman)
High perfomant Asychronous HTTP Server with support for serveral event-looping providers such as: swoole
, libevent
, ev
, libuv
, react
. Provides the following features:
- Background workers to handle multi processing, asynchronous task and cron job processing
- Socket workers to handle web socket connections
- Concurrency with Promises and built in (
async
,await
,concurrent
) functions - Real-time events with built in (
listen
,dispatch
) functions - Asynchronous queuing with built in (
enqueue
) function
# ./start.php $config = (new Config()) ->setAppPath(dirname(__DIR__)) ->setConfigPath('Configs') ->setViewPath('Views'); $app = new App($config); $app->get('/product/{id}')->to(ProductController::class, 'get'); $app->start("localhost", 8080, (new HttpServerConfig) ->setLooper(Looper::EV) ->setHttpWorkers(8) ->setTaskWorkers(4) ->addJob(function () { log_debug("Testing EVERY_MINUTE Cron Job"); }, Cron::EVERY_MINUTE) ->addJob(function () { log_debug("Testing Custom Seconds Cron Job"); }, 600) ->addJob(function () { log_debug("Testing One-Time Only Job"); }, (new DateTime('+30 seconds'))) // MessengerSocketController implements SocketControllerInterface ->addSocket(2222, MessengerSocketController::class));
Run command to start application
# Windows php start.php # Unix (Linux or Mac) [Recommended] php start.php start
Configs
Configure application
$config = (new Config()) ->setAppPath(__DIR__) ->setConfigPath('Configs') ->setViewPath('Views') ->setSecret("mysamplesecret123456") ->setCookieEncrypt(true) ->setHttp((new HttpConfig) ->setCheckCors(true) ->setAllowAnyCorsDomain(true) ->setAllowedCorsHeaders(['*']) ->setAllowedCorsMethods(['GET'])) ->setLogRequest(false) ->setSessionEnabled(true) ->setSessionLifetime(60) ->setDb((new PDOConfig) ->setConnectionDriver("mysql") ->setConnectionHost("127.0.0.1") ->setConnectionDatabase('default') ->setConnectionPort(3310) ->setConnectionUsername("root") ->setConnectionPassword("root") ->setConnectionPersist(true) ->setConnectionErrorMode(true) ->setConnectionPoolSize(10) ); $app = new App($config); ...
Using Config Files
Configs can be attached using separate configuration files.
Create Config File
Add config file to your config path. E.g myapp/Configs/database.php
# database.php // Use constant define("DB_NAME", "my-db-dev"); define("DB_HOST", "localhost"); // Use dynamic configs return [ 'db_name'=>'my-db-dev', 'db_host'=>'localhost', ]; // Access dynamic configs // Set app()->config->set('db_name', 'my-db-dev-2'); // Get app()->config->get('db_name');
Add Config File
.... $config->addFile('database') $app = new App($config); ....
Route
Add HTTP routes.
Controller Route
.... $app = new App($config); $app->get('/user/{id}')->to(UserController::class, 'get'); $app->get('/user/{id}')->to(UserController::class, 'get'); $app->post('/user/{id}')->to(UserController::class, 'create'); $app->put('/user/{id}')->to(UserController::class, 'update'), $app->delete('/user/{id}')->to(UserController::class, 'delete'), $app->run()->send();
Anonymous Route
.... $app = new App($config); $app->get('/user/{id}')->call(function (RequestInterface $request, string $id) { // Perform action ... }); $app->run()->send();
View Route
.... $app = new App($config); $app->get('/user/{id}')->view(UserPage::class); $app->run()->send();
Custom Route Class
.... $app = new App($config); // Using Custom Route Class - Single $app->router->addRoute(MyRoute::get('/user/{id}')->to(UserController::class, 'get')); // Using Custom Route Class - List $app->router->addRoutes([ MyRoute::get('/user/{id}')->to(UserController::class, 'get'), MyRoute::post('/user')->to(UserController::class, 'create'), MyRoute::put('/user/{id}')->to(UserController::class, 'update'), MyRoute::delete('/user/{id}')->to(UserController::class, 'delete'), ]); $app->run()->send();
Providers
Extend application features and configurations.
Create Provider
class CustomProvider implements ProviderInterface { /** * @inheritDoc */ public function process(App $app): void { // Perform custom action.... } }
Attach Provider
... $app = new App($config); $app->addProvider(new CustomProvider()); ...
Middleware
Intercept HTTP request and response. PSR Middleware supported.
Create Middleware
class AuthenticateMiddleware implements MiddlewareInterface { public function process(RequestInterface|RouteInterface $request, RequestHandlerInterface $handler): ResponseInterface { // Perform custom action.... // Or forward to next request handler return $handler->handle($request); } }
Attach Middleware
# Attach global middleware .... $app = new App($config); $app->addMiddleware(new AuthenticateMiddleware()) .... # Attach middleware to specific route .... $app->put('/user/{id}')->to(UserController::class, 'update')->middlewares([ new AuthenticateMiddleware() ]); $app->router->addRoute( Route::put('/user/{id}')->to(UserController::class, 'update')->middlewares([ new AuthenticateMiddleware() ]) ); ....
Bindings
Bind an interface to a particular class. Hence, the specified class object will be used when resolving dependencies.
Add Binding
.... $app = new App($config); $app->addBinding(CacheInterface::class, RedisCache::class) ....
Resolve Binding
// Manually $cache = app()->make(CacheInterface::class) // Automatically class UserController { public function __construct(private CacheInterface $cache) { } }
Views
Generic Component
Add view file to your view path. E.g myapp/Views/login.php
# In Controller (or anywhere you wish to load view) // Using app instance app()->loader->view('login', ['username' => $uname, 'password' => $pass]); // Using helpers view('login', ['username' => $uname, 'password' => $pass]);
Dedicated View Model
Add view file(s) to your view path. E.g myapp/Views/LoginPage.php
, myapp/Views/components/login.php
# In-line rendering class LoginPage extends View { public function __construct(protected LoginPageDto|BaseDto|array|null $data = null, protected $headers = array()) { } public function render() { $header = new HeaderComponent; return <<<HTML <html> <body> <div>{$header}</div> <div>Username: {$this->get("username")}</div> </body> </html> HTML; } } # Component rendering class LoginPage extends View { public function __construct(protected LoginPageDto|BaseDto|array|null $data = null, protected $headers = array()) { } public function render() { return $this->include('components/login', true); } }
Database (Armie ORM)
A simple but expressive database object-relational mapper (ORM) built on top of PHP Data Objects (PDO)
Define Model
class ProductModel extends Model { /** * @inheritDoc */ public function getFields(): array { return [ new Field('id', DataType::INT), new Field('name', DataType::STRING), new Field('type', DataType::STRING), new Field('qty', DataType::INT), new Field('categoryId', DataType::INT), new Field('createdAt', DataType::DATETIME), new Field('updatedAt', DataType::DATETIME), new Field('deletedAt', DataType::DATETIME) ]; } /** * @inheritDoc */ public function getRelations(): array { return [ new OneToOne('category', $this, new Reference(CategoryTestModel::class, ['categoryId' => 'id'])) ]; } /** * @inheritDoc */ public function getTableName(): string { return 'products'; } /** * @inheritDoc */ public function getKeyName(): ?string { return 'id'; } /** * @inheritDoc */ public function getCreatedDateName(): ?string { return 'createdAt'; } /** * @inheritDoc */ public function getUpdatedDateName(): ?string { return 'updatedAt'; } /** * @inheritDoc */ public function getSoftDeleteDateName(): ?string { return 'deletedAt'; } }
Save Model
$model = ProductModel::create(['name' => 'IPhone 14', 'qty' => 3, 'type' => 'Mobile Phone', 'categoryId' => 1]); $model = ProductModel::update(1, ['name' => 'IPhone 14', 'qty' => 3, 'type' => 'Mobile Phone', 'categoryId' => 1]); // Or ... $product = new ProductModel; $product->load(['name' => 'IPhone 14', 'qty' => 3, 'type' => 'Mobile Phone', 'categoryId' => 1]); $product->save();
Find Item
... $model = ProductModel::findById(1); // Or $model = (new ProductModel)->find(1);
Get List
... $model = ProductModel::getAll(); // Or $model = (new ProductModel)->all();
Define Repository
class ProductRepository extends Repository { public function __construct() { parent::__construct(new ProductModel); } } // Or - Use Generic Repository $productRepo = new Repository(new ProductModel)
Get Paginated List
... $productRepo = new ProductRepository(); $result = $productRepo->paginate(1, 3);
Tests
To execute the test suite, you'll need to install all development dependencies.
$ git clone https://github.com/busarm/armie
$ composer install
$ composer test
You can use PHP server built-in server to test:
$ php -S localhost:8181 -t tests/app/v1
License
The Armie Framework is licensed under the MIT license. See License File for more information.