├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── composer.json ├── composer.lock ├── examples ├── classy-rest-api │ ├── Application.php │ ├── BookService.php │ └── index.php ├── hello-world │ └── index.php ├── nesting │ └── index.php ├── rest-api │ ├── BookService.php │ └── index.php ├── var-dump │ └── index.php └── website │ ├── .htaccess │ ├── assets │ ├── main.js │ └── style.css │ ├── index.php │ └── templates │ ├── contact.phtml │ ├── features.phtml │ ├── footer.phtml │ ├── header.phtml │ └── index.phtml ├── phpunit.xml.dist ├── src └── Moo │ ├── Extendable.php │ ├── Moo.php │ ├── Request.php │ ├── Response.php │ ├── Route.php │ ├── Router.php │ ├── StatusCode.php │ └── Template.php └── test └── Tests └── Moo ├── MooTest.php └── templates ├── partial.phtml ├── test.phtml ├── test2.phtml └── test3.phtml /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | *.iml 3 | *.log 4 | .phpunit* 5 | /test/results 6 | /vendor 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Los Koderos 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: test 2 | 3 | test: 4 | XDEBUG_MODE=coverage phpunit --display-warnings --display-notices --display-errors --coverage-html test/results 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Moo 2 | 3 | Question is not _what_ is Moo, but _why_ is Moo? 4 | 5 | There is time that every PHP developer has to create its own framework. 6 | Most of them are crap, this one is no exception. 7 | 8 | So, Moo, the framework, is a spawn of PHP evil created for making life and debugging miserable. It took a couple of hours to create, but can take days to find out what and why works or does not. Like most microframeworks, Moo is quite close to HTTP world, made of bunch of pure PHP classes with no external dependencies. Just router, request, response, boom done. 9 | 10 | Of course, it would be easier, faster and wiser to just use any other off shelf framework, but there it is, Moo! 11 | 12 | ## Why Moo? 13 | Why not. 14 | 15 | ## Why use Moo? 16 | Seriously, for 99% of time you should not use this framework. Don't do it for sake of yours and other developers wellbeing and mental health. 17 | 18 | The only exception to that I can think of, is when you consider using raw .php scripts somewhere on a server. If that is the case, Moo can actually be useful. 19 | 20 | Looking for some decent PHP framework? Go learn Symfony, Laravel or anything that actually has any community around. This one does not have any. Actually, you can try writing your own micro framework, just like Moo to learn and validate your PHP skills. 21 | 22 | ## Installation 23 | If you really have to, here you go, just use composer. 24 | ~~~ 25 | composer config minimum-stability dev 26 | composer require loskoderos/moo:dev-main 27 | ~~~ 28 | 29 | ## Hello World 30 | Simplest Moo application. 31 | ~~~php 32 | get('/', function () { 36 | return "Hello, this is Moo!"; 37 | }); 38 | 39 | $moo(); 40 | ~~~ 41 | 42 | ## Usage 43 | This is the sample Moo app with more features presented like state container, plugins and parametrized routing. 44 | ~~~php 45 | bookService = new App\BookService(); 50 | 51 | // Override the before hook to set Content-Type header. 52 | $moo->before = function () use ($moo) { 53 | $moo->response->headers->set('Content-Type', 'application/json'); 54 | }; 55 | 56 | // Override the after hook to serialize output to JSON. 57 | $moo->after = function () use ($moo) { 58 | $moo->response->body = json_encode($moo->response->body); 59 | } 60 | 61 | // Custom plugin. 62 | $moo->findBookById = function ($bookId) use ($moo) { 63 | return $moo->bookService->find($bookId); 64 | } 65 | 66 | // Define index handler. 67 | $moo->get('/', function () { 68 | echo "Hello, this is Moo bookstore!"; 69 | }); 70 | 71 | // Define `GET /books/` handler. 72 | $moo->get('/books/(\d+)', function ($bookId) use ($moo) { 73 | return $moo->findBookById($bookId); 74 | }); 75 | 76 | // Run Moo. 77 | $moo(); 78 | ~~~ 79 | 80 | ## Templates 81 | Moo supports native PHP templates, however you can extend the base class and render templates with any other engine. 82 | See the `website` example in `examples`. 83 | ~~~php 84 | use Moo\Moo; 85 | use Moo\Template; 86 | 87 | $moo = new Moo(); 88 | 89 | // Create template renderer. 90 | $moo->template = new Template(__DIR__ . '/templates', [ 91 | 'foo' => 'bar' 92 | ]); 93 | 94 | // Custom template plugin. 95 | $moo->template->date = function () { 96 | return date('Y-m-d'); 97 | }; 98 | 99 | // Index page handler. 100 | $moo->get('/', function () use ($moo) { 101 | return $moo->template->render('index.phtml', [ 102 | 'hello' => 'world' 103 | ]); 104 | }); 105 | 106 | ~~~ 107 | 108 | Sample PHP template code. 109 | ~~~html 110 |
foo =
111 |
hello =
112 |
date = date() ?>
113 | ~~~ 114 | 115 | ## Classy Moo 116 | Some may need a to write a posh code, if you need style you may try the following. 117 | ~~~php 118 | get('/', [$this, 'index']); 128 | } 129 | 130 | public function index() 131 | { 132 | echo 'Hello World'; 133 | } 134 | } 135 | 136 | $app = new Application(); 137 | $app(); 138 | ~~~ 139 | 140 | Test it and make your colleagues happy. 141 | ~~~php 142 | method = 'GET'; 153 | $request->uri = '/'; 154 | 155 | $app = new Application(); 156 | $app->flush = null; // You don't want output buffer flush in the unit tests. 157 | $app($request); 158 | 159 | $this->assertEquals(200, $app->response->code); 160 | $this->assertEquals('Hello World', $app->response->body); 161 | } 162 | } 163 | ~~~ 164 | 165 | ## Nesting 166 | You can actually use one instance of Moo as a callback in another Moo. 167 | ~~~php 168 | get('/users/(\d+)', function ($id) { 174 | // ... 175 | }); 176 | 177 | $booksMoo = new Moo(); 178 | $booksMoo->get('/books/(\d+)', function ($id) { 179 | // ... 180 | }); 181 | 182 | $moo = new Moo() 183 | $moo->route('/users/.*', $usersMoo); 184 | $moo->route('/books/.*', $booksMoo); 185 | 186 | $moo(); 187 | ~~~ 188 | Note that, when nested, only the top level `flush` is being used therefore the output from `$moo` is sent only once. 189 | 190 | ## Examples 191 | There are some examples in the `examples` directory. 192 | To run them you can use builtin PHP server. 193 | ~~~ 194 | php -S 0.0.0.0:8080 examples/hello-world/index.php 195 | ~~~ 196 | 197 | ## Documentation 198 | The goal of Moo is simplicity, flexibility and ease of use. 199 | 200 | ### Concepts 201 | Moo is written in PHP and is closure based. From design perspective it is a front controller, the `Moo\Moo` class exposes a set of standard HTTP methods to bind routing handlers as closures. Additionally, Moo acts as a state container and can be extended with plugins. 202 | 203 | All Moo components reside in the PSR-4 `Moo` namespace. The main component is the `Moo\Moo` class. There are three models: `Moo\Request`, `Moo\Response`, `Moo\Route` and the `Moo\Router` that works as dispatcher. 204 | 205 | Moo does output buffering, so you can simply output with echo or return a serializable value in the closure. 206 | 207 | ### Lifecycle 208 | Here is what happends when you call `$moo(...)`: 209 | ```mermaid 210 | 211 | flowchart TD 212 | before["Pre request hook"] 213 | dispatch["Match route"] 214 | error["Run error hook if no route matched request"] 215 | after["Post request hook"] 216 | flush["Flush output"] 217 | before --> |$moo->before| dispatch 218 | dispatch --> |$moo->get, $moo->post, ...| error 219 | error --> |$moo->error| after 220 | after --> |$moo->after| flush 221 | ``` 222 | 223 | ### Routing 224 | You can bind handlers to standard HTTP methods: 225 | - GET: `$moo->get(...)` 226 | - HEAD: `$moo->head(...)` 227 | - POST: `$moo->post(...)` 228 | - PUT: `$moo->put(...)` 229 | - DELETE: `$moo->delete(...)` 230 | - CONNECT: `$moo->connect(...)` 231 | - OPTIONS: `$moo->options(...)` 232 | - TRACE: `$moo->trace(...)` 233 | - PATCH: `$moo->patch(...)` 234 | 235 | You can match multiple methods using `$moo->route(...)`. 236 | 237 | Routes are matched from the first to the last. If no route matches the request, the error handler is called `$moo->error(...)`. 238 | 239 | ### Parameters 240 | Route can be parametrized with regular expressions. 241 | ~~~php 242 | $moo->post('/orders/(\d+)/items/(\d+)', function ($orderId, $itemId) use ($moo) { 243 | return $moo->orderService->findOrderItem($orderId, $itemId); 244 | }); 245 | ~~~ 246 | 247 | ### Before & After hooks 248 | There are two handlers pre and post request, the before and the after. 249 | ~~~php 250 | $moo->before = function () { 251 | echo "Hey I'm the before hook"; 252 | }; 253 | $moo->after = function () { 254 | echo "Hey I'm the after hook"; 255 | }; 256 | $moo(); 257 | ~~~ 258 | 259 | ### Error Handling 260 | By default exceptions are handled by the error handlers, that also works when no route matches request. 261 | ~~~php 262 | $moo->error = function(\Exception $exc) use ($moo) { 263 | $moo->response = new Response([ 264 | 'code' => $exc->getCode() > 0 ? $exc->getCode() : 500, 265 | 'message' => StatusCode::message($exc->getCode()), 266 | 'body' => $exc->getMessage() 267 | ]); 268 | }; 269 | ~~~ 270 | 271 | ### Request 272 | The `Moo\Request` class consists of: 273 | - method 274 | - uri 275 | - headers 276 | - query 277 | - post 278 | - files 279 | - body 280 | 281 | Request object `$moo->request` is created before `before` with the values taken from PHP global variables, `$_SERVER`, `$_GET`, `$_POST`, `$_FILES`. 282 | 283 | *Request body is empty by default!* 284 | You can override that with ini: 285 | ~~~php 286 | $moo->before = function () ($moo) { 287 | $moo->request->body = file_get_contents('php://input'); 288 | }; 289 | ~~~ 290 | 291 | ### Response 292 | The `Moo\Response` class consists of: 293 | - body 294 | - headers 295 | - code 296 | - message 297 | 298 | Response object `$moo->response` is created before `before` and can be modified during the request lifecycle. Response body is not serialized, it is assumed you serialize it before `flush` in `after`. 299 | 300 | ### State 301 | You can use Moo to keep your application state. 302 | ~~~php 303 | $moo->something = 123; 304 | $moo->myState = [ 305 | 'config' => [ 306 | 'host' => 'localhost', 307 | 'port' => 1234 308 | ] 309 | ]; 310 | ~~~ 311 | 312 | ### Plugins 313 | You can extend Moo with custom functions. 314 | ~~~php 315 | $moo->foobar = function () { 316 | return 123; 317 | }; 318 | $moo->foobar(); 319 | ~~~ 320 | 321 | ### Flush & Output Buffering 322 | By default all output is buffered and then flushed at the end of `$moo()` execution. 323 | You can override that behaviour by setting custom flush handler. 324 | ~~~php 325 | $moo->flush = function () use ($moo) { 326 | header('HTTP/1.1 ' . $moo->response->code . ' ' . $moo->response->message); 327 | foreach ($moo->response->headers as $name => $value) { 328 | header($name.': '.$value, true); 329 | } 330 | echo $moo->response->body; 331 | }; 332 | ~~~ 333 | 334 | ## Testing 335 | Moo is unit tested, just run `make test`. 336 | 337 | ## Contributing 338 | Contributions are welcome, please submit a pull request. 339 | 340 | ## License 341 | MIT 342 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "loskoderos/moo", 3 | "description": "Moo is a PHP micro framework to make life and debugging miserable", 4 | "type": "library", 5 | "license": "MIT", 6 | "authors": [ 7 | { 8 | "name": "Lukasz Cepowski", 9 | "email": "lukasz@cepowski.com", 10 | "homepage": "https://cepowski.com" 11 | } 12 | ], 13 | "support": { 14 | "email": "hello@loskoderos.com" 15 | }, 16 | "require": { 17 | "loskoderos/generic-php": "dev-master", 18 | "php": ">=8.0" 19 | }, 20 | "require-dev": { 21 | }, 22 | "autoload": { 23 | "psr-4": { 24 | "Moo\\": "src/Moo" 25 | } 26 | }, 27 | "autoload-dev": { 28 | "psr-4": { 29 | "Tests\\Moo\\": "test/Tests/Moo" 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /composer.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_readme": [ 3 | "This file locks the dependencies of your project to a known state", 4 | "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", 5 | "This file is @generated automatically" 6 | ], 7 | "content-hash": "9979db9c0b62e8fa8f1c211ea21f1ae1", 8 | "packages": [ 9 | { 10 | "name": "loskoderos/generic-php", 11 | "version": "dev-master", 12 | "source": { 13 | "type": "git", 14 | "url": "https://github.com/loskoderos/generic-php.git", 15 | "reference": "b28d85f8dec8545cd71d30e2d556b494b60d7415" 16 | }, 17 | "dist": { 18 | "type": "zip", 19 | "url": "https://api.github.com/repos/loskoderos/generic-php/zipball/b28d85f8dec8545cd71d30e2d556b494b60d7415", 20 | "reference": "b28d85f8dec8545cd71d30e2d556b494b60d7415", 21 | "shasum": "" 22 | }, 23 | "require": { 24 | "php": ">=7.4" 25 | }, 26 | "default-branch": true, 27 | "type": "library", 28 | "autoload": { 29 | "psr-4": { 30 | "LosKoderos\\Generic\\": "src/LosKoderos/Generic" 31 | } 32 | }, 33 | "notification-url": "https://packagist.org/downloads/", 34 | "license": [ 35 | "MIT" 36 | ], 37 | "authors": [ 38 | { 39 | "name": "Lukasz Cepowski", 40 | "email": "lukasz@cepowski.com", 41 | "homepage": "https://cepa.io" 42 | } 43 | ], 44 | "description": "Dustbin for all PHP code thats Generic", 45 | "support": { 46 | "email": "hello@loskoderos.com", 47 | "issues": "https://github.com/loskoderos/generic-php/issues", 48 | "source": "https://github.com/loskoderos/generic-php/tree/master" 49 | }, 50 | "time": "2023-06-27T16:10:17+00:00" 51 | } 52 | ], 53 | "packages-dev": [], 54 | "aliases": [], 55 | "minimum-stability": "stable", 56 | "stability-flags": { 57 | "loskoderos/generic-php": 20 58 | }, 59 | "prefer-stable": false, 60 | "prefer-lowest": false, 61 | "platform": { 62 | "php": ">=8.0" 63 | }, 64 | "platform-dev": [], 65 | "plugin-api-version": "2.3.0" 66 | } 67 | -------------------------------------------------------------------------------- /examples/classy-rest-api/Application.php: -------------------------------------------------------------------------------- 1 | bookService = new BookService(); 18 | 19 | $this->get( '/', [$this, 'index']); 20 | $this->get( '/books', [$this, 'getBooks']); 21 | $this->post( '/books', [$this, 'addBook']); 22 | $this->get( '/books/(\d+)', [$this, 'getBook']); 23 | $this->put( '/books/(\d+)', [$this, 'updateBook']); 24 | $this->delete( '/books/(\d+)', [$this, 'removeBook']); 25 | } 26 | 27 | public function before() 28 | { 29 | $this->request->body = file_get_contents('php://input'); 30 | } 31 | 32 | public function after() 33 | { 34 | $this->response->headers->set('Content-Type', 'application/json'); 35 | $this->response->body = json_encode($this->response->body); 36 | } 37 | 38 | public function error(\Exception $exc) 39 | { 40 | $this->response = new Response(); 41 | $this->response->code = $exc->getCode() > 0 ? $exc->getCode() : 500; 42 | $this->response->message = StatusCode::message($this->response->code); 43 | $this->response->headers->set('Content-Type', 'application/json'); 44 | $this->response->body = json_encode(['error' => $exc->getMessage()]); 45 | } 46 | 47 | public function index() 48 | { 49 | return ['time' => date('Y-m-d H:i:s')]; 50 | } 51 | 52 | public function getBooks() 53 | { 54 | return $this->bookService->getBooks(); 55 | } 56 | 57 | public function addBook() 58 | { 59 | return $this->bookService->addBook($this->request->body); 60 | } 61 | 62 | public function getBook($id) 63 | { 64 | return $this->bookService->getBook($id); 65 | } 66 | 67 | public function updateBook($id) 68 | { 69 | return $this->bookService->updateBook($id, $this->request->body); 70 | } 71 | 72 | public function removeBook($id) 73 | { 74 | return $this->bookService->removeBook($id); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /examples/classy-rest-api/BookService.php: -------------------------------------------------------------------------------- 1 | 'Don Quixote', 'author' => 'Miguel de Cervantes']; 18 | } 19 | 20 | public function updateBook($id, $dto) 21 | { 22 | 23 | } 24 | 25 | public function removeBook($id) 26 | { 27 | 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /examples/classy-rest-api/index.php: -------------------------------------------------------------------------------- 1 | get('/', function () { 8 | echo "Hello world!"; 9 | }); 10 | 11 | $moo(); 12 | -------------------------------------------------------------------------------- /examples/nesting/index.php: -------------------------------------------------------------------------------- 1 | get('/books/(\d+)', function ($id) { 9 | echo "this is book id=$id"; 10 | }); 11 | 12 | $usersMoo = new Moo(); 13 | $usersMoo->get('/users/(\d+)', function ($id) { 14 | echo "this is user id=$id"; 15 | }); 16 | 17 | $moo = new Moo(); 18 | $moo->route('/books/.*', $booksMoo); 19 | $moo->route('/users/.*', $usersMoo); 20 | $moo(); 21 | -------------------------------------------------------------------------------- /examples/rest-api/BookService.php: -------------------------------------------------------------------------------- 1 | 'Don Quixote', 'author' => 'Miguel de Cervantes']; 18 | } 19 | 20 | public function updateBook($id, $dto) 21 | { 22 | 23 | } 24 | 25 | public function removeBook($id) 26 | { 27 | 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /examples/rest-api/index.php: -------------------------------------------------------------------------------- 1 | bookService = new BookService(); 8 | 9 | $moo->before = function () use ($moo) { 10 | $moo->request->body = file_get_contents('php://input'); 11 | }; 12 | 13 | $moo->after = function () use ($moo) { 14 | $moo->response->headers->set('Content-Type', 'application/json'); 15 | $moo->response->body = json_encode($moo->response->body); 16 | }; 17 | 18 | $moo->get('/books', function () use ($moo) { 19 | return $moo->bookService->getBooks(); 20 | }); 21 | 22 | $moo->post('/books', function () use ($moo) { 23 | return $moo->bookService->addBook($moo->request->body); 24 | }); 25 | 26 | $moo->get('/books/(\d+)', function ($id) use ($moo) { 27 | return $moo->bookService->getBook($id); 28 | }); 29 | 30 | $moo->put('/books/(\d+)', function ($id) use ($moo) { 31 | return $moo->bookService->updateBook($id, $moo->request->body); 32 | }); 33 | 34 | $moo->delete('/books/(\d+)', function ($id) use ($moo) { 35 | return $moo->bookService->removeBook($id); 36 | }); 37 | 38 | $moo(); 39 | -------------------------------------------------------------------------------- /examples/var-dump/index.php: -------------------------------------------------------------------------------- 1 | dumpServerVariables = function () { 8 | var_dump($_SERVER); 9 | var_dump($_GET); 10 | var_dump($_POST); 11 | var_dump($_FILES); 12 | }; 13 | 14 | $moo->route('/(.*)', function () use ($moo) { 15 | $moo->dumpServerVariables(); 16 | }); 17 | 18 | $moo(); 19 | -------------------------------------------------------------------------------- /examples/website/.htaccess: -------------------------------------------------------------------------------- 1 | RewriteEngine on 2 | RewriteCond %{REQUEST_FILENAME} !-f 3 | RewriteCond %{REQUEST_FILENAME} !-d 4 | RewriteRule ^.*$ index.php [L,QSA] 5 | -------------------------------------------------------------------------------- /examples/website/assets/main.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 3 | let button = document.querySelector('#contactForm button'); 4 | if (button) { 5 | button.addEventListener('click', function (event) { 6 | event.preventDefault(); 7 | 8 | const form = document.getElementById('contactForm'); 9 | const formData = new FormData(form); 10 | fetch('contact', { 11 | method: 'POST', 12 | body: formData 13 | }) 14 | .then(function (res) { 15 | return res.text(); 16 | }) 17 | .then(function (text) { 18 | console.log(text); 19 | const element = document.createElement('div'); 20 | element.classList.add('alert', 'alert-primary'); 21 | element.innerHTML = `Server replied: ${text}`; 22 | 23 | const replies = document.getElementById('replies'); 24 | if (replies) { 25 | replies.appendChild(element); 26 | } 27 | }) 28 | .catch(function (err) { 29 | console.error(err); 30 | }); 31 | }); 32 | }; 33 | 34 | })(); -------------------------------------------------------------------------------- /examples/website/assets/style.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/loskoderos/moo/f80209e332bfa8ffe18303c6819b92e15a065326/examples/website/assets/style.css -------------------------------------------------------------------------------- /examples/website/index.php: -------------------------------------------------------------------------------- 1 | template = new Template(__DIR__ . '/templates', [ 12 | 'title' => 'Moo Sample Website' 13 | ]); 14 | 15 | // Create plugin to extract baseUri. 16 | $moo->template->baseUri = function () use ($moo) { 17 | return $moo->request->baseUri; 18 | }; 19 | 20 | // Create example date plugin. 21 | $moo->template->date = function () { 22 | return date('Y-m-d'); 23 | }; 24 | 25 | // Index page handler. 26 | $moo->get('/', function () use ($moo) { 27 | return $moo->template->render('index.phtml', [ 28 | 'hello' => 'Hello World!' 29 | ]); 30 | }); 31 | 32 | // Features page handler. 33 | $moo->get('/features', function () use ($moo) { 34 | return $moo->template->render('features.phtml', [ 35 | 'features' => [ 36 | 'Simple regex routing', 37 | 'Extendable Moo class', 38 | 'Lightweight and fast', 39 | 'Allows nesting', 40 | 'Builtin PHP templating' 41 | ] 42 | ]); 43 | }); 44 | 45 | // Contact page handler. 46 | $moo->get('/contact', function () use ($moo) { 47 | return $moo->template->render('contact.phtml', [ 48 | 'features' => [] 49 | ]); 50 | }); 51 | 52 | // Contact form submit handler. 53 | $moo->post('/contact', function () use ($moo) { 54 | return strrev($moo->request->post->message); 55 | }); 56 | 57 | $moo(); 58 | -------------------------------------------------------------------------------- /examples/website/templates/contact.phtml: -------------------------------------------------------------------------------- 1 | 2 | 3 |
Contact
4 |
5 |
6 | 7 |
8 |
9 | 10 |
11 |
12 |
13 | 14 | -------------------------------------------------------------------------------- /examples/website/templates/features.phtml: -------------------------------------------------------------------------------- 1 | 2 | 3 |
Features
4 | 9 | 10 | -------------------------------------------------------------------------------- /examples/website/templates/footer.phtml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /examples/website/templates/header.phtml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | <?php echo $title ?> 11 | 12 | 13 | 14 | 15 | 36 | 37 |
38 | -------------------------------------------------------------------------------- /examples/website/templates/index.phtml: -------------------------------------------------------------------------------- 1 | 2 | 3 |

4 |

Current date is date() ?>

5 | 6 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 11 | 12 | test/* 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | src 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /src/Moo/Extendable.php: -------------------------------------------------------------------------------- 1 | $callback) { 10 | if ($key == $name) { 11 | if (is_callable($callback)) { 12 | return call_user_func_array($callback, $args); 13 | } else { 14 | throw new \BadMethodCallException(get_class($this) . "::$name is not callable"); 15 | } 16 | } 17 | } 18 | throw new \BadMethodCallException(get_class($this) . "::$name is not callable"); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/Moo/Moo.php: -------------------------------------------------------------------------------- 1 | router = new Router(); 19 | 20 | // Default pre request hook. 21 | $this->before = function() {}; 22 | 23 | // Default post request hook. 24 | $this->after = function() {}; 25 | 26 | // Default error handler. 27 | $this->error = function(\Exception $exc) { 28 | $code = $exc->getCode() > 0 ? $exc->getCode() : 500; 29 | $this->response->code = $code; 30 | $this->response->message = StatusCode::message($code); 31 | $this->response->headers->clear(); 32 | $this->response->body = $exc->getMessage(); 33 | }; 34 | 35 | // Default flush handler. 36 | $this->flush = function() { 37 | header('HTTP/1.1 ' . $this->response->code . ' ' . $this->response->message); 38 | foreach ($this->response->headers as $name => $value) { 39 | header($name.': '.$value, true); 40 | } 41 | echo $this->response->body; 42 | }; 43 | } 44 | 45 | public function route(string $uri, ?callable $callback): Moo 46 | { 47 | $this->router->register(['*'], [$uri], $callback); 48 | return $this; 49 | } 50 | 51 | public function get(string $uri, ?callable $callback): Moo 52 | { 53 | $this->router->register(['GET'], [$uri], $callback); 54 | return $this; 55 | } 56 | 57 | public function head(string $uri, ?callable $callback): Moo 58 | { 59 | $this->router->register(['HEAD'], [$uri], $callback); 60 | return $this; 61 | } 62 | 63 | public function post(string $uri, ?callable $callback): Moo 64 | { 65 | $this->router->register(['POST'], [$uri], $callback); 66 | return $this; 67 | } 68 | 69 | public function put(string $uri, ?callable $callback): Moo 70 | { 71 | $this->router->register(['PUT'], [$uri], $callback); 72 | return $this; 73 | } 74 | 75 | public function delete(string $uri, ?callable $callback): Moo 76 | { 77 | $this->router->register(['DELETE'], [$uri], $callback); 78 | return $this; 79 | } 80 | 81 | public function connect(string $uri, ?callable $callback): Moo 82 | { 83 | $this->router->register(['CONNECT'], [$uri], $callback); 84 | return $this; 85 | } 86 | 87 | public function options(string $uri, ?callable $callback): Moo 88 | { 89 | $this->router->register(['OPTIONS'], [$uri], $callback); 90 | return $this; 91 | } 92 | 93 | public function trace(string $uri, ?callable $callback): Moo 94 | { 95 | $this->router->register(['TRACE'], [$uri], $callback); 96 | return $this; 97 | } 98 | 99 | public function patch(string $uri, ?callable $callback): Moo 100 | { 101 | $this->router->register(['PATCH'], [$uri], $callback); 102 | return $this; 103 | } 104 | 105 | protected function _dispatch(?Request $request = null, ?Response $response = null): mixed 106 | { 107 | $this->request = isset($request) ? $request : $this->router->requestFactory(); 108 | $this->response = isset($response) ? $response : new Response(); 109 | 110 | ob_start(); 111 | try { 112 | is_callable($this->before) ? $this->before() : null; 113 | 114 | $result = $this->router->dispatch($this->request, $this->response); 115 | if ($result !== null) { 116 | $this->response->body = $result; 117 | } 118 | 119 | is_callable($this->after) ? $this->after() : null; 120 | 121 | } catch (\Exception $exc) { 122 | is_callable($this->error) ? $this->error($exc) : throw $exc; 123 | } 124 | $this->response->body = $this->response->body . ob_get_clean(); 125 | 126 | return ob_get_level() <= 1 && is_callable($this->flush) ? $this->flush() : null; 127 | } 128 | 129 | public function __invoke(?Request $request = null, ?Response $response = null): mixed 130 | { 131 | return $this->_dispatch($request, $response); 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /src/Moo/Request.php: -------------------------------------------------------------------------------- 1 | method = 'GET'; 22 | $this->uri = '/'; 23 | $this->baseUri = '/'; 24 | $this->headers = new Collection(); 25 | $this->query = new Collection(); 26 | $this->post = new Collection(); 27 | $this->files = new Collection(); 28 | parent::__construct($mixed); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Moo/Response.php: -------------------------------------------------------------------------------- 1 | body = null; 18 | $this->headers = new Collection(); 19 | $this->code = StatusCode::HTTP_OK; 20 | $this->message = StatusCode::message($this->code); 21 | parent::__construct($mixed); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Moo/Route.php: -------------------------------------------------------------------------------- 1 | routes = new Collection(); 14 | } 15 | 16 | public function register(array $methods, array $uris, ?callable $callback): Router 17 | { 18 | foreach ($methods as $method) { 19 | foreach ($uris as $uri) { 20 | $route = new Route(); 21 | $route->method = $method; 22 | $route->uri = $uri; 23 | $route->callback = $callback; 24 | $key = $method.' '.trim($uri, '/'); 25 | $this->routes->set($key, $route); 26 | } 27 | } 28 | return $this; 29 | } 30 | 31 | public function dispatch(Request $request, Response $response): mixed 32 | { 33 | // Parse and match to the first route. 34 | $uri = parse_url(preg_replace('/(\/+)/', '/', $request->uri))['path']; 35 | foreach ($this->routes as $route) { 36 | if ($route->method == '*' || $route->method == $request->method) { 37 | if (preg_match("#^{$route->uri}$#", $uri, $matches)) { 38 | if ($route->callback instanceof Moo) { 39 | return call_user_func($route->callback, $request, $response); 40 | } else if (is_callable($route->callback)) { 41 | return call_user_func_array($route->callback, array_slice($matches, 1)); 42 | } else { 43 | throw new \RuntimeException("Route $method $uri is not callable"); 44 | } 45 | } 46 | } 47 | } 48 | throw new \RuntimeException(StatusCode::message(404), 404); 49 | } 50 | 51 | public static function requestFactory(): Request 52 | { 53 | // Populate request with default values from $_SERVER. 54 | $request = new Request(); 55 | $request->method = isset($_SERVER['REQUEST_METHOD']) ? $_SERVER['REQUEST_METHOD'] : 'GET'; 56 | $request->uri = isset($_SERVER['REQUEST_URI']) ? $_SERVER['REQUEST_URI'] : '/'; 57 | $request->headers->populate($_SERVER); 58 | $request->query->populate($_GET); 59 | $request->post->populate($_POST); 60 | $request->files->populate($_FILES); 61 | 62 | // Auto extract base URI of the script based on execution path. 63 | if (isset($_SERVER['PHP_SELF'])) { 64 | $request->baseUri = rtrim(dirname($_SERVER['PHP_SELF']), '/') . '/'; 65 | if (str_starts_with($request->uri, $request->baseUri)) { 66 | $request->uri = '/' . substr($request->uri, strlen($request->baseUri)); 67 | } 68 | } 69 | 70 | return $request; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/Moo/StatusCode.php: -------------------------------------------------------------------------------- 1 | 'Continue', 73 | 101 => 'Switching Protocols', 74 | 200 => 'OK', 75 | 201 => 'Created', 76 | 202 => 'Accepted', 77 | 203 => 'Non-Authoritative Information', 78 | 204 => 'No Content', 79 | 205 => 'Reset Content', 80 | 206 => 'Partial Content', 81 | 300 => 'Multiple Choices', 82 | 301 => 'Moved Permanently', 83 | 302 => 'Found', 84 | 303 => 'See Other', 85 | 304 => 'Not Modified', 86 | 305 => 'Use Proxy', 87 | 307 => 'Temporary Redirect', 88 | 400 => 'Bad Request', 89 | 401 => 'Unauthorized', 90 | 402 => 'Payment Required', 91 | 403 => 'Forbidden', 92 | 404 => 'Not Found', 93 | 405 => 'Method Not Allowed', 94 | 406 => 'Not Acceptable', 95 | 407 => 'Proxy Authentication Required', 96 | 408 => 'Request Timeout', 97 | 409 => 'Conflict', 98 | 410 => 'Gone', 99 | 411 => 'Length Required', 100 | 412 => 'Precondition Failed', 101 | 413 => 'Request Entity Too Large', 102 | 414 => 'Request-URI Too Long', 103 | 415 => 'Unsupported Media Type', 104 | 416 => 'Requested Range Not Satisfiable', 105 | 417 => 'Expectation Failed', 106 | 418 => 'I\'m a teapot', 107 | 500 => 'Internal Server Error', 108 | 501 => 'Not Implemented', 109 | 502 => 'Bad Gateway', 110 | 503 => 'Service Unavailable', 111 | 504 => 'Gateway Timeout', 112 | 505 => 'HTTP Version Not Supported', 113 | ]; 114 | 115 | private function __construct() {} 116 | 117 | public static function message(int $statusCode): ?string { 118 | return isset(self::$_messages[$statusCode]) ? self::$_messages[$statusCode] : null; 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /src/Moo/Template.php: -------------------------------------------------------------------------------- 1 | lookupDir = $lookupDir; 13 | $this->defaultContext = $defaultContext; 14 | } 15 | 16 | public function render(string $script, array $context = null): string 17 | { 18 | $__path = $this->lookupDir . '/' . $script; 19 | if (!file_exists($__path)) { 20 | throw new \RuntimeException("Template not found: " . $script); 21 | } 22 | try { 23 | ob_start(); 24 | extract($this->defaultContext); 25 | extract(is_array($context) ? $context : []); 26 | if (false === @include $__path) { 27 | throw new \RuntimeException("Broken template: " . $script); 28 | } 29 | 30 | } catch (\Exception $exc) { 31 | ob_end_clean(); 32 | throw $exc; 33 | } 34 | return ob_get_clean(); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /test/Tests/Moo/MooTest.php: -------------------------------------------------------------------------------- 1 | get('/', [$this, 'index']); 31 | $this->post('/test/(\d+)', [$this, 'test']); 32 | } 33 | 34 | public function before() 35 | { 36 | $this->state++; 37 | } 38 | 39 | public function after() 40 | { 41 | $this->state++; 42 | } 43 | 44 | public function flush() 45 | { 46 | // Keep it empty. 47 | } 48 | 49 | public function error(\Exception $exc) 50 | { 51 | $this->state = -1; 52 | $this->response = new Response([ 53 | 'code' => 404, 54 | 'message' => 'Not Found', 55 | 'body' => 'error' 56 | ]); 57 | } 58 | 59 | public function index() 60 | { 61 | return 'hello world'; 62 | } 63 | 64 | public function test($x) 65 | { 66 | $this->response->code = 202; 67 | return $x; 68 | } 69 | } 70 | 71 | class MooTest extends TestCase 72 | { 73 | const METHODS = ['GET', 'HEAD', 'POST', 'PUT', 'DELETE', 'CONNECT', 'OPTIONS', 'TRACE', 'PATCH', 'GET']; 74 | 75 | public function testBeforeAfter() 76 | { 77 | $test = 0; 78 | $moo = new Moo(); 79 | $moo->flush = null; 80 | $moo->before =function () use (&$test) { 81 | $test++; 82 | }; 83 | $moo->after = function () use (&$test) { 84 | $test++; 85 | }; 86 | $moo->route('/', function () { return 'xxx'; }); 87 | $moo(); 88 | $this->assertEquals(2, $test); 89 | } 90 | 91 | public function testCustomError() 92 | { 93 | $test = 0; 94 | $moo = new Moo(); 95 | $moo->before = function () use(&$test) { 96 | $test++; 97 | throw new \RuntimeException("test"); 98 | }; 99 | $moo->error = function(\Exception $exc) use(&$test) { 100 | $test++; 101 | }; 102 | $moo(); 103 | $this->assertEquals(2, $test); 104 | } 105 | 106 | public function testFlush() 107 | { 108 | $moo = new Moo(); 109 | $moo->error = function () {}; 110 | $moo->flush = function () use ($moo) { return strtoupper($moo->response->body); }; 111 | $moo->route('/', function () { return 'hello'; }); 112 | $content = $moo(); 113 | $this->assertEquals("HELLO", $content); 114 | } 115 | 116 | public function testPluginAndState() 117 | { 118 | $moo = new Moo(); 119 | $moo->state = 0; 120 | $moo->plugin = function ($x) use ($moo) { 121 | $moo->state = $x; 122 | return $x; 123 | }; 124 | $y = $moo->plugin(123); 125 | $this->assertEquals(123, $y); 126 | $this->assertEquals(123, $moo->state); 127 | } 128 | 129 | public function testPluginException() 130 | { 131 | $this->expectException(\BadMethodCallException::class); 132 | $moo = new Moo(); 133 | $moo->nonExistentPlugin(); 134 | } 135 | 136 | public function testPluginTypeException() 137 | { 138 | $this->expectException(\BadMethodCallException::class); 139 | $moo = new Moo(); 140 | $moo->plugin = 123; 141 | $moo->plugin(); 142 | } 143 | 144 | public function testIndex() 145 | { 146 | $moo = new Moo(); 147 | $moo->flush = null; 148 | $moo->route('/', function () { 149 | return 'this is index'; 150 | }); 151 | $moo(); 152 | $this->assertEquals(200, $moo->response->code); 153 | $this->assertEquals('OK', $moo->response->message); 154 | $this->assertEquals('this is index', $moo->response->body); 155 | } 156 | 157 | public function testNotFound() 158 | { 159 | $moo = new Moo(); 160 | $moo->flush = null; 161 | $moo(new Request(['uri' => '/dfhsdhfg'])); 162 | $this->assertEquals(404, $moo->response->code); 163 | $this->assertEquals('Not Found', $moo->response->message); 164 | $this->assertEquals('Not Found', $moo->response->body); 165 | } 166 | 167 | public function testRoute() 168 | { 169 | $moo = new Moo(); 170 | $moo->flush = null; 171 | $moo->route('/', function () { 172 | return 'ok'; 173 | }); 174 | $moo(); 175 | $this->assertRouteFound($moo, self::METHODS, ['/', '//', '///'], 'ok'); 176 | $this->assertRouteNotFound($moo, self::METHODS, ['/foobar']); 177 | } 178 | 179 | public function testRouteOverride() 180 | { 181 | $moo = new Moo(); 182 | $moo->flush = null; 183 | $moo->route('/', function () { 184 | return 1; 185 | }); 186 | $moo->route('/', function () { 187 | return 2; 188 | }); 189 | $moo->route('/', function () { 190 | return 3; 191 | }); 192 | $moo(); 193 | $this->assertEquals(3, $moo->response->body); 194 | $this->assertEquals(1, $moo->router->routes->count()); 195 | } 196 | 197 | public function testRouteParameters() 198 | { 199 | $moo = new Moo(); 200 | $moo->flush = null; 201 | $moo->route('/', function () { 202 | return 'index'; 203 | }); 204 | $moo->route('/test', function () { 205 | return 'test'; 206 | }); 207 | $moo->route('/test/(\d+)', function ($x) { 208 | return $x; 209 | }); 210 | $moo->route('/test/(\d+)/test', function ($x) { 211 | return 'test 2'; 212 | }); 213 | $moo->route('/test/([a-z]+)/test', function ($x) { 214 | return 'test 3 '.$x; 215 | }); 216 | $moo(); 217 | $this->assertEquals('index', $moo->response->body); 218 | $moo(new Request(['uri' => '/test'])); 219 | $this->assertEquals('test', $moo->response->body); 220 | $moo(new Request(['uri' => '/test/123'])); 221 | $this->assertEquals('123', $moo->response->body); 222 | $moo(new Request(['uri' => '/test/123/test'])); 223 | $this->assertEquals('test 2', $moo->response->body); 224 | $moo(new Request(['uri' => '/test/ok/test'])); 225 | $this->assertEquals('test 3 ok', $moo->response->body); 226 | $moo(new Request(['uri' => '/test/ABC/test'])); 227 | $this->assertEquals(404, $moo->response->code); 228 | $this->assertEquals('Not Found', $moo->response->message); 229 | $this->assertEquals('Not Found', $moo->response->body); 230 | } 231 | 232 | public function testMultiRoute() 233 | { 234 | $moo = new Moo(); 235 | $moo->flush = null; 236 | $moo->router->register(['GET', 'POST'], ['/foo/(\d+)', '/bar/(\d+)'], function ($x) { 237 | return $x; 238 | }); 239 | $this->assertRouteFound($moo, ['GET', 'POST'], ['/foo/123', '/bar/123'], '123'); 240 | $this->assertRouteNotFound($moo, array_exclude(['GET', 'POST'], self::METHODS), ['/foo', '/bar']); 241 | $this->assertRouteNotFound($moo, self::METHODS, ['/', '/foo/abc', '/bar/xyz']); 242 | } 243 | 244 | public function testGet() 245 | { 246 | $moo = new Moo(); 247 | $moo->flush = null; 248 | $moo->get('/', function () { 249 | return 'ok'; 250 | }); 251 | $moo(); 252 | $this->assertRouteFound($moo, ['GET'], ['/'], 'ok'); 253 | $this->assertRouteNotFound($moo, array_exclude(['GET'], self::METHODS), ['/']); 254 | } 255 | 256 | public function testHead() 257 | { 258 | $moo = new Moo(); 259 | $moo->flush = null; 260 | $moo->head('/', function () { 261 | return 'ok'; 262 | }); 263 | $moo(); 264 | $this->assertRouteFound($moo, ['HEAD'], ['/'], 'ok'); 265 | $this->assertRouteNotFound($moo, array_exclude(['HEAD'], self::METHODS), ['/']); 266 | } 267 | 268 | public function testPost() 269 | { 270 | $moo = new Moo(); 271 | $moo->flush = null; 272 | $moo->post('/', function () { 273 | return 'ok'; 274 | }); 275 | $moo(); 276 | $this->assertRouteFound($moo, ['POST'], ['/'], 'ok'); 277 | $this->assertRouteNotFound($moo, array_exclude(['POST'], self::METHODS), ['/']); 278 | } 279 | 280 | public function testPut() 281 | { 282 | $moo = new Moo(); 283 | $moo->flush = null; 284 | $moo->put('/', function () { 285 | return 'ok'; 286 | }); 287 | $moo(); 288 | $this->assertRouteFound($moo, ['PUT'], ['/'], 'ok'); 289 | $this->assertRouteNotFound($moo, array_exclude(['PUT'], self::METHODS), ['/']); 290 | } 291 | 292 | public function testDelete() 293 | { 294 | $moo = new Moo(); 295 | $moo->flush = null; 296 | $moo->delete('/', function () { 297 | return 'ok'; 298 | }); 299 | $moo(); 300 | $this->assertRouteFound($moo, ['DELETE'], ['/'], 'ok'); 301 | $this->assertRouteNotFound($moo, array_exclude(['DELETE'], self::METHODS), ['/']); 302 | } 303 | 304 | public function testConnect() 305 | { 306 | $moo = new Moo(); 307 | $moo->flush = null; 308 | $moo->connect('/', function () { 309 | return 'ok'; 310 | }); 311 | $moo(); 312 | $this->assertRouteFound($moo, ['CONNECT'], ['/'], 'ok'); 313 | $this->assertRouteNotFound($moo, array_exclude(['CONNECT'], self::METHODS), ['/']); 314 | } 315 | 316 | public function testOptions() 317 | { 318 | $moo = new Moo(); 319 | $moo->flush = null; 320 | $moo->options('/', function () { 321 | return 'ok'; 322 | }); 323 | $moo(); 324 | $this->assertRouteFound($moo, ['OPTIONS'], ['/'], 'ok'); 325 | $this->assertRouteNotFound($moo, array_exclude(['OPTIONS'], self::METHODS), ['/']); 326 | } 327 | 328 | public function testTrace() 329 | { 330 | $moo = new Moo(); 331 | $moo->flush = null; 332 | $moo->trace('/', function () { 333 | return 'ok'; 334 | }); 335 | $moo(); 336 | $this->assertRouteFound($moo, ['TRACE'], ['/'], 'ok'); 337 | $this->assertRouteNotFound($moo, array_exclude(['TRACE'], self::METHODS), ['/']); 338 | } 339 | 340 | public function testPatch() 341 | { 342 | $moo = new Moo(); 343 | $moo->flush = null; 344 | $moo->patch('/', function () { 345 | return 'ok'; 346 | }); 347 | $moo(); 348 | $this->assertRouteFound($moo, ['PATCH'], ['/'], 'ok'); 349 | $this->assertRouteNotFound($moo, array_exclude(['PATCH'], self::METHODS), ['/']); 350 | } 351 | 352 | public function testCallableReturnValueOverrideBody() 353 | { 354 | $moo = new Moo(); 355 | $moo->flush = null; 356 | $moo->get('/test1', function () use ($moo) { 357 | $moo->response->body = 123; 358 | }); 359 | $moo->get('/test2', function () use ($moo) { 360 | $moo->response->body = 456; 361 | return 'xyz'; 362 | }); 363 | 364 | $moo(new Request(['uri' => '/test1'])); 365 | $this->assertEquals(123, $moo->response->body); 366 | 367 | $moo(new Request(['uri' => '/test2'])); 368 | $this->assertEquals('xyz', $moo->response->body); 369 | } 370 | 371 | public function testClassyMoo() 372 | { 373 | $app = new ClassyMooMock(); 374 | 375 | $app(new Request()); 376 | $this->assertEquals(200, $app->response->code); 377 | $this->assertEquals('hello world', $app->response->body); 378 | $this->assertEquals(2, $app->state); 379 | 380 | $app(new Request(['method' => 'POST', 'uri' => '/test/123'])); 381 | $this->assertEquals(202, $app->response->code); 382 | $this->assertEquals('123', $app->response->body); 383 | $this->assertEquals(4, $app->state); 384 | 385 | $app(new Request(['method' => 'GET', 'uri' => '/test/123'])); 386 | $this->assertEquals(404, $app->response->code); 387 | $this->assertEquals('error', $app->response->body); 388 | $this->assertEquals(-1, $app->state); 389 | } 390 | 391 | public function testNestedMoo() 392 | { 393 | $mooA = new Moo(); 394 | $mooA->get('/a/test', function () { 395 | return 'test a'; 396 | }); 397 | 398 | $mooB = new Moo(); 399 | $mooB->get('/b/test', function () { 400 | return 'test b'; 401 | }); 402 | 403 | $moo = new Moo(); 404 | $moo->flush = null; 405 | $moo->route('/a/.*', $mooA); 406 | $moo->route('/b/.*', $mooB); 407 | 408 | $moo(new Request()); 409 | $this->assertEquals(404, $moo->response->code); 410 | 411 | $moo(new Request(['uri' => '/a/'])); 412 | $this->assertEquals(404, $moo->response->code); 413 | 414 | $moo(new Request(['uri' => '/a/xxx'])); 415 | $this->assertEquals(404, $moo->response->code); 416 | 417 | $moo(new Request(['uri' => '/a/test'])); 418 | $this->assertEquals(200, $moo->response->code); 419 | $this->assertEquals('test a', $moo->response->body); 420 | 421 | $moo(new Request(['uri' => '/b/'])); 422 | $this->assertEquals(404, $moo->response->code); 423 | 424 | $moo(new Request(['uri' => '/b/xxx'])); 425 | $this->assertEquals(404, $moo->response->code); 426 | 427 | $moo(new Request(['uri' => '/b/test'])); 428 | $this->assertEquals(200, $moo->response->code); 429 | $this->assertEquals('test b', $moo->response->body); 430 | } 431 | 432 | public function testTemplateErrors() 433 | { 434 | $moo = new Moo(); 435 | $moo->flush = null; 436 | 437 | $moo->template = function (string $script) use ($moo) { 438 | return (new Template(__DIR__ . '/templates'))->render($script); 439 | }; 440 | 441 | $moo->get('/', function () use ($moo) { 442 | return $moo->template('dummy.phtml'); 443 | }); 444 | 445 | $moo->get('/test3', function () use ($moo) { 446 | return $moo->template('test3.phtml'); 447 | }); 448 | 449 | $moo(); 450 | $this->assertEquals(500, $moo->response->code); 451 | 452 | $moo(new Request(['uri' => '/test3'])); 453 | $this->assertEquals(500, $moo->response->code); 454 | } 455 | 456 | public function testTemplateLoadWithPartial() 457 | { 458 | $moo = new Moo(); 459 | $moo->flush = null; 460 | 461 | $moo->template = function (string $script) use ($moo) { 462 | return (new Template(__DIR__ . '/templates'))->render($script); 463 | }; 464 | 465 | $moo->get('/', function () use ($moo) { 466 | return $moo->template('test.phtml'); 467 | }); 468 | 469 | $moo(); 470 | $this->assertEquals(200, $moo->response->code); 471 | $this->assertStringContainsString('test partial', $moo->response->body); 472 | } 473 | 474 | public function testTemplateContextAndPlugin() 475 | { 476 | $moo = new Moo(); 477 | $moo->flush = null; 478 | 479 | $moo->template = function (string $script, array $context = []) use ($moo) { 480 | $template = new Template(__DIR__ . '/templates', [ 481 | 'global' => 'test' 482 | ]); 483 | $template->plugin = function ($x) { return $x; }; 484 | return $template->render($script, $context); 485 | }; 486 | 487 | $moo->get('/', function () use ($moo) { 488 | return $moo->template('test2.phtml', [ 489 | 'local' => 'ok', 490 | 'var' => 123 491 | ]); 492 | }); 493 | 494 | $moo(); 495 | $this->assertEquals(200, $moo->response->code); 496 | $this->assertStringContainsString('test ok 123', $moo->response->body); 497 | } 498 | 499 | /** 500 | * Keep this test last because it overrides values of global variables. 501 | */ 502 | public function testRequestFactory() 503 | { 504 | global $_SERVER; 505 | global $_GET; 506 | global $_POST; 507 | global $_FILES; 508 | 509 | $_SERVER['REQUEST_METHOD'] = 'POST'; 510 | $_SERVER['REQUEST_URI'] = '/test?xxx=123'; 511 | $_GET['xxx'] = 123; 512 | $_POST['yyy'] = 456; 513 | $_FILES['zzz'] = ['name' => 'foo.bar']; 514 | 515 | $moo = new Moo(); 516 | $moo->flush = null; 517 | $moo->post('/test', function () { 518 | return 'test'; 519 | }); 520 | 521 | $request = Router::requestFactory(); 522 | $this->assertEquals('POST', $request->method); 523 | $this->assertEquals('/test?xxx=123', $request->uri); 524 | $this->assertEquals(1, $request->query->count()); 525 | $this->assertEquals(123, $request->query->xxx); 526 | $this->assertEquals(1, $request->post->count()); 527 | $this->assertEquals(456, $request->post->yyy); 528 | $this->assertEquals(1, $request->files->count()); 529 | $this->assertEquals('foo.bar', $request->files->zzz['name']); 530 | 531 | $moo($request); 532 | $this->assertEquals(200, $moo->response->code); 533 | $this->assertEquals('OK', $moo->response->message); 534 | $this->assertEquals('test', $moo->response->body); 535 | } 536 | 537 | protected function assertRouteFound(Moo $moo, array $methods, array $uris, string $expected) 538 | { 539 | foreach ($methods as $method) { 540 | foreach ($uris as $uri) { 541 | $moo(new Request(['method' => $method, 'uri' => $uri])); 542 | $this->assertEquals($expected, $moo->response->body); 543 | } 544 | } 545 | } 546 | 547 | protected function assertRouteNotFound(Moo $moo, array $methods, array $uris) 548 | { 549 | foreach ($methods as $method) { 550 | foreach ($uris as $uri) { 551 | $moo(new Request(['method' => $method, 'uri' => $uri])); 552 | $this->assertEquals(404, $moo->response->code); 553 | $this->assertEquals('Not Found', $moo->response->message); 554 | $this->assertEquals('Not Found', $moo->response->body); 555 | } 556 | } 557 | } 558 | } 559 | -------------------------------------------------------------------------------- /test/Tests/Moo/templates/partial.phtml: -------------------------------------------------------------------------------- 1 | partial -------------------------------------------------------------------------------- /test/Tests/Moo/templates/test.phtml: -------------------------------------------------------------------------------- 1 | test -------------------------------------------------------------------------------- /test/Tests/Moo/templates/test2.phtml: -------------------------------------------------------------------------------- 1 | plugin($var) ?> -------------------------------------------------------------------------------- /test/Tests/Moo/templates/test3.phtml: -------------------------------------------------------------------------------- 1 | dummy() ?> 2 | --------------------------------------------------------------------------------