Skip to content

judus/minimal-framework

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Minimal Framework

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

Introduction

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

Requirements

  1. PHP version 7.x
  2. composer

Install

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

Usage

Quickstart example | Routing | Dependency Injection | Providers | Middlewares | Controllers | Views | Assets | CLI

Quickstart example

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

Routing

Direct output:
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 
Using controllers
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

Route groups
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...

    });
});
File download
Router::get('download/pdf', function () {
    Response::header('Content-Type: application/pdf');
    Response::header('Content-Disposition: attachment; filename="downloaded.pdf"');
    readfile('sample.pdf');
});
Single route execution from anywhere
$widget = App::execute('route/of/widget')

Quickstart example | Routing | Dependency Injection | Providers | Middlewares | Controllers | Views | Assets | CLI

Dependency injection

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

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

Middlewares

// 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
    }
}
Standalone example
$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

Controllers

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

Views

// 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

Assets

// 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

CLI

List all registered routes
$ 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    |                                                                 |
-----------------------------------------------------------------------------------------------------------------------------------------------------------
List all registered modules
$ 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 |
---------------------------------------------------------------------------------------------------------------------------------------------------------
List all registered bindings
$ php minimal bindings
List all registered providers
$ php minimal providers
List all events and subscribers
$ php minimal events
List all registered config
$ php minimal config

Quickstart example | Routing | Dependency Injection | Providers | Middlewares | Controllers | Views | Assets | CLI


Components

Minimal requires at least these packages:

These packages are also included but are not necessary:


License

The Minimal framework is open-sourced software licensed under the MIT license

Quickstart example | Routing | Dependency Injection | Providers | Middlewares | Controllers | Views | Assets | CLI