Please acknowledge that this is solely a Tech/Skill Demo. It is not intended for use in actual projects, although it is fully functional and tested under PHP 7.
Minimal is a MVC web application framework for PHP.
App::dispatch(function () {
DB::connections(Config::database());
Router::get('space-game/(:num)/(:num)', function ($characterId, $levelId) {
return [
Character::with('sprite', 'trait')->getById($characterId)->toArray(),
LevelSpec::with('sprite', 'entity.trait')->getById($levelId)->toArray()
];
});
}
The code snippet demonstrates a monolithic approach to the framework, employing facades to define a single-function web application endpoint. There is much more than static classes under the hood! It is an efficient solution for developing small REST APIs. The framework automatically converts ORM returned data (models implementing the JsonableInterface) into JSON, streamlining the API response process. Conversely, the framework accommodates alternative setup familiar in other frameworks, supporting a traditional modular MVC architecture for more complex, larger-scale projects.
...and if you're not happy with the Router (or any other component), you could do:
App::bind(RouterInterface::class, MyCustomRouter::class);
...yes, SOLID principles.
Quickstart example | Routing | Dependency Injection | Providers | Middlewares | Controllers | Views | Assets | CLI
Key features:
- Build MVC-, REST-, CLI-APIs and apps and query databases with a ORM
- Take advantage of inversion of control and facades
- Easy install via command line and works out of the box
- No dependencies to third party libraries (except in development mode: PHPUnit, Symfony VarDumper)
- Most of the core components work standalone
- Plain PHP in the views/templates
- PHP version 7.x
- composer
With the default directory structure:
$ composer create-project minimal/framework
Then point your server's document root to the public directory.
If you use the PHP-builtin webserver then do:
$ cd public
$ php -S 0.0.0.0:8000 server.php
Vendor libraries only:
$ composer require minimal/framework
Minimal installation for code style like in the introduction above:
$ composer require minimal/minimal
Quickstart example | Routing | Dependency Injection | Providers | Middlewares | Controllers | Views | Assets | CLI
App::dispatch(function () {
// Register additional services
App::register(['Demo' => DemoServiceProvider::class]);
// Respond on GET request
Router::get('/', function () {
return 'Hello from Minimal!';
});
// Respond on GET request with uri paramters
Router::get('hello/(:any)/(:num)', function ($any, $num) {
return 'Hello ' . $any . ' ' . $num ;
});
// Respond on POST request
Router::post('/', function () {
return Request::post();
});
// Respond with HTTP location
Router::get('redirection', function () {
Response::redirect('/');
});
// Respond with a view
Router::get('view', function () {
return View::render('fancy-html', ['param' => 'value']);
});
// Test the database connection
Router::get('database', function () {
DB::connections(Config::database());
return 'Successfully connected to database';
});
// Route group
Router::group([
'uriPrefix' => 'route-groups',
'namespace' => 'App\\Demo\\Base\\Controllers\\',
'middlewares' => [
'App\\Demo\\Base\\Middlewares\\CheckPermission',
'App\\Demo\\Base\\Middlewares\\ReportAccess',
]
], function () {
// Responds to GET route-groups/controller-action/with/middlewares'
Router::get('controller-action/with/middlewares', [
'middlewares' => ['App\\Demo\\Base\\Middlewares\\Cache' => [10]],
'controller' => 'YourController',
'action' => 'timeConsumingAction'
]);
// Do database stuff
Router::get('users', function () {
// Connect to database
DB::connections(Config::database());
// Truncate tables
Role::instance()->truncate();
User::instance()->truncate();
// Create 2 new roles
Role::create([['name' => 'admin'], ['name' => 'member']]);
// Get all the roles
$roles = Role::all();
// Create a user
$user = User::create(['username' => 'john']);
// Assign all roles to this user
$user->roles()->attach($roles);
// Get the first username 'john' with his roles
return $user->with('roles')->where(['username', 'john'])->first();
});
// ... subgroups are possible ...
});
});
Quickstart example | Routing | Dependency Injection | Providers | Middlewares | Controllers | Views | Assets | CLI
Router::get('hello/(:any)/(:any)', function($firstname, $lastname) {
return 'Hello ' . ucfirst($firstname) . ' ' . ucfirst($lastname);
});
// (:segment) match anything between two slashes
// (:any) match anything until next wildcard or end of uri
// (:num) match integer only
http://localhost/hello/julien/duseyau -> Hello Julien Duseyau
// Router::get() responds to GET requests
// Router::post() responds to POST requests
// Router::put() ...you get it
// Router::patch()
// Router::delete()
// 1st parameter: string uri pattern
// 2nd parameter: a closure with return sends a response to the client, a array
// of key/value pairs sets the attributes of the route object, which are:
// 'controller': the controller class to load,
// 'action':, the method to execute
// 'uriPrefix': a string that prefixes the uri pattern
// 'middlewares': a multidimensional array of middleware with optional params
// 'params': array of values that will be injected to the method
Router::get(hello/(:any)/(:any)', 'App\\Demo\\Base\\Controllers\\YourController@yourMethod');
or
Router::get(hello/(:any)/(:any), [
'controller' => YourController::class,
'action' => 'yourMethod'
]);
class App\Demo\Base\Controllers\YourController
{
public function yourMethod($name, $lastname)
{
return 'Hello ' . ucfirst($name) . ' ' . ucfirst($lastname);
}
}
http://localhost/hello/julien/duseyau -> Hello Julien Duseyau
Router::group([
// Prefixes all urls in the group with 'auth/'
'uriPrefix' => 'auth',
// Define the class namespace for all routes in this group
// Will be prefixed to the controllers
'namespace' => 'App\\Demo\\Auth\\Controllers\\'
], function () {
// GET request: 'auth/login'
// Controller 'App\\Demo\\Auth\\Controllers\AuthController
Router::get('login', [
'controller' => 'AuthController',
'action' => 'loginForm' // Show the login form
]);
// POST request: 'auth/login'
// Controller 'App\\Demo\\Auth\\Controllers\AuthController
Router::post('login', [
'controller' => 'AuthController',
'action' => 'login' // Login the user
]);
/**
* Subgroup with middlewares
*/
Router::group([
// Middlewares apply to all route in this (sub)group
'middlewares' => [
// Check if the client is authorised to access these routes
'App\\Demo\\Auth\\Middlewares\\CheckPermission',
// Log or send a access report
'App\\Demo\\Auth\\Middlewares\\ReportAccess',
]
], function () {
// No access to these routes if middleware CheckPermission fails
// GET request: 'auth/users'
// Controller 'App\\Demo\\Auth\\Controllers\UserController
Router::get('users', [
'controller' => 'UserController',
'action' => 'list' // Show a list of users
]);
// etc...
});
});
Router::get('download/pdf', function () {
Response::header('Content-Type: application/pdf');
Response::header('Content-Disposition: attachment; filename="downloaded.pdf"');
readfile('sample.pdf');
});
$widget = App::execute('route/of/widget')
Quickstart example | Routing | Dependency Injection | Providers | Middlewares | Controllers | Views | Assets | CLI
Binding a interface to a implementation is straight forward:
App::bind([
'App\\InterfaceA' => App\ClassA::class,
'App\\InterfaceB' => App\ClassB::class,
'App\\InterfaceC' => App\ClassC::class
]);
or in config/bindings.php
return [
'App\\InterfaceA' => \App\ClassA::class,
'App\\InterfaceB' => \App\ClassB::class,
'App\\InterfaceC' => \App\ClassC::class
];
class ClassA {}
class ClassB {}
class ClassC
{
public function __construct(InterfaceB $classB) {}
}
class MyClass
{
public function __construct(InterfaceA $classA, InterfaceC $classC) {}
}
$MyClass = App::make(MyClass::class);
Quickstart example | Routing | Dependency Injection | Providers | Middlewares | Controllers | Views | Assets | CLI
Providers are service providers
App::register([
'MyService' => \App\MyService::class,
'App\MyClass' => \App\MyClass::class,
'MyOtherClassA' => \App\MyOtherClassAFactory::class,
'any-key-name-will-do' => \App\MyOtherClassB::class,
]);
or in config/providers.php
return [
'MyService' => \App\MyServiceProvider::class,
'App\\MyClass' => \App\MyClass::class,
'MyOtherClassA' => \App\MyOtherClassA::class,
'any-key-name-will-do' => \App\MyOtherClassB::class,
];
class MyServiceProvider extends AbstractProvider
{
/**
* This is what happens when we call App::resolve('MyService')
*/
public function resolve()
{
// Do something before the class is instantiated
$time = time();
$settings = Config::item('settings');
// return new instance
return App::make(MyService::class, [$time, $settings]);
// ... or make singleton and resolve dependencies
return $this->singleton('MySingleton', App::make(App\\MyService::class, [
App::resolve('App\\MyOtherClassA'),
App::resolve('App\\MyOtherClassB'),
$time,
$settings
]);
}
/**
* Optional: Register additional config if needed
*/
public function config(): array
{
return [
'key' => 'value'
];
}
/**
* Optional: Register additional bindings if needed
*/
public function bindings(): array
{
return [
'SomeInterface' => SomeClass::class
];
}
/**
* Optional: Register additional services if needed
*/
public function providers(): array
{
return [
'SomeService' => SomeServiceProvider:class
];
}
/**
* Optional: Register additional event subscribers
*/
public function subscribers(): array
{
return [
'event.name' => EventSubscriber::Class
];
}
/**
* Optional: Register additional routes
*/
public function routes()
{
Router::get('my-service', MySerciceController::class . '@myControllerMethod')
}
}
$myService = App::resolve('MyService');
Quickstart example | Routing | Dependency Injection | Providers | Middlewares | Controllers | Views | Assets | CLI
// in config/routes.php
Router::get('users', [
'controller' => 'UsersController',
'action' => 'list',
'middlewares' => [
// Check if the client is authorized to access this route
'App\\Middlewares\\checkPermission',
// Send a email to the administrator
'App\\Middlewares\\ReportAccess',
// Cache for x seconds
'App\\Middlewares\\Cache' => [(1*1*10)]
]
]);
// in app/Middlewares/CheckPermission.php
class CheckPermission implements MiddlewareInterface
{
...
// Inject what you want, instance is created through
// IOC::make() which injects any dependencies
public function __construct(
RequestInterface $request,
ResponseInterface $response,
RouteInterface $route
) {
$this->request = $request;
$this->response = $response;
$this->route = $route;
}
// Executed before dispatch
public function before() {
// If not authorised...
// ... send appropriate response ...
$this->response->addHeader();
$this->response->setContent();
$this->response->send()->exit();
// ... or redirect to login page
$this->response->redirect('login');
// ... or set error and cancel dispatch
$this->request->setError();
return false;
}
}
// in app/Middlewares/Cache.php
class Cache implements MiddlewareInterface
{
...
// Executed before dispatch
public function before() {
// return cached contents
}
// Executed after dispatch
public function after() {
// delete old cache
// create new cache
}
}
$result = Middleware::dispatch(function() {
return 'the task, for example FrontController::dispatch(Router::route())';
}, [
'App\\Middlewares\\checkPermission',
'App\\Middlewares\\ReportAccess',
'App\\Middlewares\\Cache' => [(1*1*10)]
]);
Quickstart example | Routing | Dependency Injection | Providers | Middlewares | Controllers | Views | Assets | CLI
The controllers specified in the routes are instantiated through Provider->make() (e.g. App::make()), which will always look for a singleton first, then search the service container for a provider or factory or else just create a instance and inject dependencies. Which means there is nothing to do to make this controller with concrete dependencies work:
class MyController
{
public function __construct(MyModelA $myModelA, MyModelB $myModelB)
{
$this->modelA = $myModelA;
$this->modelB = $myModelB;
}
}
In order to use interfaces, bindings have to be registered. See also config/bindings.php
App::bind(MyModelInterface::class, MyModel::class);
class MyController
{
public function __construct(MyModelInterface $myModel)
{
$this->model = $myModel;
}
}
For a more control register a factory. See also config/providers.php
App::register(MyController::class, MyControllerFactory::class);
class MyControllerFactory extends AbstractProvider
{
public function resolve()
{
return new MyController('value1', 'value2');
}
}
class MyController
{
public function __construct($optionA, $optionB)
{
// $optionA is 'value1', $optionB is 'value2'
}
}
Quickstart example | Routing | Dependency Injection | Providers | Middlewares | Controllers | Views | Assets | CLI
// The base directory to start from
View::setBase('../resources/views/');
// The theme directory in base directory, is optional and can be ingored
View::setTheme('my-theme');
// The layout file without '.php' from the base/theme directory
View::setLayout('layouts/my-layout');
// Set variables for the view
View::set('viewValue1', 'someValue1')
// By default variables are only accessible in the current view
// To share a variable $title across all layout and views
View::share('title', 'My title');
// Render a view without layout
View::view('pages/my-view', [
'viewValue2' => 'someValue2' // Same as View::set()
]);
// Render a view with layout, but in case of ajax only the view
View::render('pages/my-view', [
'viewValue2' => 'someValue2' // Same as View::set()
]);
<!-- resources/views/my-theme/layouts/my-layout.php -->
<!DOCTYPE html>
<html>
<head>
<title><?=$title?></title>
</head>
<body>
<?= self::view() ?>
</body>
</html>
<!-- resources/views/my-theme/main/my-view.php -->
<p><?= $viewValue1 ?></p>
<p><?= $viewValue2 ?></p>
Result:
<!DOCTYPE html>
<html>
<head>
<title>My title</title>
</head>
<body>
<p>someValue1</p>
<p>someValue2</p>
</body>
</html>
Where to do these View calls? Anywhere is fine. But one place could be:
class BaseController
{
public function __construct()
{
View::setBase(__DIR__'/../views/');
View::setTheme('my-theme');
View::setLayout('layouts/my-layout');
Assets::setBase(__DIR__'/../assets');
Assets::setTheme('my-theme');
}
}
then
class MyController extends BaseController
{
private $user;
public function __construct(UserInterface $user)
{
parent::__construct();
$this->user = $user;
}
public function myAction()
{
View::render('my-view', ['user' => $this->user->find(1)]);
}
}
Quickstart example | Routing | Dependency Injection | Providers | Middlewares | Controllers | Views | Assets | CLI
// The base directory to start from
Assets::setBase('../app/Pages/resources/assets');
// The theme directory in base directory, is optional and can be ingored
Assets::setTheme('my-theme');
// Directory for css (default 'css')
Assets::setCssDir('css');
// Directory for js (default 'js')
Assets::setJsDir('js');
// Register css files
Assets::addCss(['normalize.css', 'main.css']);
//Register js files with keyword
Assets::addJs(['vendor/modernizr-2.8.3.min.js'], 'top');
// Register more js files with another keyword
Assets::addJs(['plugins.js', 'main.js'], 'bottom');
// Js from CDN
Assets::addExternalJs(['https://code.jquery.com/jquery-3.1.0.min.js'], 'bottom');
// Add inline javascript
Assets::addInlineScripts('jQueryFallback', function () use ($view) {
return View::render('scripts/jquery-fallback', [], true);
});
<!-- resources/views/my-theme/layouts/my-layout.php -->
<html>
<head>
<title><?=$title?></title>
<?= Assets::getCss() ?>
<?= Assets::getJs('top') ?>
</head>
<body>
<div class="content">
...
</div>
<?= Assets::getExternalJs('bottom') ?>
<?= Assets::getInlineScripts('jQueryFallback') ?>
<?= Assets::getJs('bottom') ?>
<?= Assets::getInlineScripts() ?>
</body>
</html>
Outputs:
<html>
<head>
<title>My title</title>
<link rel="stylesheet" href="assets/my-theme/css/normalize.css">
<link rel="stylesheet" href="assets/my-theme/css/main.css">
<script src="assets/my-theme/js/vendor/modernizr-2.8.3.min.js" ></script>
</head>
<body>
<div class="content">
...
</div>
<script src="https://code.jquery.com/jquery-3.1.0.min.js" ></script>
<script>window.jQuery || document.write('...blablabla...')</script>
<script src="assets/my-theme/js/plugins.js" ></script>
<script src="assets/my-theme/js/main.js" ></script>
</body>
</html>
Quickstart example | Routing | Dependency Injection | Providers | Middlewares | Controllers | Views | Assets | CLI
$ php minimal routes
-----------------------------------------------------------------------------------------------------------------------------------------------------------
| Type | Pattern | Action | Middlewares |
-----------------------------------------------------------------------------------------------------------------------------------------------------------
| GET | / | <= Closure() | |
| GET | /hello/(:any)/(:any) | <= Closure() | |
| GET | /welcome/(:any)/(:any) | App\Controllers\YourController@yourMethod | |
| GET | /auth/login | App\Controllers\AuthController@loginForm | |
| POST | /auth/login | App\Controllers\AuthController@login | |
| GET | /auth/logout | App\Controllers\AuthController@logout | |
| GET | /auth/users | App\Controllers\UserController@list | App\Middlewares\CheckPermission, App\Middlewares\ReportAccess |
| GET | /auth/users/create | App\Controllers\UserController@createForm | App\Middlewares\CheckPermission, App\Middlewares\ReportAccess |
| GET | /auth/users/edit/(:num) | App\Controllers\UserController@editForm | App\Middlewares\CheckPermission, App\Middlewares\ReportAccess |
| GET | /download/pdf | <= Closure() | |
| GET | /huge/data/table | App\Controllers\YourController@timeConsumingAction | App\Middlewares\Cache(10) |
| GET | /pages/(:any) | App\Pages\Controllers\PagesController@getStaticPage | App\Middlewares\Cache(10) |
| GET | /pages/info | App\Pages\Controllers\PagesController@info | App\Middlewares\Cache(10) |
| GET | /assets/(:any) | App\Assets\Controllers\AssetsController@getAsset | |
-----------------------------------------------------------------------------------------------------------------------------------------------------------
$ php minimal modules
---------------------------------------------------------------------------------------------------------------------------------------------------------
| Name | Path | Config | Routes | Providers | Bindings |
---------------------------------------------------------------------------------------------------------------------------------------------------------
| Pages | app/Pages/ | app/Pages/Config/config.php | app/Pages/Config/routes.php | app/Pages/Config/providers.php | app/Pages/Config/bindings.php |
| Assets | app/Assets/ | app/Assets/Config/config.php | app/Assets/Config/routes.php | app/Assets/Config/providers.php | app/Assets/Config/bindings.php |
---------------------------------------------------------------------------------------------------------------------------------------------------------
$ php minimal bindings
$ php minimal providers
$ php minimal events
$ php minimal config
Quickstart example | Routing | Dependency Injection | Providers | Middlewares | Controllers | Views | Assets | CLI
Minimal requires at least these packages:
- judus/minimal-collections - a simple iterator
- judus/minimal-config - handles a multidimensional array
- judus/minimal-controllers - the frontcontroller
- judus/minimal-http - request and response objects
- judus/minimal-middlewares - a unconventional middleware implementation
- judus/minimal-minimal - the application object that binds all together
- judus/minimal-provider - service provider and dependency injector
- judus/minimal-routing - the router
These packages are also included but are not necessary:
- judus/minimal-assets - register css and js during runtime, dump html link and script tags
- judus/minimal-benchmark - this "benchmarking" class is only used in the demo
- judus/minimal-cli - a command line interface, which will be completely redone asap
- judus/minimal-database - a pdo connector, a mysql query builder and a ORM
- judus/minimal-event - a simple event dispatcher
- judus/minimal-html - for now just a html table class
- judus/minimal-log - a simple logger
- judus/minimal-paths - might help creating paths and urls
- judus/minimal-translation - translations in a pretty printed json file
- judus/minimal-views - simple php views and layouts
The Minimal framework is open-sourced software licensed under the MIT license
Quickstart example | Routing | Dependency Injection | Providers | Middlewares | Controllers | Views | Assets | CLI