Git Product home page Git Product logo

pux's Introduction

Pux

Pux is a faster PHP router, it also includes out-of-box controller helpers.

Latest Stable Version Total Downloads Latest Unstable Version License Monthly Downloads Daily Downloads

2.0.x Branch Build Status (This branch is under development)

Benchmark

FEATURES

  • Low memory footprint (only 6KB with simple routes and extension installed) .
  • Low overhead.
  • PCRE pattern path support. (Sinatra-style syntax)
  • Controller auto-mounting - you mount a controller automatically without specifying paths for each action.
  • Controller annotation support - you may override the default path from controller through the annotations.
  • Route with optional pattern.
  • Request constraints
    • Request method condition support.
    • Domain condition support.
    • HTTPS condition support.

REQUIREMENT

  • PHP 5.4+

INSTALLATION

composer require corneltek/pux "2.0.x-dev"

SYNOPSIS

The routing usage is dead simple:

require 'vendor/autoload.php'; // use PCRE patterns you need Pux\PatternCompiler class.
use Pux\RouteExecutor;

class ProductController {
    public function listAction() {
        return 'product list';
    }
    public function itemAction($id) { 
        return "product $id";
    }
}
$mux = new Pux\Mux;
$mux->any('/product', ['ProductController','listAction']);
$mux->get('/product/:id', ['ProductController','itemAction'] , [
    'require' => [ 'id' => '\d+', ],
    'default' => [ 'id' => '1', ]
]);
$mux->post('/product/:id', ['ProductController','updateAction'] , [
    'require' => [ 'id' => '\d+', ],
    'default' => [ 'id' => '1', ]
]);
$mux->delete('/product/:id', ['ProductController','deleteAction'] , [
    'require' => [ 'id' => '\d+', ],
    'default' => [ 'id' => '1', ]
]);

// If you use ExpandableController, it will automatically expands your controller actions into a sub-mux
$mux->mount('/page', new PageController);

$submux = new Pux\Mux;
$submux->any('/bar');
$mux->mount('/foo',$submux); // mount as /foo/bar

// RESTful Mux Builder
$builder = new RESTfulMuxBuilder($mux, [ 'prefix' => '/=' ]);
$builder->addResource('product', new ProductResourceController); // expand RESTful resource point at /=/product
$mux = $builder->build();


if ($route = $mux->dispatch('/product/1')) {
    $response = RouteExecutor::execute($route);

    $responder = new Pux\Responder\SAPIResponder();
    // $responder->respond([ 200, [ 'Content-Type: text/plain' ], 'Hello World' ]);
    $responder->respond($response);
}

Mux

Mux is where you define your routes, and you can mount multiple mux to a parent one.

$mainMux = new Mux;

$pageMux = new Mux;
$pageMux->any('/page1', [ 'PageController', 'page1' ]);
$pageMux->any('/page2', [ 'PageController', 'page2' ]);

// short-hand syntax
$pageMux->any('/page2', 'PageController:page2'  );

$mainMux->mount('/sub', $pageMux);

foreach( ['/sub/page1', '/sub/page2'] as $p ) {
    $route = $mainMux->dispatch($p);

    // The $route contains [ pcre (boolean), path (string), callback (callable), options (array) ]
    list($pcre, $path, $callback, $options) = $route;
}

Methods

  • Mux->add( {path}, {callback array or callable object}, { route options })
  • Mux->post( {path}, {callback array or callable object}, { route options })
  • Mux->get( {path}, {callback array or callable object}, { route options })
  • Mux->put( {path}, {callback array or callable object}, { route options })
  • Mux->any( {path}, {callback array or callable object}, { route options })
  • Mux->delete( {path}, {callback array or callable object}, { route options })
  • Mux->mount( {path}, {mux object}, { route options })
  • Mux->length() returns length of routes.
  • Mux->export() returns Mux constructor via __set_state static method in php code.
  • Mux->dispatch({path}) dispatch path and return matched route.
  • Mux->getRoutes() returns routes array.
  • Mux::__set_state({object member array}) constructs and returns a Mux object.

Sorting routes

You need to sort routes when not using compiled routes, it's because pux sorts longer path to front:

$pageMux = new Mux;
$pageMux->add('/', [ 'PageController', 'page1' ]);
$pageMux->add('/pa', [ 'PageController', 'page1' ]);
$pageMux->add('/page', [ 'PageController', 'page1' ]);
$pageMux->sort();

This sorts routes to:

/page
/pa
/

So pux first compares /page, /pa, than /.

Different String Comparison Strategies

When expand is enabled, the pattern comparison strategy for strings will match the full string.

When expand is disabled, the pattern comparison strategy for strings will match the prefix.

RouteRequest

RouteRequest maintains the information of the current request environment, it also provides some constraint checking methods that helps you to identify a request, e.g.:

if ($request->queryStringMatch(...)) {

}
if ($request->hostEqual('some.dev')) {

}
if ($request->pathEqual('/foo/bar')) {

}
use Pux\Environment;
$env = Environment::createFromGlobals();
$request = RouteRequest::createFromEnv($env);

if ($route = $mux->dispatchRequest($request)) {

}

APCDispatcher

Although Pux\Mux is already fast, you can still add APCDispatcher to boost the performance, which is to avoid re-lookup route.

This is pretty useful when you have a lot of PCRE routes.

use Pux\Dispatcher\APCDispatcher;
$dispatcher = new APCDispatcher($mux, array(
    'namespace' => 'app_',
    'expiry' => ...,
));
$route = $dispatcher->dispatch('/request/uri');
var_dump($route);

Controller

Pux provides the ability to map your controller methods to paths automatically, done either through a simple, fast controller in the C extension or its pure PHP counterpart:

class ProductController extends \Pux\Controller
{
    // translate to path ""
    public function indexAction() { }

    // translate to path "/add"
    public function addAction() { }

    // translate to path "/del"
    public function delAction() { }
}

$mux = new Pux\Mux;
$submux = $controller->expand();
$mux->mount( '/product' , $submux );

// or even simpler
$mux->mount( '/product' , $controller);

$mux->dispatch('/product');       // ProductController->indexAction
$mux->dispatch('/product/add');   // ProductController->addAction
$mux->dispatch('/product/del');   // ProductController->delAction

You can also use @Route and @Method annotations to override the default \Pux\Controller::expand() functionality:

class ProductController extends \Pux\Controller
{
    /**
     * @Route("/all")
     * @Method("GET")
     */
    public function indexAction() {
        // now available via GET /all only
    }
    
    /**
     * @Route("/create")
     * @Method("POST")
     */
    public function addAction() {
        // now available via POST /create only
    }
    
    /**
     * @Route("/destroy")
     * @Method("DELETE")
     */
    public function delAction() {
        // now available via DELETE /destroy only
    }
}

This is especially helpful when you want to provide more specific or semantic (e.g., HTTP method-specific) actions. Note that by default, expanded controller routes will be available via any HTTP method - specifying @Method will restrict it to the provided method.

  • Pux\Controller::expand() returns an instance of \Pux\Mux that contains the controller's methods mapped to URIs, intended to be mounted as a sub mux in another instance of \Pux\Mux.

Route RouteExecutor

Pux\RouteExecutor executes your route by creating the controller object, and calling the controller action method.

Route executor take the returned route as its parameter, you simply pass the route to executor the controller and get the execution result.

Here the simplest example of the usage:

use Pux\RouteExecutor;
$mux = new Pux\Mux;
$mux->any('/product/:id', ['ProductController','itemAction']);
$route = $mux->dispatch('/product/1');
$result = RouteExecutor::execute($route);

You can also define the arguments to the controller's constructor method:

class ProductController extends Pux\Controller {
    public function __construct($param1, $param2) {
        // do something you want
    }
    public function itemAction($id) {
        return "Product $id";
    }
}

use Pux\RouteExecutor;
$mux = new Pux\Mux;
$mux->any('/product/:id', ['ProductController','itemAction'], [ 
    'constructor_args' => [ 'param1', 'param2' ],
]);
$route = $mux->dispatch('/product/1');
$result = RouteExecutor::execute($route); // returns "Product 1"

Dispatching Strategy

There are two route dispatching strategies in Pux while Symfony/Routing only provides PCRE pattern matching:

  1. Plain string comparison.
  2. PCRE pattern comparison.

You've already knew that PCRE pattern matching is slower than plain string comparison, although PHP PCRE caches the compiled patterns.

The plain string comparison is designed for static routing paths, it improves the performance while you have a lot of simple routes.

The PCRE pattern comparison is used when you have some dynamic routing paths, for example, you can put some place holders in your routing path, and pass these path arguments to your controller later.

Pux sorts and compiles your routes to single cache file, it also uses longest matching so it sorts patterns by pattern length in descending order before compiling the routes to cache.

Pux uses indexed array as the data structure for storing route information so it's faster.

Routing Path Format

Static route:

/post

PCRE route:

/post/:id                  => matches /post/33

PCRE route with optional pattern:

/post/:id(/:title)         => matches /post/33, /post/33/post%20title
/post/:id(\.:format)       => matches /post/33, /post/33.json .. /post/33.xml

Q & A

Why It's Faster

  • Pux uses simpler data structure (indexed array) to store the patterns and flags. (In PHP internals, zend_hash_index_find is faster than zend_hash_find).

  • When matching routes, symfony uses a lot of function calls for each route:

    https://github.com/symfony/Routing/blob/master/Matcher/UrlMatcher.php#L124

    Pux fetches the pattern from an indexed-array:

    https://github.com/c9s/Pux/blob/master/src/Pux/Mux.php#L189

  • Even you enabled APC or other bytecode cache extension, you are still calling methods and functions in the runtime. Pux reduces the route building to one static method call. __set_state.

  • Pux separates static routes and dynamic routes automatically, Pux uses hash table to look up static routes without looping the whole route array.

  • Pux\Mux is written in C extension, method calls are faster!

  • With C extension, there is no class loading overhead.

  • Pux compiles your routes to plain PHP array, the compiled routes can be loaded very fast. you don't need to call functions to register your routes before using it.

Why It's Here

Most of us use a lot of machines to run our applications, however, it uses too much energy and too many resources.

Some people thinks routing is not the bottleneck, the truth is this project does not claim routing is the bottleneck.

Actually the "bottleneck" is always different in different applications, if you have a lot of heavy db requests, then your bottleneck is your db; if you have a lot of complex computation, then the bottleneck should be your algorithm.

You might start wondering since the bottleneck is not routing, why do we implement route dispatcher in C extension? The answer is simple, if you put a pure PHP routing component with some empty callbacks and use apache benchmark tool to see how many requests you can handle per second, you will find out the routing component consumes a lot of computation time and the request number will decrease quite a few. (and it does nothing, all it does is ... just routing)

Pux tries to reduce the overheads of loading PHP classes and the runtime method/function calls, and you can run your application faster without the overheads.

Pros & Cons of Grouped Pattern Matching Strategy

An idea of matching routes is to combine all patterns into one pattern and compare the given path with pcre_match in one time.

However this approach does not work if you have optional group or named capturing group, the pcre_match can not return detailed information about what pattern is matched if you use one of them.

And since you compile all patterns into one, you can't compare with other same patterns with different conditions, for example:

/users  # GET
/users  # POST
/users  # with HTTP_HOST=somedomain

The trade off in Pux is to compare routes in sequence because the same pattern might be in different HTTP method or different host name.

The best approach is to merge & compile the regexp patterns into a FSM (Finite state machine), complex conditions can also be merged into this FSM, and let this FSM to dispatch routes. And this is the long-term target of Pux.

Contributing

Testing XHProf Middleware

Define your XHPROF_ROOT in your phpunit.xml, you can copy phpunit.xml.dist to phpunit.xml, for example:

  <php>
    <env name="XHPROF_ROOT" value="/Users/c9s/src/php/xhprof"/>
  </php>

Hacking Pux C extension

  1. Discuss your main idea on GitHub issue page.

  2. Fork this project and open a branch for your hack.

  3. Development Cycle:

     cd ext
     ./compile
     ... hack hack hack ...
    
     # compile and run phpunit test
     ./compile && ./test -- --debug tests/Pux/MuxTest.php
    
     # use lldb to debug extension code
     ./compile && ./test -l -- tests/Pux/MuxTest.php
    
     # use gdb to debug extension code
     ./compile && ./test -g -- tests/Pux/MuxTest.php
    
  4. Commit!

  5. Send pull request and describe what you've done and what is changed.

pux's People

Contributors

aasiutin avatar andylibrian avatar c9s avatar caferyukseloglu avatar jbboehr avatar oytuntez avatar rubensayshi avatar sjinks avatar zoowii avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

pux's Issues

Use C struct as the presentation of route

Since we seldom use getRoutes method, we can handle the routes in an efficient way internally. That is, use C struct to save route pcre flag, pattern string instead of looping the zval array and extra zend _index_find calls.

default action path for indexAction from controller

Hi @mway

it seems the path for indexAction is defined to / by default, however if we mount a controller to something like "/product" we got "/product/", the correct path should be /product.

I think the default path of indexAction should be an empty string. :-)

you may run the unit test to see the result:

 cd ext/
 ./compile && ./run-test -- test/Pux/ControllerTest.php

zend_mm_heap corrupted

By running the MuxTest:

   ./run-test -- --repeat 1000 test/Pux/MuxTest.php
   ....zend_mm_heap corrupted

Unknown License

Hi c9s,

My team and I are willing to make a php Framework for a student project and we were wondering under which license is Pux ?

Best regards,

Thomas.

Implement pux_fetch_mux and pux_store_mux to support persistent mux.

to reach the goal of #18, we should first implement the basic functions to store/fetch mux.

currently we've done the pux_store_mux function, now we need to implement pux_fetch_mux.

the API is something like APC, but we store the zval without serialization/unserialization:

if (  ($mux = pux_fetch_mux('app_pmux')) == null ) {
   $mux = new Mux;
   pux_store_mux('app_pmux', $mux);
}

Annotation value case exception?

Hi @mway,

The doc_var_len is decided by a space char. but what if an user ends the line with \n not a space? that might return a path like /path\n?

int doc_var_val_len = strstr(doc_var_substr_start, " ") - doc_var_substr_start - 1;

Is it possible to read the Route like Symfony do? because our annotation actually reads this syntax:

class PostController extends Controller
{
    /**
     * @Route("/")
     * @Method("GET")
     */
    public function indexAction()
    {
        // ...
    }
}

Provide a generic controller in C extension

  1. provide empty "before" method for triggering.
  2. provide empty "after" method for triggering.
  3. provide a method to get all action method.
  4. provide a toJson method to encode data in JSON format.

route '/' handling differs

Sample code:

<?php
require __DIR__ . '/../vendor/autoload.php';
$mux = new Pux\Mux;
$mux->add('/', [ 'PageController', 'page1' ]);
$route = $mux->dispatch('/');
var_dump($route);

Result with C extension 1.3.1:

array (size=4)
  0 => boolean false
  1 => string '/' (length=1)
  2 => 
    array (size=2)
      0 => string 'PageController' (length=14)
      1 => string 'page1' (length=5)
  3 => 
    array (size=0)
      empty

Pux 1.3.2 Result without C extension:

null

The result without C extension seem to be a bug.

Typed Function Calls

Hi!

We're interested in adding typed function calls to Pux and contributing them back to the project. Taking this example from the docs:

$mux->get('/product/:id', ['ProductController','itemAction'] , [
    'require' => [ 'id' => '\d+', ],
    'default' => [ 'id' => '1', ]
]);

would become:

$mux->get('/product/:id', ['ProductController','itemAction'] , [
    'require' => [ 'id' => Pux\Mux::TYPE_INTEGER, ],
    'default' => [ 'id' => '1', ]
]);

This would automatically match against \d+ and result in a call to as ProductionController()->itemAction( (int) $id) instead of passing a string type.

Why?

This will simplify controller logic and allow invalid input to be sent to a 404 page instead of through controller logic.

It also allows folks to start converting over to Hack / HHVM for other performance gains.

Any concerns over this feature? Will this have any impact on the C extension (which we don't use so I wouldn't be adding any code in).

Support to alias and reverse's URL

Hello.

Congratulations for the excellent router!

I am sending a suggestion to improve it in usability for developers (seriously, its a great feature, not in performance, but..it can help for big sites): Allow developer to specify a alias related to a router and reverse the URL based on its alias+options.

Say i have the code that follows:

    require 'vendor/autoload.php';
    use Pux\Executor;

    class ProductController {
        public function listAction() {
            return 'product list';
        }
        public function itemAction($id) { 
            return "product $id";
        }
    }
    $mux = new Pux\Mux;
    $mux->add('/product', ['ProductController','listAction']);
    $mux->add('/product/:id', ['ProductController','itemAction'] , [
        'require' => [ 'id' => '\d+', ],
        'default' => [ 'id' => '1', ],
        'alias' => 'product-page'
    ]);

If i do the following in some place of the code (its great specially for use in templates, but..):

    $url = $mux->reverse('product-page', [
        'id' => '10'
    ]);

And $url will need to have /product/10 string, all with the great speed of Pux. \o/

What you think?

Persistent Mux in the extension

To improve even more the performance, you can make create an alias for the Mux class and it be stored in the extension persistent memory.

For web requests, the first request will load from the compiled router file. For subsequent request it can use the persisted object in memory. It will avoid file access to read the compiled file and re-load all the objects for every request.

You can make something like:

$mux = \Pux\MuxCache::load('compiled_router.php', 'app_routes');
// If not stored, include compiled_router and persist it, otherwise just use the persisted object from app_routes alias

\Pux\MuxCache::clear(); // Remove all persisted objects

Something like that. Thoughts?

Compile route patterns to one single pattern

Currently we're looping all PCRE patterns to match the correct route, which provides a nice flexibility - you can mount sub-routes to a parent one, or dispatch the path in sub-router dynamically.

However in different case, for example: like 100 pattern routes, we should have different dispatching strategy to dispatch routes.

One idea is to Compile all route patterns into one grouped pattern, and the dispatching can be done in one regular expression comparison.

strcmp vs strncmp

I have a question:
Mux.php
line 241
In a route like $mux->add('/page.html') it matches /page.html but too /page.html1234abcd.
Changing
if ( strncmp($route[1] , $path, strlen($route[1]) ) === 0 ) {
with
if ( strcmp($route[1] , $path ) === 0 ) {
that not happen, responds only the exact match /page.html.
Is there a reason for using strncmp?

Expanding route for Mux to mount causes gc scan access fail.

Hi @mway,

I tested the new AnnotationController, and found there is a weird gc bug, which is not shown previously, not sure what changed/forgot to set the refcount? :

https://github.com/c9s/Pux/blob/develop/test/Pux/ControllerAnnotationTest.php#L27

Here is the code that causes gc failed:

        // works fine
        // $submux = $controller->expand();
        // $mux->mount('/product', $submux );

        // gc scan bug
        $mux->mount('/product', $controller->expand() );

to reproduce this issue , simply run the test cases with --repeat 500 or so:

./run-test --  --repeat 500 --debug test/Pux/ControllerAnnotationTest.php

you might need a zts enabled + debug php to trace this issue.

Add Pux pure PHP to the benchmark information

I hate to say it, but comparing a C extension to a pure PHP implementation is like comparing a racehorse against a donkey. Especially for something like this. So saying the extension is faster than the Symphony router isn't saying much.

Personally, I'd like to know where the pure PHP implementation of Pux stands in relation to the extension, as well as other libraries.

route '/' handling differs

Sample code:

<?php
require __DIR__ . '/../vendor/autoload.php';
$mux = new Pux\Mux;
$mux->add('/', [ 'PageController', 'page1' ]);
$route = $mux->dispatch('/abc');
var_dump($route);

Result of 1.4.0:

NULL

Result of 1.5.0:

array(4) {
  [0] =>
  bool(false)
  [1] =>
  string(1) "/"
  [2] =>
  array(2) {
    [0] =>
    string(14) "PageController"
    [1] =>
    string(5) "page1"
  }
  [3] =>
  array(0) {
  }
}

Is this a bug or not?

memory leak of getRequestMethodConstant

Hi @mway,

it seems like we have memory leak in PHP_METHOD(Mux, getRequestMethodConstant).

mthit = mthp is estrdup from req_method. but it's not freed via efree.

branch: feature/annotation

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.