{"id":13792141,"url":"https://github.com/vlucas/bulletphp","last_synced_at":"2025-05-16T09:00:24.485Z","repository":{"id":3178464,"uuid":"4210155","full_name":"vlucas/bulletphp","owner":"vlucas","description":"A resource-oriented micro PHP framework","archived":false,"fork":false,"pushed_at":"2021-07-23T14:01:07.000Z","size":423,"stargazers_count":418,"open_issues_count":6,"forks_count":52,"subscribers_count":29,"default_branch":"master","last_synced_at":"2025-05-12T13:48:54.939Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"http://bulletphp.com","language":"PHP","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"bsd-3-clause","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/vlucas.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE.txt","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2012-05-03T03:25:35.000Z","updated_at":"2025-05-04T14:45:01.000Z","dependencies_parsed_at":"2022-07-18T08:13:50.040Z","dependency_job_id":null,"html_url":"https://github.com/vlucas/bulletphp","commit_stats":null,"previous_names":[],"tags_count":44,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/vlucas%2Fbulletphp","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/vlucas%2Fbulletphp/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/vlucas%2Fbulletphp/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/vlucas%2Fbulletphp/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/vlucas","download_url":"https://codeload.github.com/vlucas/bulletphp/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":254501548,"owners_count":22081526,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-08-03T22:01:08.692Z","updated_at":"2025-05-16T09:00:24.399Z","avatar_url":"https://github.com/vlucas.png","language":"PHP","funding_links":[],"categories":["基础框架"],"sub_categories":["构建/部署"],"readme":"Bullet\n======\n\nBullet is a resource-oriented micro PHP framework built around HTTP URIs.\nBullet takes a unique functional-style approach to URL routing by parsing\neach path part independently and one at a time using nested closures. The\npath part callbacks are nested to produce different responses and to follow\nand execute deeper paths as paths and parameters are matched.\n\n[![Build Status](https://travis-ci.org/vlucas/bulletphp.svg?branch=master)](https://travis-ci.org/vlucas/bulletphp)\n\nPROJECT MAINTENANCE RESUMES\n------------\nBullet becomes an active project again. Currently there's a changing of\nthe guard. Feel free to further use and contribute to the framework.\n\nRequirements\n------------\n\n * PHP 5.6+ (PHP 7.1 recommended)\n * [Composer](http://getcomposer.org) for all package management and\n   autoloading (may require command-line access)\n\nRules\n-----\n\n * Apps are **built around HTTP URIs** and defined paths, not forced MVC\n   (but MVC-style separation of concerns is still highly recommenended and\n   encouraged)\n * Bullet handles **one segment of the path at a time**, and executes the\n   callback for that path segment before proceesing to the next segment \n   (path callbacks are executed from left to right, until the entire path\n   is consumed).\n * If the entire path cannot be consumed, a 404 error will be returned\n   (note that some callbacks may have been executed before Bullet can\n   know this due to the nature of callbacks and closures). Example: path\n   `/events/45/edit` may return a 404 because there is no `edit` path\n   callback, but paths `events` and `45` would have already been executed\n   before Bullet can know to return a 404. This is why all your primary\n   logic should be contained in `get`, `post`, or other method callbacks\n   or in the model layer (and not in the bare `path` handlers).\n * If the path can be fully consumed, and HTTP method handlers are present\n   in the path but none are matched, a 405 \"Method Not Allowed\" response\n   will be returned.\n * If the path can be fully consumed, and format handlers are present in\n   the path but none are matched, a 406 \"Not Acceptable\" response will\n   be returned.\n\nAdvantages\n----------\n\n * **Super flexible routing**. Because of the way the routing callbacks are\n   nested, Bullet's routing system is one of the most flexible of any other PHP\n   framework or library. You can build any URL you want and respond to any HTTP\n   method on that URL. Routes are not restricted to specific patterns or URL\n   formats, and do not require a controller with specific method names to\n   respond to specific HTTP methods. You can nest routes as many levels deep as\n   you want to expose nested resources like `posts/42/comments/943/edit` with a\n   level of ease not found in most other routing libraries or frameworks.\n\n * **Reduced code duplication (DRY)**. Bullet takes full advantage of its nested\n   closure routing system to reduce a lot of typical code duplication required\n   in most other frameworks. In a typical MVC framework controller, some code\n   has to be duplicated across methods that perform CRUD operations to run ACL\n   checks and load required resources like a Post object to view, edit or delete.\n   With Bullet's nested closure style, this code can be written just once in a\n   path or param callback, and then you can `use` the loaded object in subsequent\n   path, param, or HTTP method handlers. This eliminates the need for \"before\"\n   hooks and filters, because you can just run the checks and load objects you\n   need before you define other nested paths and `use` them when required.\n\nInstalling with Composer\n-----\nUse the [basic usage guide](http://getcomposer.org/doc/01-basic-usage.md),\nor follow the steps below:\n\nSetup your `composer.json` file at the root of your project\n\n```json\n{\n    \"require\": {\n        \"vlucas/bulletphp\": \"~1.7\"\n    }\n}\n```\n\nInstall Composer\n\n    curl -s http://getcomposer.org/installer | php\n\n\nInstall Dependencies (will download Bullet)\n\n    php composer.phar install\n\n\nCreate `index.php` (use the minimal example below to get started)\n\n```php\n\u003c?php\nrequire __DIR__ . '/vendor/autoload.php';\n\n/* Simply build the application around your URLs */\n$app = new Bullet\\App();\n\n$app-\u003epath('/', function($request) {\n    return \"Hello World!\";\n});\n$app-\u003epath('/foo', function($request) {\n    return \"Bar!\";\n}); \n\n/* Run the app! (takes $method, $url or Bullet\\Request object)\n * run() always return a \\Bullet\\Response object (or throw an exception) */\n\n$app-\u003erun(new Bullet\\Request())-\u003esend();\n```\n\nThis application can be placed into your server's document root. (Make sure it \nis correctly configured to serve php applications.) If `index.php` is in the \ndocument root on your local host, the application may be called like this:\n\n    http://localhost/index.php?u=/\n\nand \n\n    http://localhost/index.php?u=/foo\n\nIf you're using Apache, use an `.htaccess` file to beautify the URLs. You need \nmod_rewrite to be installed and enabled.\n\n    \u003cIfModule mod_rewrite.c\u003e\n      RewriteEngine On\n\n      # Reroute any incoming requestst that is not an existing directory or file\n      RewriteCond %{REQUEST_FILENAME} !-d\n      RewriteCond %{REQUEST_FILENAME} !-f\n      RewriteRule ^(.*)$ index.php?u=$1 [L,QSA,B]\n    \u003c/IfModule\u003e\n\nWith this file in place Apache will pass the request URI to `index.php` using \nthe $_GET['u'] parameter. This works in subdirectories *as expected* i.e. you \ndon't have to explicitly take care of removing the path prefix e.g. if you use \nmod_userdir, or just install a Bullet application under an existing web app to \nserve an API or simple, quick dynamic pages. Now your application will answer to these pretty urls:\n\n    http://localhost/\n\nand\n\n    http://localhost/foo\n\nNGinx also has a `rewrite` command, and can be used to the same end:\n\n    server {\n        # ...\n        location / {\n            # ...\n            rewrite ^/(.*)$ /index.php?u=/$1;\n            try_files $uri $uri/ =404;\n            # ...\n        }\n        # ...\n    }\n\nIf the Bullet application is inside a subdirectory, you need to modify the\n`rewrite` line to serve it correctly:\n\n    server {\n        # ...\n        location / {\n            rewrite ^/bulletapp/(.*)$ /bulletapp/index.php?u=/$1;\n            try_files $uri $uri/ =404;\n        }\n        # ...\n    }\n\nNote that if you need to serve images, stylesheets, or javascript too, you\nneed to add a `location` for the static root directory *without* the `reqrite`\nto avoid passing those URLs to index.php.\n\nView it in your browser!\n\nSyntax\n------\n\nBullet is not your typical PHP micro framework. Instead of defining a full\npath pattern or a typical URL route with a callback and parameters mapped\nto a REST method (GET, POST, etc.), Bullet parses only ONE URL segment\nat a time, and only has two methods for working with paths: `path` and\n`param`. As you may have guessed, `path` is for static path names like\n\"blog\" or \"events\" that won't change, and `param` is for variable path\nsegments that need to be captured and used, like \"42\" or \"my-post-title\".\nYou can then respond to paths using nested HTTP method callbacks that\ncontain all the logic for the action you want to perform.\n\nThis type of unique callback nesting eliminates repetitive code for\nloading records, checking authentication, and performing other setup\nwork found in typical MVC frameworks or other microframeworks where each\ncallback or action is in a separate scope or controller method.\n\n\n```php\n$app = new Bullet\\App(array(\n    'template.cfg' =\u003e array('path' =\u003e __DIR__ . '/templates')\n));\n\n// 'blog' subdirectory\n$app-\u003epath('blog', function($request) use($app) {\n\n    $blog = somehowGetBlogMapper(); // Your ORM or other methods here\n\n    // 'posts' subdirectory in 'blog' ('blog/posts')\n    $app-\u003epath('posts', function() use($app, $blog) {\n\n        // Load posts once for handling by GET/POST/DELETE below\n        $posts = $blog-\u003eallPosts(); // Your ORM or other methods here\n\n        // Handle GET on this path\n        $app-\u003eget(function() use($posts) {\n            // Display all $posts\n            return $app-\u003etemplate('posts/index', compact('posts'));\n        });\n\n        // Handle POST on this path\n        $app-\u003epost(function() use($posts) {\n            // Create new post\n            $post = new Post($request-\u003epost());\n            $mapper-\u003esave($post);\n            return $this-\u003eresponse($post-\u003etoJSON(), 201);\n        });\n\n        // Handle DELETE on this path\n        $app-\u003edelete(function() use($posts) {\n            // Delete entire posts collection\n            $posts-\u003edeleteAll();\n            return 200;\n        });\n\n    });\n});\n\n// Run the app and echo the response\necho $app-\u003erun(\"GET\", \"blog/posts\");\n```\n\n\n### Capturing Path Parameters\n\nPerhaps the most compelling use of URL routing is to capture path\nsegments and use them as parameters to fetch items from a database, like\n`/posts/42` and `/posts/42/edit`. Bullet has a special `param` handler\nfor this that takes two arguments: a `test` callback that validates the\nparameter type for use, and and a `Closure` callback. If the `test`\ncallback returns boolean `false`, the closure is never executed, and the\nnext path segment or param is tested. If it returns boolean `true`, the\ncaptured parameter is passed to the Closure as the second argument.\n\nJust like regular paths, HTTP method handlers can be nested inside param\ncallbacks, as well as other paths, more parameters, etc.\n\n\n```php\n$app = new Bullet\\App(array(\n    'template.cfg' =\u003e array('path' =\u003e __DIR__ . '/templates')\n));\n$app-\u003epath('posts', function($request) use($app) {\n    // Integer path segment, like 'posts/42'\n    $app-\u003eparam('int', function($request, $id) use($app) {\n        $app-\u003eget(function($request) use($id) {\n            // View post\n            return 'view_' . $id;\n        });\n        $app-\u003eput(function($request) use($id) {\n            // Update resource\n            $post-\u003edata($request-\u003epost());\n            $post-\u003esave();\n            return 'update_' . $id;\n        });\n        $app-\u003edelete(function($request) use($id) {\n            // Delete resource\n            $post-\u003edelete();\n            return 'delete_' . $id;\n        });\n    });\n    // All printable characters except space\n    $app-\u003eparam('ctype_graph', function($request, $slug) use($app) {\n        return $slug; // 'my-post-title'\n    });\n});\n\n// Results of above code\necho $app-\u003erun('GET',   '/posts/42'); // 'view_42'\necho $app-\u003erun('PUT',   '/posts/42'); // 'update_42'\necho $app-\u003erun('DELETE', '/posts/42'); // 'delete_42'\n\necho $app-\u003erun('DELETE', '/posts/my-post-title'); // 'my-post-title'\n```\n\n\nReturning JSON (Useful for PHP JSON APIs)\n-----------------------------------------\n\nBullet has built-in support for returning JSON responses. If you return\nan array from a route handler (callback), Bullet will assume the\nresponse is JSON and automatically `json_encode` the array and return the\nHTTP response with the appropriate `Content-Type: application/json` header.\n\n\n```php\n$app-\u003epath('/', function($request) use($app) {\n    $app-\u003eget(function($request) use($app) {\n        // Links to available resources for the API\n        $data = array(\n            '_links' =\u003e array(\n                'restaurants' =\u003e array(\n                    'title' =\u003e 'Restaurants',\n                    'href' =\u003e $app-\u003eurl('restaurants')\n                ),\n                'events' =\u003e array(\n                    'title' =\u003e 'Events',\n                    'href' =\u003e $app-\u003eurl('events')\n                )\n            )\n        );\n\n        // Format responders\n        $app-\u003eformat('json', function($request), use($app, $data) {\n            return $data; // Auto json_encode on arrays for JSON requests\n        });\n        $app-\u003eformat('xml', function($request), use($app, $data) {\n            return custom_function_convert_array_to_xml($data);\n        });\n        $app-\u003eformat('html', function($request), use($app, $data) {\n            return $app-\u003etemplate('index', array('links' =\u003e $data));\n        });\n    });\n});\n```\n\n\n### HTTP Response Bullet Sends:\n\n    Content-Type:application/json\n\n```json\n{\"_links\":{\"restaurants\":{\"title\":\"Restaurants\",\"href\":\"http:\\/\\/yourdomain.local\\/restaurants\"},\"events\":{\"title\":\"Events\",\"href\":\"http:\\/\\/yourdomain.local\\/events\"}}}\n```\n\n\nBullet Response Types\n--------------\n\nThere are many possible values you can return from a route handler in\nBullet to produce a valid HTTP response. Most types can be either\nreturned directly, or wrapped in the `$app-\u003eresponse()` helper for\nadditional customization.\n\n### Strings\n\n\n```php\n$app = new Bullet\\App();\n$app-\u003epath('/', function($request) use($app) {\n    return \"Hello World\";\n});\n$app-\u003epath('/', function($request) use($app) {\n    return $app-\u003eresponse(\"Hello Error!\", 500);\n});\n```\n\nStrings result in a 200 OK response with a body containing the returned\nstring. If you want to return a quick string response with a different\nHTTP status code, use the `$app-\u003eresponse()` helper.\n\n### Booleans\n\n\n```php\n$app = new Bullet\\App();\n$app-\u003epath('/', function($request) use($app) {\n    return true;\n});\n$app-\u003epath('notfound', function($request) use($app) {\n    return false;\n});\n```\n\nBoolean `false` results in a 404 \"Not Found\" HTTP response, and boolean\n`true` results in a 200 \"OK\" HTTP response.\n\n### Integers\n\n\n```php\n$app = new Bullet\\App();\n$app-\u003epath('teapot', function($request) use($app) {\n    return 418;\n});\n```\n\nIntegers are mapped to their corresponding HTTP status code. In this\nexample, a 418 \"I'm a Teapot\" HTTP response would be sent.\n\n### Arrays\n\n```php\n$app = new Bullet\\App();\n$app-\u003epath('foo', function($request) use($app) {\n    return array('foo' =\u003e 'bar');\n});\n$app-\u003epath('bar', function($request) use($app) {\n    return $app-\u003eresponse(array('bar' =\u003e 'baz'), 201);\n});\n```\n\nArrays are automatically passed through `json_encode` and the appropriate\n`Content-Type: application/json` HTTP response header is sent.\n\n### Templates\n\n```php\n// Configure template path with constructor\n$app = new Bullet\\App(array(\n    'template.cfg' =\u003e array('path' =\u003e __DIR__ . '/templates')\n));\n\n// Routes\n$app-\u003epath('foo', function($request) use($app) {\n    return $app-\u003etemplate('foo');\n});\n$app-\u003epath('bar', function($request) use($app) {\n    return $app-\u003etemplate('bar', array('bar' =\u003e 'baz'), 201);\n});\n```\n\nThe `$app-\u003etemplate()` helper returns an instance of\n`Bullet\\View\\Template` that is lazy-rendered on `__toString` when the\nHTTP response is sent. The first argument is a template name, and the\nsecond (optional) argument is an array of parameters to pass to the\ntemplate for use.\n\n### Serving large responses\n\nBullet works by wrapping every possible reponse with a Response object. This \nwould normally mean that the entire request must be known (~be in memory) when \nyou construct a new Response (either explicitly, or trusting Bullet to \nconstruct one for you).\n\nThis would be bad news for those serving large files or contents of big \ndatabase tables or collections, since everything would have to be loaded into \nmemory.\n\nHere comes `\\Bullet\\Response\\Chunked` for the rescue.\n\nThis response type requires some kind of iterable type. It works with regular\narrays or array-like objects, but most importatnly, it works with generator\nfunctions too. Here's an example (database functions are purely fictional):\n\n```php\n$app-\u003epath('foo', function($request) use($app) {\n    $g = function () {\n        $cursor = new ExampleDatabaseQuery(\"select * from giant_table\");\n        foreach ($cursor as $row) {\n            yield example_format_db_row($row);\n        }\n        $cursor-\u003eclose();\n    };\n    return new \\Bullet\\Response\\Chunked($g());\n});\n```\n\nThe `$g` variable will contain a Closure that uses `yield` to fetch, process,\nand return data from a big dataset, using only a fraction of the memory needed\nto store all the rows at once.\n\nThis results in a HTTP chunked response. See \nhttps://tools.ietf.org/html/rfc7230#section-4.1 for the technical details.\n\n### HTTP Server Sent Events\n\nServer sent events are one way to open up a persistent channel to a web server, \nand receive notifications. This can be used to implement a simple webchat for \nexample.\n\nThis standard is part of HTML5, see \nhttps://html.spec.whatwg.org/multipage/server-sent-events.html#server-sent-events \nfor details.\n\nThe example below show a simple application using the fictional send_message \nand receive_message functions for communications. These can be implemented over \nvarious message queues, or simple named pipes.\n\n```php\n$app-\u003epath('sendmsg', function($request) {\n    $this-\u003epost(function($request) {\n        $data = $request-\u003epostParam('message');\n        send_message($data);\n        return 201;\n    });\n});\n\n$app-\u003epath('readmsgs', function($request) {\n    $this-\u003eget(function($request) {\n        $g = function () {\n            while (true) {\n                $data = receive_message();\n                yield [\n                    'event' =\u003e 'message',\n                    'data'  =\u003e $data\n                ];\n            }\n        };\n        \\Bullet\\Response\\Sse::cleanupOb(); // Remove any output buffering\n        return new \\Bullet\\Response\\Sse($g());\n    });\n});\n```\n\nThe SSE response uses chunked encoding, contrary to the recommendation in the \nstandard. We can do this, since we tailoe out chunks to be exactly \nmessage-sized.\n\nThis will not confuse upstream servers when they see no chunked encoding, AND\nno Content-Length header field, and might try to \"fix\" this by either reading\nthe entire response, or doing the chunking on their own.\n\nPHP's output buffering can also interfere with messaging, hence the call to\n\\Bullet\\Response\\Sse::cleanupOb(). This method flushes and ends every level of\noutput buffering that might present before sending the response.\n\nThe SSE response automatically sends the `X-Accel-Buffering: no` header to \nprevent the server from buffering the messages.\n\n\nNested Requests (HMVC style code re-use)\n----------------------------------------\n\nSince you explicitly `return` values from Bullet routes instead of\nsending output directly, nested/sub requests are straightforward and easy.\nAll route handlers will return `Bullet\\Response` instances (even if they\nreturn a raw string or other data type, they are wrapped in a response\nobject by the `run` method), and they can be composed to form a single\nHTTP response.\n\n```php\n$app = new Bullet\\App();\n$app-\u003epath('foo', function($request) use($app) {\n    return \"foo\";\n});\n$app-\u003epath('bar', function($request) use($app) {\n    $foo = $app-\u003erun('GET', 'foo'); // $foo is now a `Bullet\\Response` instance\n    return $foo-\u003econtent() . \"bar\";\n});\necho $app-\u003erun('GET', 'bar'); // echos 'foobar' with a 200 OK status\n```\n\n\n\nRunning Tests\n-------------\n\nTo run the Bullet test suite, simply run `vendor/bin/phpunit` in the\nroot of the directory where the bullet files are in. Please make sure\nto add tests and run the test suite before submitting pull requests for\nany contributions.\n\nCredits\n-------\n\nBullet - and specifically path-based callbacks that fully embrace HTTP\nand encourage a more resource-oriented design - is something I have been\nthinking about for a long time, and was finally moved to create it after\nseeing [@joshbuddy](https://github.com/joshbuddy) give a presentation on [Renee](http://reneerb.com/)\n(Ruby) at [Confoo](http://confoo.ca) 2012 in Montréal.\n\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fvlucas%2Fbulletphp","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fvlucas%2Fbulletphp","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fvlucas%2Fbulletphp/lists"}