Skip to content

Instantly share code, notes, and snippets.

@kafene
Forked from EmanueleMinotto/README.md
Last active February 24, 2017 22:48
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save kafene/5da827091656964ef05f to your computer and use it in GitHub Desktop.
Save kafene/5da827091656964ef05f to your computer and use it in GitHub Desktop.
PHP Microframework

Microframework

This is a PHP (5.3+) microframework based on anonymous functions.

Features

  • requested URLs matched using regular expressions
  • request methods (matches using regular expressions too)
  • differenced FIFO queues for each $priority
  • command line usage
  • backward compatibility
  • integrated Dependency Injection and settings system
  • named patterns

CGI Usage

Hello World

<?php
$mf = require_once('microframework.php');

$mf('/', function () {
    echo 'Hello World!';
});
?>

second argument must be callable, so you can use

<?php
$mf('/', function () { /* ... */ });
$mf('/', 'function');
$mf('/', array($Object, 'method'));
$mf('/', array('Class', 'staticMethod'));
$mf('/', 'Class::staticMethod');
$mf('/', $Object); // Used with __invoke
?>

HTTP request methods are the third parameter and are controlled using a regular expression.

<?php
$mf('/', function () {
    echo 'Hello ' . $_POST['name'] . '!';
}, 'POST');
?>
<?php
$mf('/', function () {
    echo 'Hello ' . $_SERVER['REQUEST_METHOD'] . '!';
}, 'GET|POST');
?>

Routes are defined using a regular expression too.

<?php
$mf('/hello/world', function () {
    echo 'Hello ';
});
$mf('/hello(.*)', function () {
    echo 'World!';
});
// Output: Hello World!
?>

Use named subpatterns to define parameters order.

<?php
$mf('/(?P<user>[^\/]+)/(?P<verb>[^\/]+)', function ($verb, $user) {
    echo 'Output: ' . $verb . ' ' . $user . '!';
});

// http://localhost/bob/hello
// Output: hello bob!
?>

CLI Usage

Hello World

<?php
$mf = require_once('microframework.php');

// Usage: $ php test.php

$mf(function () {
    echo 'Hello World!';
});
?>

Like CGI usage, first argument must be callable. Function arguments are $argv arguments

<?php
// Usage: $ php test.php foo bar

$mf(function ($a, $b) {
    echo $b . ' ' . $a;
});

// Output: bar foo
?>

Priority

Priority is used set callbacks order, each function is called using highest priority 0 but you can set it passing a not negative integer or a not negative float number.

<?php
// $mf('/', function () { echo 'A'; });
$mf('/', function () { echo 'A'; }, 'GET', 0);
$mf('/', function () { echo 'B'; }, 'GET', 1);

// Output: AB
?>
<?php
$mf('/', function () { echo 'A'; }, 'GET', 1);
$mf('/', function () { echo 'B'; }, 'GET', 0);

// Output: BA
?>

In command line usage priority is the second parameter.

To break callbacks chain use the exit() function.

Backward compatibility

<?php
// Source code on 2012-01-01
$mf('/([a-zA-Z]+)', function ($name) {
  echo 'Hi ' . $name;
}, 'GET|POST');
?>

If after some time this microframework will change adding the possibility to set the method as an array

<?php
// Source code of 2013-01-01 (current)
$mf('/([a-zA-Z]+)', function ($name) {
    echo 'Hello ' . $name;
}, array('GET', 'POST'));
?>

old things can be saved changing variable names

<?php
// Source code of 2013-01-01 (current)
$mf('/([a-zA-Z]+)', function ($name) {
    echo 'Hello ' . $name;
}, array('GET', 'POST'));

// Source code of 2012-01-01
$mf_old('/([a-zA-Z]+)', function ($name) {
    echo 'Hi ' . $name;
}, 'GET|POST');
?>

This is used for a fast replacement of old $mf functions in your codebase by replacing the variable, when you've done and started relaxing you can update your old source.

<?php
// Source code of 2013-01-01 (current)
$mf('/([a-zA-Z]+)', function ($name) {
    echo 'Hello ' . $name;
}, array('GET', 'POST'));

// Source code of 2012-01-01
// updated on 2013-01-02 to current version
$mf('/([a-zA-Z]+)', function ($name) {
    echo 'Hi ' . $name;
}, array('GET', 'POST'));
?>

Backward compatibility is not guaranteed forever, of course.

404

The 404 error is defined as a regular expression too and is matched if no one of currently defined regular expressions match current URL. To retrieve the 404 regular expression call the function without arguments (available only if not in CLI mode).

<?php
$mf('/', function () {
    echo 'Hello World!';
});

$mf($mf(), function () {
    echo 'Error 404: Page not Found';
});
?>

Dependency Injection

Some days ago Faryshta Mextly told me that this isn't a microframework, it's only a routing system, and he was right, so I started thinking which services should a framework have and final solution was a dependency injection system that could be used for every other service.

It works like the routing system, except for the first parameter that must match the regular expression [a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]* and every parameter is stored in the $GLOBALS variable. Second parameter should be an anonymous function, but can be a callable.

<?php
class Bar
{
    public $foo = 5;
}

$mf('bar', function () {
    return new Bar;
});

$mf('/', function () use ($mf) {
    var_dump($mf('bar') -> foo);
});

var_dump(
    $bar,
    $bar(),
    $bar() -> foo
);
// object(Closure)[2]
// object(Bar)[5]
//   public 'foo' => int 5
// int 5
// int 5
?>

Settings

Settings work like dependency injection, but you can't pass anything callable.

<?php
$mf('pi', M_PI);

$mf('/', function () use ($mf) {
    var_dump($mf('pi'));
});

// Output: 3.1415926535898
?>

Tip

It's suggested to create the unused PHP main function.

<?php
if (!function_exists('main')) {
    function main() {
        static $mf = null;
        if (is_null($mf))
            $mf = require_once('microframework.php');
        return call_user_func_array($mf, func_get_args());
    }
}

main('/', function () {
    echo 'Hello World!';
});
?>
<?php
/**
* A PHP (5.3+) microframework based on anonymous functions.
*
* @author Emanuele Minotto <minottoemanuele@gmail.com>
* @link https://gist.github.com/EmanueleMinotto/4648707
* @link https://twitter.com/EmanueleMinotto
* @version 1.2.0
*/
return function () {
/**
* Used to store functions and allow recursive callbacks.
* @var null|callable
*/
static $deploy = null;
/**
* Defined matches.
* @var array
*/
static $ms = array();
/**
* Dependency Injection callbacks, used for settings too.
* @var null|array
*/
static $di = null;
// there's already a container for variables
if (is_null($di)) {
$di =& $GLOBALS;
}
/**
* This variable is a constant during an instance.
* @var null|string
*/
static $base = null;
// base path for each route defined once
if (is_null($base)) {
$base = quotemeta(rtrim(dirname($_SERVER['SCRIPT_NAME']), '/'));
}
// used to shorten code
$num_args = func_num_args();
$get_args = func_get_args();
// used to retrieve currently defined matches
// http://www.php.net/manual/en/regexp.reference.conditional.php
// http://stackoverflow.com/questions/14598972/catch-all-regular-expression
switch ($num_args) {
case 0: {
if (PHP_SAPI !== 'cli') {
return '/?(?!(' . implode('|', $ms) . ')$).*';
}
break;
}
case 1: {
if (is_scalar($get_args[0])) {
// using $GLOBALS as a container, variable names must match
// this regular expression
// http://www.php.net/manual/en/language.variables.basics.php
if (preg_match('#^[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*#', $get_args[0])) {
return is_callable($di[$get_args[0]])
? call_user_func($di[$get_args[0]])
: $di[$get_args[0]];
}
}
break;
}
case 2: {
if (is_scalar($get_args[0])) {
// using $GLOBALS as a container, variable names must match
// this regular expression
// http://www.php.net/manual/en/language.variables.basics.php
if (preg_match('#^[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*#', $get_args[0])) {
// functions used for Dependency Injection and settings
return $di[$get_args[0]] = $get_args[1];
}
}
break;
}
}
// functions have to be stored only once
if (is_null($deploy) && PHP_SAPI === 'cli') {
/**
* Command line interface for the main function.
*
* @link http://php.net/manual/en/language.types.float.php
*
* @param callback $cb Function invoked when script ends
* @param integer $priority Set `$cb` priority from 0 (high) to ~1.8e308 (low)
* @return void
*/
$deploy = function ($cb, $priority = 0) use (&$deploy) {
// Checking well formed call
assert(is_callable($cb));
assert(is_numeric($priority));
/**
* Arguments passed to the script.
* @link http://php.net/manual/en/reserved.variables.argv.php
* @var array
*/
$argv = $GLOBALS['argv'];
if ($priority > 0) {
// Recursion is used to set callback priority
register_shutdown_function($deploy, $cb, $priority - 1);
} else {
$argv[0] = $cb;
// register_shutdown_function is used to call added functions when script ends
// http://it2.php.net/manual/en/function.register-shutdown-function.php
call_user_func_array('register_shutdown_function', $argv);
}
};
} elseif (is_null($deploy)) {
/**
* Function used as a router.
*
* @link http://php.net/manual/en/language.types.float.php
*
* @param string $regex Regular expression used to match requested URL
* @param callback $cb Function invoked when there's a match
* @param string $method Request method(s)
* @param float $priority Set `$cb` priority from 0 (high) to ~1.8e308 (low)
* @return void
*/
$deploy = function ($regex, $cb, $method = 'GET', $priority = 0) use (&$deploy, $ms, $base) {
// Checking well formed call
assert(is_string($regex));
assert(is_callable($cb));
assert(is_string($method));
assert(is_numeric($priority));
// match stored as unique using the Adler-32 algorithm that is faster than md5
// http://en.wikipedia.org/wiki/Adler-32
// http://3v4l.org/7MC3j
$ms[hash('adler32', $regex)] = $regex;
if ($priority > 0) {
// Recursion is used to set callback priority
register_shutdown_function($deploy, $regex, $cb, $method, $priority - 1);
} elseif (preg_match('#' . $method . '#', $_SERVER['REQUEST_METHOD'])) {
if (preg_match('#^' . $base . $regex . '$#', $_SERVER['REQUEST_URI'], $matches)) {
// Named subpatterns are allowed
// http://it2.php.net/manual/en/regexp.reference.subpatterns.php
$matches = array_unique($matches);
// If matches is provided, then it is filled with the results of search.
// $matches[0] will contain the text that matched the full pattern,
// $matches[1] will have the text that matched the first captured parenthesized
// subpattern, and so on.
$start_match = $matches[0];
unset($matches[0]);
// Snippet used to extract parameter from a callable object.
$Reflector = (is_string($cb) && function_exists($cb)) || $cb instanceof Closure
? new ReflectionFunction($cb)
: new ReflectionMethod($cb);
$params = array();
foreach ($Reflector -> getParameters() as $parameter) {
// reset to prevent key value
$params[$parameter -> name] = null;
}
// user can use named parameters only if explicitly requested
if (array_intersect(array_keys($params), array_keys($matches))) {
$matches = array_merge($params, $matches);
}
array_unshift($matches, $cb);
// register_shutdown_function is used to call added functions when script ends
// http://it2.php.net/manual/en/function.register-shutdown-function.php
call_user_func_array('register_shutdown_function', $matches);
}
}
};
}
return call_user_func_array($deploy, func_get_args());
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment