Code Monkey home page Code Monkey logo

bulletphp's Introduction

Bullet

Bullet is a resource-oriented micro PHP framework built around HTTP URIs. Bullet takes a unique functional-style approach to URL routing by parsing each path part independently and one at a time using nested closures. The path part callbacks are nested to produce different responses and to follow and execute deeper paths as paths and parameters are matched.

Build Status

PROJECT MAINTENANCE RESUMES

Bullet becomes an active project again. Currently there's a changing of the guard. Feel free to further use and contribute to the framework.

Requirements

  • PHP 5.6+ (PHP 7.1 recommended)
  • Composer for all package management and autoloading (may require command-line access)

Rules

  • Apps are built around HTTP URIs and defined paths, not forced MVC (but MVC-style separation of concerns is still highly recommenended and encouraged)
  • Bullet handles one segment of the path at a time, and executes the callback for that path segment before proceesing to the next segment (path callbacks are executed from left to right, until the entire path is consumed).
  • If the entire path cannot be consumed, a 404 error will be returned (note that some callbacks may have been executed before Bullet can know this due to the nature of callbacks and closures). Example: path /events/45/edit may return a 404 because there is no edit path callback, but paths events and 45 would have already been executed before Bullet can know to return a 404. This is why all your primary logic should be contained in get, post, or other method callbacks or in the model layer (and not in the bare path handlers).
  • If the path can be fully consumed, and HTTP method handlers are present in the path but none are matched, a 405 "Method Not Allowed" response will be returned.
  • If the path can be fully consumed, and format handlers are present in the path but none are matched, a 406 "Not Acceptable" response will be returned.

Advantages

  • Super flexible routing. Because of the way the routing callbacks are nested, Bullet's routing system is one of the most flexible of any other PHP framework or library. You can build any URL you want and respond to any HTTP method on that URL. Routes are not restricted to specific patterns or URL formats, and do not require a controller with specific method names to respond to specific HTTP methods. You can nest routes as many levels deep as you want to expose nested resources like posts/42/comments/943/edit with a level of ease not found in most other routing libraries or frameworks.

  • Reduced code duplication (DRY). Bullet takes full advantage of its nested closure routing system to reduce a lot of typical code duplication required in most other frameworks. In a typical MVC framework controller, some code has to be duplicated across methods that perform CRUD operations to run ACL checks and load required resources like a Post object to view, edit or delete. With Bullet's nested closure style, this code can be written just once in a path or param callback, and then you can use the loaded object in subsequent path, param, or HTTP method handlers. This eliminates the need for "before" hooks and filters, because you can just run the checks and load objects you need before you define other nested paths and use them when required.

Installing with Composer

Use the basic usage guide, or follow the steps below:

Setup your composer.json file at the root of your project

{
    "require": {
        "vlucas/bulletphp": "~1.7"
    }
}

Install Composer

curl -s http://getcomposer.org/installer | php

Install Dependencies (will download Bullet)

php composer.phar install

Create index.php (use the minimal example below to get started)

<?php
require __DIR__ . '/vendor/autoload.php';

/* Simply build the application around your URLs */
$app = new Bullet\App();

$app->path('/', function($request) {
    return "Hello World!";
});
$app->path('/foo', function($request) {
    return "Bar!";
}); 

/* Run the app! (takes $method, $url or Bullet\Request object)
 * run() always return a \Bullet\Response object (or throw an exception) */

$app->run(new Bullet\Request())->send();

This application can be placed into your server's document root. (Make sure it is correctly configured to serve php applications.) If index.php is in the document root on your local host, the application may be called like this:

http://localhost/index.php?u=/

and

http://localhost/index.php?u=/foo

If you're using Apache, use an .htaccess file to beautify the URLs. You need mod_rewrite to be installed and enabled.

<IfModule mod_rewrite.c>
  RewriteEngine On

  # Reroute any incoming requestst that is not an existing directory or file
  RewriteCond %{REQUEST_FILENAME} !-d
  RewriteCond %{REQUEST_FILENAME} !-f
  RewriteRule ^(.*)$ index.php?u=$1 [L,QSA,B]
</IfModule>

With this file in place Apache will pass the request URI to index.php using the $_GET['u'] parameter. This works in subdirectories as expected i.e. you don't have to explicitly take care of removing the path prefix e.g. if you use mod_userdir, or just install a Bullet application under an existing web app to serve an API or simple, quick dynamic pages. Now your application will answer to these pretty urls:

http://localhost/

and

http://localhost/foo

NGinx also has a rewrite command, and can be used to the same end:

server {
    # ...
    location / {
        # ...
        rewrite ^/(.*)$ /index.php?u=/$1;
        try_files $uri $uri/ =404;
        # ...
    }
    # ...
}

If the Bullet application is inside a subdirectory, you need to modify the rewrite line to serve it correctly:

server {
    # ...
    location / {
        rewrite ^/bulletapp/(.*)$ /bulletapp/index.php?u=/$1;
        try_files $uri $uri/ =404;
    }
    # ...
}

Note that if you need to serve images, stylesheets, or javascript too, you need to add a location for the static root directory without the reqrite to avoid passing those URLs to index.php.

View it in your browser!

Syntax

Bullet is not your typical PHP micro framework. Instead of defining a full path pattern or a typical URL route with a callback and parameters mapped to a REST method (GET, POST, etc.), Bullet parses only ONE URL segment at a time, and only has two methods for working with paths: path and param. As you may have guessed, path is for static path names like "blog" or "events" that won't change, and param is for variable path segments that need to be captured and used, like "42" or "my-post-title". You can then respond to paths using nested HTTP method callbacks that contain all the logic for the action you want to perform.

This type of unique callback nesting eliminates repetitive code for loading records, checking authentication, and performing other setup work found in typical MVC frameworks or other microframeworks where each callback or action is in a separate scope or controller method.

$app = new Bullet\App(array(
    'template.cfg' => array('path' => __DIR__ . '/templates')
));

// 'blog' subdirectory
$app->path('blog', function($request) use($app) {

    $blog = somehowGetBlogMapper(); // Your ORM or other methods here

    // 'posts' subdirectory in 'blog' ('blog/posts')
    $app->path('posts', function() use($app, $blog) {

        // Load posts once for handling by GET/POST/DELETE below
        $posts = $blog->allPosts(); // Your ORM or other methods here

        // Handle GET on this path
        $app->get(function() use($posts) {
            // Display all $posts
            return $app->template('posts/index', compact('posts'));
        });

        // Handle POST on this path
        $app->post(function() use($posts) {
            // Create new post
            $post = new Post($request->post());
            $mapper->save($post);
            return $this->response($post->toJSON(), 201);
        });

        // Handle DELETE on this path
        $app->delete(function() use($posts) {
            // Delete entire posts collection
            $posts->deleteAll();
            return 200;
        });

    });
});

// Run the app and echo the response
echo $app->run("GET", "blog/posts");

Capturing Path Parameters

Perhaps the most compelling use of URL routing is to capture path segments and use them as parameters to fetch items from a database, like /posts/42 and /posts/42/edit. Bullet has a special param handler for this that takes two arguments: a test callback that validates the parameter type for use, and and a Closure callback. If the test callback returns boolean false, the closure is never executed, and the next path segment or param is tested. If it returns boolean true, the captured parameter is passed to the Closure as the second argument.

Just like regular paths, HTTP method handlers can be nested inside param callbacks, as well as other paths, more parameters, etc.

$app = new Bullet\App(array(
    'template.cfg' => array('path' => __DIR__ . '/templates')
));
$app->path('posts', function($request) use($app) {
    // Integer path segment, like 'posts/42'
    $app->param('int', function($request, $id) use($app) {
        $app->get(function($request) use($id) {
            // View post
            return 'view_' . $id;
        });
        $app->put(function($request) use($id) {
            // Update resource
            $post->data($request->post());
            $post->save();
            return 'update_' . $id;
        });
        $app->delete(function($request) use($id) {
            // Delete resource
            $post->delete();
            return 'delete_' . $id;
        });
    });
    // All printable characters except space
    $app->param('ctype_graph', function($request, $slug) use($app) {
        return $slug; // 'my-post-title'
    });
});

// Results of above code
echo $app->run('GET',   '/posts/42'); // 'view_42'
echo $app->run('PUT',   '/posts/42'); // 'update_42'
echo $app->run('DELETE', '/posts/42'); // 'delete_42'

echo $app->run('DELETE', '/posts/my-post-title'); // 'my-post-title'

Returning JSON (Useful for PHP JSON APIs)

Bullet has built-in support for returning JSON responses. If you return an array from a route handler (callback), Bullet will assume the response is JSON and automatically json_encode the array and return the HTTP response with the appropriate Content-Type: application/json header.

$app->path('/', function($request) use($app) {
    $app->get(function($request) use($app) {
        // Links to available resources for the API
        $data = array(
            '_links' => array(
                'restaurants' => array(
                    'title' => 'Restaurants',
                    'href' => $app->url('restaurants')
                ),
                'events' => array(
                    'title' => 'Events',
                    'href' => $app->url('events')
                )
            )
        );

        // Format responders
        $app->format('json', function($request), use($app, $data) {
            return $data; // Auto json_encode on arrays for JSON requests
        });
        $app->format('xml', function($request), use($app, $data) {
            return custom_function_convert_array_to_xml($data);
        });
        $app->format('html', function($request), use($app, $data) {
            return $app->template('index', array('links' => $data));
        });
    });
});

HTTP Response Bullet Sends:

Content-Type:application/json
{"_links":{"restaurants":{"title":"Restaurants","href":"http:\/\/yourdomain.local\/restaurants"},"events":{"title":"Events","href":"http:\/\/yourdomain.local\/events"}}}

Bullet Response Types

There are many possible values you can return from a route handler in Bullet to produce a valid HTTP response. Most types can be either returned directly, or wrapped in the $app->response() helper for additional customization.

Strings

$app = new Bullet\App();
$app->path('/', function($request) use($app) {
    return "Hello World";
});
$app->path('/', function($request) use($app) {
    return $app->response("Hello Error!", 500);
});

Strings result in a 200 OK response with a body containing the returned string. If you want to return a quick string response with a different HTTP status code, use the $app->response() helper.

Booleans

$app = new Bullet\App();
$app->path('/', function($request) use($app) {
    return true;
});
$app->path('notfound', function($request) use($app) {
    return false;
});

Boolean false results in a 404 "Not Found" HTTP response, and boolean true results in a 200 "OK" HTTP response.

Integers

$app = new Bullet\App();
$app->path('teapot', function($request) use($app) {
    return 418;
});

Integers are mapped to their corresponding HTTP status code. In this example, a 418 "I'm a Teapot" HTTP response would be sent.

Arrays

$app = new Bullet\App();
$app->path('foo', function($request) use($app) {
    return array('foo' => 'bar');
});
$app->path('bar', function($request) use($app) {
    return $app->response(array('bar' => 'baz'), 201);
});

Arrays are automatically passed through json_encode and the appropriate Content-Type: application/json HTTP response header is sent.

Templates

// Configure template path with constructor
$app = new Bullet\App(array(
    'template.cfg' => array('path' => __DIR__ . '/templates')
));

// Routes
$app->path('foo', function($request) use($app) {
    return $app->template('foo');
});
$app->path('bar', function($request) use($app) {
    return $app->template('bar', array('bar' => 'baz'), 201);
});

The $app->template() helper returns an instance of Bullet\View\Template that is lazy-rendered on __toString when the HTTP response is sent. The first argument is a template name, and the second (optional) argument is an array of parameters to pass to the template for use.

Serving large responses

Bullet works by wrapping every possible reponse with a Response object. This would normally mean that the entire request must be known (~be in memory) when you construct a new Response (either explicitly, or trusting Bullet to construct one for you).

This would be bad news for those serving large files or contents of big database tables or collections, since everything would have to be loaded into memory.

Here comes \Bullet\Response\Chunked for the rescue.

This response type requires some kind of iterable type. It works with regular arrays or array-like objects, but most importatnly, it works with generator functions too. Here's an example (database functions are purely fictional):

$app->path('foo', function($request) use($app) {
    $g = function () {
        $cursor = new ExampleDatabaseQuery("select * from giant_table");
        foreach ($cursor as $row) {
            yield example_format_db_row($row);
        }
        $cursor->close();
    };
    return new \Bullet\Response\Chunked($g());
});

The $g variable will contain a Closure that uses yield to fetch, process, and return data from a big dataset, using only a fraction of the memory needed to store all the rows at once.

This results in a HTTP chunked response. See https://tools.ietf.org/html/rfc7230#section-4.1 for the technical details.

HTTP Server Sent Events

Server sent events are one way to open up a persistent channel to a web server, and receive notifications. This can be used to implement a simple webchat for example.

This standard is part of HTML5, see https://html.spec.whatwg.org/multipage/server-sent-events.html#server-sent-events for details.

The example below show a simple application using the fictional send_message and receive_message functions for communications. These can be implemented over various message queues, or simple named pipes.

$app->path('sendmsg', function($request) {
    $this->post(function($request) {
        $data = $request->postParam('message');
        send_message($data);
        return 201;
    });
});

$app->path('readmsgs', function($request) {
    $this->get(function($request) {
        $g = function () {
            while (true) {
                $data = receive_message();
                yield [
                    'event' => 'message',
                    'data'  => $data
                ];
            }
        };
        \Bullet\Response\Sse::cleanupOb(); // Remove any output buffering
        return new \Bullet\Response\Sse($g());
    });
});

The SSE response uses chunked encoding, contrary to the recommendation in the standard. We can do this, since we tailoe out chunks to be exactly message-sized.

This will not confuse upstream servers when they see no chunked encoding, AND no Content-Length header field, and might try to "fix" this by either reading the entire response, or doing the chunking on their own.

PHP's output buffering can also interfere with messaging, hence the call to \Bullet\Response\Sse::cleanupOb(). This method flushes and ends every level of output buffering that might present before sending the response.

The SSE response automatically sends the X-Accel-Buffering: no header to prevent the server from buffering the messages.

Nested Requests (HMVC style code re-use)

Since you explicitly return values from Bullet routes instead of sending output directly, nested/sub requests are straightforward and easy. All route handlers will return Bullet\Response instances (even if they return a raw string or other data type, they are wrapped in a response object by the run method), and they can be composed to form a single HTTP response.

$app = new Bullet\App();
$app->path('foo', function($request) use($app) {
    return "foo";
});
$app->path('bar', function($request) use($app) {
    $foo = $app->run('GET', 'foo'); // $foo is now a `Bullet\Response` instance
    return $foo->content() . "bar";
});
echo $app->run('GET', 'bar'); // echos 'foobar' with a 200 OK status

Running Tests

To run the Bullet test suite, simply run vendor/bin/phpunit in the root of the directory where the bullet files are in. Please make sure to add tests and run the test suite before submitting pull requests for any contributions.

Credits

Bullet - and specifically path-based callbacks that fully embrace HTTP and encourage a more resource-oriented design - is something I have been thinking about for a long time, and was finally moved to create it after seeing @joshbuddy give a presentation on Renee (Ruby) at Confoo 2012 in Montréal.

bulletphp's People

Contributors

acelot avatar acicali avatar ellotheth avatar entraigas avatar geertdd avatar grahamcampbell avatar jcbwlkr avatar ladycailin avatar netom avatar omnicolor avatar peter279k avatar rogeriopradoj avatar sam2332 avatar tonyroberts avatar vlucas avatar znote 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

bulletphp's Issues

Response Handlers cannot be overloaded

Currently, once a handler has been added, it cannot be overloaded. Because new handlers are not prepended, new handlers that match similar, but perhaps more restrictive, conditions will not be executed. Problem was introduced by f1f3c21.

Example of code I want to use:

$app->registerResponseHandler(
    function($response) {
        return is_array($response->content());
    },
    function($response) use ($app) {
        if (BULLET_ENV !== 'production') {
            $options = JSON_PRETTY_PRINT;
        } else {
            $options = null;
        }
        $response->contentType('application/vnd.api+json');
        $response->content(json_encode($response->content(), $options));
    }
);

Handle Query Strings in Passed URL and REQUEST_URI

Currently, Bullet does not seem to properly handle requests when the query string is in the URL that is passed into Bullet, or when the URL is in the $_SERVER['REQUEST_URI'] constant. This happens with the internal built-in PHP webserver as well as some setups with nginx, depending on how it is configured.

$req = new Bullet\Request('GET', '/test?foo=bar');

A route for test will incorrectly throw a 404 error, because Bullet looks for the path /test?foo=bar instead of only /test.

A full reproducible test case:

    public function testGetWithQueryString()
    {
        $app = new Bullet\App();
        $app->path('test', function($request) use($app) {
            $app->get(function($request) {
                return $request->foo;
            });
        });
        $req = new Bullet\Request('GET', '/test?foo=bar');
        $res = $app->run($req);
        $this->assertEquals('bar', $res->content());
    }

Default $method = null for App->run()

Since the constructor for Request() defaults $method to null anyways, I vote that App->run() also have its $method defaulted to null.

This doesn't seem to break anything, but allows us to let $app->run() create the Request object instead of having to provide it.

Templates are rendered twice

Response::content() sets or retrieves stored rendered response content. Template extends Response and overrides content(), but without any internal storage--the template content is rendered every time content() is called.

In the Bullet app flow, content() is initially called in the Bullet response handler (App::_handleResponse()), which checks to see if the response should be rendered as JSON. content() is called again via Response::__toString() when App::run() is printed. For Template responses, that means the full template (including the layout) is rendered at least twice (more if more response handlers are registered).

index.php

<?php
global $tpl_counter; // tracks how many times the template is rendered
$tpl_counter = 0;

require_once dirname(__DIR__).'/vendor/autoload.php';

$app = new \Bullet\App(array(
    'template.cfg' => array('path' => dirname(__DIR__).'/src/')
));

$app->path('/', function($request) use ($app) {
    $app->get(function($request) use ($app) {
        return $app->template('template');
    });
});

echo $app->run(new \Bullet\Request());

template.html.php

<?php
global $tpl_counter;
$tpl_counter++; // increment the counter for each each template rendering
?>
content in the template: <?php echo $tpl_counter; ?>

page output

content in the template: 2

A possible solution could be to use the internal content storage in Template, so calling Template::content() only renders new content if the internal storage is empty. Alternatively the response handlers could skip any Template-type responses.

Packagist not updated with release 1.4.0

composer.json was not updated for tag v1.4.0. Better than updating composer.json with the new version would be to remove the version completely, which would allow Comoser to automatically detect new versions from tags: https://getcomposer.org/doc/02-libraries.md#tags

edit: it looks like the version was already removed in 959c2fb, but packagist has not been refreshed. FWIW, it is possible to auto-refresh packagist under https://github.com/vlucas/bulletphp/settings/hooks

Bullet doesn't fully support periods in URIs

There are times when one might like to have periods in the URI, but not a file extension. One such use case is for tokens, e.g. http://jwt.io/

Here's the offending source:
https://github.com/vlucas/bulletphp/blob/master/src/Bullet/App.php#L179

One quick solution would be to wrap that line in a check for a match against the listed mime types.

<?php

if(isset($this->_request->_mimeTypes[$ext])) {
    $this->_requestPath = substr($this->_requestPath, 0, -(strlen($this->_request->format())+1));
}

This is just for illustration, the _mimeTypes array is protected. This approach passes existing unit tests, but would mean support for any additional extensions would need to be added to the _mimeTypes array. Perhaps a method for that?

OR... probably the better solution would be to only strip the extension from the path if there's a matching call to $app->format('.ext'). This approach won't pass existing unit tests.

For my token example, I'll either need to append a faux extension to the path (yuck), pass it using a POST call (yuck), pass it via a query string (yuck), or pass it via a request header (yuck). I'll end up moving forward using the least yucky of these... not sure which that is yet though :)

Slug params

When I create a path like /about/slug, I can continue adding params and it still matches the callback:

Using the below code, /about/slug/foo/bar would return 'about-bar'

I am expecting it to only work one level deep, /about/slug , otherwise return as a 404.

Am I missing something? I'm using v1.1.4

$app->path('about', function() use ($app){

    $page = 'about';

    $app->get(function() use ($app,$page){

        return $app->template('about',compact('page'));

    });

    $app->param('slug',function($request,$slug) use ($app,$page){

        $app->get(function() use ($app,$page,$slug){

            return $page.'-'.$slug;

        });

    });

});

Add 'domain' routes

These would work similar to the subdomain routes, but would match on the HTTP_HOST instead. This would be useful for serving multiple sites from the same repo (not sure if that is commonly desired, but it's easy to add as a feature).

Support HEAD request as GET + no body if HEAD not present

Bullet should support a HEAD request as a GET request without a response body. Currently HEAD requests without explicit $app->head handlers returns a 404. Marking as a bug because the current behavior differs from HTTP, and is returning an incorrect response.

HTTP Basic Auth in Request Class

Support should be added for retrieving the HTTP auth username and password in the request class, maybe with $request->user() and $request->pass() or similar. This is because there can be a number of ways to do it depending on the server setup, and the variables PHP_AUTH_USER and PHP_AUTH_PW are not always set, sometimes requiring the user to manually parse the Authorization header, and well... that just sucks.

PHP Manual: http://php.net/manual/en/features.http-auth.php
Same issue fixed in Symfony HttpFoundation: https://github.com/symfony/symfony/pull/3551/files

Query params available from $app->param('key') but not $app->query('key')

Is this the expected behavior and the documentation is just outdated?
... or am I missing something obvious?

This is all I'm doing:

$app = new Bullet\App();

$app->path('test', function($request) use($app){
    return $request->query('foo'); // outputs "OK"
    // return $request->param('foo'); /* outputs value of $_GET['foo'], e.g. "bar" */
    // die(var_dump($request));
});

echo $app->run(new Bullet\Request());

And the URL:

/test?foo=bar

Here's a var_dump() of the request object:

object(Bullet\Request)#12 (10) {
  ["_method":protected]=>
  string(3) "GET"
  ["_url":protected]=>
  string(5) "/test"
  ["_format":protected]=>
  string(4) "html"
  ["_headers":protected]=>
  array(30) {
    ["USER"]=>
    string(5) "nginx"
    ["HOME"]=>
    string(14) "/var/lib/nginx"
    ["FCGI_ROLE"]=>
    string(9) "RESPONDER"
    ["QUERY_STRING"]=>
    string(0) ""
    ["REQUEST_METHOD"]=>
    string(3) "GET"
    ["CONTENT_TYPE"]=>
    string(0) ""
    ["CONTENT_LENGTH"]=>
    string(0) ""
    ["SCRIPT_NAME"]=>
    string(10) "/index.php"
    ["REQUEST_URI"]=>
    string(13) "/test?foo=bar"
    ["DOCUMENT_URI"]=>
    string(10) "/index.php"
    ["DOCUMENT_ROOT"]=>
    string(22) "/var/www/bullet/public"
    ["SERVER_PROTOCOL"]=>
    string(8) "HTTP/1.1"
    ["GATEWAY_INTERFACE"]=>
    string(7) "CGI/1.1"
    ["SERVER_SOFTWARE"]=>
    string(12) "nginx/1.0.15"
    ["REMOTE_ADDR"]=>
    string(12) "192.168.56.1"
    ["REMOTE_PORT"]=>
    string(5) "52223"
    ["SERVER_ADDR"]=>
    string(13) "192.168.56.25"
    ["SERVER_PORT"]=>
    string(2) "80"
    ["SERVER_NAME"]=>
    string(6) "bullet"
    ["SCRIPT_FILENAME"]=>
    string(32) "/var/www/bullet/public/index.php"
    ["REDIRECT_STATUS"]=>
    string(3) "200"
    ["HTTP_HOST"]=>
    string(6) "bullet"
    ["HTTP_CONNECTION"]=>
    string(10) "keep-alive"
    ["HTTP_ACCEPT"]=>
    string(74) "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8"
    ["HTTP_USER_AGENT"]=>
    string(109) "Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/38.0.2125.104 Safari/537.36"
    ["HTTP_ACCEPT_ENCODING"]=>
    string(17) "gzip,deflate,sdch"
    ["HTTP_ACCEPT_LANGUAGE"]=>
    string(14) "en-US,en;q=0.8"
    ["PHP_SELF"]=>
    string(10) "/index.php"
    ["REQUEST_TIME_FLOAT"]=>
    float(1414093153.1291)
    ["REQUEST_TIME"]=>
    int(1414093153)
  }
  ["_params":protected]=>
  array(1) {
    ["foo"]=>
    string(3) "bar"
  }
  ["_postParams":protected]=>
  array(0) {
  }
  ["_queryParams":protected]=>
  array(0) {
  }
  ["_accept":protected]=>
  array(5) {
    ["text/html"]=>
    string(9) "text/html"
    ["application/xhtml+xml"]=>
    string(21) "application/xhtml+xml"
    ["image/webp"]=>
    string(10) "image/webp"
    ["application/xml"]=>
    string(15) "application/xml"
    ["*/*"]=>
    string(3) "*/*"
  }
  ["_raw":protected]=>
  NULL
  ["_mimeTypes":protected]=>
  array(22) {
    ["txt"]=>
    string(10) "text/plain"
    ["html"]=>
    string(9) "text/html"
    ["xhtml"]=>
    string(21) "application/xhtml+xml"
    ["xml"]=>
    string(15) "application/xml"
    ["css"]=>
    string(8) "text/css"
    ["js"]=>
    string(22) "application/javascript"
    ["json"]=>
    string(16) "application/json"
    ["csv"]=>
    string(8) "text/csv"
    ["png"]=>
    string(9) "image/png"
    ["jpe"]=>
    string(10) "image/jpeg"
    ["jpeg"]=>
    string(10) "image/jpeg"
    ["jpg"]=>
    string(10) "image/jpeg"
    ["gif"]=>
    string(9) "image/gif"
    ["bmp"]=>
    string(9) "image/bmp"
    ["ico"]=>
    string(24) "image/vnd.microsoft.icon"
    ["tiff"]=>
    string(10) "image/tiff"
    ["tif"]=>
    string(10) "image/tiff"
    ["svg"]=>
    string(13) "image/svg+xml"
    ["svgz"]=>
    string(13) "image/svg+xml"
    ["zip"]=>
    string(15) "application/zip"
    ["rar"]=>
    string(28) "application/x-rar-compressed"
    ["pdf"]=>
    string(15) "application/pdf"
  }
}

Thank you.

Automatic OPTIONS request handling

Bullet should automatically handle OPTIONS requests and use the Allow header to reveal which HTTP methods are allowed on a particular URL path.

Returning file content

Hi,

Recently I had to return the content of a file for a specific path. This file is outside webspace so for now the only thing I could do was to read the file content and put it into response->content. Something like this :

$downloadFileName = $response->content()->getDownloadName();
$response->contentType($response->content()->getMimeType());
$data = file_get_contents($response->content()->getLocalPath());
$response->content($data);
$response->header("Content-Disposition", 'filename="' . $downloadFileName . '"');

My file are not that big so it's fine but I will have to use bigger files in the future so reading all the file content is not a good thing.

I'm willing to contribute this feature if you're interested in it and if you give me some pointers to avoid wasting too much time ;)

Nginx example

I just wanted to provide an example for nginx. The following seems to do the trick, although I haven't tested all constellations yet.

location / {
    try_files $uri $uri/ /index.php$is_args?u=$args;
}

The full configuration looks like this:

upstream php {
    server unix:/var/run/php5-fpm.sock;
}

server {
    listen 80;
    server_name mysite.local;

    access_log /var/log/nginx/mysite.log;
    error_log /var/log/nginx/mysite-error.log;

    root /data;
    index index.php;

    location / {
        try_files $uri $uri/ /index.php$is_args?u=$args;
    }

    #caching of static files
    location ~* \.(js|css|png|jpg|jpeg|gif|swf|xml|txt|ico|pdf|flv)$ {
        access_log off;
        log_not_found off;
        expires max;
    }

    location = /favicon.ico {
        log_not_found off;
        access_log off;
    }

    location = /robots.txt {
        allow all;
        log_not_found off;
        access_log off;
    }

    # Deny all attempts to access hidden files such as .htaccess, .htpasswd, .DS_Store (Mac)
    location ~ /\. {
        deny all;
    }

    location ~ .php$ {
        fastcgi_intercept_errors on;
        fastcgi_pass php;
        include fastcgi_params;
    }
}

Please let me know if you find this useful or found improvements.

Should Bullet v2.0 Use Symfony HttpFoundation?

Using Symfony's HttpFoundation component is something I have been considering lightly since the beginning of Bullet, but the more time goes on, the more heavily I am considering using it. Here is my analysis:

Why Change From Bullet's Own Request/Response?

The primary reason for a switch to something like Symfony's HttpFoundation is that the current custom Request/Response classes that come with Bullet are not core to how Bullet functions, nor do they contribute to the uniqueness of Bullet itself. The primary selling point of Bullet is the routing style and structure, which will remain unchanged in Bullet\App.

Pros

  • A library like Symfony HttpFoundation is more widely used, thoroughly tested, etc.
  • More features than Bullet's current Request/Response classes (specifically session and cookie handling that are currently absent in Bullet)
  • Bullet will be more attractive to people who have used Symfony HttpFoundation before (Symfony, Laravel, Silex, many others)
  • Less code to maintain for Bullet

Cons

  • Heavier/slower than Bullet's custom Request/Response classes
  • Have to change the current template implementation as well (currently the Template object extends Bullet\Response so a template can be a literal response)
  • Will force lots of code changes to existing apps written with Bullet to upgrade to v2.0

Your Thoughts

Now I'd like your thoughts - will this affect you? How? For better or worse? What would you think about this change?

Request Params Polluting Globals for Nested Sub-Requests

When you construct an instance of \Bullet\Request with params we inject those values into the global space. I'm assuming it is because of these lines in the constructor:

    $this->_postParams =& $_POST;
    $this->_getParams =& $_GET;

Consider the following code:

$app = new \Bullet\App();

$app->path("/", function ($request) use ($app) {
    $big    = $app->run(new \Bullet\Request("GET", "name", array("upper" => 1)));
    $little = $app->run(new \Bullet\Request("GET", "name"));
    return $big . $little;
});

$app->path("/name", function ($request) use ($app) {
    return $request->upper ? "JACOB" : "jacob";
});

echo $app->run("GET", "/");

I would expect the output to be JACOBjacob but instead it's JACOBJACOB because the params from the first request are still around for the second.

I don't know the fix, but if you agree that this is a problem then you can use this test to identify the problem.

public function testRequestsDoNotShareParams()
{
    $first_request  = new Bullet\Request('GET', '/', array('foo' => 'bar'));
    $second_request = new Bullet\Request('GET', '/', array());
    $this->assertNull($second_request->foo);
}

Adding a format handler and allowing default handling

I may just be missing it...

If you add a format handler for a method, do you have to add format handlers for all types that you want to support?

For example, all of my endpoints respond with application/json by default. On one of them, I'd like to be able to send an "Accept: application/schema+json" and get back the JSON schema for that content type. But then making a request without an Accept header returns 406 Not Acceptable.

Is it possible to add a format handler without messing up default behavior?

Possible problem with Routing when using a int param with a path inside of it

problem:
routing not working as expected

expected behavior:
the route to work

observed behavior:
route showing 404 when route is present in code

test case:
take the below test case and try to navigate to
{url}/admin/client/1/toggleVisiblity/item

test router:

$app->path('admin', function($request) use($app) {
//{...}
    $app->path('client', function($request) use($app) {
        $app->param('int', function($request,$id) use($app){
            $app->path('toggleVisiblity', function($request) use($app,$id) {
                $app->path('item', function($request) use($app,$id) {
                    $app->get(function($request)use($app){
                        //{...}
                    });     
                });
            });
        });
    });
});

Short-circuit path handling?

I'm attempting to add authentication to my bullet-based web service. I'd like to be able to handle ALL requests and load a template for users that fail authentication, but this is proving more difficult than I expected.

I authenticate all requests (even those that might otherwise result in a 404). Also, I authenticate via headers - the request must include valid ('X-Session-Username' AND 'X-Session-Password') OR ('X-Session-Token'). Lastly, I don't do any redirection - all URIs should return a 403 without proper authentication.

The only way I can see to handle this "globally" is to use the 'before' event, but that won't stop the actual route from running.

Am I missing something?

Making bullet work with Whoops exception library

This is more of a question than an "issue" ;)

I was interested in getting BulletPHP working with the Whoops https://github.com/filp/whoops exception library.

The code I tried so far was:

$whoops = new \Whoops\Run;
$whoops->pushHandler(new \Whoops\Handler\PrettyPageHandler);
$whoops->register();

$app = new Bullet\App();
$app->path('/', function($request) {
    return $blah;
});

This appears to work, in that it gives me a stack dump, seemingly Whoops-generated as so:

exception 'Whoops\Exception\ErrorException' with message 'Undefined variable: blah' in /var/sites/mysite/public/index.php:12
Stack trace:
    #0 /var/sites/mysite/public/index.php(12): Whoops\Run->handleError(8, 'Undefined varia...', '/var/sites/mysi...', 12, Array)
    #1 [internal function]: Closure->{closure}(Object(Bullet\Request))
    #2 /var/sites/mysite/vendor/vlucas/bulletphp/src/Bullet/App.php(262): call_user_func(Object(Closure), Object(Bullet\Request))
    #3 /var/sites/mysite/vendor/vlucas/bulletphp/src/Bullet/App.php(193): Bullet\App->_runPath('GET', '')
    #4 /var/sites/mysite/public/index.php(16): Bullet\App->run(Object(Bullet\Request))
    #5 {main}

but it's just that dump... no pretty page, no HTML at all. The dump pasted above is from the view source in the browser so it's only dumping the text itself.

I know this isn't part of BulletPHP but I was wondering if you had any thoughts on why this might not be working? I will probably ask the Whoops folks too.

Add 'resource' route as alias to 'path'

Mainly just a naming thing, but resource sounds a bit more inviting if people are expecting REST terminology (and this is a framework that fully embraces REST, so they should be).

Support subdomains as path closures

Bullet needs the ability to conditionally branch based on subdomain in addition to path. Ideal syntax:

$app->subdomain('api', function($request) {
    // API routes and other things included here...
});

Redirect inside a 'before' event handler?

I have read the documentation on redirects and events but I can't seem to find the recommended way to accomplish what I am trying. Basically I want to run a check before every request is handled to see if the user is logged-in. If they are not I would like to redirect to the login page. The best I could come up with was this, but is this recommended practice?

$app->on('before', function($request, $response) use($app) {
    if ( !Session::loggedIn() ) {
        $app->response()->redirect('/login', 302)->send();
        exit;
    }
});

An improvement/bug on the run() method

Hello,
I'm building a web app using bulletphp, and to answer some doubts I start digging into the code...
And it was a nice surprise, I really like your framework :)
Anyway, digging into the code I think I found a bug... let me explain better.
Inside the "App.php" file, at the very end of the run() method there are these lines:

// Trigger events based on HTTP request format and HTTP response code
$this->filter(array_filter(array($this->_request->format(), $response->status(), 'after')));

But the array_filter function expect 2 args (not 3) and the filter method expect the first argument to be a scalar (not an array).
Additionally, there is no need to pass the request-format and the response-status to the ´filter´ method because it will use the (already setup) $this->_request and $this->_response objects.
So, I think the code should be:

// Trigger events based on HTTP request format and HTTP response code
$this->filter('after', array($this->_request->format(), $response->status()) );
//or even 
$this->filter('after');

Please, let me know your thoughts.

Marcelo.

Raw Request Not Always Available

Depending on how your app is ran, the raw body of a request might not be available inside your routes. The problem is that \Bullet\App instantiates a new \Bullet\Request which consumes the contents of php://input which, as is mentioned in the comments, can only be read once.

Here's the setup. Given that I have this app:

<?php

require __DIR__ . "/vendor/autoload.php";

$app = new \Bullet\App();

$app->path("/test", function($request) use($app) {
    $app->put(function($request) use($app) {
        return "You sent me: " . $request->raw();
    });
});

echo $app->run(new \Bullet\Request());

And my client code looks like this:

<?php

$ch = curl_init("http://127.0.0.1/test");

$body = json_encode(array("foo" => "bar"));

curl_setopt($ch, CURLOPT_POSTFIELDS,    $body);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "PUT");

curl_exec($ch);

print PHP_EOL;

I should get the following output

You sent me: {"foo":"bar"}

But instead I get

You sent me: 

Digging in to the issue it seems that the problem is that when I call $app->path to define my route, the app internally calls $this->request()->isHHVM() which defines a new \Bullet\Request. That instance of request consumes php://input so the instance I define in echo $app->run(new \Bullet\Request()); doesn't see the raw request.

If I instantiate a \Bullet\Request before I define my app then it gets the raw body. Or if I change the last line of my server code to echo $app->run($app->request()); I can use the instance that has the raw body.

Honestly, I'm not sure how to test this. Is it possible to inject up in to php://input or mock it somehow?

HHVM (HipHop) Support

Right now full and proper support for this is blocked until Facebook adds support for Closure::bind and Closure::bindTo to HHVM: facebook/hhvm#1203

For now, Bullet will run on HHVM, but none of the closures will support the use of $this (what is achieved via Closure::bindTo) when running on HHVM.

Configuration Setting and Getting

Bullet needs a way to manage configuration beyond the provided way with Pimple. Some pseudocode:

$app->config('some.nested.key', 'default_value_if_not_set');

This would make retrieving config settings easy and provide the ability to set a default value in the case the specified config key was not set.

Bullet can't parse bad JSON into request params

Test JSON:

"{\"title\":\"Updated_New_Post_Title\",\"body\":\"<p>A_much_better_post_body</p>\"}\n"

Test to reproduce the issue:

function testRawJsonBodyIsDecodedWithBadJSON()
{
    $r = new Bullet\Request('PUT', '/test', array(), array('Content-Type' => 'application/json'), '{\"title\":\"Updated New Post Title\",\"body\":\"<p>A much better post body</p>\"}\n');
    $app = new Bullet\App();
    $app->path('test', function($request) use($app) {
        $app->put(function($request) {
            return 'title: ' . $request->get('title');
        });
    });
    $res = $app->run($r);
    $this->assertEquals('title: Updated New Post Title', $res->content());
}

Add Plugin/Service Provider Class

The more apps I create with Bullet, the more I wish certain ways I configure and use Bullet were more modular and portable across other Bullet apps. I think it's time to look at creating a more serious/structured way to register a service with Bullet. I think a class file and interface should be used so that: (1) It is easy to register and autoload, and (2) so that it has predicable methods that we can call to add the desired functionality.

Example use cases:

  • Doctrine ORM
  • Twig or other template engine
  • Better exception/error handling (like with #38)
  • many, many others...

Example Class file

use Bullet\Plugin, Bullet\PluginInterface;

class TwigPlugin extends Plugin implements PluginInterface
{
    public function run()
    {
        // do stuff with $this->app (instance of Bullet that is injected in constructor)
    }
}

Thoughts?

Support for PATCH method

Now that the PATCH HTTP spec is nearly official, Bullet should support it with a named handler like it does GET, POST, PUT, etc.

How does the caching system work?

The bulletphp.com homepage states that BulletPHP comes with caching ("Bullet is resource and URI-oriented and comes pre-loaded with powerful HTTP features like content-negotiation and caching.").

How does the caching system work?

Params matching another path renders wrong path

In my application I have /about and /rels. Rels accepts an argument and one of the supported values is "about". The problem is that pathing to /rels/about renders /about instead.

Example code:

<?php
$app->path('/about/', function($request) use ($app) {
    return "Some text about my app";
});

$app->path('/rels/', function($request) use($app) {                                                                                                                    
    $app->param('slug', function($request, $rel) use($app) {
        // return the documentation page for /rels/{rel} such as /rels/about
        return $app->template($rel)
                   ->set(array('rel' => $rel))
                   ->layout('rels')
                   ->status(200);
    });
});

Expected Output: The template for rel 'about'
Actual output: The text "Some text about my app"

Allow variable passing from template to layout

There should be a way to pass locally set variables up to the layout so things like the page title, body class, stylesheets, javascripts, etc. can be changed or set in a per-template basis.

Just a dumb question

Yes, I read the docs and it's not there...
Lets say I have a blog and I want to fetch all comments from a given article.
The URL is something like GET /blog/article/{id}/comments
I wonder if this is possible to define an $app->path inside an $app->param. Something like this:

$app->path('blog/articles', function($request) use($app) {

    //GET blog/articles/123
    $app->param('int', function($request, $articleId) use($app) {

        //GET blog/articles/123/comments
        $app->path('comments', function($request) use($app, $articleId) {
            //return all comments from article 123
        }
    }
}

Getting raw JSON PUT in Bullet\Request

Hi,

I use bulletphp with backbone.js and I request PUT /myapi with raw JSON parameter (like {"id":"1"}).

example at http://bulletphp.com

        $app->put(function($request) use($id) {
            // Update resource
            $post->data($request->post());
            $post->save();
            return 'update_' . $id;
        });

but $request->post() returns an empty array.

It seems parameter is set as a key in _params property like

  ["_params":protected]=>
  array(2) {
    ["_method"]=>
    string(3) "PUT"
    ["{"id":"1"}"]=>
    string(0) ""
  }

Could you tell me if I'm doing wrong or it's not supported?

Add headers before response content is set

Hi again,

I'm adding paging to my Rest API based on bullet and I'd like to add some headers (X-Total-Count and Link).

Judging by the first lines of function header in response.php adding headers is impossible if the response has no content. Is there a reason why ?

For now I'm storing the content of my two headers in my $app ($app["X-Total-Count"] and $app["Link"]) and I add the headers in my response handler. It does not feel right even if it works without too much work. See below

Do you see a better solution ?

Thanks in advance.

Use of Bullet with a JSON only api service

Hi,

First thanks for this framework, I'm using it for a few weeks now and I'm starting to like it a lot ;).

Now my question I want to use Bullet to build an simple Json API (in the same spirit of https://api.github.com). So, even if the request comes from a browser, JSON must always be returned. For now I've added the two format for each paths but I have to admit that hurts a little ;).

Is there a simple way to do that with Bullet ?

I've also had some weird errors trying to use Bullet without any template at all, I'll figure a proper testcase and open another issue.

Thanks again and I'm waiting eagerly for another update.

$app->subdomain catch all

is there some code i can use to make it so my users get a subdomain on my server? like subdomain param or something?

HMVC nested routing

There seems to be a problem when calling App->run() from inside another App->run() instance. There doesn't seem to be separation of paths internally causing an recursive death.

$app = new Bullet\App();

$app->path('a', function($request) use($app) {

    $app->path('b', function($request) use($app) {
        return 'a/b';
    });
});

$app->path('c', function($request) use($app) {

    $app->path('a', function($request) use($app) {

        $app->path('b', function($request) use($app) {

            $a = $app->run('GET', 'a/b');

            return $a->content() . " + c/a/b\n";
        });
    });
});

echo $app->run('GET', 'c/a/b');

[Template] How to use Twig

It would be nice if other templating systems could be supported more easily in bulletphp. I have got Twig support by overriding the template method in the App class and calling a custom class that extends Template called TwigTemplate.

This lets you take advantage of template inheritance, horizontal re-use, macros, template caching, etc.

They are not pull request material, but I will put them here in case someone else is looking do the same.

Treffynnon/Bullet/App.php

<?php 

namespace Treffynnon\Bullet;

class App extends \Bullet\App {

    /**
     * Return instance of Treffynnon\Bullet\View\TwigTemplate
     *
     * @param string $name Template name
     * @param array $params Array of params to set
     */
    public function template($name, array $params = array())
    {
        $tpl = new View\TwigTemplate($name);
        $tpl->set($params);
        return $tpl;
    }
}

Treffynnon/Bullet/View/TwigTemplate.php

<?php
namespace Treffynnon\Bullet\View;

class TwigTemplate extends \Bullet\View\Template {

    protected $_twig = null;
    protected $_cache = false;

    public function init() {
        $loader = new \Twig_Loader_Filesystem($this->path());
        $this->_twig = new \Twig_Environment($loader, array(
            'cache' => $this->cache(),
        ));
    }

    /**
     * Get/Set path to look in for templates
     */
    public function cache($path = null) {
        if(null === $path) {
            return ($this->_cache) ? $this->_cache : self::$_config['cache'];
        } else {
            $this->_cache = $path;
            return $this; // Fluent interface
        }
    }

    /**
     * Read template file into content string and return it
     *
     * @return string
     */
    public function content($content = null) {
        return $this->_twig->render($this->fileName(), $this->vars());
    }

}

Setup in bootstrap.php or index.php

$loader = require VENDOR_DIR . 'autoload.php'; // re-use Composers class autoloader!
$loader->add('Treffynnon\\Bullet', LIB_DIR); // override Bullet base classes for Twig templating support

$app = new Treffynnon\Bullet\App(array(
    'template.cfg' => array(
        'path' => APP_DIR . 'templates',
        'cache' => APP_DIR . 'templates/cache',
        'default_extension' => 'twig',
    )
));

$request = new Bullet\Request();

require_once ROUTE_DIR . 'index.php';

echo $app->run($request);

Using in a route remains the same

$app->path('/', function($request) use ($app) {
    $errors = 'some error';
    $title = 'My page title';
    return $app->template('index', array(
        'errors' => $errors,
        'title' => $title,
    ));
}

Templates

In you template directory create a Twig file (in this case) called index.html.twig much like with bullet's default templates.

Directory structure I have

  • app
    • routes
      • index.php
    • templates
      • cache
      • index.html.twig
    • bootstrap.php
  • lib
    • Treffynnon
      • Bullet
        • View
          • TwigTemplate.php
        • App.php
  • pub
    • css
    • images
    • js
    • index.php
  • vendor
    • pimple
    • vlucas
    • twig

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.