├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .php_cs ├── .scrutinizer.yml ├── LICENSE ├── README.md ├── composer.json ├── docs ├── di.md ├── getting_started.md ├── handlers │ ├── cli_handler.md │ ├── controller_handler.md │ ├── direct_handler.md │ ├── index.md │ ├── jsonrpc_handler.md │ ├── pattern_handler.md │ └── rest_handler.md ├── index.md └── plugins │ └── index.md ├── doxygen.conf ├── mkdocs.yml ├── phpunit.xml ├── src └── Vectorface │ └── SnappyRouter │ ├── Authentication │ ├── AbstractAuthenticator.php │ ├── AuthenticatorInterface.php │ └── CallbackAuthenticator.php │ ├── Config │ ├── Config.php │ └── ConfigInterface.php │ ├── Controller │ └── AbstractController.php │ ├── Di │ ├── Di.php │ ├── DiInterface.php │ ├── DiProviderInterface.php │ └── ServiceProvider.php │ ├── Encoder │ ├── AbstractEncoder.php │ ├── EncoderInterface.php │ ├── JsonEncoder.php │ ├── JsonpEncoder.php │ ├── NullEncoder.php │ └── TwigViewEncoder.php │ ├── Exception │ ├── AccessDeniedException.php │ ├── EncoderException.php │ ├── HandlerException.php │ ├── InternalErrorException.php │ ├── MethodNotAllowedException.php │ ├── PluginException.php │ ├── ResourceNotFoundException.php │ ├── RouterExceptionInterface.php │ └── UnauthorizedException.php │ ├── Handler │ ├── AbstractCliHandler.php │ ├── AbstractHandler.php │ ├── AbstractRequestHandler.php │ ├── BatchRequestHandlerInterface.php │ ├── CliTaskHandler.php │ ├── ControllerHandler.php │ ├── DirectScriptHandler.php │ ├── JsonRpcHandler.php │ ├── PatternMatchHandler.php │ └── RestHandler.php │ ├── Plugin │ ├── AbstractControllerPlugin.php │ ├── AbstractPlugin.php │ ├── AccessControl │ │ └── CrossOriginRequestPlugin.php │ ├── Authentication │ │ ├── AbstractAuthenticationPlugin.php │ │ └── HttpBasicAuthenticationPlugin.php │ ├── ControllerPluginInterface.php │ ├── HttpHeader │ │ └── RouterHeaderPlugin.php │ └── PluginInterface.php │ ├── Request │ ├── AbstractRequest.php │ ├── HttpRequest.php │ ├── HttpRequestInterface.php │ ├── JsonRpcRequest.php │ └── RequestInterface.php │ ├── Response │ ├── AbstractResponse.php │ ├── JsonRpcResponse.php │ ├── Response.php │ └── ResponseInterface.php │ ├── SnappyRouter.php │ └── Task │ ├── AbstractTask.php │ └── TaskInterface.php └── tests ├── Vectorface └── SnappyRouterTests │ ├── Authentication │ └── CallbackAuthenticatorTest.php │ ├── Config │ └── ConfigTest.php │ ├── Controller │ ├── NonNamespacedController.php │ ├── TestDummyController.php │ └── Views │ │ └── test │ │ ├── array.twig │ │ └── default.twig │ ├── Di │ ├── DiTest.php │ └── ServiceProviderTest.php │ ├── Encoder │ ├── AbstractEncoderTest.php │ ├── JsonEncoderTest.php │ ├── JsonpEncoderTest.php │ └── NullEncoderTest.php │ ├── Exception │ ├── InternalErrorExceptionTest.php │ └── MethodNotAllowedExceptionTest.php │ ├── Handler │ ├── ControllerHandlerTest.php │ ├── DirectScriptHandlerTest.php │ ├── JsonRpcHandlerTest.php │ ├── PatternMatchHandlerTest.php │ ├── RestHandlerTest.php │ └── test_script.php │ ├── Plugin │ ├── AccessControl │ │ └── CrossOriginRequestPluginTest.php │ ├── Authentication │ │ ├── AbstractAuthenticationPluginTest.php │ │ ├── HttpBasicAuthenticationPluginTest.php │ │ └── TestAuthenticationPlugin.php │ ├── HttpHeader │ │ └── RouterHeaderPluginTest.php │ ├── TestPlugin.php │ └── TestPluginTest.php │ ├── Request │ ├── HttpRequestTest.php │ └── JsonRpcRequestTest.php │ ├── Response │ └── JsonRpcResponseTest.php │ ├── SnappyRouterTest.php │ └── Task │ ├── CliTaskHandlerTest.php │ └── DummyTestTask.php └── bootstrap.php /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | branches: 9 | - master 10 | 11 | jobs: 12 | test: 13 | name: PHP ${{ matrix.php-versions }} on ${{ matrix.operating-system }} 14 | runs-on: ${{ matrix.operating-system }} 15 | strategy: 16 | fail-fast: false 17 | matrix: 18 | operating-system: [ubuntu-latest] 19 | php-versions: ['8.0', '8.1', '8.2', '8.3'] 20 | 21 | steps: 22 | - name: Checkout 23 | uses: actions/checkout@v4 24 | 25 | - name: Setup PHP, with composer and extensions 26 | uses: shivammathur/setup-php@v2 #https://github.com/shivammathur/setup-php 27 | with: 28 | php-version: ${{ matrix.php-versions }} 29 | coverage: pcov 30 | tools: composer:v2 31 | env: 32 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 33 | 34 | - name: Get composer cache directory 35 | id: composer-cache 36 | run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT 37 | 38 | - name: Setup problem matchers 39 | run: | 40 | echo "::add-matcher::${{ runner.tool_cache }}/php.json" 41 | echo "::add-matcher::${{ runner.tool_cache }}/phpunit.json" 42 | 43 | - name: Cache composer dependencies 44 | uses: actions/cache@v3 45 | with: 46 | path: ${{ steps.composer-cache.outputs.dir }} 47 | key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }} 48 | restore-keys: ${{ runner.os }}-composer- 49 | 50 | - name: Install Composer dependencies 51 | run: composer install --prefer-dist 52 | 53 | - name: Test with phpunit 54 | run: composer test 55 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | .php_cs.cache 3 | composer.lock 4 | composer.phar 5 | coverage/ 6 | site/ 7 | vendor/ 8 | .phpunit.result.cache 9 | -------------------------------------------------------------------------------- /.php_cs: -------------------------------------------------------------------------------- 1 | true, 4 | // additional rules 5 | 'array_syntax' => ['syntax' => 'short'], 6 | 'binary_operator_spaces' => [ 7 | 'default' => 'single_space', 8 | 'operators' => [ 9 | '=>' => 'align_single_space_minimal', 10 | ], 11 | ], 12 | 'cast_spaces' => false, 13 | 'combine_consecutive_issets' => true, 14 | 'function_declaration' => ['closure_function_spacing' => 'none'], 15 | 'function_typehint_space' => true, 16 | 'hash_to_slash_comment' => true, 17 | 'include' => true, 18 | 'method_chaining_indentation' => true, 19 | 'no_blank_lines_after_class_opening' => true, 20 | 'no_closing_tag' => true, 21 | 'no_empty_statement' => true, 22 | 'no_multiline_whitespace_before_semicolons' => true, 23 | 'no_short_echo_tag' => true, 24 | 'no_trailing_whitespace' => true, 25 | 'no_trailing_whitespace_in_comment' => true, 26 | 'no_unneeded_control_parentheses' => ['return'], 27 | 'no_useless_return' => true, 28 | 'no_whitespace_before_comma_in_array' => true, 29 | 'no_whitespace_in_blank_line' => true, 30 | 'not_operator_with_successor_space' => false, 31 | 'semicolon_after_instruction' => true, 32 | 'standardize_not_equals' => true, 33 | 'ternary_operator_spaces' => true, 34 | 'ternary_to_null_coalescing' => true, 35 | 'trim_array_spaces' => true, 36 | 'whitespace_after_comma_in_array' => true, 37 | ]; 38 | $excludes = [ 39 | 'vendor', 40 | 'node_modules', 41 | ]; 42 | return PhpCsFixer\Config::create() 43 | ->setRules($rules) 44 | ->setFinder( 45 | PhpCsFixer\Finder::create() 46 | ->exclude($excludes) 47 | ->notName('*.js') 48 | ->notName('*.css') 49 | ->notName('*.md') 50 | ->notName('*.xml') 51 | ->notName('*.yml') 52 | ->notName('*.tpl.php') 53 | ); 54 | -------------------------------------------------------------------------------- /.scrutinizer.yml: -------------------------------------------------------------------------------- 1 | tools: 2 | external_code_coverage: true 3 | 4 | checks: 5 | php: 6 | code_rating: true -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 VectorFace, Inc. 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 13 | all 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 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SnappyRouter 2 | 3 | [![Build Status](https://travis-ci.org/Vectorface/SnappyRouter.svg?branch=master)](https://travis-ci.org/Vectorface/SnappyRouter) 4 | [![Code Coverage](https://scrutinizer-ci.com/g/Vectorface/SnappyRouter/badges/coverage.png?b=master)](https://scrutinizer-ci.com/g/Vectorface/SnappyRouter/?branch=master) 5 | [![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/Vectorface/SnappyRouter/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/Vectorface/SnappyRouter/?branch=master) 6 | [![Latest Stable Version](https://poser.pugx.org/Vectorface/Snappy-Router/v/stable.svg)](https://packagist.org/packages/Vectorface/Snappy-Router) 7 | [![License](https://poser.pugx.org/Vectorface/Snappy-Router/license.svg)](https://packagist.org/packages/Vectorface/Snappy-Router) 8 | 9 | SnappyRouter is a lightweight router written in PHP. The router offers features 10 | standard in most other routers such as: 11 | 12 | - Controller/Action based routes 13 | - Rest-like routes with API versioning 14 | - Pattern matching routes (based off [nikic/FastRoute](https://github.com/nikic/FastRoute)) 15 | - Direct file invocation (wrap paths to specific files through the router) 16 | 17 | SnappyRouter makes it easy to write your own routing handler for any imaginable 18 | custom routing scheme. 19 | 20 | *SnappyRouter is designed to work with your existing "seasoned" 21 | codebase to provide a common entry point for your code base.* SnappyRouter is 22 | ideal for existing projects that lack the features of a modern framework. By 23 | providing a number of flexible different routing handlers, any PHP code base 24 | can be retrofitted behind the router (usually) without requiring changes to 25 | your existing code. For more information on why you want to use a router, 26 | [see the documentation](https://snappyrouter.readthedocs.org/en/latest/#why-would-i-want-to-use-snappyrouter). 27 | 28 | For more information, view the detailed [documentation](https://snappyrouter.readthedocs.org/en/latest/). 29 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vectorface/snappy-router", 3 | "description": "A quick and snappy routing framework.", 4 | "keywords": [ 5 | "MVC", "routing", "router", "drinking bird" 6 | ], 7 | "type": "library", 8 | "license": "MIT", 9 | "authors": [ 10 | { 11 | "name": "Daniel Bruce", 12 | "email": "dbruce@vectorface.com", 13 | "role": "Developer" 14 | } 15 | ], 16 | "autoload": { 17 | "psr-4": { 18 | "Vectorface\\SnappyRouter\\": "./src/Vectorface/SnappyRouter" 19 | } 20 | }, 21 | "autoload-dev": { 22 | "psr-4": { 23 | "Vectorface\\SnappyRouterTests\\": "./tests/Vectorface/SnappyRouterTests" 24 | } 25 | }, 26 | "homepage": "https://github.com/Vectorface/snappy-router", 27 | "support": { 28 | "issues": "https://github.com/Vectorface/snappy-router/issues", 29 | "source": "https://github.com/Vectorface/snappy-router" 30 | }, 31 | "require": { 32 | "php": ">=8.0", 33 | "vectorface/whip": "^0.5", 34 | "twig/twig": "^2.0", 35 | "nikic/fast-route":"^1.0.0", 36 | "psr/log": "^1.0 || ^2.0 || ^3.0", 37 | "ext-json": "*" 38 | }, 39 | "require-dev": { 40 | "phpunit/phpunit": "^9.5.10", 41 | "squizlabs/php_codesniffer": "^2.0" 42 | }, 43 | "scripts": { 44 | "test": [ 45 | "@test-unit" 46 | ], 47 | "test-unit": "phpunit --color=always" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /docs/di.md: -------------------------------------------------------------------------------- 1 | # Dependency Injection 2 | 3 | SnappyRouter provides a dependency injection (DI) layer for convenience and to 4 | improve code testability. At its core, the DI layer is simply a key/value pair 5 | matching strings to services. By providing your class dependencies using DI 6 | instead of direct instantiation, you can control the inner workings of a 7 | method at runtime through the use of mock and stub objects. 8 | 9 | DI also allows for a single point of instantiation for commonly used services 10 | such as CURL wrappers, mailers, database adapters, etc. 11 | 12 | ## Adding and Retrieving Services 13 | 14 | Services can be added to the DI layer either directly or as a callback. It is 15 | recommended to use a callback so that instantiation of the service can be 16 | delayed until needed. 17 | 18 | Example: 19 | 20 | ```php 21 | set('database', function(Vectorface\SnappyRouter\Di\Di $di) { 25 | return new \PDO( 26 | 'mysql:dbname=database;host=127.0.0.1', 27 | 'username', 28 | 'password' 29 | ); 30 | }); 31 | $db = $di->get('database'); 32 | ... 33 | ``` 34 | 35 | ## Specifying the DI Layer 36 | 37 | The SnappyRouter configuration allows for specifying a default DI class. This 38 | class can be your own code (subclassing the built-in class 39 | `Vectorface\SnappyRouter\Di\Di`). For example: 40 | 41 | ```php 42 | function(Di $di) { 55 | return new \PDO( 56 | 'mysql:dbname=database;host=127.0.0.1', 57 | 'username', 58 | 'password' 59 | ); 60 | }, 61 | ... 62 | ]); 63 | } 64 | } 65 | ``` 66 | 67 | Next specify the DI class in the configuration. 68 | 69 | ```php 70 | 'Vendor\\MyNamespace\\Di\\MyDi', 76 | Config::KEY_HANDLERS => [ 77 | ... 78 | ] 79 | ]); 80 | $router = new Vectorface\SnappyRouter\SnappyRouter($config); 81 | echo $router->handleRoute(); 82 | ``` 83 | 84 | ## Bootstrapping the DI Layer 85 | 86 | SnappyRouter also provides a default DI class that can be configured when your 87 | application bootstraps. For example: 88 | 89 | ```php 90 | set('database', function ($di) { 102 | // return the database 103 | })->set('mailer', function($di) { 104 | // return the mailer 105 | })->set('...', function($di) { 106 | ... 107 | }); 108 | 109 | echo $router->handleRoute(); 110 | ``` 111 | ## Using the Built-in DI Layer 112 | 113 | Many of the classes provided by SnappyRouter also provide direct access to the 114 | DI layer for convenience. The interface 115 | `Vectorface\SnappyRouter\Di\DiProviderInterface` provides the two key methods 116 | `get` and `set`. This interface is implemented by popular classes such as 117 | `AbstractController`, `AbstractTask`, `AbstractPlugin`, etc. 118 | 119 | Here is an example with a custom controller that has access to the DI layer 120 | by simply extending `AbstractController`. 121 | 122 | ```php 123 | set('lastMethodCalled', __METHOD__); 135 | 136 | $database = $this->get('database'); // retrieve the database from the DI layer 137 | // do something with $database 138 | } 139 | } 140 | ``` 141 | -------------------------------------------------------------------------------- /docs/getting_started.md: -------------------------------------------------------------------------------- 1 | # Getting Started 2 | 3 | In this tutorial, we will build a new application from scratch using 4 | SnappyRouter. 5 | 6 | The full tutorial application can be found 7 | [here](https://github.com/Vectorface/SnappyTutorial). 8 | 9 | ## Creating the Project Structure 10 | 11 | We begin by creating the project folder and recommended subfolders. 12 | 13 | ```shell 14 | $> mkdir tutorial tutorial/app tutorial/public 15 | $> mkdir tutorial/app/Controllers tutorial/app/Views tutorial/app/Views/index tutorial/app/Models 16 | $> cd tutorial 17 | ``` 18 | 19 | The folder structure should look like this: 20 | 21 | ``` 22 | tutorial/ 23 | app/ 24 | Controllers/ 25 | Models/ 26 | Views/ 27 | index/ 28 | public/ 29 | ``` 30 | 31 | ## Redirects, Composer and index.php 32 | 33 | We will use .htaccess files to redirect all incoming requests to a single entry 34 | point in our application (`public/index.php`). 35 | 36 | Create the following files: 37 | 38 | ``` 39 | #/tutorial/.htaccess 40 | 41 | RewriteEngine on 42 | RewriteRule ^$ public/ [L] 43 | RewriteRule (.*) public/$1 [L] 44 | 45 | ``` 46 | 47 | ``` 48 | #/tutorial/public/.htaccess 49 | 50 | RewriteEngine On 51 | RewriteCond %{REQUEST_FILENAME} !-d 52 | RewriteCond %{REQUEST_FILENAME} !-f 53 | RewriteRule ^(.*)$ index.php [QSA,L] 54 | 55 | ``` 56 | 57 | We will also make use of Composer to provide dependencies and to autoload our 58 | application classes. If you do not have Composer installed, follow the 59 | documentation at [getcomposer.org](https://getcomposer.org/doc/00-intro.md). 60 | 61 | Create the file `tutorial/composer.json` with the following contents: 62 | 63 | ```json 64 | { 65 | "name": "vectorface/snappy-tutorial", 66 | "autoload": { 67 | "psr-4": { 68 | "Vectorface\\SnappyTutorial\\": "./app" 69 | } 70 | }, 71 | "require": { 72 | "php": ">=7.0.0", 73 | "vectorface/snappy-router": "v0.3.0" 74 | } 75 | } 76 | ``` 77 | 78 | and run 79 | 80 | ```shell 81 | $> composer install 82 | ``` 83 | 84 | and finally the contents of `public/index.php`. 85 | 86 | ```php 87 | TutorialDi::class, 97 | Config::KEY_HANDLERS => [ 98 | 'PageHandler' => [ 99 | Config::KEY_CLASS => ControllerHandler::class, 100 | Config::KEY_OPTIONS => [ 101 | Config::KEY_NAMESPACES => 'Vectorface\\SnappyTutorial\\Controllers', 102 | ControllerHandler::KEY_BASE_PATH => '/tutorial', 103 | ControllerHandler::KEY_VIEWS => [ 104 | ControllerHandler::KEY_VIEWS_PATH => realpath(__DIR__.'/../app/Views') 105 | ] 106 | ] 107 | ] 108 | ] 109 | ]); 110 | $router = new Vectorface\SnappyRouter\SnappyRouter($config); 111 | echo $router->handleRoute(); 112 | ``` 113 | 114 | For simplicity, we include the configuration settings directly in 115 | `public/index.php`. It is probably a better practice to store these settings 116 | in a separate config file and include it in the index. For example: 117 | 118 | ```php 119 | ... 120 | $configArray = require_once __DIR__.'/../app/config.php'; 121 | $config = new Config($configArray); 122 | ... 123 | ``` 124 | *N.B.* Any file placed in the public folder will be directly accessible through 125 | the web browser. This folder should be used for any web assets (javascript, images, 126 | css, fonts) or direct PHP scripts you wish to expose. Any script exposed through 127 | the public folder will *not* be run through SnappyRouter. 128 | 129 | ## Setting up the DI Container 130 | 131 | Dependency injection (DI) is a powerful tool for injecting services and 132 | dependencies across your application at runtime. Some common examples include 133 | the database adapter, cache adapters, mail senders, etc. 134 | 135 | For this tutorial, we specify a class to use for DI. Create the file 136 | `app/Models/TutorialDi.php` with the following contents: 137 | 138 | ```php 139 | getDiArray()); 151 | } 152 | 153 | protected function getDiArray() 154 | { 155 | return [ 156 | 'projectTitle' => function(Di $di) { 157 | return 'SnappyRouter Tutorial'; 158 | } 159 | ]; 160 | } 161 | } 162 | ``` 163 | 164 | This container registers only the `projectTitle` key. 165 | 166 | ## Controllers and Views 167 | 168 | We will set up an `IndexController` that extends our own abstract controller. 169 | It is good practice to always include your own base controller on top of 170 | `Vectorface\SnappyRouter\Controller\AbstractController` to provide common logic 171 | across all your controllers. 172 | 173 | The `BaseController` implements the `initialize` method which is invoked by 174 | SnappyRouter before any action is invoked. Note that we retrieve the 175 | `projectTitle` from the DI layer and hand it off to the view. 176 | 177 | ```php 178 | viewContext['projectTitle'] = $this->get('projectTitle'); 193 | } 194 | } 195 | ``` 196 | And the `IndexController`: 197 | 198 | ```php 199 | 'Hello SnappyRouter!' 209 | ]; 210 | } 211 | } 212 | ``` 213 | 214 | Note that there are many ways to pass variables to the view. 215 | 216 | 1. Using the associative array `$this->viewContext` provided by 217 | `Vectorface\SnappyRouter\Controller\AbstractController`. 218 | 2. Returning an associative array (this array will be merged with 219 | `$this->viewContext`). 220 | 3. Directly rendering the view with `$this->renderView`. More details for this 221 | method can be found [here](handlers/controller_handler/#integration-with-twig). 222 | 223 | We will divide our view into two files. The first file `app/Views/layout.twig` will 224 | provide common boilerplate that we could reuse across multiple pages. 225 | 226 | ```html 227 | 228 | 229 | 230 | 231 | 232 | 233 | {{ projectTitle|e }} 234 | 235 | 236 | 237 | {% block content %} 238 | {% endblock %} 239 | 240 | 241 | 242 | 243 | ``` 244 | 245 | And a very simple view for our `indexAction` in `app/Views/index/index.twig`: 246 | 247 | ```html 248 | {% extends 'layout.twig' %} 249 | 250 | {% block content %} 251 |
252 |

{{ content }}

253 |
254 | {% endblock %} 255 | ``` 256 | 257 | Once you add the `tutorial` folder to your standard web root, you should have 258 | a working application at `http://localhost/tutorial/`. 259 | -------------------------------------------------------------------------------- /docs/handlers/cli_handler.md: -------------------------------------------------------------------------------- 1 | # CLI Task Handler 2 | 3 | The CLI (command line interface) task handler allows for execution of PHP in the 4 | standard shell instead of through a web server like Apache. Command line scripts 5 | are structured as tasks, which are similar to controllers (task/action pattern). 6 | 7 | Tasks must follow the naming convention `"${NAME}Task"`. Actions do not require 8 | any naming convention. Actions can (optionally) take an array as an argument 9 | which will be populated with any additional command line options passed to the 10 | script. 11 | 12 | An example task: 13 | 14 | ```php 15 | [ 42 | 'CliHandler' => [ 43 | Config::KEY_CLASS => CliTaskHandler::class, 44 | Config::KEY_OPTIONS => [ 45 | Config::KEY_TASKS => [ 46 | 'DatabaseTask' => 'Vendor\\MyNamespace\\Tasks\\DatabaseTask' 47 | ] 48 | ] 49 | ] 50 | ] 51 | ]); 52 | $router = new Vectorface\SnappyRouter\SnappyRouter($config); 53 | echo $router->handleRoute(); 54 | ``` 55 | 56 | Suppose the above code is in a file named `router.php`. To execute the cleanup 57 | action we can use the following command: 58 | 59 | ```shell 60 | $> php router.php --task Database --action cleanup 61 | ``` 62 | 63 | # Specifying Tasks in the Configuration 64 | 65 | There are three ways to specify the list of tasks in the configuration. Tasks 66 | are listed in the `options` key within the handler. 67 | 68 | ## Explicit List of Tasks 69 | 70 | The list of tasks can be explicitly listed as a key/value pair. The key for the 71 | task must match the convention `"${NAME}Task"` and the value must be valid 72 | PHP class. 73 | 74 | Example: 75 | 76 | ```php 77 | ... 78 | Config::KEY_OPTIONS => [ 79 | Config::KEY_TASKS => [ 80 | 'DatabaseTask' => 'Vendor\\MyNamespace\\Tasks\\DatabaseTask', 81 | 'EmailTask' => 'Vendor\\MyNamespace\\Tasks\\SendEmailTask', 82 | ... 83 | ] 84 | ], 85 | ... 86 | ``` 87 | 88 | ## Registering a list of Task Namespaces 89 | 90 | If your code is namespaced, you can register a list of namespaces for 91 | SnappyRouter to use to autodetect the appropriate task class. 92 | 93 | ```php 94 | ... 95 | Config::KEY_OPTIONS => [ 96 | Config::KEY_NAMESPACES => [ 97 | 'Vendor\\MyNamespace\\Tasks', 98 | 'Vendor\\AnotherNamespace\\Tasks', 99 | ... 100 | ] 101 | ], 102 | ... 103 | ``` 104 | 105 | The namespaces will be scanned in the order listed in the array. 106 | 107 | ## Registering a Folder of Task PHP Files 108 | 109 | If your code is not namespaced, you can give SnappyRouter a list of folders 110 | to check (recursively) for a PHP file matching `${NAME}Task.php`. 111 | 112 | ```php 113 | ... 114 | Config::KEY_OPTIONS => [ 115 | Config::KEY_FOLDERS => [ 116 | '/home/user/project/app/tasks', 117 | '/home/user/project/app/moreTasks', 118 | ... 119 | ] 120 | ], 121 | ... 122 | ``` 123 | -------------------------------------------------------------------------------- /docs/handlers/direct_handler.md: -------------------------------------------------------------------------------- 1 | # Direct Script Handler 2 | 3 | The direct script handler maps web requests to raw PHP scripts. For PHP 4 | applications that do not use the front controller pattern and do not have a 5 | single entry point, this handler can be used to provide one without affecting 6 | any of the current code. 7 | 8 | ## How to use it 9 | 10 | The handler works by scanning the path for a specific prefix and matching it 11 | to a locally stored folder. 12 | 13 | For example we may have a folder structure like: 14 | 15 | ``` 16 | /home/user/ 17 | webroot/ 18 | index.html 19 | scripts/ 20 | test_script.php 21 | ``` 22 | 23 | with our web server configured to use `/home/user/webroot` as the default 24 | document root. Accessing the script directly would be done through a url like: 25 | 26 | ``` 27 | http://localhost/scripts/test_script.php 28 | ``` 29 | 30 | The handler can then be configured as such: 31 | 32 | ```php 33 | [ 40 | 'DirectHandler' => [ 41 | Config::KEY_CLASS => DirectScriptHandler::class, 42 | Config::KEY_OPTIONS => [ 43 | DirectScriptHandler::KEY_PATH_MAP => [ 44 | '/scripts/' => '/home/user/webroot/scripts', 45 | '/' => '/home/user/webroot/scripts' 46 | ] 47 | ], 48 | Config::KEY_PLUGINS => [ 49 | // optional list of plugins to put in front of your scripts 50 | ] 51 | ] 52 | ] 53 | ]); 54 | $router = new Vectorface\SnappyRouter\SnappyRouter($config); 55 | echo $router->handleRoute(); 56 | ``` 57 | 58 | ## Path Map Fallback Strategy 59 | 60 | Many application environments use virtual hosts, path aliases, symbolic 61 | links, etc. The URL to a particular script may not always be obvious and 62 | consistent across production, test, and local development environments. 63 | For this reason, the `DirectScriptHandler::KEY_PATH_MAP` config 64 | option supports multiple path prefixes which the handler will try each one 65 | iteratively (in order) until it finds a matching script. 66 | 67 | In the above example, we may have a virtual host pointing directly to the 68 | scripts folder, and the above configuration would continue to work. For example: 69 | 70 | ``` 71 | http://livesite.example.com/test_script.php 72 | ``` 73 | -------------------------------------------------------------------------------- /docs/handlers/index.md: -------------------------------------------------------------------------------- 1 | # Handlers 2 | 3 | SnappyRouter uses a number of different route handlers to serve web and CLI 4 | requests. Handlers are a core component in the router and are the starting point 5 | for customizing the router for your own application. 6 | 7 | ## What is a Handler? 8 | 9 | The router makes very few assumptions about the executing environment (command 10 | line, web server, folder structure, etc). To handle each environment 11 | differently, the router passes off control to a handler. The handler is 12 | responsible for most of the routing logic. 13 | 14 | ## Built-in Handlers 15 | 16 | SnappyRouter provides a number of built in routing handlers. 17 | 18 | ### Controller Handler 19 | 20 | The controller handler provides the "VC" part of MVC. The router assumes your 21 | web requests match a pattern such as 22 | `/prefix/controller/action/param1/param2` and will attempt to find the 23 | appropriate controller and action to invoke. Furthermore, the Twig view engine 24 | can be initialized to provide an easy to use controller-view binding. 25 | 26 | [More details](controller_handler.md) 27 | 28 | ### Rest Handler 29 | 30 | The rest handler provides REST-like API urls such as 31 | `/api/v1.2/resource/1234` and extends the Controller Handler to route to an 32 | appropriate controller. Responses are encoded as JSON by default. 33 | 34 | [More details](rest_handler.md) 35 | 36 | ### Pattern Match Handler 37 | 38 | The pattern match handler uses the powerful 39 | [FastRoute](https://github.com/nikic/FastRoute) library to allow custom routes 40 | to map directly to a callable function. This handler will seem familiar to users 41 | of [Silex](http://silex.sensiolabs.org/) or [Express.js](http://expressjs.com/). 42 | 43 | [More details](pattern_handler.md) 44 | 45 | ### Direct Script Handler 46 | 47 | This handler allows the route to fall through to an existing PHP script. If your 48 | application entry points are paths to scripts directly this handler can be used 49 | to wrap access to those scripts through the router. 50 | 51 | [More details](direct_handler.md) 52 | 53 | ### CLI Task Handler 54 | 55 | This handler provides a command line entry point for tasks. 56 | 57 | [More details](cli_handler.md) 58 | 59 | ### JSON-RPC Handler 60 | 61 | This handler is for exposing class methods to remote clients via the JSON-RPC 62 | protocol, versions 1 and 2. 63 | 64 | [More details](jsonrpc_handler.md) 65 | 66 | 67 | ## Writing your own Handler 68 | 69 | Every application has unique conventions and workflows. SnappyRouter handlers 70 | are easy to extend and building you can write your own handler for any custom 71 | routing your application may need. 72 | 73 | To begin, add a class that extends one of the abstract handler classes. For a 74 | web request handler, it is recommended to extend 75 | `Vectorface\\SnappyRouter\\Handler\\AbstractRequestHandler`. 76 | 77 | ```php 78 | [ 122 | 'MyHandler' => [ 123 | Config::KEY_CLASS => MyCustomHandler::class, 124 | Config::KEY_OPTIONS => [ 125 | // an array of options 126 | ] 127 | ] 128 | ] 129 | ]); 130 | $router = new Vectorface\SnappyRouter\SnappyRouter($config); 131 | echo $router->handleRoute(); 132 | ``` 133 | -------------------------------------------------------------------------------- /docs/handlers/jsonrpc_handler.md: -------------------------------------------------------------------------------- 1 | # JSON-RPC 1.0 + 2.0 Handler 2 | 3 | The class `Vectorface\SnappyRouter\Handler\JsonRpcHandler` provides a means of 4 | calling class methods via the 5 | [JSON-RPC](http://json-rpc.org/wiki/specification) protocol. Version 1.0 and 6 | 2.0 of the protocol should both be fully supported. 7 | 8 | Some items of interest in this implementation: 9 | 10 | * Supports batch calls; Many calls in a single request. 11 | * Notification calls; Drops responses without a request id. 12 | * Handles both parameter arrays, and named parameters. 13 | * Transparently handles both the JSON-RPC 1.0 and 2.0 spec. 14 | * JSON-RPC 1.0 class hinting *is deliberately not supported*. 15 | 16 | ## Why JSON-RPC? 17 | 18 | JSON-RPC allows calling server-side methods on the client side nearly 19 | transparently. The only limitations are generally limitations of JSON 20 | serialization; PHP associative arrays become untyped JSON objects. 21 | 22 | Put differently, a remote procedure call can be abstracted out to look almost 23 | identical to a local method call, making it very easy to integrate server-side 24 | calls into client-side or remote code. 25 | 26 | API clients can also be simpler too because the local and remote method 27 | signatures can be the same. There is no need to map URLs and/or parameters as 28 | in REST APIs. 29 | 30 | For example, one could expose the following class on the server: 31 | 32 | ```php 33 | add(1, 1); // 2! 51 | ``` 52 | 53 | Or it could be called remotely: 54 | 55 | ```php 56 | $adder = new MyJsonRpcClient("http://.../Adder"); // Any JSON-RPC client. 57 | $adder->add(1, 1); // 2! 58 | ``` 59 | 60 | ## Usage 61 | 62 | To expose the example `Adder` class listed in the previous section, configure a 63 | router instance as follows: 64 | 65 | ```php 66 | [ 74 | 'JsonRpcHandler' => [ 75 | Config::KEY_CLASS => JsonRpcHandler::class, 76 | Config::KEY_OPTIONS => [ 77 | Config::KEY_SERVICES => [ 78 | 'Adder' => Adder::class, // Adder, as above 79 | ], 80 | ] 81 | ] 82 | ] 83 | ]); 84 | $router = new Vectorface\SnappyRouter\SnappyRouter($config); 85 | echo $router->handleRoute(); 86 | ``` 87 | 88 | If the router is called with a URI ending in `Adder(.php)` and a valid JSON-RPC 89 | request POSTed for the method "add", the router will response with a JSON-RPC 90 | encoded response with the sum of the two arguments. 91 | -------------------------------------------------------------------------------- /docs/handlers/pattern_handler.md: -------------------------------------------------------------------------------- 1 | # Pattern Match Handler 2 | 3 | Direct pattern matching in SnappyRouter is supported by the pattern match 4 | handler. Similar to many other popular routers, the routing configuration is 5 | specified as a list of regular expression patterns mapping to callback 6 | functions. 7 | 8 | An example configuration: 9 | 10 | ```php 11 | [ 18 | 'PatternHandler' => [ 19 | Config::KEY_CLASS => PatternMatchHandler::class, 20 | Config::KEY_OPTIONS => [ 21 | PatternMatchHandler::KEY_ROUTES => [ 22 | '/users/{name}/{id:[0-9]+}' => [ 23 | 'get' => function ($routeParams) { 24 | // invoked only for GET calls 25 | }, 26 | 'post' => function ($routeParams) { 27 | // invoked only for POST calls 28 | } 29 | ], 30 | '/users' => function($routeParams) { 31 | // invoked for all HTTP verbs 32 | } 33 | ] 34 | ] 35 | ] 36 | ] 37 | ]); 38 | $router = new Vectorface\SnappyRouter\SnappyRouter($config); 39 | echo $router->handleRoute(); 40 | ``` 41 | 42 | ## Specifying Routes 43 | 44 | Routes are listed as arrays using regular expressions with named parameters. For the documentation on the individual patterns see the 45 | [FastRoute library](https://github.com/nikic/FastRoute). The routes must be 46 | specified in the options of the handler. 47 | 48 | The patterns should be listed as the keys to the array and must map to a 49 | `callable` function or another array with HTTP verbs as keys. 50 | 51 | ### Examples 52 | 53 | A route using the same callback for all HTTP verbs. 54 | 55 | ```php 56 | ... 57 | PatternMatchHandler::KEY_ROUTES => [ 58 | '/api/{version}/{controller}/{action}' => function ($routeParams) { 59 | // invoked for all HTTP verbs 60 | } 61 | ], 62 | ... 63 | ``` 64 | 65 | A route specifying individual HTTP verbs. 66 | 67 | ```php 68 | ... 69 | PatternMatchHandler::KEY_ROUTES => [ 70 | '/api/{version}/{controller}/{action}' => [ 71 | 'get' => function ($routeParams) { 72 | // handle GET requests 73 | }, 74 | 'post' => function ($routeParams) { 75 | // handle POST requests 76 | }, 77 | 'put' => function ($routeParams) { 78 | // handle PUT requests 79 | }, 80 | 'delete' => function ($routeParams) { 81 | // handle DELETE requests 82 | }, 83 | 'options' => function ($routeParams) { 84 | // handle OPTIONS requests 85 | }, 86 | 'head' => function ($routeParams) { 87 | // handle HEAD requests 88 | } 89 | ] 90 | ], 91 | ... 92 | ``` 93 | -------------------------------------------------------------------------------- /docs/handlers/rest_handler.md: -------------------------------------------------------------------------------- 1 | # Rest Handler 2 | 3 | The class `Vectorface\SnappyRouter\Handler\RestHandler` provides a simple 4 | "by convention" api routing handler that builds on top of the 5 | [controller handler](handlers/controller_handler) by mapping specific route 6 | patterns to controllers and actions. 7 | 8 | ## Rest Routing 9 | 10 | The following route patterns are supported: 11 | 12 | ``` 13 | /(optional/base/path/)v{$apiVersion}/${controller}/${objectId}/${action} 14 | /(optional/base/path/)v{$apiVersion}/${controller}/${action}/${objectId} 15 | /(optional/base/path/)v{$apiVersion}/${controller}/${action} 16 | /(optional/base/path/)v{$apiVersion}/${controller}/${objectId} 17 | /(optional/base/path/)v{$apiVersion}/${controller} 18 | ``` 19 | 20 | Examples: 21 | 22 | ``` 23 | /api/v1.2/users/1234/details 24 | /api/v1.2/users/details/1234 25 | /api/v1.2/users/search 26 | /api/v1.2/users/1234 27 | /api/v1.2/users 28 | ``` 29 | 30 | ## JSON Serialization 31 | 32 | Unlike the Twig view handler used in standard controller handler, the rest 33 | handler is configured by default to encode all responses in JSON text. To 34 | use a different encoder it is recommended to subclass the `RestHandler` class 35 | and override a couple of methods. 36 | 37 | Example: 38 | 39 | ```php 40 | getDetails(); 92 | } 93 | } 94 | ``` 95 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # SnappyRouter 2 | 3 | SnappyRouter is a lightweight router written in PHP. The router offers features 4 | standard in most other routers such as: 5 | 6 | - Controller/Action based routes 7 | - Rest-like routes with API versioning 8 | - Pattern matching routes (based off [nikic/FastRoute](https://github.com/nikic/FastRoute)) 9 | - Direct file invocation (wrap paths to specific files through the router) 10 | 11 | SnappyRouter makes it easy to write your own routing handler for any imaginable 12 | custom routing scheme. It is designed to work with your existing "seasoned" 13 | codebase to provide a common entry point for your code base. 14 | 15 | ## What makes SnappyRouter unique? 16 | 17 | SnappyRouter is very fast and flexible. The router can be put in front of 18 | existing PHP scripts with very little noticeable overhead. The core design of 19 | the router means it gets out of the way quickly and executes your own code as 20 | soon as possible. *You should be able to add SnappyRouter to your existing 21 | project without modifying any existing code*. 22 | 23 | SnappyRouter supports PHP 5.3, 5.4, 5.5, and 5.6, as well as HHVM. 24 | 25 | ## Why would I want to use SnappyRouter? 26 | 27 | Modern best practices in PHP applications has lead to the so-called 28 | [front controller pattern](https://en.wikipedia.org/wiki/Front_Controller_pattern) 29 | (a single entry point to your application). The benefits of a single entry 30 | point include: 31 | 32 | - Better flexibility over global initialization and shut down (say goodbye to 33 | "global.php", "common.php" and auto_prepend_file directives). 34 | - Easier to manage code base due to each entry point not needing to include 35 | global setup code. 36 | - A more consistent project code base (your project is no longer a collection 37 | of related PHP scripts). 38 | - Flexible pretty URLs (great for SEO and application UX). 39 | -------------------------------------------------------------------------------- /docs/plugins/index.md: -------------------------------------------------------------------------------- 1 | # Plugins 2 | 3 | SnappyRouter provides a basic plugin system allowing you to interject your own 4 | code before the router hands off control and after the route has finished. 5 | 6 | Note that to interrupt the standard route, the plugin method *must* throw an 7 | exception. The return value of the plugin methods are not used. 8 | 9 | ## Enabling Plugins 10 | 11 | Plugins are specified in the configuration under options for each handler. 12 | Plugins can be specified as an arbitrary key mapping to the name of the 13 | class or as an array with fields for the file and class. 14 | 15 | ```php 16 | ... 17 | Config::KEY_HANDLERS => array( 18 | 'MyHandler' => array( 19 | Config::KEY_CLASS => 'Vendor\\MyNamespace\\Handler\\MyCustomHandler', 20 | Config::KEY_OPTIONS => array( 21 | Config::KEY_PLUGINS => array( 22 | 'RouterHeaderPlugin' => 'Vectorface\\SnappyRouter\\Plugin\\HttpHeader\\RouterHeaderPlugin', 23 | 'MyCustomPlugin' => array( 24 | Config::KEY_CLASS => '\MyCustomPlugin', 25 | Config::KEY_FILE => '/home/user/project/plugins/MyCustomPlugin.php' 26 | ) 27 | ) 28 | ) 29 | ) 30 | ) 31 | ... 32 | ``` 33 | 34 | ## Writing your own Plugin 35 | 36 | Plugins are very easy to implement, simply extend the class 37 | `Vectorface\SnappyRouter\Plugin\AbstractPlugin` and implement the desired 38 | methods. 39 | 40 | Note that you *do not* need to implement all the methods. You are free to 41 | implement only the methods that you care about. 42 | 43 | Example: 44 | 45 | ```php 46 | get('authentication'); 93 | if ($authentication->isAuthorized() === false) { 94 | throw new UnauthorizedException('Authorization not valid.'); 95 | } 96 | } 97 | } 98 | ... 99 | ``` 100 | 101 | ### `afterFullRouteInvoked` 102 | 103 | The method `afterFullRouteInvoked` is (unsurprisingly) invoked after the 104 | router has invoked the route. This method is primarily to allow for clean up, 105 | logging, etc. 106 | 107 | An example demonstrating a specific call being logged. 108 | 109 | ```php 110 | getRequest(); 116 | if ($request->getController() === 'SomeController' && 117 | $request->getAction() === 'SomeAction') { 118 | $this->get('logger')->log('SomeAction was invoked.'); 119 | } 120 | } 121 | } 122 | ... 123 | ``` 124 | 125 | -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: 'SnappyRouter' 2 | pages: 3 | - Home: 4 | - Home: index.md 5 | - Getting Started: getting_started.md 6 | - Handlers: 7 | - Handlers: handlers/index.md 8 | - Controller Handler: handlers/controller_handler.md 9 | - Rest Handler: handlers/rest_handler.md 10 | - Pattern Match Handler: handlers/pattern_handler.md 11 | - Direct Script Handler: handlers/direct_handler.md 12 | - JSON RPC Handler: handlers/jsonrpc_handler.md 13 | - CLI Task Handler: handlers/cli_handler.md 14 | - Plugins: 15 | - Plugins: plugins/index.md 16 | - Dependency Injection: 17 | - Dependency Injection: di.md 18 | theme: 'readthedocs' 19 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 10 | 11 | tests/ 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/Vectorface/SnappyRouter/Authentication/AbstractAuthenticator.php: -------------------------------------------------------------------------------- 1 | 10 | */ 11 | abstract class AbstractAuthenticator implements AuthenticatorInterface 12 | { 13 | } 14 | -------------------------------------------------------------------------------- /src/Vectorface/SnappyRouter/Authentication/AuthenticatorInterface.php: -------------------------------------------------------------------------------- 1 | realAuthMechanism = $wrappedAuthMechanism; 13 | * } 14 | * public function authenticate($cred) { 15 | * return $this->realAuthMechanism->login($cred->user, $cred->pass); 16 | * } 17 | * } 18 | * \endcode 19 | * 20 | * @copyright Copyright (c) 2014, VectorFace, Inc. 21 | * @author J. Anderson 22 | */ 23 | interface AuthenticatorInterface 24 | { 25 | /** 26 | * Authenticate a set of credentials, typically a username and password. 27 | * 28 | * @param mixed $credentials Credentials in some form; A string username and password, an auth token, etc. 29 | * @return bool Returns true if the identity was authenticated, or false otherwise. 30 | */ 31 | public function authenticate($credentials); 32 | } 33 | -------------------------------------------------------------------------------- /src/Vectorface/SnappyRouter/Authentication/CallbackAuthenticator.php: -------------------------------------------------------------------------------- 1 | login($credentials['username'], $credentials['password']); 14 | * }); 15 | * \endcode 16 | * 17 | * @copyright Copyright (c) 2014, VectorFace, Inc. 18 | * @author J. Anderson 19 | */ 20 | class CallbackAuthenticator extends AbstractAuthenticator 21 | { 22 | /** Stores the callback to be used for authentication.*/ 23 | private $callback; 24 | 25 | /** 26 | * Wrap another authentication mechanism via a callback. 27 | * 28 | * @param Closure $callback The callback, which is expected to have the same signature as $this->authenticate. 29 | */ 30 | public function __construct(Closure $callback) 31 | { 32 | $this->callback = $callback; 33 | } 34 | 35 | /** 36 | * Authenticate a set of credentials using a callback. 37 | * 38 | * @param mixed $credentials One or more credentials; A string password, or an array for multi-factor auth. 39 | * @return bool Returns true if the identity was authenticated, or false otherwise. 40 | */ 41 | public function authenticate($credentials) 42 | { 43 | return (bool)call_user_func($this->callback, $credentials); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Vectorface/SnappyRouter/Config/Config.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | class Config implements ArrayAccess, ConfigInterface 14 | { 15 | /** the config key for the list of handlers */ 16 | const KEY_HANDLERS = 'handlers'; 17 | /** the config key for the DI provider */ 18 | const KEY_DI = 'di'; 19 | /** the config key for the list of handler options */ 20 | const KEY_OPTIONS = 'options'; 21 | /** the config key for a class */ 22 | const KEY_CLASS = 'class'; 23 | /** the config key for a file */ 24 | const KEY_FILE = 'file'; 25 | /** the config key for the list of services (deprecated) */ 26 | const KEY_SERVICES = 'services'; 27 | /** the config key for the list of controllers */ 28 | const KEY_CONTROLLERS = 'services'; 29 | /** the config key for the list of plugins */ 30 | const KEY_PLUGINS = 'plugins'; 31 | /** the config key for the list of controller namespaces */ 32 | const KEY_NAMESPACES = 'namespaces'; 33 | /** the config key for the list of controller folders */ 34 | const KEY_FOLDERS = 'folders'; 35 | /** the config key for the list of tasks */ 36 | const KEY_TASKS = 'tasks'; 37 | /** the config key for debug mode */ 38 | const KEY_DEBUG = 'debug'; 39 | 40 | // the internal config array 41 | private $config; 42 | 43 | /** 44 | * Constructor for the class. 45 | * @param mixed $config An array of config settings (or something that easily 46 | * typecasts to an array like an stdClass). 47 | */ 48 | public function __construct($config) 49 | { 50 | $this->config = (array)$config; 51 | } 52 | 53 | /** 54 | * Returns whether or not the given key exists in the config. 55 | * @param string $offset The key to be checked. 56 | * @return bool Returns true if the key exists and false otherwise. 57 | */ 58 | #[\ReturnTypeWillChange] 59 | public function offsetExists($offset) 60 | { 61 | return isset($this->config[$offset]); 62 | } 63 | 64 | /** 65 | * Returns the value associated with the key or null if no value exists. 66 | * @param string $offset The key to be fetched. 67 | * @return bool Returns the value associated with the key or null if no value exists. 68 | */ 69 | #[\ReturnTypeWillChange] 70 | public function offsetGet($offset) 71 | { 72 | return $this->offsetExists($offset) ? $this->config[$offset] : null; 73 | } 74 | 75 | /** 76 | * Sets the value associated with the given key. 77 | * 78 | * @param string $offset The key to be used. 79 | * @param mixed $value The value to be set. 80 | * @throws Exception 81 | */ 82 | #[\ReturnTypeWillChange] 83 | public function offsetSet($offset, $value) 84 | { 85 | if (null === $offset) { 86 | throw new Exception('Config values must contain a key.'); 87 | } 88 | $this->config[$offset] = $value; 89 | } 90 | 91 | /** 92 | * Removes the value set to the given key. 93 | * @param string $offset The key to unset. 94 | * @return void 95 | */ 96 | #[\ReturnTypeWillChange] 97 | public function offsetUnset($offset) 98 | { 99 | unset($this->config[$offset]); 100 | } 101 | 102 | /** 103 | * Returns the value associated with the given key. An optional default value 104 | * can be provided and will be returned if no value is associated with the key. 105 | * @param string $key The key to be used. 106 | * @param mixed $defaultValue The default value to return if the key currently 107 | * has no value associated with it. 108 | * @return mixed Returns the value associated with the key or the default value if 109 | * no value is associated with the key. 110 | */ 111 | public function get($key, $defaultValue = null) 112 | { 113 | return $this->offsetExists($key) ? $this->offsetGet($key) : $defaultValue; 114 | } 115 | 116 | /** 117 | * Sets the current value associated with the given key. 118 | * 119 | * @param string $key The key to be set. 120 | * @param mixed $value The value to be set to the key. 121 | * @throws Exception 122 | */ 123 | public function set($key, $value) 124 | { 125 | $this->offsetSet($key, $value); 126 | } 127 | 128 | /** 129 | * Returns an array representation of the whole configuration. 130 | * @return array An array representation of the whole configuration. 131 | */ 132 | public function toArray() 133 | { 134 | return $this->config; 135 | } 136 | 137 | /** 138 | * Returns whether or not we are in debug mode. 139 | * @return boolean Returns true if the router is in debug mode and false 140 | * otherwise. 141 | */ 142 | public function isDebug() 143 | { 144 | return (bool)$this->get(self::KEY_DEBUG, false); 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /src/Vectorface/SnappyRouter/Config/ConfigInterface.php: -------------------------------------------------------------------------------- 1 | 10 | */ 11 | interface ConfigInterface 12 | { 13 | /** 14 | * Returns the value associated with the given key. An optional default value 15 | * can be provided and will be returned if no value is associated with the key. 16 | * @param string $key The key to be used. 17 | * @param mixed $defaultValue The default value to return if the key currently 18 | * has no value associated with it. 19 | * @return mixed Returns the value associated with the key or the default value if 20 | * no value is associated with the key. 21 | */ 22 | public function get($key, $defaultValue = null); 23 | 24 | /** 25 | * Sets the current value associated with the given key. 26 | * @param string $key The key to be set. 27 | * @param mixed $value The value to be set to the key. 28 | * @return void 29 | */ 30 | public function set($key, $value); 31 | 32 | /** 33 | * Returns an array representation of the whole configuration. 34 | * @return array An array representation of the whole configuration. 35 | */ 36 | public function toArray(); 37 | } 38 | -------------------------------------------------------------------------------- /src/Vectorface/SnappyRouter/Controller/AbstractController.php: -------------------------------------------------------------------------------- 1 | 15 | */ 16 | abstract class AbstractController implements DiProviderInterface 17 | { 18 | /** The web request being made. */ 19 | private $request; 20 | 21 | /** The array of view context variables. */ 22 | protected $viewContext; 23 | 24 | /** The handler being used by the router. */ 25 | protected $handler; 26 | 27 | /** 28 | * This method is called before invoking any specific controller action. 29 | * Override this method to provide your own logic for the subclass but 30 | * ensure you make a call to parent::initialize() as well. 31 | * @param HttpRequest $request The web request being made. 32 | * @param AbstractRequestHandler $handler The handler the router is using. 33 | * @return AbstractController Returns $this. 34 | */ 35 | public function initialize(HttpRequest $request, AbstractRequestHandler $handler) 36 | { 37 | $this->request = $request; 38 | $this->handler = $handler; 39 | $this->viewContext = []; 40 | return $this; 41 | } 42 | 43 | /** 44 | * Renders the view for the given controller and action. 45 | * 46 | * @param array $viewVariables An array of additional parameters to add 47 | * to the existing view context. 48 | * @param string $template The name of the view template. 49 | * @return string The rendered view as a string. 50 | * @throws Exception 51 | */ 52 | public function renderView($viewVariables, $template) 53 | { 54 | $encoder = $this->handler->getEncoder(); 55 | if (method_exists($encoder, 'renderView')) { 56 | return $encoder->renderView( 57 | $template, 58 | array_merge($this->viewContext, (array)$viewVariables) 59 | ); 60 | } else { 61 | throw new Exception('The current encoder does not support the render view method.'); 62 | } 63 | } 64 | 65 | /** 66 | * Returns the request object. 67 | * @return HttpRequest The request object. 68 | */ 69 | public function getRequest() 70 | { 71 | return $this->request; 72 | } 73 | 74 | /** 75 | * Returns the view context. 76 | * @return array The view context. 77 | */ 78 | public function getViewContext() 79 | { 80 | return $this->viewContext; 81 | } 82 | 83 | /** 84 | * Retrieve an element from the DI container. 85 | * 86 | * @param string $key The DI key. 87 | * @param boolean $useCache (optional) An optional indicating whether we 88 | * should use the cached version of the element (true by default). 89 | * @return mixed Returns the DI element mapped to that key. 90 | * @throws Exception 91 | */ 92 | public function get($key, $useCache = true) 93 | { 94 | return Di::getDefault()->get($key, $useCache); 95 | } 96 | 97 | /** 98 | * Sets an element in the DI container for the specified key. 99 | * @param string $key The DI key. 100 | * @param mixed $element The DI element to store. 101 | * @return Di Returns the Di instance. 102 | */ 103 | public function set($key, $element) 104 | { 105 | return Di::getDefault()->set($key, $element); 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/Vectorface/SnappyRouter/Di/Di.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | class Di implements DiInterface 13 | { 14 | private $elements; // a cache of instantiated elements 15 | private $elementMap; // the map between keys and their elements 16 | 17 | private static $instance; // a static instance of this class for static use 18 | 19 | /** 20 | * Constructor for the class. 21 | * @param array $elementMap An optional initial set of elements to use. 22 | */ 23 | public function __construct($elementMap = []) 24 | { 25 | $this->elementMap = is_array($elementMap) ? $elementMap : []; 26 | $this->elements = []; 27 | } 28 | 29 | /** 30 | * Returns the element associated with the specified key. 31 | * @param string $element The key for the element. 32 | * @param boolean $useCache An optional flag for whether we can use the 33 | * cached version of the element (defaults to true). 34 | * @return mixed Returns the associated element. 35 | * @throws Exception Throws an exception if no element is registered for 36 | * the given key. 37 | */ 38 | public function get($element, $useCache = true) 39 | { 40 | if ($useCache && isset($this->elements[$element])) { 41 | // return the cached version 42 | return $this->elements[$element]; 43 | } 44 | 45 | if (isset($this->elementMap[$element])) { 46 | if (is_callable($this->elementMap[$element])) { 47 | // if we have callback, invoke it and cache the result 48 | $this->elements[$element] = call_user_func( 49 | $this->elementMap[$element], 50 | $this 51 | ); 52 | } else { 53 | // otherwise simply cache the result and return it 54 | $this->elements[$element] = $this->elementMap[$element]; 55 | } 56 | return $this->elements[$element]; 57 | } 58 | 59 | throw new Exception('No element registered for key: '.$element); 60 | } 61 | 62 | /** 63 | * Assigns a specific element to the given key. This method will override 64 | * any previously assigned element for the given key. 65 | * @param string $element The key for the specified element. 66 | * @param mixed $value The specified element. This can be an instance of the 67 | * element or a callback to be invoked. 68 | * @return self $this. 69 | */ 70 | public function set($element, $value) 71 | { 72 | // clear the cached element 73 | unset($this->elements[$element]); 74 | $this->elementMap[$element] = $value; 75 | return $this; 76 | } 77 | 78 | /** 79 | * Returns whether or not a given element has been registered. 80 | * @param string $element The key for the element. 81 | * @return bool true if the element is registered and false otherwise. 82 | */ 83 | public function hasElement($element) 84 | { 85 | return isset($this->elementMap[$element]); 86 | } 87 | 88 | /** 89 | * Returns an array of all registered keys. 90 | * @return array An array of all registered keys. 91 | */ 92 | public function allRegisteredElements() 93 | { 94 | return array_keys($this->elementMap); 95 | } 96 | 97 | /** 98 | * Returns the current default DI instance. 99 | * @return Di The current default DI instance. 100 | */ 101 | public static function getDefault() 102 | { 103 | if (isset(self::$instance)) { 104 | return self::$instance; 105 | } 106 | self::$instance = new self(); 107 | return self::$instance; 108 | } 109 | 110 | /** 111 | * Sets the current default DI instance.. 112 | * @param DiInterface $instance An instance of DI. 113 | * @return DiInterface Returns the new default DI instance. 114 | */ 115 | public static function setDefault(DiInterface $instance) 116 | { 117 | self::$instance = $instance; 118 | return self::$instance; 119 | } 120 | 121 | /** 122 | * Clears the current default DI instance. 123 | */ 124 | public static function clearDefault() 125 | { 126 | self::$instance = null; 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /src/Vectorface/SnappyRouter/Di/DiInterface.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | interface DiInterface 15 | { 16 | /** 17 | * Returns the element associated with the specified key. 18 | * @param string $element The key for the element. 19 | * @param boolean $useCache An optional flag for whether we can use the 20 | * cached version of the element (defaults to true). 21 | * @return mixed Returns the associated element. 22 | * @throws Exception Throws an exception if no element is registered for 23 | * the given key. 24 | */ 25 | public function get($element, $useCache = true); 26 | 27 | /** 28 | * Assigns a specific element to the given key. This method will override 29 | * any previously assigned element for the given key. 30 | * @param string $element The key for the specified element. 31 | * @param mixed $value The specified element. This can be an instance of the 32 | * element or a callback to be invoked. 33 | * @return DiInterface Returns $this. 34 | */ 35 | public function set($element, $value); 36 | 37 | /** 38 | * Returns whether or not a given element has been registered. 39 | * @param string $element The key for the element. 40 | * @return boolean Returns true if the element is registered and false otherwise. 41 | */ 42 | public function hasElement($element); 43 | 44 | /** 45 | * Returns an array of all registered elements (their keys). 46 | * @return array An array of all registered elements (their keys). 47 | */ 48 | public function allRegisteredElements(); 49 | } 50 | -------------------------------------------------------------------------------- /src/Vectorface/SnappyRouter/Di/DiProviderInterface.php: -------------------------------------------------------------------------------- 1 | 10 | */ 11 | interface DiProviderInterface 12 | { 13 | /** 14 | * Retrieve an element from the DI container. 15 | * @param string $key The DI key. 16 | * @param boolean $useCache (optional) An optional indicating whether we 17 | * should use the cached version of the element (true by default). 18 | * @return mixed Returns the DI element mapped to that key. 19 | */ 20 | public function get($key, $useCache = true); 21 | 22 | /** 23 | * Sets an element in the DI container for the specified key. 24 | * @param string $key The DI key. 25 | * @param mixed $element The DI element to store. 26 | * @return Di Returns the Di instance. 27 | */ 28 | public function set($key, $element); 29 | } 30 | -------------------------------------------------------------------------------- /src/Vectorface/SnappyRouter/Encoder/AbstractEncoder.php: -------------------------------------------------------------------------------- 1 | 10 | */ 11 | abstract class AbstractEncoder implements EncoderInterface 12 | { 13 | // an array of options for the encoder 14 | private $options; 15 | 16 | /** 17 | * Constructor for the encoder. 18 | * @param array $options An array of encoder options. 19 | */ 20 | public function __construct($options = []) 21 | { 22 | $this->options = (array)$options; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Vectorface/SnappyRouter/Encoder/EncoderInterface.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | interface EncoderInterface 14 | { 15 | /** 16 | * @param AbstractResponse $response The response to be encoded. 17 | * @return string Returns the response encoded as a string. 18 | */ 19 | public function encode(AbstractResponse $response); 20 | } 21 | -------------------------------------------------------------------------------- /src/Vectorface/SnappyRouter/Encoder/JsonEncoder.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | class JsonEncoder extends AbstractEncoder 14 | { 15 | /** 16 | * @param AbstractResponse $response The response to be encoded. 17 | * @return string Returns the response encoded as a string. 18 | * @throws EncoderException 19 | */ 20 | public function encode(AbstractResponse $response) 21 | { 22 | $responseObject = $response->getResponseObject(); 23 | if (null === $responseObject || is_array($responseObject) || is_scalar($responseObject)) { 24 | return json_encode($responseObject); 25 | } elseif (is_object($responseObject)) { 26 | if (method_exists($responseObject, 'jsonSerialize')) { 27 | return json_encode($responseObject->jsonSerialize()); 28 | } 29 | 30 | return json_encode(get_object_vars($responseObject)); 31 | } 32 | 33 | throw new EncoderException('Unable to encode response as JSON.'); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Vectorface/SnappyRouter/Encoder/JsonpEncoder.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | class JsonpEncoder extends JsonEncoder 15 | { 16 | /** the config key for the client side method to invoke */ 17 | const KEY_CLIENT_METHOD = 'clientMethod'; 18 | 19 | /** The method the client is invoking. */ 20 | private $clientMethod; 21 | 22 | /** 23 | * Constructor for the encoder. 24 | * 25 | * @param array $options (optional) The array of plugin options. 26 | * @throws Exception 27 | */ 28 | public function __construct($options = []) 29 | { 30 | parent::__construct($options); 31 | if (!isset($options[self::KEY_CLIENT_METHOD])) { 32 | throw new Exception('Client method missing from plugin options.'); 33 | } 34 | $this->clientMethod = (string)$options[self::KEY_CLIENT_METHOD]; 35 | } 36 | 37 | /** 38 | * @param AbstractResponse $response The response to be encoded. 39 | * @return string Returns the response encoded in JSON. 40 | * @throws EncoderException 41 | */ 42 | public function encode(AbstractResponse $response) 43 | { 44 | return sprintf( 45 | '%s(%s);', 46 | $this->clientMethod, 47 | parent::encode($response) 48 | ); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/Vectorface/SnappyRouter/Encoder/NullEncoder.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | class NullEncoder extends AbstractEncoder 13 | { 14 | /** 15 | * @param AbstractResponse $response The response to be encoded. 16 | * @return string Returns the response encoded as a string. 17 | */ 18 | public function encode(AbstractResponse $response) 19 | { 20 | if (is_string($response->getResponseObject())) { 21 | return $response->getResponseObject(); 22 | } 23 | 24 | return ''; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Vectorface/SnappyRouter/Encoder/TwigViewEncoder.php: -------------------------------------------------------------------------------- 1 | 18 | */ 19 | class TwigViewEncoder extends AbstractEncoder 20 | { 21 | // the template to encode 22 | private $template; 23 | 24 | // the twig view environment 25 | private $viewEnvironment; 26 | 27 | /** 28 | * Constructor for the encoder. 29 | * 30 | * @param array $viewConfig The view configuration. 31 | * @param string $template The name of the default template to render 32 | * @noinspection PhpMissingParentConstructorInspection 33 | * @throws InternalErrorException 34 | */ 35 | public function __construct($viewConfig, $template) 36 | { 37 | if (!isset($viewConfig[ControllerHandler::KEY_VIEWS_PATH])) { 38 | throw new InternalErrorException( 39 | 'View environment missing views path.' 40 | ); 41 | } 42 | $loader = new FilesystemLoader($viewConfig[ControllerHandler::KEY_VIEWS_PATH]); 43 | $this->viewEnvironment = new Environment($loader, $viewConfig); 44 | $this->template = $template; 45 | } 46 | 47 | /** 48 | * Returns the Twig view environment. 49 | * @return Environment The configured twig environment. 50 | */ 51 | public function getViewEnvironment() 52 | { 53 | return $this->viewEnvironment; 54 | } 55 | 56 | /** 57 | * @param AbstractResponse $response The response to be encoded. 58 | * @return string Returns the response encoded as a string. 59 | * @throws LoaderError|RuntimeError|SyntaxError 60 | */ 61 | public function encode(AbstractResponse $response) 62 | { 63 | $responseObject = $response->getResponseObject(); 64 | if (is_string($responseObject)) { 65 | return $responseObject; 66 | } 67 | 68 | return $this->viewEnvironment 69 | ->load($this->template) 70 | ->render((array)$responseObject); 71 | } 72 | 73 | /** 74 | * Renders an arbitrary view with arbitrary parameters. 75 | * 76 | * @param string $template The template to render. 77 | * @param array $variables The variables to use. 78 | * @return string Returns the rendered template as a string. 79 | * @throws LoaderError|RuntimeError|SyntaxError 80 | */ 81 | public function renderView($template, $variables) 82 | { 83 | return $this->getViewEnvironment() 84 | ->load($template) 85 | ->render((array)$variables); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/Vectorface/SnappyRouter/Exception/AccessDeniedException.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | class AccessDeniedException extends Exception implements RouterExceptionInterface 14 | { 15 | /** 16 | * Returns the associated status code with the exception. 17 | * @return int The associated status code. 18 | */ 19 | public function getAssociatedStatusCode() 20 | { 21 | return AbstractResponse::RESPONSE_FORBIDDEN; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Vectorface/SnappyRouter/Exception/EncoderException.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | class EncoderException extends InternalErrorException 11 | { 12 | } 13 | -------------------------------------------------------------------------------- /src/Vectorface/SnappyRouter/Exception/HandlerException.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | class HandlerException extends InternalErrorException 11 | { 12 | } 13 | -------------------------------------------------------------------------------- /src/Vectorface/SnappyRouter/Exception/InternalErrorException.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | class InternalErrorException extends Exception implements RouterExceptionInterface 14 | { 15 | /** 16 | * Returns the associated status code with the exception. By default, most exceptions correspond 17 | * to a server error (HTTP 500). Override this method if you want your exception to generate a 18 | * different status code. 19 | * @return int The associated status code. 20 | */ 21 | public function getAssociatedStatusCode() 22 | { 23 | return AbstractResponse::RESPONSE_SERVER_ERROR; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Vectorface/SnappyRouter/Exception/MethodNotAllowedException.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | class MethodNotAllowedException extends Exception implements RouterExceptionInterface 14 | { 15 | // as per RFC2616 (http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4.6) 16 | // we must specify a comma separated list of allowed methods if we return 17 | // a 405 response 18 | private $allowedMethods; 19 | 20 | /** 21 | * Constructor for the method. 22 | * @param string $message The error message string. 23 | * @param array $allowedMethods The array of methods that are allowed. 24 | */ 25 | public function __construct($message, $allowedMethods) 26 | { 27 | parent::__construct($message); 28 | $this->allowedMethods = $allowedMethods; 29 | } 30 | 31 | /** 32 | * Returns the associated status code with the exception. By default, most exceptions correspond 33 | * to a server error (HTTP 500). Override this method if you want your exception to generate a 34 | * different status code. 35 | * @return int The associated status code. 36 | */ 37 | public function getAssociatedStatusCode() 38 | { 39 | if (!empty($this->allowedMethods) && is_array($this->allowedMethods)) { 40 | @header(sprintf('Allow: %s', implode(',', $this->allowedMethods))); 41 | } 42 | return AbstractResponse::RESPONSE_METHOD_NOT_ALLOWED; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Vectorface/SnappyRouter/Exception/PluginException.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | class PluginException extends InternalErrorException 11 | { 12 | } 13 | -------------------------------------------------------------------------------- /src/Vectorface/SnappyRouter/Exception/ResourceNotFoundException.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | class ResourceNotFoundException extends Exception implements RouterExceptionInterface 15 | { 16 | /** 17 | * Returns the associated status code with the exception. 18 | * @return int The associated status code. 19 | */ 20 | public function getAssociatedStatusCode() 21 | { 22 | return AbstractResponse::RESPONSE_NOT_FOUND; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Vectorface/SnappyRouter/Exception/RouterExceptionInterface.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | interface RouterExceptionInterface 11 | { 12 | /** 13 | * Returns the associated status code with the exception. 14 | * @return int The associated status code. 15 | */ 16 | public function getAssociatedStatusCode(); 17 | } 18 | -------------------------------------------------------------------------------- /src/Vectorface/SnappyRouter/Exception/UnauthorizedException.php: -------------------------------------------------------------------------------- 1 | 14 | * @author Dan Bruce 15 | */ 16 | class UnauthorizedException extends Exception implements RouterExceptionInterface 17 | { 18 | /** 19 | * Gets the status code that corresponds to this exception. This is usually 20 | * an HTTP status code. 21 | * 22 | * @return int The status code associated with this exception. 23 | */ 24 | public function getAssociatedStatusCode() 25 | { 26 | return AbstractResponse::RESPONSE_UNAUTHORIZED; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Vectorface/SnappyRouter/Handler/AbstractCliHandler.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | abstract class AbstractCliHandler extends AbstractHandler 14 | { 15 | /** 16 | * Constructor for the class. 17 | * 18 | * @param array $options An array of options for the plugin. 19 | * @throws PluginException 20 | */ 21 | public function __construct($options) 22 | { 23 | if (isset($options[Config::KEY_TASKS])) { 24 | $options[Config::KEY_CONTROLLERS] = $options[Config::KEY_TASKS]; 25 | unset($options[Config::KEY_TASKS]); 26 | } 27 | parent::__construct($options); 28 | } 29 | 30 | /** 31 | * Determines whether the current handler is appropriate for the given 32 | * path components. 33 | * @param array $components The path components as an array. 34 | * @return boolean Returns true if the handler is appropriate and false otherwise. 35 | */ 36 | abstract public function isAppropriate($components); 37 | 38 | /** 39 | * Returns whether a handler should function in a CLI environment. 40 | * @return bool Returns true if the handler should function in a CLI 41 | * environment and false otherwise. 42 | */ 43 | public function isCliHandler() 44 | { 45 | return true; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/Vectorface/SnappyRouter/Handler/AbstractHandler.php: -------------------------------------------------------------------------------- 1 | 18 | */ 19 | abstract class AbstractHandler implements DiProviderInterface 20 | { 21 | /** An array of handler-specific options */ 22 | protected $options; 23 | 24 | /** A sorted array of handler plugins */ 25 | private $plugins; 26 | 27 | /** The service provider to use */ 28 | private $serviceProvider; 29 | 30 | /** 31 | * Constructor for the class. 32 | * 33 | * @param array $options An array of options for the plugin. 34 | * @throws PluginException 35 | */ 36 | public function __construct($options) 37 | { 38 | $this->options = $options; 39 | $this->plugins = []; 40 | if (isset($options[Config::KEY_PLUGINS])) { 41 | $this->setPlugins((array)$options[Config::KEY_PLUGINS]); 42 | } 43 | // configure the service provider 44 | $services = []; 45 | if (isset($options[Config::KEY_CONTROLLERS])) { 46 | $services = (array)$options[Config::KEY_CONTROLLERS]; 47 | } 48 | $this->serviceProvider = new ServiceProvider($services); 49 | if (isset($options[Config::KEY_NAMESPACES])) { 50 | // namespace provisioning 51 | $this->serviceProvider->setNamespaces((array)$options[Config::KEY_NAMESPACES]); 52 | } elseif (isset($options[Config::KEY_FOLDERS])) { 53 | // folder provisioning 54 | $this->serviceProvider->setFolders((array)$options[Config::KEY_FOLDERS]); 55 | } 56 | } 57 | 58 | /** 59 | * Performs the actual routing. 60 | * @return mixed Returns the result of the route. 61 | */ 62 | abstract public function performRoute(); 63 | 64 | /** 65 | * Retrieve an element from the DI container. 66 | * 67 | * @param string $key The DI key. 68 | * @param boolean $useCache (optional) An optional indicating whether we 69 | * should use the cached version of the element (true by default). 70 | * @return mixed Returns the DI element mapped to that key. 71 | * @throws Exception 72 | */ 73 | public function get($key, $useCache = true) 74 | { 75 | return Di::getDefault()->get($key, $useCache); 76 | } 77 | 78 | /** 79 | * Sets an element in the DI container for the specified key. 80 | * @param string $key The DI key. 81 | * @param mixed $element The DI element to store. 82 | * @return Di Returns the Di instance. 83 | */ 84 | public function set($key, $element) 85 | { 86 | return Di::getDefault()->set($key, $element); 87 | } 88 | 89 | /** 90 | * Returns the active service provider for this handler. 91 | * @return ServiceProvider The active service provider for this handler. 92 | */ 93 | public function getServiceProvider() 94 | { 95 | return $this->serviceProvider; 96 | } 97 | 98 | /** 99 | * Returns the array of plugins registered with this handler. 100 | */ 101 | public function getPlugins() 102 | { 103 | return $this->plugins; 104 | } 105 | 106 | /** 107 | * Sets the current list of plugins. 108 | * 109 | * @param array $plugins The array of plugins. 110 | * @return AbstractHandler Returns $this. 111 | * @throws PluginException 112 | */ 113 | public function setPlugins($plugins) 114 | { 115 | $this->plugins = []; 116 | foreach ($plugins as $key => $plugin) { 117 | $pluginClass = $plugin; 118 | if (is_array($plugin)) { 119 | if (!isset($plugin[Config::KEY_CLASS])) { 120 | throw new PluginException('Invalid or missing class for plugin '.$key); 121 | } elseif (!class_exists($plugin[Config::KEY_CLASS])) { 122 | throw new PluginException('Invalid or missing class for plugin '.$key); 123 | } 124 | $pluginClass = $plugin[Config::KEY_CLASS]; 125 | } 126 | $options = []; 127 | if (isset($plugin[Config::KEY_OPTIONS])) { 128 | $options = (array)$plugin[Config::KEY_OPTIONS]; 129 | } 130 | $this->plugins[] = new $pluginClass($options); 131 | } 132 | $this->plugins = $this->sortPlugins($this->plugins); 133 | return $this; 134 | } 135 | 136 | /** 137 | * Sorts the list of plugins according to their execution order 138 | * 139 | * @param array $plugins 140 | * @return array 141 | */ 142 | private function sortPlugins($plugins) 143 | { 144 | usort($plugins, function($a, $b) { 145 | return $a->getExecutionOrder() - $b->getExecutionOrder(); 146 | }); 147 | return $plugins; 148 | } 149 | 150 | /** 151 | * Invokes the plugin hook against all the listed plugins. 152 | * @param string $hook The hook to invoke. 153 | * @param array $args The arguments to pass to the call. 154 | */ 155 | public function invokePluginsHook($hook, $args) 156 | { 157 | foreach ($this->getPlugins() as $plugin) { 158 | if (method_exists($plugin, $hook)) { 159 | call_user_func_array([$plugin, $hook], $args); 160 | } 161 | } 162 | } 163 | 164 | /** 165 | * Returns whether a handler should function in a CLI environment. 166 | * @return bool Returns true if the handler should function in a CLI 167 | * environment and false otherwise. 168 | */ 169 | abstract public function isCliHandler(); 170 | 171 | /** 172 | * Returns the active response encoder. 173 | * @return EncoderInterface Returns the response encoder. 174 | */ 175 | public function getEncoder() 176 | { 177 | return new NullEncoder(); 178 | } 179 | 180 | /** 181 | * Provides the handler with an opportunity to perform any last minute 182 | * error handling logic. The returned value will be serialized by the 183 | * handler's encoder. 184 | * @param Exception $e The exception that was thrown. 185 | * @return string Returns a serializable value that will be encoded and returned to the client. 186 | */ 187 | public function handleException(Exception $e) 188 | { 189 | return $e->getMessage(); 190 | } 191 | 192 | /** 193 | * Returns the array of options. 194 | * @return array $options The array of options. 195 | */ 196 | public function getOptions() 197 | { 198 | return $this->options; 199 | } 200 | } 201 | -------------------------------------------------------------------------------- /src/Vectorface/SnappyRouter/Handler/AbstractRequestHandler.php: -------------------------------------------------------------------------------- 1 | 15 | */ 16 | abstract class AbstractRequestHandler extends AbstractHandler 17 | { 18 | /** 19 | * Returns true if the handler determines it should handle this request and false otherwise. 20 | * @param string $path The URL path for the request. 21 | * @param array $query The query parameters. 22 | * @param array $post The post data. 23 | * @param string $verb The HTTP verb used in the request. 24 | * @return boolean Returns true if this handler will handle the request and false otherwise. 25 | */ 26 | abstract public function isAppropriate($path, $query, $post, $verb); 27 | 28 | /** 29 | * Returns a request object extracted from the request details (path, query, etc). The method 30 | * isAppropriate() must have returned true, otherwise this method should return null. 31 | * @return HttpRequest|null Returns a Request object or null if this handler is not appropriate. 32 | */ 33 | abstract public function getRequest(); 34 | 35 | /** 36 | * Provides the handler with an opportunity to perform any last minute 37 | * error handling logic. The returned value will be serialized by the 38 | * handler's encoder. 39 | * 40 | * @param Exception $e The exception that was thrown. 41 | * @return mixed Returns a serializable value that will be encoded and returned to the client. 42 | * @throws Exception 43 | */ 44 | public function handleException(Exception $e) 45 | { 46 | $responseCode = AbstractResponse::RESPONSE_SERVER_ERROR; 47 | if ($e instanceof RouterExceptionInterface) { 48 | $responseCode = $e->getAssociatedStatusCode(); 49 | } 50 | if (!headers_sent()) { 51 | http_response_code($responseCode); 52 | } 53 | return parent::handleException($e); 54 | } 55 | 56 | /** 57 | * Returns whether a handler should function in a CLI environment. 58 | * @return bool Returns true if the handler should function in a CLI 59 | * environment and false otherwise. 60 | */ 61 | public function isCliHandler() 62 | { 63 | return false; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/Vectorface/SnappyRouter/Handler/BatchRequestHandlerInterface.php: -------------------------------------------------------------------------------- 1 | 10 | */ 11 | interface BatchRequestHandlerInterface 12 | { 13 | /** 14 | * Returns an array of batched requests. 15 | * @return array An array of batched requests. 16 | */ 17 | public function getRequests(); 18 | } 19 | -------------------------------------------------------------------------------- /src/Vectorface/SnappyRouter/Handler/CliTaskHandler.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | class CliTaskHandler extends AbstractCliHandler 15 | { 16 | /** 17 | * Determines whether the current handler is appropriate for the given 18 | * path components. 19 | * @param array $components The path components as an array. 20 | * @return boolean Returns true if the handler is appropriate and false otherwise. 21 | */ 22 | public function isAppropriate($components) 23 | { 24 | $components = array_values(array_filter(array_map('trim', $components), 'strlen')); 25 | $this->options = []; 26 | if (count($components) < 5) { 27 | return false; 28 | } 29 | 30 | if ($components[1] !== '--task' || $components[3] !== '--action') { 31 | return false; 32 | } 33 | $this->options['task'] = $components[2]; 34 | $this->options['action'] = $components[4]; 35 | 36 | try { 37 | // ensure we have this task registered 38 | $this->getServiceProvider()->get($this->options['task']); 39 | } catch (Exception $e) { 40 | return false; 41 | } 42 | 43 | return true; 44 | } 45 | 46 | /** 47 | * Performs the actual routing. 48 | * 49 | * @return mixed Returns the result of the route. 50 | * @throws ResourceNotFoundException 51 | * @throws Exception 52 | */ 53 | public function performRoute() 54 | { 55 | $task = $this->getServiceProvider()->getServiceInstance($this->options['task']); 56 | if (false === method_exists($task, $this->options['action'])) { 57 | throw new ResourceNotFoundException( 58 | sprintf( 59 | '%s task does not have action %s.', 60 | $this->options['task'], 61 | $this->options['action'] 62 | ) 63 | ); 64 | } 65 | 66 | // call the task's init function 67 | if ($task instanceof TaskInterface) { 68 | $task->init($this->options); 69 | } 70 | 71 | $taskParams = array_splice($_SERVER['argv'], 5); 72 | $action = $this->options['action']; 73 | return $task->$action($taskParams); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/Vectorface/SnappyRouter/Handler/DirectScriptHandler.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | class DirectScriptHandler extends AbstractRequestHandler 14 | { 15 | /** Options key for the path mapping array */ 16 | const KEY_PATH_MAP = 'pathMap'; 17 | 18 | private $scriptPath; 19 | 20 | /** 21 | * Returns true if the handler determines it should handle this request and false otherwise. 22 | * @param string $path The URL path for the request. 23 | * @param array $query The query parameters. 24 | * @param array $post The post data. 25 | * @param string $verb The HTTP verb used in the request. 26 | * @return boolean Returns true if this handler will handle the request and false otherwise. 27 | */ 28 | public function isAppropriate($path, $query, $post, $verb) 29 | { 30 | $options = $this->getOptions(); 31 | $pathMaps = []; 32 | if (isset($options[self::KEY_PATH_MAP])) { 33 | $pathMaps = (array)$options[self::KEY_PATH_MAP]; 34 | } 35 | foreach ($pathMaps as $pathPrefix => $folder) { 36 | if (false !== ($pos = strpos($path, $pathPrefix))) { 37 | $scriptPath = $folder.DIRECTORY_SEPARATOR.substr($path, $pos + strlen($pathPrefix)); 38 | if (file_exists($scriptPath) && is_readable($scriptPath)) { 39 | $this->scriptPath = realpath($scriptPath); 40 | return true; 41 | } 42 | } 43 | } 44 | return false; 45 | } 46 | 47 | /** 48 | * Returns a request object extracted from the request details (path, query, etc). The method 49 | * isAppropriate() must have returned true, otherwise this method should return null. 50 | * @return HttpRequest|null Returns a Request object or null if this handler is not appropriate. 51 | */ 52 | public function getRequest() 53 | { 54 | return null; 55 | } 56 | 57 | /** 58 | * Performs the actual routing. 59 | * @return string Returns the result of the route. 60 | */ 61 | public function performRoute() 62 | { 63 | ob_start(); 64 | require $this->scriptPath; 65 | return ob_get_clean(); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/Vectorface/SnappyRouter/Handler/PatternMatchHandler.php: -------------------------------------------------------------------------------- 1 | 16 | */ 17 | class PatternMatchHandler extends AbstractRequestHandler 18 | { 19 | /** the config key for the list of routes */ 20 | const KEY_ROUTES = 'routes'; 21 | 22 | /** the config key for the route cache */ 23 | const KEY_CACHE = 'routeCache'; 24 | 25 | // the currently active callback 26 | private $callback; 27 | // the currently active route parameters 28 | private $routeParams; 29 | 30 | /** All supported HTTP verbs */ 31 | private static $allHttpVerbs = [ 32 | 'GET', 33 | 'POST', 34 | 'PUT', 35 | 'DELETE', 36 | 'OPTIONS' 37 | ]; 38 | 39 | /** The route information from FastRoute */ 40 | private $routeInfo; 41 | 42 | /** 43 | * Returns true if the handler determines it should handle this request and false otherwise. 44 | * @param string $path The URL path for the request. 45 | * @param array $query The query parameters. 46 | * @param array $post The post data. 47 | * @param string $verb The HTTP verb used in the request. 48 | * @return boolean Returns true if this handler will handle the request and false otherwise. 49 | */ 50 | public function isAppropriate($path, $query, $post, $verb) 51 | { 52 | $routeInfo = $this->getRouteInfo($verb, $path); 53 | if (Dispatcher::FOUND !== $routeInfo[0]) { 54 | return false; 55 | } 56 | $this->callback = $routeInfo[1]; 57 | $this->routeParams = $routeInfo[2] ?? []; 58 | return true; 59 | } 60 | 61 | /** 62 | * Returns the array of route info from the routing library. 63 | * @param string $verb The HTTP verb used in the request. 64 | * @param string $path The path to match against the patterns. 65 | * @param boolean $useCache (optional) An optional flag whether to use the 66 | * cached route info or not. Defaults to false. 67 | * @return array Returns the route info as an array. 68 | */ 69 | protected function getRouteInfo($verb, $path, $useCache = false) 70 | { 71 | if (!$useCache || !isset($this->routeInfo)) { 72 | $dispatcher = $this->getDispatcher($this->getRoutes()); 73 | $this->routeInfo = $dispatcher->dispatch(strtoupper($verb), $path); 74 | } 75 | return $this->routeInfo; 76 | } 77 | 78 | /** 79 | * Returns the array of routes. 80 | * @return array The array of routes. 81 | */ 82 | protected function getRoutes() 83 | { 84 | $options = $this->getOptions(); 85 | return $options[self::KEY_ROUTES] ?? []; 86 | } 87 | 88 | /** 89 | * Performs the actual routing. 90 | * @return mixed Returns the result of the route. 91 | */ 92 | public function performRoute() 93 | { 94 | return call_user_func($this->callback, $this->routeParams); 95 | } 96 | 97 | /** 98 | * Returns a request object extracted from the request details (path, query, etc). The method 99 | * isAppropriate() must have returned true, otherwise this method should return null. 100 | * @return HttpRequest|null Returns a Request object or null if this handler is not appropriate. 101 | */ 102 | public function getRequest() 103 | { 104 | return null; 105 | } 106 | 107 | /** 108 | * Returns an instance of the FastRoute dispatcher. 109 | * @param array $routes The array of specified routes. 110 | * @return Dispatcher The dispatcher to use. 111 | */ 112 | private function getDispatcher($routes) 113 | { 114 | $verbs = self::$allHttpVerbs; 115 | $f = function(RouteCollector $collector) use ($routes, $verbs) { 116 | foreach ($routes as $pattern => $route) { 117 | if (is_array($route)) { 118 | foreach ($route as $verb => $callback) { 119 | $collector->addRoute(strtoupper($verb), $pattern, $callback); 120 | } 121 | } else { 122 | foreach ($verbs as $verb) { 123 | $collector->addRoute($verb, $pattern, $route); 124 | } 125 | } 126 | } 127 | }; 128 | 129 | $options = $this->getOptions(); 130 | $cacheData = []; 131 | if (isset($options[self::KEY_CACHE])) { 132 | $cacheData = (array)$options[self::KEY_CACHE]; 133 | } 134 | 135 | if (empty($cacheData)) { 136 | return simpleDispatcher($f); 137 | } 138 | return cachedDispatcher($f, $cacheData); 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /src/Vectorface/SnappyRouter/Handler/RestHandler.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | class RestHandler extends ControllerHandler 14 | { 15 | /** Constants indicating the type of route */ 16 | const MATCHES_ID = 8; 17 | const MATCHES_CONTROLLER_AND_ID = 9; 18 | const MATCHES_CONTROLLER_ACTION_AND_ID = 11; 19 | 20 | /** API version pattern */ 21 | const ROUTE_PATTERN_VERSION_ONE = 'v{version:\d+}'; 22 | const ROUTE_PATTERN_VERSION_TWO = 'v{version:\d+\.\d+}'; 23 | const ROUTE_PATTERN_VERSION_THREE = 'v{version:\d+\.\d+\.\d+}'; 24 | 25 | /** object ID version pattern */ 26 | const ROUTE_PATTERN_OBJECT_ID = '{objectId:\d+}'; 27 | 28 | /** 29 | * Returns true if the handler determines it should handle this request and false otherwise. 30 | * 31 | * @param string $path The URL path for the request. 32 | * @param array $query The query parameters. 33 | * @param array $post The post data. 34 | * @param string $verb The HTTP verb used in the request. 35 | * @return boolean Returns true if this handler will handle the request and false otherwise. 36 | * @throws InternalErrorException 37 | */ 38 | public function isAppropriate($path, $query, $post, $verb) 39 | { 40 | // use the parent method to match the routes 41 | if (false === parent::isAppropriate($path, $query, $post, $verb)) { 42 | return false; 43 | } 44 | 45 | // determine the route information from the path 46 | $routeInfo = $this->getRouteInfo($verb, $path, true); 47 | $this->routeParams = [$routeInfo[2]['version']]; 48 | if ($routeInfo[1] & self::MATCHES_ID) { 49 | $this->routeParams[] = intval($routeInfo[2]['objectId']); 50 | } 51 | 52 | // use JSON encoder by default 53 | $this->encoder = new JsonEncoder(); 54 | 55 | return true; 56 | } 57 | 58 | /** 59 | * Returns the array of routes. 60 | * @return array The array of routes. 61 | */ 62 | protected function getRoutes() 63 | { 64 | $c = parent::ROUTE_PATTERN_CONTROLLER; 65 | $a = parent::ROUTE_PATTERN_ACTION; 66 | $v1 = self::ROUTE_PATTERN_VERSION_ONE; 67 | $v2 = self::ROUTE_PATTERN_VERSION_TWO; 68 | $v3 = self::ROUTE_PATTERN_VERSION_THREE; 69 | $o = self::ROUTE_PATTERN_OBJECT_ID; 70 | return [ 71 | "/$v1/$c" => self::MATCHES_CONTROLLER, 72 | "/$v1/$c/" => self::MATCHES_CONTROLLER, 73 | "/$v2/$c" => self::MATCHES_CONTROLLER, 74 | "/$v2/$c/" => self::MATCHES_CONTROLLER, 75 | "/$v3/$c" => self::MATCHES_CONTROLLER, 76 | "/$v3/$c/" => self::MATCHES_CONTROLLER, 77 | "/$v1/$c/$a" => self::MATCHES_CONTROLLER_AND_ACTION, 78 | "/$v1/$c/$a/" => self::MATCHES_CONTROLLER_AND_ACTION, 79 | "/$v2/$c/$a" => self::MATCHES_CONTROLLER_AND_ACTION, 80 | "/$v2/$c/$a/" => self::MATCHES_CONTROLLER_AND_ACTION, 81 | "/$v3/$c/$a" => self::MATCHES_CONTROLLER_AND_ACTION, 82 | "/$v3/$c/$a/" => self::MATCHES_CONTROLLER_AND_ACTION, 83 | "/$v1/$c/$o" => self::MATCHES_CONTROLLER_AND_ID, 84 | "/$v1/$c/$o/" => self::MATCHES_CONTROLLER_AND_ID, 85 | "/$v2/$c/$o" => self::MATCHES_CONTROLLER_AND_ID, 86 | "/$v2/$c/$o/" => self::MATCHES_CONTROLLER_AND_ID, 87 | "/$v3/$c/$o" => self::MATCHES_CONTROLLER_AND_ID, 88 | "/$v3/$c/$o/" => self::MATCHES_CONTROLLER_AND_ID, 89 | "/$v1/$c/$a/$o" => self::MATCHES_CONTROLLER_ACTION_AND_ID, 90 | "/$v1/$c/$a/$o/" => self::MATCHES_CONTROLLER_ACTION_AND_ID, 91 | "/$v1/$c/$o/$a" => self::MATCHES_CONTROLLER_ACTION_AND_ID, 92 | "/$v1/$c/$o/$a/" => self::MATCHES_CONTROLLER_ACTION_AND_ID, 93 | "/$v2/$c/$a/$o" => self::MATCHES_CONTROLLER_ACTION_AND_ID, 94 | "/$v2/$c/$a/$o/" => self::MATCHES_CONTROLLER_ACTION_AND_ID, 95 | "/$v2/$c/$o/$a" => self::MATCHES_CONTROLLER_ACTION_AND_ID, 96 | "/$v2/$c/$o/$a/" => self::MATCHES_CONTROLLER_ACTION_AND_ID, 97 | "/$v3/$c/$a/$o" => self::MATCHES_CONTROLLER_ACTION_AND_ID, 98 | "/$v3/$c/$a/$o/" => self::MATCHES_CONTROLLER_ACTION_AND_ID, 99 | "/$v3/$c/$o/$a" => self::MATCHES_CONTROLLER_ACTION_AND_ID, 100 | "/$v3/$c/$o/$a/" => self::MATCHES_CONTROLLER_ACTION_AND_ID, 101 | ]; 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/Vectorface/SnappyRouter/Plugin/AbstractControllerPlugin.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | abstract class AbstractControllerPlugin extends AbstractPlugin implements ControllerPluginInterface 15 | { 16 | /** 17 | * Invoked before the handler decides which controller will be used. 18 | * @param AbstractHandler $handler The handler selected by the router. 19 | * @param HttpRequest $request The request to be handled. 20 | */ 21 | public function beforeControllerSelected( 22 | AbstractHandler $handler, 23 | HttpRequest $request 24 | ) { 25 | } 26 | 27 | /** 28 | * Invoked after the router has decided which controller will be used. 29 | * @param AbstractHandler $handler The handler selected by the router. 30 | * @param HttpRequest $request The request to be handled. 31 | * @param AbstractController $controller The controller determined to be used. 32 | * @param string $action The name of the action that will be invoked. 33 | */ 34 | public function afterControllerSelected( 35 | AbstractHandler $handler, 36 | HttpRequest $request, 37 | AbstractController $controller, 38 | $action 39 | ) { 40 | } 41 | 42 | /** 43 | * Invoked before the handler invokes the selected action. 44 | * @param AbstractHandler $handler The handler selected by the router. 45 | * @param HttpRequest $request The request to be handled. 46 | * @param AbstractController $controller The controller determined to be used. 47 | * @param string $action The name of the action that will be invoked. 48 | */ 49 | public function beforeActionInvoked( 50 | AbstractHandler $handler, 51 | HttpRequest $request, 52 | AbstractController $controller, 53 | $action 54 | ) { 55 | } 56 | 57 | /** 58 | * Invoked after the handler invoked the selected action. 59 | * @param AbstractHandler $handler The handler selected by the router. 60 | * @param HttpRequest $request The request to be handled. 61 | * @param AbstractController $controller The controller determined to be used. 62 | * @param string $action The name of the action that will be invoked. 63 | * @param mixed $response The response from the controller action. 64 | */ 65 | public function afterActionInvoked( 66 | AbstractHandler $handler, 67 | HttpRequest $request, 68 | AbstractController $controller, 69 | $action, 70 | $response 71 | ) { 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/Vectorface/SnappyRouter/Plugin/AbstractPlugin.php: -------------------------------------------------------------------------------- 1 | 15 | */ 16 | abstract class AbstractPlugin implements PluginInterface, DiProviderInterface 17 | { 18 | /** the default priority of a plugin */ 19 | const PRIORITY_DEFAULT = 1000; 20 | 21 | /** A string constant indicating the whitelist/blacklist applies to all 22 | actions within a controller */ 23 | const ALL_ACTIONS = 'all'; 24 | 25 | /** The plugin options */ 26 | protected $options; 27 | 28 | // properties for plugin/service compatibility 29 | // both properties cannot be set at the same time (one or the other or both 30 | // must be null at any point) 31 | private $whitelist; 32 | private $blacklist; 33 | 34 | /** 35 | * Constructor for the plugin. 36 | * @param array $options The array of options. 37 | */ 38 | public function __construct($options) 39 | { 40 | $this->options = $options; 41 | } 42 | 43 | /** 44 | * Invoked directly after the router decides which handler will be used. 45 | * @param AbstractHandler $handler The handler selected by the router. 46 | */ 47 | public function afterHandlerSelected(AbstractHandler $handler) 48 | { 49 | } 50 | 51 | /** 52 | * Invoked after the entire route has been handled. 53 | * @param AbstractHandler $handler The handler selected by the router. 54 | */ 55 | public function afterFullRouteInvoked(AbstractHandler $handler) 56 | { 57 | } 58 | 59 | /** 60 | * Invoked if an exception is thrown during the route. 61 | * @param AbstractHandler $handler The handler selected by the router. 62 | * @param Exception $exception The exception that was thrown. 63 | */ 64 | public function errorOccurred(AbstractHandler $handler, Exception $exception) 65 | { 66 | } 67 | 68 | /** 69 | * Returns a sortable number for sorting plugins by execution priority. A lower number indicates 70 | * higher priority. 71 | * @return integer The execution priority (as a number). 72 | */ 73 | public function getExecutionOrder() 74 | { 75 | return self::PRIORITY_DEFAULT; 76 | } 77 | 78 | /** 79 | * Sets the controller/action whitelist of this particular plugin. Note that 80 | * setting a whitelist will remove any previously set blacklists. 81 | * @param array $whitelist The controller/action whitelist. 82 | * @return self Returns $this. 83 | */ 84 | public function setWhitelist($whitelist) 85 | { 86 | $this->whitelist = $whitelist; 87 | $this->blacklist = null; 88 | return $this; 89 | } 90 | 91 | /** 92 | * Sets the controller/action blacklist of this particular plugin. Note that 93 | * setting a blacklist will remove any previously set whitelists. 94 | * @param array $blacklist The controller/action blacklist. 95 | * @return self Returns $this. 96 | */ 97 | public function setBlacklist($blacklist) 98 | { 99 | $this->whitelist = null; 100 | $this->blacklist = $blacklist; 101 | return $this; 102 | } 103 | 104 | /** 105 | * Returns whether or not the given controller and action requested should 106 | * invoke this plugin. 107 | * @param string $controller The requested controller. 108 | * @param string $action The requested action. 109 | * @return boolean Returns true if the given plugin is allowed to run against 110 | * this controller/action and false otherwise. 111 | */ 112 | public function supportsControllerAndAction($controller, $action) 113 | { 114 | if (null === $this->blacklist) { 115 | if (null === $this->whitelist) { 116 | // plugin has global scope 117 | return true; 118 | } 119 | // we use a whitelist so ensure the controller is in the whitelist 120 | if (!isset($this->whitelist[$controller])) { 121 | return false; 122 | } 123 | // the whitelisted controller could be an array of actions or it could 124 | // be mapped to the "all" string 125 | if (is_array($this->whitelist[$controller])) { 126 | return in_array($action, $this->whitelist[$controller]); 127 | } 128 | return self::ALL_ACTIONS === (string)$this->whitelist[$controller]; 129 | } 130 | 131 | // if the controller isn't in the blacklist at all, we're good 132 | if (!isset($this->blacklist[$controller])) { 133 | return true; 134 | } 135 | 136 | // if the controller is not an array we return false 137 | // otherwise we check if the action is listed in the array 138 | return is_array($this->blacklist[$controller]) && 139 | !in_array($action, $this->blacklist[$controller]); 140 | } 141 | 142 | /** 143 | * Retrieve an element from the DI container. 144 | * 145 | * @param string $key The DI key. 146 | * @param boolean $useCache (optional) An optional indicating whether we 147 | * should use the cached version of the element (true by default). 148 | * @return mixed Returns the DI element mapped to that key. 149 | * @throws Exception 150 | */ 151 | public function get($key, $useCache = true) 152 | { 153 | return Di::getDefault()->get($key, $useCache); 154 | } 155 | 156 | /** 157 | * Sets an element in the DI container for the specified key. 158 | * @param string $key The DI key. 159 | * @param mixed $element The DI element to store. 160 | * @return Di Returns the Di instance. 161 | */ 162 | public function set($key, $element) 163 | { 164 | return Di::getDefault()->set($key, $element); 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /src/Vectorface/SnappyRouter/Plugin/Authentication/AbstractAuthenticationPlugin.php: -------------------------------------------------------------------------------- 1 | 18 | * @author Dan Bruce 19 | */ 20 | abstract class AbstractAuthenticationPlugin extends AbstractPlugin 21 | { 22 | /** 23 | * The default dependency injection key for the authentication mechanism. 24 | * 25 | * This also serves as the name of the DI key option, for consistency. 26 | */ 27 | const DI_KEY_AUTH = 'AuthMechanism'; 28 | 29 | /** The key for fetching the authentication mechanism from dependency 30 | injection. */ 31 | protected $authKey = self::DI_KEY_AUTH; 32 | 33 | /** 34 | * Constructor for the class. 35 | * 36 | * @param array $options An array of options for the plugin. 37 | */ 38 | public function __construct($options) 39 | { 40 | parent::__construct($options); 41 | 42 | if (isset($options[self::DI_KEY_AUTH])) { 43 | $this->authKey = $options[self::DI_KEY_AUTH]; 44 | } 45 | } 46 | 47 | /** 48 | * Invoked directly after the router decides which handler will be used. 49 | * 50 | * @param AbstractHandler $handler The handler selected by the router. 51 | * @throws InternalErrorException 52 | * @throws UnauthorizedException 53 | */ 54 | public function afterHandlerSelected(AbstractHandler $handler) 55 | { 56 | parent::afterHandlerSelected($handler); 57 | 58 | $auth = $this->get($this->authKey); 59 | if (!($auth instanceof AuthenticatorInterface)) { 60 | throw new InternalErrorException(sprintf( 61 | "Implementation of AuthenticationInterface required. Please check your %s configuration.", 62 | $this->authKey 63 | )); 64 | } 65 | 66 | if (!($credentials = $this->getCredentials())) { 67 | throw new UnauthorizedException("Authentication is required to access this resource."); 68 | } 69 | 70 | if (!$auth->authenticate($credentials)) { 71 | throw new UnauthorizedException("Authentication is required to access this resource."); 72 | } 73 | } 74 | 75 | /** 76 | * Extract credentials from the request. 77 | * 78 | * @return mixed An array of credentials; A username and password pair, or false if credentials aren't available 79 | */ 80 | abstract public function getCredentials(); 81 | } 82 | -------------------------------------------------------------------------------- /src/Vectorface/SnappyRouter/Plugin/Authentication/HttpBasicAuthenticationPlugin.php: -------------------------------------------------------------------------------- 1 | 16 | * @author Dan Bruce 17 | */ 18 | class HttpBasicAuthenticationPlugin extends AbstractAuthenticationPlugin 19 | { 20 | /** The authentication realm, usually presented to the user in a 21 | username/password dialog box. */ 22 | private $realm = "Authentication Required"; 23 | 24 | /** 25 | * Create a new HTTP/Basic Authentication plugin. 26 | * 27 | * @param array $options An associative array of options. Supports AuthMechanism and realm options. 28 | */ 29 | public function __construct($options) 30 | { 31 | parent::__construct($options); 32 | 33 | if (!empty($options['realm'])) { 34 | $this->realm = $options['realm']; 35 | } 36 | } 37 | 38 | /** 39 | * Invoked directly after the router decides which handler will be used. 40 | * 41 | * @param AbstractHandler $handler The handler selected by the router. 42 | * @throws UnauthorizedException 43 | * @throws InternalErrorException 44 | */ 45 | public function afterHandlerSelected(AbstractHandler $handler) 46 | { 47 | try { 48 | parent::afterHandlerSelected($handler); 49 | } catch (UnauthorizedException $e) { 50 | @header(sprintf('WWW-Authenticate: Basic realm="%s"', $this->realm)); 51 | throw $e; 52 | } 53 | } 54 | 55 | /** 56 | * Extract credentials from the request, PHP's PHP_AUTH_(USER|PW) server variables in this case. 57 | * 58 | * @return mixed An array of credentials; A username and password pair, or false if credentials aren't available 59 | */ 60 | public function getCredentials() 61 | { 62 | if (isset($_SERVER['PHP_AUTH_USER'], $_SERVER['PHP_AUTH_PW'])) { 63 | return [$_SERVER['PHP_AUTH_USER'], $_SERVER['PHP_AUTH_PW']]; 64 | } 65 | return false; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/Vectorface/SnappyRouter/Plugin/ControllerPluginInterface.php: -------------------------------------------------------------------------------- 1 | 16 | */ 17 | interface ControllerPluginInterface extends PluginInterface 18 | { 19 | /** 20 | * Invoked before the handler decides which controller will be used. 21 | * @param AbstractHandler $handler The handler selected by the router. 22 | * @param HttpRequest $request The request to be handled. 23 | */ 24 | public function beforeControllerSelected( 25 | AbstractHandler $handler, 26 | HttpRequest $request 27 | ); 28 | 29 | /** 30 | * Invoked after the router has decided which controller will be used. 31 | * @param AbstractHandler $handler The handler selected by the router. 32 | * @param HttpRequest $request The request to be handled. 33 | * @param AbstractController $controller The controller determined to be used. 34 | * @param string $action The name of the action that will be invoked. 35 | */ 36 | public function afterControllerSelected( 37 | AbstractHandler $handler, 38 | HttpRequest $request, 39 | AbstractController $controller, 40 | $action 41 | ); 42 | 43 | /** 44 | * Invoked before the handler invokes the selected action. 45 | * @param AbstractHandler $handler The handler selected by the router. 46 | * @param HttpRequest $request The request to be handled. 47 | * @param AbstractController $controller The controller determined to be used. 48 | * @param string $action The name of the action that will be invoked. 49 | */ 50 | public function beforeActionInvoked( 51 | AbstractHandler $handler, 52 | HttpRequest $request, 53 | AbstractController $controller, 54 | $action 55 | ); 56 | 57 | /** 58 | * Invoked after the handler invoked the selected action. 59 | * @param AbstractHandler $handler The handler selected by the router. 60 | * @param HttpRequest $request The request to be handled. 61 | * @param AbstractController $controller The controller determined to be used. 62 | * @param string $action The name of the action that will be invoked. 63 | * @param mixed $response The response from the controller action. 64 | */ 65 | public function afterActionInvoked( 66 | AbstractHandler $handler, 67 | HttpRequest $request, 68 | AbstractController $controller, 69 | $action, 70 | $response 71 | ); 72 | } 73 | -------------------------------------------------------------------------------- /src/Vectorface/SnappyRouter/Plugin/HttpHeader/RouterHeaderPlugin.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | class RouterHeaderPlugin extends AbstractPlugin 14 | { 15 | /** 16 | * Invoked directly after the router decides which handler will be used. 17 | * @param AbstractHandler $handler The handler selected by the router. 18 | */ 19 | public function afterHandlerSelected(AbstractHandler $handler) 20 | { 21 | parent::afterHandlerSelected($handler); 22 | @header('X-Router: SnappyRouter'); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Vectorface/SnappyRouter/Plugin/PluginInterface.php: -------------------------------------------------------------------------------- 1 | 15 | */ 16 | interface PluginInterface 17 | { 18 | /** 19 | * Invoked directly after the router decides which handler will be used. 20 | * @param AbstractHandler $handler The handler selected by the router. 21 | */ 22 | public function afterHandlerSelected(AbstractHandler $handler); 23 | 24 | /** 25 | * Invoked after the entire route has been handled. 26 | * @param AbstractHandler $handler The handler selected by the router. 27 | */ 28 | public function afterFullRouteInvoked(AbstractHandler $handler); 29 | 30 | /** 31 | * Invoked if an exception is thrown during the route. 32 | * @param AbstractHandler $handler The handler selected by the router. 33 | * @param Exception $exception The exception that was thrown. 34 | */ 35 | public function errorOccurred(AbstractHandler $handler, Exception $exception); 36 | 37 | /** 38 | * Returns a sortable number for sorting plugins by execution priority. A lower number indicates 39 | * higher priority. 40 | * @return integer The execution priority (as a number). 41 | */ 42 | public function getExecutionOrder(); 43 | 44 | /** 45 | * Sets the controller/action whitelist of this particular plugin. Note that 46 | * setting a whitelist will remove any previously set blacklists. 47 | * @param array $whitelist The controller/action whitelist. 48 | * @return self Returns $this. 49 | */ 50 | public function setWhitelist($whitelist); 51 | 52 | /** 53 | * Sets the controller/action blacklist of this particular plugin. Note that 54 | * setting a blacklist will remove any previously set whitelists. 55 | * @param array $blacklist The controller/action blacklist. 56 | * @return self Returns $this. 57 | */ 58 | public function setBlacklist($blacklist); 59 | 60 | /** 61 | * Returns whether or not the given controller and action requested should 62 | * invoke this plugin. 63 | * @param string $controller The requested controller. 64 | * @param string $action The requested action. 65 | * @return boolean Returns true if the given plugin is allowed to run against 66 | * this controller/action and false otherwise. 67 | */ 68 | public function supportsControllerAndAction($controller, $action); 69 | } 70 | -------------------------------------------------------------------------------- /src/Vectorface/SnappyRouter/Request/AbstractRequest.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | class AbstractRequest implements RequestInterface 11 | { 12 | /** The controller to use in the request. */ 13 | private $controller; 14 | /** The action to invoke in the request. */ 15 | private $action; 16 | 17 | /** 18 | * Constructor for the abstract request. 19 | * @param string $controller The controller to be used. 20 | * @param string $action The action to be invoked. 21 | */ 22 | public function __construct($controller, $action) 23 | { 24 | $this->setController($controller); 25 | $this->setAction($action); 26 | } 27 | 28 | /** 29 | * Returns the controller to be used in the request. 30 | * @return string Returns the controller DI key to be used in the request. 31 | */ 32 | public function getController() 33 | { 34 | return $this->controller; 35 | } 36 | 37 | /** 38 | * Sets the controller to be used in the request. 39 | * @param string $controller The controller DI key to be used in the request. 40 | * @return RequestInterface Returns $this. 41 | */ 42 | public function setController($controller) 43 | { 44 | $this->controller = $controller; 45 | return $this; 46 | } 47 | 48 | /** 49 | * Returns the action to be invoked as a string. 50 | * @return string The action to be invoked. 51 | */ 52 | public function getAction() 53 | { 54 | return $this->action; 55 | } 56 | 57 | /** 58 | * Sets the action to be invoked by the request 59 | * @param string $action The action to be invoked by the request. 60 | * @return RequestInterface Returns $this. 61 | */ 62 | public function setAction($action) 63 | { 64 | $this->action = (string)$action; 65 | return $this; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/Vectorface/SnappyRouter/Request/HttpRequestInterface.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | interface HttpRequestInterface 11 | { 12 | /** 13 | * Returns the HTTP verb used in the request. 14 | * @return string The HTTP verb used in the request. 15 | */ 16 | public function getVerb(); 17 | 18 | /** 19 | * Sets the HTTP verb used in the request. 20 | * @param string $verb The HTTP verb used in the request. 21 | * @return HttpRequestInterface Returns $this. 22 | */ 23 | public function setVerb($verb); 24 | 25 | /** 26 | * Returns the GET data parameter associated with the specified key. 27 | * @param string $param The GET data parameter. 28 | * @param mixed $defaultValue The default value to use when the key is not present. 29 | * @param array $filters The array of filters to apply to the data. 30 | * @return mixed Returns the data from the GET parameter after being filtered (or 31 | * the default value if the parameter is not present) 32 | */ 33 | public function getQuery($param, $defaultValue = null, $filters = []); 34 | 35 | /** 36 | * Sets all the QUERY data for the current request. 37 | * @param array $queryData The query data for the current request. 38 | * @return HttpRequestInterface Returns $this. 39 | */ 40 | public function setQuery($queryData); 41 | 42 | /** 43 | * Returns the POST data parameter associated with the specified key. 44 | * @param string $param The POST data parameter. 45 | * @param mixed $defaultValue The default value to use when the key is not present. 46 | * @param array $filters The array of filters to apply to the data. 47 | * @return mixed Returns the data from the POST parameter after being filtered (or 48 | * the default value if the parameter is not present) 49 | */ 50 | public function getPost($param, $defaultValue = null, $filters = []); 51 | 52 | /** 53 | * Sets all the POST data for the current request. 54 | * @param array $postData The post data for the current request. 55 | * @return HttpRequestInterface Returns $this. 56 | */ 57 | public function setPost($postData); 58 | 59 | /** 60 | * Returns an array of all the input parameters from the query and post data. 61 | * @return array An array of all input. 62 | */ 63 | public function getAllInput(); 64 | } 65 | -------------------------------------------------------------------------------- /src/Vectorface/SnappyRouter/Request/JsonRpcRequest.php: -------------------------------------------------------------------------------- 1 | method)) { 31 | $action = $payload->method; 32 | } 33 | parent::__construct($controller, $action, $verb); 34 | $this->payload = $payload; 35 | } 36 | 37 | /** 38 | * Returns the POST data parameter associated with the specified key. 39 | * 40 | * Since JSON-RPC and POST'ed data are mutually exclusive this returns null, or the default if provided. 41 | * 42 | * @param string $param The POST data parameter to retrieve. 43 | * @param mixed $defaultValue The default value to use when the key is not present. 44 | * @param mixed $filters The array of filters (or single filter) to apply to the data. Ignored. 45 | * @return mixed Returns null because POST is not possible, or the default value if the parameter is not present. 46 | */ 47 | public function getPost($param, $defaultValue = null, $filters = []) 48 | { 49 | return $defaultValue ?? null; 50 | } 51 | 52 | /** 53 | * Get the request version. 54 | * 55 | * @return string The request's version string. "1.0" is assumed if version is not present in the request. 56 | */ 57 | public function getVersion() 58 | { 59 | return $this->payload->jsonrpc ?? "1.0"; 60 | } 61 | 62 | /** 63 | * Get the request method. 64 | * 65 | * @return string The request method name. 66 | */ 67 | public function getMethod() 68 | { 69 | return $this->payload->method; 70 | } 71 | 72 | /** 73 | * Get the request identifier 74 | * 75 | * @return mixed The request identifier. This is generally a string, but the JSON-RPC spec isn't strict. 76 | */ 77 | public function getIdentifier() 78 | { 79 | return $this->payload->id ?? null; 80 | } 81 | 82 | /** 83 | * Get request parameters. 84 | * 85 | * Note: Since PHP does not support named params, named params are turned into a single request object parameter. 86 | * 87 | * @return array An array of request paramters 88 | */ 89 | public function getParameters() 90 | { 91 | if (isset($this->payload->params)) { 92 | /* JSON-RPC 2 can pass named params. For PHP's sake, turn that into a single object param. */ 93 | return is_array($this->payload->params) ? $this->payload->params : [$this->payload->params]; 94 | } 95 | return []; 96 | } 97 | 98 | /** 99 | * Returns whether this request is minimally valid for JSON RPC. 100 | * @return bool Returns true if the payload is valid and false otherwise. 101 | */ 102 | public function isValid() 103 | { 104 | $action = $this->getAction(); 105 | return is_object($this->payload) && !empty($action); 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/Vectorface/SnappyRouter/Request/RequestInterface.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | interface RequestInterface 14 | { 15 | /** 16 | * Returns the controller to be used in the request. 17 | * @return string Returns the controller DI key to be used in the request. 18 | */ 19 | public function getController(); 20 | 21 | /** 22 | * Sets the controller to be used in the request. 23 | * @param AbstractController $controller The controller to be used in the request. 24 | * @return RequestInterface Returns $this. 25 | */ 26 | public function setController($controller); 27 | 28 | /** 29 | * Returns the action to be invoked as a string. 30 | * @return string The action to be invoked. 31 | */ 32 | public function getAction(); 33 | 34 | /** 35 | * Sets the action to be invoked by the request 36 | * @param string $action The action to be invoked by the request. 37 | * @return RequestInterface Returns $this. 38 | */ 39 | public function setAction($action); 40 | } 41 | -------------------------------------------------------------------------------- /src/Vectorface/SnappyRouter/Response/AbstractResponse.php: -------------------------------------------------------------------------------- 1 | 10 | */ 11 | abstract class AbstractResponse implements ResponseInterface 12 | { 13 | /** HTTP response code for OK */ 14 | const RESPONSE_OK = 200; 15 | /** HTTP response code for a bad request */ 16 | const RESPONSE_BAD_REQUEST = 400; 17 | /** HTTP response code for unauthorized */ 18 | const RESPONSE_UNAUTHORIZED = 401; 19 | /** HTTP response code for forbidden */ 20 | const RESPONSE_FORBIDDEN = 403; 21 | /** HTTP response code for not found */ 22 | const RESPONSE_NOT_FOUND = 404; 23 | /** HTTP response code for method not allowed */ 24 | const RESPONSE_METHOD_NOT_ALLOWED = 405; 25 | /** HTTP response code for too many requests in a period of time 26 | * See: http://en.wikipedia.org/wiki/List_of_HTTP_status_codes#429 */ 27 | const RESPONSE_RATE_LIMITED = 429; 28 | /** HTTP response code for a server error */ 29 | const RESPONSE_SERVER_ERROR = 500; 30 | /** HTTP response code for server unavailable */ 31 | const RESPONSE_SERVICE_UNAVAILABLE = 503; 32 | 33 | /** 34 | * Constructor for the response. 35 | * @param mixed $responseObject A response object that can be serialized to a string. 36 | * @param int $statusCode The HTTP response. 37 | */ 38 | public function __construct($responseObject, $statusCode = self::RESPONSE_OK) 39 | { 40 | $this->setResponseObject($responseObject); 41 | $this->setStatusCode($statusCode); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/Vectorface/SnappyRouter/Response/JsonRpcResponse.php: -------------------------------------------------------------------------------- 1 | error = (object)[ 30 | 'code' => $error->getCode(), 31 | 'message' => $error->getMessage() 32 | ]; 33 | } else { 34 | $response->result = $result; 35 | } 36 | 37 | if ($request) { 38 | /* 1.0: omit version, 2.0 and newer, echo back version. */ 39 | if ($request->getVersion() != "1.0") { 40 | $response->jsonrpc = $request->getVersion(); 41 | } 42 | 43 | /* For notifications (null id), return nothing. Otherwise, pass back the id. */ 44 | if ($request->getIdentifier() === null) { 45 | $response = ""; 46 | } else { 47 | $response->id = $request->getIdentifier(); 48 | } 49 | } 50 | 51 | parent::__construct($response); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/Vectorface/SnappyRouter/Response/Response.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | class Response extends AbstractResponse 11 | { 12 | private $responseObject; // the serializable response object 13 | private $statusCode; // the http response code 14 | 15 | /** 16 | * Returns the serializable response object. 17 | * @return mixed The serializable response object. 18 | */ 19 | public function getResponseObject() 20 | { 21 | return $this->responseObject; 22 | } 23 | 24 | /** 25 | * Sets the serializable response object. 26 | * @param mixed $responseObject The serializable response object. 27 | * @return ResponseInterface Returns $this. 28 | */ 29 | public function setResponseObject($responseObject) 30 | { 31 | $this->responseObject = $responseObject; 32 | return $this; 33 | } 34 | 35 | /** 36 | * Returns the HTTP status code associated with this response. 37 | * @return integer The HTTP status code associated with this response. 38 | */ 39 | public function getStatusCode() 40 | { 41 | return $this->statusCode; 42 | } 43 | 44 | /** 45 | * Sets the HTTP status code associated with this response. 46 | * @param int $statusCode The HTTP status code associated with this response. 47 | * @return ResponseInterface Returns $this. 48 | */ 49 | public function setStatusCode($statusCode) 50 | { 51 | $statusCode = intval($statusCode); 52 | $this->statusCode = ($statusCode > 0) ? $statusCode : self::RESPONSE_OK; 53 | return $this; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/Vectorface/SnappyRouter/Response/ResponseInterface.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | interface ResponseInterface 11 | { 12 | /** 13 | * Returns the serializable response object. 14 | * @return mixed The serializable response object. 15 | */ 16 | public function getResponseObject(); 17 | 18 | /** 19 | * Sets the serializable response object. 20 | * @param mixed $responseObject The serializable response object. 21 | * @return ResponseInterface Returns $this. 22 | */ 23 | public function setResponseObject($responseObject); 24 | 25 | /** 26 | * Returns the HTTP status code associated with this response. 27 | * @return integer The HTTP status code associated with this response. 28 | */ 29 | public function getStatusCode(); 30 | 31 | /** 32 | * Sets the HTTP status code associated with this response. 33 | * @param int $statusCode The HTTP status code associated with this response. 34 | * @return ResponseInterface Returns $this. 35 | */ 36 | public function setStatusCode($statusCode); 37 | } 38 | -------------------------------------------------------------------------------- /src/Vectorface/SnappyRouter/Task/AbstractTask.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | class AbstractTask implements DiProviderInterface, TaskInterface 15 | { 16 | // an array of cli handler options 17 | private $options; 18 | 19 | /** 20 | * Initializes the cli task from the given configuration. 21 | * @param array $options The task options. 22 | */ 23 | public function init($options) 24 | { 25 | $this->setOptions($options); 26 | } 27 | 28 | /** 29 | * Returns the current set of options. 30 | * @return array The current set of options. 31 | */ 32 | public function getOptions() 33 | { 34 | return $this->options; 35 | } 36 | 37 | /** 38 | * Sets the current set of options. 39 | * @param array $options The set of options. 40 | * @return AbstractTask Returns $this. 41 | */ 42 | public function setOptions($options) 43 | { 44 | $this->options = $options; 45 | return $this; 46 | } 47 | 48 | /** 49 | * Retrieve an element from the DI container. 50 | * 51 | * @param string $key The DI key. 52 | * @param boolean $useCache (optional) An optional indicating whether we 53 | * should use the cached version of the element (true by default). 54 | * @return mixed Returns the DI element mapped to that key. 55 | * @throws Exception 56 | */ 57 | public function get($key, $useCache = true) 58 | { 59 | return Di::getDefault()->get($key, $useCache); 60 | } 61 | 62 | /** 63 | * Sets an element in the DI container for the specified key. 64 | * @param string $key The DI key. 65 | * @param mixed $element The DI element to store. 66 | * @return Di Returns the Di instance. 67 | */ 68 | public function set($key, $element) 69 | { 70 | return Di::getDefault()->set($key, $element); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/Vectorface/SnappyRouter/Task/TaskInterface.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | interface TaskInterface 11 | { 12 | /** 13 | * Initializes the cli task from the given configuration. 14 | * @param array $options The task options. 15 | */ 16 | public function init($options); 17 | } 18 | -------------------------------------------------------------------------------- /tests/Vectorface/SnappyRouterTests/Authentication/CallbackAuthenticatorTest.php: -------------------------------------------------------------------------------- 1 | assertTrue($auth->authenticate(['a', 'b'])); 22 | 23 | $bool = false; 24 | $auth = new CallbackAuthenticator(function() use ($bool) { 25 | return $bool; 26 | }); 27 | $this->assertFalse($auth->authenticate(['a', 'b'])); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /tests/Vectorface/SnappyRouterTests/Config/ConfigTest.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | class ConfigTest extends TestCase 15 | { 16 | /** 17 | * Demonstrates basic usage of the Config wrapper class. 18 | * 19 | * @throws Exception 20 | */ 21 | public function testSynopsis() 22 | { 23 | $arrayConfig = [ 24 | 'key1' => 'value1', 25 | 'key2' => 'value2' 26 | ]; 27 | 28 | // initialize the class from an array 29 | $config = new Config($arrayConfig); 30 | 31 | // assert all the keys and values match 32 | foreach ($arrayConfig as $key => $value) { 33 | // using the array accessor syntax 34 | $this->assertEquals($value, $config[$key]); 35 | // using the get method 36 | $this->assertEquals($value, $config->get($key)); 37 | } 38 | 39 | $config['key3'] = 'value3'; 40 | $this->assertEquals('value3', $config['key3']); 41 | 42 | $config->set('key4', 'value4'); 43 | $this->assertEquals('value4', $config['key4']); 44 | 45 | unset($config['key4']); 46 | $this->assertNull($config['key4']); // assert we unset the value 47 | $this->assertEquals(false, $config->get('key4', false)); // test default values 48 | 49 | unset($config['key3']); 50 | $this->assertEquals($arrayConfig, $config->toArray()); 51 | } 52 | 53 | /** 54 | * Test that we cannot append to the config class like we would a normal array. 55 | */ 56 | public function testExceptionThrownWhenConfigIsAppended() 57 | { 58 | $this->expectException(Exception::class); 59 | $this->expectExceptionMessage("Config values must contain a key."); 60 | 61 | $config = new Config([]); 62 | $config[] = 'new value'; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /tests/Vectorface/SnappyRouterTests/Controller/NonNamespacedController.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | class TestDummyController extends AbstractController 14 | { 15 | public function indexAction() 16 | { 17 | } 18 | 19 | public function testAction() 20 | { 21 | return 'This is a test service.'; 22 | } 23 | 24 | public function genericExceptionAction() 25 | { 26 | throw new InternalErrorException('A generic exception.'); 27 | } 28 | 29 | public function defaultAction() 30 | { 31 | // ensure some abstract methods work 32 | $this->set('request', $this->getRequest()); 33 | $this->get('request'); 34 | } 35 | 36 | public function arrayAction() 37 | { 38 | $this->viewContext['variable'] = 'broken'; 39 | return ['variable' => 'test']; 40 | } 41 | 42 | public function otherViewAction() 43 | { 44 | return $this->renderView( 45 | ['variable' => 'test'], 46 | 'test/array.twig' 47 | ); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /tests/Vectorface/SnappyRouterTests/Controller/Views/test/array.twig: -------------------------------------------------------------------------------- 1 | This is a {{ variable }} service. -------------------------------------------------------------------------------- /tests/Vectorface/SnappyRouterTests/Controller/Views/test/default.twig: -------------------------------------------------------------------------------- 1 | Hello world -------------------------------------------------------------------------------- /tests/Vectorface/SnappyRouterTests/Di/DiTest.php: -------------------------------------------------------------------------------- 1 | set($key, $element); 24 | // check that we get back what we expect 25 | $this->assertEquals( 26 | $expected, 27 | $di->get($key, false) 28 | ); 29 | // check we get the same value if we use the cache 30 | $this->assertEquals( 31 | $expected, 32 | $di->get($key, true) 33 | ); 34 | // and again if we force a "no cache" hit 35 | $this->assertEquals( 36 | $expected, 37 | $di->get($key, false) 38 | ); 39 | $this->assertTrue($di->hasElement($key)); 40 | $this->assertEquals( 41 | [$key], 42 | $di->allRegisteredElements() 43 | ); 44 | } 45 | 46 | /** 47 | * Data provider for the method testSetAndGetService. 48 | */ 49 | public function setAndGetServiceProvider() 50 | { 51 | return [ 52 | [ 53 | 'HelloWorldService', 54 | 'Hello world!', 55 | 'Hello world!' 56 | ], 57 | [ 58 | 'HelloWorldService', 59 | function() { 60 | return 'Hello world!'; 61 | }, 62 | 'Hello world!' 63 | ] 64 | ]; 65 | } 66 | 67 | /** 68 | * Tests the methods for getting, setting and clearing the default 69 | * service provider. 70 | */ 71 | public function testGetDefaultAndSetDefault() 72 | { 73 | Di::clearDefault(); // guard condition 74 | $di = Di::getDefault(); // get a fresh default 75 | $this->assertInstanceOf(Di::class, $di); 76 | 77 | Di::setDefault($di); 78 | $this->assertEquals($di, Di::getDefault()); 79 | } 80 | 81 | /** 82 | * Tests the exception is thrown when we ask for a service that has not 83 | * been registered. 84 | */ 85 | public function testMissingServiceThrowsException() 86 | { 87 | $this->expectException(Exception::class); 88 | $this->expectExceptionMessage("No element registered for key: TestElement"); 89 | 90 | $di = new Di(); 91 | $di->get('TestElement'); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /tests/Vectorface/SnappyRouterTests/Di/ServiceProviderTest.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | class ServiceProviderTest extends TestCase 16 | { 17 | /** 18 | * An overview of how to use the ServiceProvider class. 19 | * 20 | * @throws Exception 21 | */ 22 | public function testSynopsis() 23 | { 24 | // instantiate the class 25 | $config = [ 26 | 'TestController' => TestDummyController::class 27 | ]; 28 | $serviceProvider = new ServiceProvider($config); 29 | 30 | // public setters (object chaining) 31 | $services = array_merge( 32 | $config, 33 | [ 34 | 'AnotherService' => '/path/to/anotherService.php', 35 | 'AnotherServiceForFileClass' => null 36 | ] 37 | ); 38 | 39 | $serviceProvider->setService('AnotherService', '/path/to/anotherService.php'); 40 | $serviceProvider->setService( 41 | 'AnotherServiceForFileClass', 42 | [ 43 | 'file' => 'tests/Vectorface/SnappyRouterTests/Controller/NonNamespacedController.php', 44 | 'class' => 'NonNamespacedController', 45 | ] 46 | ); 47 | 48 | // public getters 49 | $this->assertEquals( 50 | array_keys($services), 51 | $serviceProvider->getServices() 52 | ); 53 | $this->assertEquals( 54 | '/path/to/anotherService.php', 55 | $serviceProvider->getService('AnotherService') 56 | ); 57 | 58 | $this->assertInstanceOf( 59 | TestDummyController::class, 60 | $serviceProvider->getServiceInstance('TestController') 61 | ); 62 | 63 | //Tests instanceCache 64 | $this->assertInstanceOf( 65 | TestDummyController::class, 66 | $serviceProvider->getServiceInstance('TestController') 67 | ); 68 | 69 | $this->assertInstanceOf( 70 | 'NonNamespacedController', 71 | $serviceProvider->getServiceInstance('AnotherServiceForFileClass') 72 | ); 73 | } 74 | 75 | /** 76 | * Test that we can retrieve a non namespaced service. 77 | * 78 | * @throws Exception 79 | */ 80 | public function testNonNamespacedService() 81 | { 82 | $config = [ 83 | 'NonNamespacedController' => 'tests/Vectorface/SnappyRouterTests/Controller/NonNamespacedController.php' 84 | ]; 85 | $serviceProvider = new ServiceProvider($config); 86 | 87 | $this->assertInstanceOf( 88 | 'NonNamespacedController', 89 | $serviceProvider->getServiceInstance('NonNamespacedController') 90 | ); 91 | } 92 | 93 | /** 94 | * Test that we can retrieve a service while in namespace provisioning mode. 95 | * 96 | * @throws Exception 97 | */ 98 | public function testNamespaceProvisioning() 99 | { 100 | $serviceProvider = new ServiceProvider([]); 101 | $namespaces = ['Vectorface\SnappyRouterTests\Controller']; 102 | $serviceProvider->setNamespaces($namespaces); 103 | 104 | $this->assertInstanceOf( 105 | TestDummyController::class, 106 | $serviceProvider->getServiceInstance('TestDummyController') 107 | ); 108 | } 109 | 110 | /** 111 | * Test that we get an exception if we cannot find the service in any 112 | * of the given namespaces. 113 | * 114 | * @throws Exception 115 | */ 116 | public function testNamespaceProvisioningMissingService() 117 | { 118 | $this->expectException(Exception::class); 119 | $this->expectExceptionMessage("Controller class TestDummyController was not found in any listed namespace."); 120 | 121 | $serviceProvider = new ServiceProvider([]); 122 | $serviceProvider->setNamespaces([]); 123 | 124 | $this->assertInstanceOf( 125 | TestDummyController::class, 126 | $serviceProvider->getServiceInstance('TestDummyController') 127 | ); 128 | } 129 | 130 | /** 131 | * Test that we can retrieve a service while in folder provisioning mode. 132 | * 133 | * @throws Exception 134 | */ 135 | public function testFolderProvisioning() 136 | { 137 | $serviceProvider = new ServiceProvider([]); 138 | $folders = [realpath(__DIR__.'/../')]; 139 | $serviceProvider->setFolders($folders); 140 | 141 | $this->assertInstanceOf( 142 | 'NonNamespacedController', 143 | $serviceProvider->getServiceInstance('NonNamespacedController') 144 | ); 145 | } 146 | 147 | /** 148 | * Test that we get an exception if we cannot find the service in any 149 | * of the given folders (recursively checking). 150 | * 151 | * @throws Exception 152 | */ 153 | public function testFolderProvisioningMissingService() 154 | { 155 | $this->expectException(Exception::class); 156 | $this->expectExceptionMessage("Controller class NonExistentController not found in any listed folder."); 157 | 158 | $serviceProvider = new ServiceProvider([]); 159 | $folders = [realpath(__DIR__.'/../')]; 160 | $serviceProvider->setFolders($folders); 161 | 162 | $serviceProvider->getServiceInstance('NonExistentController'); 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /tests/Vectorface/SnappyRouterTests/Encoder/AbstractEncoderTest.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | abstract class AbstractEncoderTest extends TestCase 15 | { 16 | /** 17 | * Tests the encode method of the encoder. 18 | * 19 | * @dataProvider encodeProvider 20 | * @param string $expected 21 | * @param mixed $input 22 | */ 23 | public function testEncode($expected, $input) 24 | { 25 | $encoder = $this->getEncoder(); 26 | $this->assertEquals( 27 | $expected, 28 | $encoder->encode( 29 | new Response($input) 30 | ) 31 | ); 32 | } 33 | 34 | /** 35 | * Returns the encoder to be tested. 36 | * @return AbstractEncoder Returns an instance of an encoder. 37 | */ 38 | abstract public function getEncoder(); 39 | 40 | /** 41 | * A data provider for the testEncode method. 42 | */ 43 | abstract public function encodeProvider(); 44 | } 45 | -------------------------------------------------------------------------------- /tests/Vectorface/SnappyRouterTests/Encoder/JsonEncoderTest.php: -------------------------------------------------------------------------------- 1 | 15 | */ 16 | class JsonEncoderTest extends AbstractEncoderTest 17 | { 18 | /** 19 | * Returns the encoder to be tested. 20 | * @return AbstractEncoder Returns an instance of an encoder. 21 | */ 22 | public function getEncoder() 23 | { 24 | return new JsonEncoder(); 25 | } 26 | 27 | /** 28 | * A data provider for the testEncode method. 29 | */ 30 | public function encodeProvider() 31 | { 32 | $testObject = new stdClass(); 33 | $testObject->id = 1234; 34 | return [ 35 | [ 36 | '"test1234"', 37 | 'test1234' 38 | ], 39 | [ 40 | '{"id":1234}', 41 | ['id' => 1234] 42 | ], 43 | [ 44 | '{"id":1234}', 45 | $testObject, 46 | ], 47 | [ 48 | 'null', 49 | null 50 | ], 51 | [ 52 | '"testSerialize"', 53 | $this 54 | ] 55 | ]; 56 | } 57 | 58 | /** 59 | * Tests that we get an exception if we attempt to encode something that 60 | * is not serializable as JSON. 61 | * 62 | * @throws EncoderException 63 | */ 64 | public function testNonSerializableEncode() 65 | { 66 | $this->expectException(EncoderException::class); 67 | $this->expectExceptionMessage("Unable to encode response as JSON."); 68 | 69 | $encoder = new JsonEncoder(); 70 | $resource = fopen(__FILE__, 'r'); // resources can't be serialized 71 | $encoder->encode(new Response($resource)); 72 | } 73 | 74 | public function jsonSerialize() 75 | { 76 | return 'testSerialize'; 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /tests/Vectorface/SnappyRouterTests/Encoder/JsonpEncoderTest.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | class JsonpEncoderTest extends AbstractEncoderTest 15 | { 16 | /** 17 | * Returns the encoder to be tested. 18 | * 19 | * @return AbstractEncoder Returns an instance of an encoder. 20 | * @throws Exception 21 | */ 22 | public function getEncoder() 23 | { 24 | $options = [ 25 | 'clientMethod' => 'doSomething' 26 | ]; 27 | return new JsonpEncoder($options); 28 | } 29 | 30 | /** 31 | * A data provider for the testEncode method. 32 | */ 33 | public function encodeProvider() 34 | { 35 | return [ 36 | [ 37 | 'doSomething("test1234");', 38 | 'test1234' 39 | ] 40 | ]; 41 | } 42 | 43 | /** 44 | * Tests that an exception is thrown if the client method is missing from 45 | * the options. 46 | */ 47 | public function testMissingClientMethodThrowsException() 48 | { 49 | $this->expectException(Exception::class); 50 | $this->expectExceptionMessage("Client method missing from plugin options."); 51 | 52 | new JsonpEncoder([]); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /tests/Vectorface/SnappyRouterTests/Encoder/NullEncoderTest.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | class NullEncoderTest extends AbstractEncoderTest 14 | { 15 | /** 16 | * Returns the encoder to be tested. 17 | * @return AbstractEncoder Returns an instance of an encoder. 18 | */ 19 | public function getEncoder() 20 | { 21 | return new NullEncoder(); 22 | } 23 | 24 | /** 25 | * A data provider for the testEncode method. 26 | */ 27 | public function encodeProvider() 28 | { 29 | return [ 30 | [ 31 | 'test1234', 32 | 'test1234' 33 | ], 34 | [ 35 | '', 36 | null 37 | ] 38 | ]; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /tests/Vectorface/SnappyRouterTests/Exception/InternalErrorExceptionTest.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | class InternalErrorExceptionTest extends TestCase 14 | { 15 | /** 16 | * An overview of how to use the exception. 17 | */ 18 | public function testSynopsis() 19 | { 20 | $message = 'hello world'; 21 | $exception = new InternalErrorException($message); 22 | 23 | $this->assertEquals($message, $exception->getMessage()); 24 | $this->assertEquals(500, $exception->getAssociatedStatusCode()); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /tests/Vectorface/SnappyRouterTests/Exception/MethodNotAllowedExceptionTest.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | class MethodNotAllowedExceptionTest extends TestCase 14 | { 15 | /** 16 | * An overview of how the class works. 17 | */ 18 | public function testSynopsis() 19 | { 20 | $exception = new MethodNotAllowedException( 21 | 'Cannot use GET', 22 | ['POST'] 23 | ); 24 | $this->assertEquals(405, $exception->getAssociatedStatusCode()); 25 | try { 26 | throw $exception; 27 | } catch (MethodNotAllowedException $e) { 28 | $this->assertEquals('Cannot use GET', $e->getMessage()); 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /tests/Vectorface/SnappyRouterTests/Handler/DirectScriptHandlerTest.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | class DirectScriptHandlerTest extends TestCase 15 | { 16 | /** 17 | * An overview of how to use the class. 18 | * 19 | * @throws PluginException 20 | */ 21 | public function testSynopsis() 22 | { 23 | // the configuration maps a path like /cgi-bin to this folder 24 | $config = [ 25 | DirectScriptHandler::KEY_PATH_MAP => [ 26 | '/cgi-bin' => __DIR__ 27 | ] 28 | ]; 29 | $handler = new DirectScriptHandler($config); 30 | $path = '/cgi-bin/test_script.php'; 31 | // the file itself exists so we should get back true 32 | $this->assertTrue( 33 | $handler->isAppropriate($path, [], [], 'GET') 34 | ); 35 | // the test script simply has `echo "Hello world!"` 36 | $expected = 'Hello world!'; 37 | $this->assertEquals($expected, $handler->performRoute()); 38 | 39 | // the script is not found so the handler should not be marked as 40 | // appropriate 41 | $path = '/cgi-bin/script_not_found.php'; 42 | $this->assertFalse( 43 | $handler->isAppropriate($path, [], [], 'GET') 44 | ); 45 | } 46 | 47 | /** 48 | * Tests that the getRequest() method returns null. 49 | * 50 | * @throws PluginException 51 | */ 52 | public function testGetRequest() 53 | { 54 | $config = [ 55 | DirectScriptHandler::KEY_PATH_MAP => [ 56 | '/cgi-bin' => __DIR__ 57 | ] 58 | ]; 59 | $handler = new DirectScriptHandler($config); 60 | $this->assertTrue($handler->isAppropriate('/cgi-bin/test_script.php', [], [], 'GET')); 61 | $this->assertNull($handler->getRequest()); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /tests/Vectorface/SnappyRouterTests/Handler/PatternMatchHandlerTest.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | class PatternMatchHandlerTest extends TestCase 15 | { 16 | /** 17 | * Demonstrates how to use the PatternMatchHandler class. 18 | * 19 | * @throws PluginException 20 | */ 21 | public function testSynopsis() 22 | { 23 | $config = [ 24 | 'routes' => [ 25 | '/user/{name}/{id:[0-9]+}' => [ 26 | 'get' => function($routeParams) { 27 | return print_r($routeParams, true); 28 | } 29 | ], 30 | '/anotherRoute' => function() { 31 | return false; 32 | } 33 | ] 34 | ]; 35 | $handler = new PatternMatchHandler($config); 36 | $this->assertTrue($handler->isAppropriate('/user/asdf/1234', [], [], 'GET')); 37 | $expected = print_r(['name' => 'asdf', 'id' => 1234], true); 38 | $this->assertEquals($expected, $handler->performRoute()); 39 | 40 | // not a matching pattern 41 | $this->assertFalse($handler->isAppropriate('/user/1234', [], [], 'GET')); 42 | 43 | // matching pattern but invalid HTTP verb 44 | $this->assertFalse($handler->isAppropriate('/user/asdf/1234', [], [], 'POST')); 45 | } 46 | 47 | /** 48 | * Tests that the cached route handler works as well. 49 | * 50 | * @throws PluginException 51 | */ 52 | public function testCachedRouteHandler() 53 | { 54 | $cacheFile = __DIR__.'/routes.cache'; 55 | if (file_exists($cacheFile)) { 56 | unlink($cacheFile); 57 | } 58 | $config = [ 59 | 'routes' => [ 60 | '/user/{name}/{id:[0-9]+}' => [ 61 | 'get' => function($routeParams) { 62 | return print_r($routeParams, true); 63 | } 64 | ], 65 | '/anotherRoute' => function() { 66 | return false; 67 | } 68 | ], 69 | 'routeCache' => [ 70 | 'cacheFile' => $cacheFile 71 | ] 72 | ]; 73 | 74 | $handler = new PatternMatchHandler($config); 75 | $this->assertTrue($handler->isAppropriate('/user/asdf/1234', [], [], 'GET')); 76 | $this->assertNotEmpty(file_get_contents($cacheFile)); 77 | unlink($cacheFile); 78 | } 79 | 80 | /** 81 | * Tests that the getRequest() method returns null. 82 | * 83 | * @throws PluginException 84 | */ 85 | public function testGetRequest() 86 | { 87 | $config = [ 88 | 'routes' => [ 89 | '/testRoute' => function() { 90 | return false; 91 | } 92 | ] 93 | ]; 94 | $handler = new PatternMatchHandler($config); 95 | $this->assertTrue($handler->isAppropriate('/testRoute', [], [], 'GET')); 96 | $this->assertNull($handler->getRequest()); 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /tests/Vectorface/SnappyRouterTests/Handler/RestHandlerTest.php: -------------------------------------------------------------------------------- 1 | 17 | */ 18 | class RestHandlerTest extends TestCase 19 | { 20 | /** 21 | * An overview of how to use the RestHandler class. 22 | * 23 | * @throws InternalErrorException|PluginException|ResourceNotFoundException 24 | */ 25 | public function testSynopsis() 26 | { 27 | $options = [ 28 | RestHandler::KEY_BASE_PATH => '/', 29 | Config::KEY_CONTROLLERS => [ 30 | 'TestController' => TestDummyController::class 31 | ] 32 | ]; 33 | $handler = new RestHandler($options); 34 | $this->assertTrue($handler->isAppropriate('/v1/test', [], [], 'GET')); 35 | $result = json_decode($handler->performRoute()); 36 | $this->assertTrue(empty($result)); 37 | } 38 | 39 | /** 40 | * Tests the possible paths that could be handled by the RestHandler. 41 | * 42 | * @dataProvider restPathsProvider 43 | * @param bool $expected 44 | * @param string $path 45 | * @throws InternalErrorException 46 | * @throws PluginException 47 | */ 48 | public function testRestHandlerHandlesPath($expected, $path) 49 | { 50 | $options = [ 51 | RestHandler::KEY_BASE_PATH => '/', 52 | Config::KEY_CONTROLLERS => [ 53 | 'TestController' => TestDummyController::class, 54 | ] 55 | ]; 56 | $handler = new RestHandler($options); 57 | $this->assertEquals($expected, $handler->isAppropriate($path, [], [], 'GET')); 58 | } 59 | 60 | /** 61 | * The data provider for testing various paths against the RestHandler. 62 | */ 63 | public function restPathsProvider() 64 | { 65 | return [ 66 | [ 67 | true, 68 | '/v1/test' 69 | ], 70 | [ 71 | true, 72 | '/v1.2/test' 73 | ], 74 | [ 75 | true, 76 | '/v1.2/Test' 77 | ], 78 | [ 79 | true, 80 | '/v1.2/test/1234' 81 | ], 82 | [ 83 | true, 84 | '/v1.2/test/someAction' 85 | ], 86 | [ 87 | true, 88 | '/v1.2/test/1234/someAction' 89 | ], 90 | [ 91 | false, 92 | '/v1.2' 93 | ], 94 | [ 95 | true, 96 | '/v1.2/noController' 97 | ], 98 | [ 99 | false, 100 | '/v1.2/1234/5678' 101 | ] 102 | ]; 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /tests/Vectorface/SnappyRouterTests/Handler/test_script.php: -------------------------------------------------------------------------------- 1 | 19 | * @author Dan Bruce 20 | */ 21 | class AbstractAuthenticationPluginTest extends TestCase 22 | { 23 | /** 24 | * Authentication of service requests happens by intercepting preInvoke; Validate that. 25 | * 26 | * @throws InternalErrorException|UnauthorizedException|PluginException 27 | */ 28 | public function testAfterHandlerInvoked() 29 | { 30 | $ignored = new ControllerHandler([]); 31 | 32 | /* Configure DI */ 33 | $bool = false; 34 | $auth = new CallbackAuthenticator(function() use (&$bool) { 35 | return $bool; 36 | }); 37 | $di = new Di(['AuthMechanism' => false]); 38 | Di::setDefault($di); 39 | 40 | /* Direct testing. */ 41 | $plugin = new TestAuthenticationPlugin([]); 42 | 43 | try { 44 | $plugin->afterHandlerSelected($ignored); 45 | $this->fail("An invalid authenticator should yield an internal error"); 46 | } catch (InternalErrorException $e) { 47 | $this->assertEquals(500, $e->getAssociatedStatusCode()); /* HTTP 500 ISE */ 48 | } 49 | 50 | /* From here on out, use the "Do whatever I say" authenticator. :) */ 51 | $di->set('AuthMechanism', $auth); 52 | 53 | $plugin->credentials = false; 54 | try { 55 | $plugin->afterHandlerSelected($ignored); 56 | $this->fail("No username and password are available. UnauthorizedException expected."); 57 | } catch (UnauthorizedException $e) { 58 | $this->assertEquals(401, $e->getAssociatedStatusCode()); /* HTTP 401 Unauthorized */ 59 | } 60 | 61 | $plugin->credentials = ['ignored', 'ignored']; 62 | try { 63 | $plugin->afterHandlerSelected($ignored); 64 | $this->fail("Callback expected to return false auth result. UnauthorizedException expected."); 65 | } catch (UnauthorizedException $e) { 66 | // we expect the exception to be thrown 67 | } 68 | 69 | /* With a true result, preInvoke should pass through. */ 70 | $bool = true; 71 | $this->assertTrue($bool); 72 | $plugin->afterHandlerSelected($ignored); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /tests/Vectorface/SnappyRouterTests/Plugin/Authentication/HttpBasicAuthenticationPluginTest.php: -------------------------------------------------------------------------------- 1 | 18 | * @author Dan Bruce 19 | */ 20 | class HttpBasicAuthenticationPluginTest extends TestCase 21 | { 22 | /** 23 | * Test the HTTPBasicAuthenticationPlugin; All in one test! 24 | * 25 | * @throws InternalErrorException|UnauthorizedException|PluginException 26 | */ 27 | public function testBasicHTTPAuth() 28 | { 29 | $ignored = new ControllerHandler([]); 30 | 31 | /* Configure DI */ 32 | $di = new Di(['MyCustomAuth' => false]); 33 | Di::setDefault($di); 34 | 35 | /* Direct testing. */ 36 | $plugin = new HttpBasicAuthenticationPlugin([ 37 | 'AuthMechanism' => 'MyCustomAuth', 38 | 'realm' => 'Authentication Test' 39 | ]); 40 | 41 | try { 42 | $plugin->afterHandlerSelected($ignored); 43 | $this->fail("An invalid authenticator should yield an internal error"); 44 | } catch (InternalErrorException $e) { 45 | $this->assertEquals(500, $e->getAssociatedStatusCode()); /* HTTP 500 ISE */ 46 | } 47 | 48 | /* From here on out, use the "Do whatever I say" authenticator. :) */ 49 | $bool = false; 50 | $auth = new CallbackAuthenticator(function() use (&$bool) { 51 | return $bool; 52 | }); 53 | $di->set('MyCustomAuth', $auth); 54 | 55 | 56 | $_SERVER['PHP_AUTH_USER'] = $_SERVER['PHP_AUTH_PW'] = null; 57 | try { 58 | $plugin->afterHandlerSelected($ignored); 59 | $this->fail("No username and password are available. UnauthorizedException expected."); 60 | } catch (UnauthorizedException $e) { 61 | $this->assertEquals(401, $e->getAssociatedStatusCode()); /* HTTP 401 Unauthorized */ 62 | } 63 | 64 | $_SERVER['PHP_AUTH_USER'] = $_SERVER['PHP_AUTH_PW'] = 'ignored'; 65 | try { 66 | $plugin->afterHandlerSelected($ignored); 67 | $this->fail("Callback expected to return false auth result. UnauthorizedException expected."); 68 | } catch (UnauthorizedException $e) { 69 | // we expect the exception to be thrown 70 | } 71 | 72 | /* With a true result, preInvoke should pass through. */ 73 | $bool = true; 74 | $this->assertTrue($bool); 75 | $plugin->afterHandlerSelected($ignored); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /tests/Vectorface/SnappyRouterTests/Plugin/Authentication/TestAuthenticationPlugin.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | class TestAuthenticationPlugin extends AbstractAuthenticationPlugin 15 | { 16 | /** 17 | * The "credentials" returned by getCredentials for testing. 18 | * 19 | * @var mixed 20 | */ 21 | public $credentials = ['ignored', 'ignored']; 22 | 23 | /** 24 | * Extract credentials from the "request"... Or the hard-coded test values above. 25 | * 26 | * @return mixed An array of credentials; A username and password pair, or false if credentials aren't available 27 | * @throws Exception 28 | */ 29 | public function getCredentials() 30 | { 31 | return $this->set('credentials', $this->credentials)->get('credentials'); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /tests/Vectorface/SnappyRouterTests/Plugin/HttpHeader/RouterHeaderPluginTest.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | class RouterHeaderPluginTest extends TestCase 16 | { 17 | /** 18 | * An overview of how to use the plugin. 19 | * 20 | * @throws PluginException 21 | */ 22 | public function testSynopsis() 23 | { 24 | $handler = new ControllerHandler([]); 25 | $plugin = new RouterHeaderPlugin([]); 26 | $plugin->afterHandlerSelected($handler); 27 | $this->assertEquals(1000, $plugin->getExecutionOrder()); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /tests/Vectorface/SnappyRouterTests/Plugin/TestPlugin.php: -------------------------------------------------------------------------------- 1 | assertEquals( 19 | AbstractPlugin::PRIORITY_DEFAULT, 20 | $plugin->getExecutionOrder() 21 | ); 22 | 23 | $plugin->setWhitelist( 24 | [ 25 | 'TestController' => AbstractPlugin::ALL_ACTIONS 26 | ] 27 | ); 28 | $this->assertTrue( 29 | $plugin->supportsControllerAndAction( 30 | 'TestController', 31 | 'someAction' 32 | ) 33 | ); 34 | } 35 | 36 | /** 37 | * Tests the supportsControllerAndAction methods. 38 | */ 39 | public function testSupportsControllerAndAction() 40 | { 41 | $plugin = new TestPlugin([]); 42 | 43 | // no lists yet, so plugin supports everything 44 | $this->assertTrue( 45 | $plugin->supportsControllerAndAction( 46 | 'TestController', 47 | 'anyAction' 48 | ) 49 | ); 50 | 51 | // set a whitelist 52 | $plugin->setWhitelist( 53 | [ 54 | 'TestController' => AbstractPlugin::ALL_ACTIONS, 55 | 'AnotherController' => [ 56 | 'specificAction' 57 | ] 58 | ] 59 | ); 60 | // all actions enabled for this controller 61 | $this->assertTrue( 62 | $plugin->supportsControllerAndAction( 63 | 'TestController', 64 | 'anyAction' 65 | ) 66 | ); 67 | // specific action enabled 68 | $this->assertTrue( 69 | $plugin->supportsControllerAndAction( 70 | 'AnotherController', 71 | 'specificAction' 72 | ) 73 | ); 74 | // controller is missing from whitelist 75 | $this->assertFalse( 76 | $plugin->supportsControllerAndAction( 77 | 'MissingController', 78 | 'anyAction' 79 | ) 80 | ); 81 | // action is missing from whitelist 82 | $this->assertFalse( 83 | $plugin->supportsControllerAndAction( 84 | 'AnotherController', 85 | 'differentAction' 86 | ) 87 | ); 88 | 89 | // now the reverse logic for the blacklist 90 | $plugin->setBlacklist( 91 | [ 92 | 'TestController' => [ 93 | 'bannedAction' 94 | ], 95 | 'BannedController' => AbstractPlugin::ALL_ACTIONS 96 | ] 97 | ); 98 | // controller is missing from blacklist 99 | $this->assertTrue( 100 | $plugin->supportsControllerAndAction( 101 | 'MissingController', 102 | 'anyAction' 103 | ) 104 | ); 105 | // action is blacklisted specifically 106 | $this->assertFalse( 107 | $plugin->supportsControllerAndAction( 108 | 'TestController', 109 | 'bannedAction' 110 | ) 111 | ); 112 | // all actions for the controller are banned 113 | $this->assertFalse( 114 | $plugin->supportsControllerAndAction( 115 | 'BannedController', 116 | 'anyAction' 117 | ) 118 | ); 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /tests/Vectorface/SnappyRouterTests/Request/HttpRequestTest.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | class HttpRequestTest extends TestCase 15 | { 16 | /** 17 | * An overview of how to use the RPCRequest class. 18 | */ 19 | public function testSynopsis() 20 | { 21 | // instantiate the class 22 | $request = new HttpRequest('MyService', 'MyMethod', 'GET', 'php://memory'); 23 | 24 | $this->assertEquals('GET', $request->getVerb()); 25 | $this->assertEquals('POST', $request->setVerb('POST')->getVerb()); 26 | 27 | $queryData = ['id' => '1234']; 28 | $this->assertSame( 29 | 1234, 30 | $request->setQuery($queryData)->getQuery('id', 0, 'int') 31 | ); 32 | 33 | $postData = ['username' => ' TEST_USER ']; 34 | $this->assertEquals( 35 | 'test_user', 36 | $request->setPost($postData)->getPost('username', '', ['trim', 'lower']) 37 | ); 38 | 39 | $this->assertEquals( 40 | ['id' => '1234', 'username' => ' TEST_USER '], 41 | $request->getAllInput() 42 | ); 43 | } 44 | 45 | /** 46 | * Tests successful input stream set and fetch cases 47 | * 48 | * @throws InternalErrorException 49 | */ 50 | public function testInputStream() 51 | { 52 | $tempStream = fopen('php://memory', 'w'); 53 | fwrite($tempStream, "test"); 54 | rewind($tempStream); 55 | 56 | /* Mock a stream in memory */ 57 | $request = new HttpRequest('TestService', 'TestMethod', 'GET', $tempStream); 58 | $this->assertEquals("test", $request->getBody()); 59 | fclose($tempStream); 60 | 61 | /* Fetch previously stored value */ 62 | $this->assertEquals("test", $request->getBody()); 63 | 64 | /* Specify php://memory as a string */ 65 | $request = new HttpRequest('TestService', 'TestMethod', 'GET', 'php://memory'); 66 | $this->assertEquals("", $request->getBody()); 67 | } 68 | 69 | /** 70 | * Tests the input stream functionality where the stream source is not a string or a stream 71 | * 72 | * @throws InternalErrorException 73 | */ 74 | public function testInputStreamIncorrectTypeFailure() 75 | { 76 | $this->expectException(InternalErrorException::class); 77 | 78 | $request = new HttpRequest('TestService', 'TestMethod', 'GET', 1); 79 | $request->getBody(); 80 | } 81 | 82 | /** 83 | * Tests the input stream functionality where the stream source does not exist 84 | * 85 | * @throws InternalErrorException 86 | */ 87 | public function testInputStreamIncorrectFileFailure() 88 | { 89 | $this->expectException(InternalErrorException::class); 90 | 91 | $request = new HttpRequest('TestService', 'TestMethod', 'GET', 'file'); 92 | $request->getBody(); 93 | } 94 | 95 | /** 96 | * Tests the various filters. 97 | * 98 | * @dataProvider filtersProvider 99 | * @param string $expected 100 | * @param string $value 101 | * @param string $filters 102 | */ 103 | public function testInputFilters($expected, $value, $filters) 104 | { 105 | $request = new HttpRequest('TestService', 'TestMethod', 'GET', 'php://input'); 106 | $request->setQuery(['key' => $value]); 107 | $this->assertSame($expected, $request->getQuery('key', null, $filters)); 108 | } 109 | 110 | /** 111 | * The data provider for testInputFilters. 112 | */ 113 | public function filtersProvider() 114 | { 115 | return [ 116 | [ 117 | 1234, 118 | '1234', 119 | 'int' 120 | ], 121 | [ 122 | 1234.5, 123 | ' 1234.5 ', 124 | 'float' 125 | ], 126 | [ 127 | 'hello world', 128 | "\t".'hello world '.PHP_EOL, 129 | 'trim' 130 | ], 131 | [ 132 | 'hello world', 133 | 'HELLO WORLD', 134 | 'lower' 135 | ], 136 | [ 137 | 'HELLO WORLD', 138 | 'hello world', 139 | 'upper' 140 | ], 141 | [ 142 | 'test'.PHP_EOL.'string', 143 | 'test'.PHP_EOL.' '.PHP_EOL.PHP_EOL.'string', 144 | 'squeeze' 145 | ], 146 | ]; 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /tests/Vectorface/SnappyRouterTests/Request/JsonRpcRequestTest.php: -------------------------------------------------------------------------------- 1 | 'remoteProcedure', 23 | 'params' => [1, 2, 3], 24 | 'id' => 'uniqueidentifier' 25 | ]); 26 | 27 | $this->assertEquals('POST', $request->getVerb()); 28 | $this->assertEquals('remoteProcedure', $request->getMethod()); 29 | $this->assertEquals('1.0', $request->getVersion()); 30 | $this->assertEquals([1, 2, 3], $request->getParameters()); 31 | $this->assertEquals('uniqueidentifier', $request->getIdentifier()); 32 | $this->assertNull($request->getPost('anything')); // Post should be ignored. 33 | 34 | /* Handles JSON-RPC 2.0 requests. */ 35 | $request = new JsonRpcRequest('MyService', (object)[ 36 | 'jsonrpc' => '2.0', 37 | 'method' => 'remoteProcedure', 38 | 'id' => 'uniqueidentifier' 39 | ]); 40 | 41 | $this->assertEquals('2.0', $request->getVersion()); 42 | $this->assertEquals([], $request->getParameters()); 43 | 44 | /* Catches invalid request format. */ 45 | $request = new JsonRpcRequest('MyService', (object)[ 46 | 'jsonrpc' => '2.0', 47 | 'method' => null 48 | ]); 49 | $this->assertFalse($request->isValid()); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /tests/Vectorface/SnappyRouterTests/Response/JsonRpcResponseTest.php: -------------------------------------------------------------------------------- 1 | '2.0', 25 | 'method' => 'remoteProcedure', 26 | 'params' => [1, 2, 3], 27 | 'id' => 'identifier' 28 | ]); 29 | 30 | $response = new JsonRpcResponse('object, array, or scalar', null, $request); 31 | $obj = $response->getResponseObject(); 32 | $this->assertEquals("2.0", $obj->jsonrpc, "Responds with the same version"); 33 | $this->assertEquals('object, array, or scalar', $obj->result, "Result is passed through"); 34 | $this->assertEquals("identifier", $obj->id, "Request ID is passed back"); 35 | 36 | /* Notifications generate no response */ 37 | $request = new JsonRpcRequest('MyService', (object)['method' => 'notifyProcedure']); 38 | $response = new JsonRpcResponse("anything", null, $request); 39 | $this->assertEquals("", $response->getResponseObject()); 40 | 41 | /* An error passes back a message and a code */ 42 | $request = new JsonRpcRequest('MyService', (object)['method' => 'any', 'id' => 123]); 43 | $response = new JsonRpcResponse(null, new Exception("ex", 123), $request); 44 | $obj = $response->getResponseObject(); 45 | $this->assertEquals(123, $obj->error->code); 46 | $this->assertEquals("ex", $obj->error->message); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /tests/Vectorface/SnappyRouterTests/SnappyRouterTest.php: -------------------------------------------------------------------------------- 1 | 17 | */ 18 | class SnappyRouterTest extends TestCase 19 | { 20 | /** 21 | * An overview of how to use the SnappyRouter class. 22 | * 23 | * @throws Exception 24 | */ 25 | public function testSynopsis() 26 | { 27 | // an example configuration of the router 28 | $config = $this->getStandardConfig(); 29 | // instantiate the router 30 | $router = new SnappyRouter(new Config($config)); 31 | // configure a logger, if insight into router behavior is desired 32 | $router->setLogger(new NullLogger()); 33 | 34 | // an example MVC request 35 | $_SERVER['REQUEST_URI'] = '/Test/test'; 36 | $_SERVER['REQUEST_METHOD'] = 'GET'; 37 | $_GET['param'] = 'value'; 38 | $response = $router->handleRoute('apache2handler'); 39 | 40 | $expectedResponse = 'This is a test service.'; 41 | $this->assertEquals($expectedResponse, $response); 42 | 43 | unset($_SERVER['REQUEST_URI']); 44 | $_GET = []; 45 | $_POST = []; 46 | } 47 | 48 | /** 49 | * Returns a standard router config array. 50 | * @return array A standard router config. 51 | */ 52 | private function getStandardConfig() 53 | { 54 | return [ 55 | Config::KEY_DI => 'Vectorface\SnappyRouter\Di\Di', 56 | Config::KEY_HANDLERS => [ 57 | 'BogusCliHandler' => [ 58 | Config::KEY_CLASS => 'Vectorface\SnappyRouter\Handler\CliTaskHandler' 59 | ], 60 | 'ControllerHandler' => [ 61 | Config::KEY_CLASS => 'Vectorface\SnappyRouter\Handler\ControllerHandler', 62 | Config::KEY_OPTIONS => [ 63 | ControllerHandler::KEY_BASE_PATH => '/', 64 | Config::KEY_CONTROLLERS => [ 65 | 'TestController' => 'Vectorface\SnappyRouterTests\Controller\TestDummyController' 66 | ], 67 | Config::KEY_PLUGINS => [ 68 | 'TestPlugin' => [ 69 | Config::KEY_CLASS => 'Vectorface\SnappyRouterTests\Plugin\TestPlugin', 70 | Config::KEY_OPTIONS => [] 71 | ], 72 | 'AnotherPlugin' => 'Vectorface\SnappyRouterTests\Plugin\TestPlugin' 73 | ] 74 | ] 75 | ], 76 | 'CliHandler' => [ 77 | Config::KEY_CLASS => 'Vectorface\SnappyRouter\Handler\CliTaskHandler', 78 | Config::KEY_OPTIONS => [ 79 | 'tasks' => [ 80 | 'TestTask' => 'Vectorface\SnappyRouterTests\Task\DummyTestTask' 81 | ] 82 | ] 83 | ] 84 | ] 85 | ]; 86 | } 87 | 88 | /** 89 | * Tests that the router handles a generic exception. 90 | * 91 | * @throws Exception 92 | */ 93 | public function testGenericException() 94 | { 95 | $config = $this->getStandardConfig(); 96 | $router = new SnappyRouter(new Config($config)); 97 | 98 | // an example MVC request 99 | $path = '/Test/genericException'; 100 | $query = ['jsoncall' => 'testMethod']; 101 | $response = $router->handleHttpRoute($path, $query, [], 'get'); 102 | 103 | $expectedResponse = 'A generic exception.'; 104 | $this->assertEquals($expectedResponse, $response); 105 | } 106 | 107 | /** 108 | * Tests that an empty config array results in no handler being found. 109 | * 110 | * @throws Exception 111 | */ 112 | public function testNoHandlerFoundException() 113 | { 114 | // turn on debug mode so we get a verbose description of the exception 115 | $router = new SnappyRouter(new Config([ 116 | 'debug' => true 117 | ])); 118 | 119 | // an example MVC request 120 | $path = '/Test/test'; 121 | $query = ['jsoncall' => 'testMethod']; 122 | $response = $router->handleHttpRoute($path, $query, [], 'get'); 123 | $this->assertEquals('No handler responded to the request.', $response); 124 | } 125 | 126 | /** 127 | * Tests that an exception is thrown if a handler class does not exist. 128 | * 129 | * @throws Exception 130 | */ 131 | public function testInvalidHandlerClass() 132 | { 133 | $this->expectException(Exception::class); 134 | $this->expectExceptionMessage("Cannot instantiate instance of Vectorface\SnappyRouter\Handler\NonexistentHandler"); 135 | 136 | $config = $this->getStandardConfig(); 137 | $config[Config::KEY_HANDLERS]['InvalidHandler'] = [ 138 | 'class' => 'Vectorface\SnappyRouter\Handler\NonexistentHandler' 139 | ]; 140 | $router = new SnappyRouter(new Config($config)); 141 | 142 | // an example MVC request 143 | $path = '/Test/test'; 144 | $query = ['jsoncall' => 'testMethod']; 145 | $response = $router->handleHttpRoute($path, $query, [], 'get'); 146 | 147 | $expectedResponse = 'No handler responded to request.'; 148 | $this->assertEquals($expectedResponse, $response); 149 | } 150 | 151 | /** 152 | * Tests that the CLI routing functionality works. 153 | * 154 | * @throws Exception 155 | */ 156 | public function testStandardCliRoute() 157 | { 158 | $config = $this->getStandardConfig(); 159 | $router = new SnappyRouter(new Config($config)); 160 | 161 | $_SERVER['argv'] = [ 162 | 'dummyScript.php', 163 | '--task', 164 | 'TestTask', 165 | '--action', 166 | 'testMethod' 167 | ]; 168 | $_SERVER['argc'] = count($_SERVER['argv']); 169 | $response = $router->handleRoute(); 170 | 171 | $expected = 'Hello World'.PHP_EOL; 172 | $this->assertEquals($expected, $response); 173 | } 174 | 175 | /** 176 | * Tests a CLI route that throws an exception. 177 | * 178 | * @throws Exception 179 | */ 180 | public function testCliRouteWithException() 181 | { 182 | $config = $this->getStandardConfig(); 183 | $router = new SnappyRouter(new Config($config)); 184 | 185 | $_SERVER['argv'] = [ 186 | 'dummyScript.php', 187 | '--task', 188 | 'TestTask', 189 | '--action', 190 | 'throwsException' 191 | ]; 192 | $_SERVER['argc'] = count($_SERVER['argv']); 193 | $response = $router->handleRoute(); 194 | 195 | $expected = 'An exception was thrown.'.PHP_EOL; 196 | $this->assertEquals($expected, $response); 197 | } 198 | 199 | /** 200 | * Tests that a CLI route with no appropriate handlers throws an 201 | * exception. 202 | * 203 | * @throws Exception 204 | */ 205 | public function testCliRouteWithNoHandler() 206 | { 207 | $config = $this->getStandardConfig(); 208 | $router = new SnappyRouter(new Config($config)); 209 | 210 | $_SERVER['argv'] = [ 211 | 'dummyScript.php', 212 | '--task', 213 | 'NotDefinedTask', 214 | '--action', 215 | 'anyAction' 216 | ]; 217 | $_SERVER['argc'] = count($_SERVER['argv']); 218 | $response = $router->handleRoute(); 219 | 220 | $expected = 'No CLI handler registered.'.PHP_EOL; 221 | $this->assertEquals($expected, $response); 222 | } 223 | } 224 | -------------------------------------------------------------------------------- /tests/Vectorface/SnappyRouterTests/Task/CliTaskHandlerTest.php: -------------------------------------------------------------------------------- 1 | 15 | */ 16 | class CliTaskHandlerTest extends TestCase 17 | { 18 | /** 19 | * An overview of how to use the CliTaskHandler class. 20 | * 21 | * @throws PluginException 22 | */ 23 | public function testSynopsis() 24 | { 25 | $options = [ 26 | Config::KEY_TASKS => [ 27 | 'TestTask' => DummyTestTask::class, 28 | ] 29 | ]; 30 | $handler = new CliTaskHandler($options); 31 | $components = [ 32 | 'dummyScript.php', 33 | '--task', 34 | 'TestTask', 35 | '--action', 36 | 'testAction' 37 | ]; 38 | 39 | // the components needs to be at least 5 elements with --task and --action 40 | $this->assertTrue($handler->isAppropriate($components)); 41 | 42 | // assert the handler is not appropriate if we only have 4 elements 43 | $this->assertFalse($handler->isAppropriate(array_slice($components, 0, 4))); 44 | 45 | // assert the handler is not appropriate if --task and --action are missing 46 | $badComponents = $components; 47 | $badComponents[1] = '--service'; 48 | $this->assertFalse($handler->isAppropriate($badComponents)); 49 | } 50 | 51 | /** 52 | * A test that asserts an exception is thrown if we call an action missing 53 | * from a registered task. 54 | * 55 | * @throws PluginException|ResourceNotFoundException 56 | */ 57 | public function testMissingActionOnTask() 58 | { 59 | $this->expectException(ResourceNotFoundException::class); 60 | $this->expectExceptionMessage("TestTask task does not have action missingAction."); 61 | 62 | $options = [ 63 | Config::KEY_TASKS => [ 64 | 'TestTask' => DummyTestTask::class, 65 | ] 66 | ]; 67 | $handler = new CliTaskHandler($options); 68 | $components = [ 69 | 'dummyScript.php', 70 | '--task', 71 | 'TestTask', 72 | '--action', 73 | 'missingAction' 74 | ]; 75 | $this->assertTrue($handler->isAppropriate($components)); 76 | $handler->performRoute(); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /tests/Vectorface/SnappyRouterTests/Task/DummyTestTask.php: -------------------------------------------------------------------------------- 1 | getOptions(); 17 | $this->set('taskOptions', $options); 18 | $this->set('response', 'Hello World'); 19 | return $this->get('response'); 20 | } 21 | 22 | /** 23 | * @throws Exception 24 | */ 25 | public function throwsException() 26 | { 27 | throw new Exception('An exception was thrown.'); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /tests/bootstrap.php: -------------------------------------------------------------------------------- 1 |