Skip to content

Commit 7dbe865

Browse files
committed
first commit with all files
0 parents  commit 7dbe865

40 files changed

+7470
-0
lines changed

.codeclimate.yml

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
---
2+
engines:
3+
duplication:
4+
enabled: true
5+
config:
6+
languages:
7+
- ruby
8+
- javascript
9+
- python
10+
- php
11+
fixme:
12+
enabled: true
13+
phpmd:
14+
enabled: true
15+
Controversial/Superglobals:
16+
enabled: false
17+
exclude_fingerprints:
18+
- 04184a89cc95d4de27194c1bfae58468
19+
- 113dfe251ce1988d4de94f4141f41267
20+
- c994a1b3b901807afd4128bef6310a69
21+
- d3f4156691d2262bda1a34326a6a2786
22+
- 481ad10f1f811b709de0c99951196766
23+
- 672a2e1ab76ccbf5bdc4fd80e17c21e1
24+
ratings:
25+
paths:
26+
- "**.inc"
27+
- "**.js"
28+
- "**.jsx"
29+
- "**.module"
30+
- "**.php"
31+
- "**.py"
32+
- "**.rb"
33+
exclude_paths:
34+
- tests/
35+
- vendor/

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
.DS_Store
2+
/vendor
3+
/tests/_output/*
4+
composer.lock

.travis.yml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
dist: trusty
2+
language: php
3+
php:
4+
- 5.6
5+
- 7.0
6+
- hhvm
7+
before_script:
8+
- composer self-update
9+
- composer install --prefer-source --no-interaction
10+
script: vendor/bin/codecept run

LICENSE

Lines changed: 674 additions & 0 deletions
Large diffs are not rendered by default.

README.md

Lines changed: 340 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,340 @@
1+
# JetRouter - WordPress Routing, Fast & Easy
2+
3+
[![Code Climate](https://codeclimate.com/github/sformisano/jetrouter/badges/gpa.svg)](https://codeclimate.com/github/sformisano/jetrouter) [![Build Status](https://travis-ci.org/sformisano/jetrouter.svg?branch=master)](https://travis-ci.org/sformisano/jetrouter)
4+
5+
This WordPress plugin adds a regular expression based router on top of the WordPress standard routing system. Any route declared through this router will take priority over standard WordPress urls.
6+
7+
- [Installation](#installation)
8+
- [Usage](#usage)
9+
- [Initialization & Configuration](#initialization--configuration)
10+
- [Configuration properties](#available-router-configuration-properties)
11+
- [Adding Routes](#adding-routes)
12+
- [The basics](#the-basics)
13+
- [Dynamic routes](#dynamic-routes)
14+
- [Parameters regular expressions](#parameters-regular-expressions)
15+
- [Optional parameters](#optional-parameters)
16+
- [Reverse Routing](#reverse-routing)
17+
- [Dynamic routes reverse routing](#dynamic-routes-reverse-routing)
18+
- [Handler's respond_to Feature](#handlers-respond_to-feature)
19+
- [Why I built it](#why-i-built-the-jetrouter)
20+
- [Credits](#credits)
21+
22+
23+
## Installation
24+
25+
#### Download zip
26+
27+
##### Download via WordPress plugin page
28+
29+
You can visit the [WordPress plugin page](https://wordpress.org/plugins/jetrouter/) and download the latest plugin version there.
30+
31+
##### Download via GitHub repo release page
32+
33+
You can also grab the latest release from this repository's [releases page](https://github.com/sformisano/jetrouter/releases).
34+
35+
#### Composer
36+
37+
If your setup uses [Composer](https://getcomposer.org/) for proper dependency management (check out the awesome [Bedrock project](https://roots.io/bedrock/) if you are interested in running WordPress as a 12 factor app) installing the JetRouter is quite easy.
38+
39+
##### Composer via WordPress Packagist (simpler & better)
40+
41+
[WordPress Packagist](https://wpackagist.org/) is a site that scans the WordPress Subversion repository every hour for plugins and themes and then mirrors them as Composer repositories.
42+
43+
Search for "jetrouter" (here's the [search results for that](https://wpackagist.org/search?q=jetrouter) ), click on the latest version in the "Versions" column of the record and you will be given the code to paste in your `composer.json` file in the `require` property.
44+
45+
It should look something like this:
46+
47+
```
48+
"wpackagist-plugin/jetrouter": "0.1.2.1"
49+
```
50+
51+
**Please note:** I have had no time to verify this, but even without using the [Bedrock](https://roots.io/bedrock/) WordPress boilerplate, wpackagist packages should automatically install in the plugins directory (which is what you would want) rather than the default composer vendor directory. If that is not the case, reference the *Composer via GitHub releases* section below to learn how to tell composer where to install your WordPress plugins.
52+
53+
##### Composer via GitHub releases
54+
55+
Add this package to the "repositories" property of your composer json definition (make sure the release version is the one you want to install):
56+
57+
```json
58+
{
59+
"repositories": [
60+
{
61+
"type": "package",
62+
"package": {
63+
"name": "jetrouter",
64+
"type": "wordpress-plugin",
65+
"version": "0.1.2.1",
66+
"dist": {
67+
"type": "zip",
68+
"url": "https://github.com/sformisano/jetrouter/releases/download/v0.1.2/jetrouter-v0.1.2.1.zip",
69+
"reference": "v0.1.2.1"
70+
},
71+
"autoload": {
72+
"classmap": ["."]
73+
}
74+
}
75+
}
76+
]
77+
}
78+
```
79+
80+
Once that's done, add the JetRouter in the composer require property (the version has to match the package version):
81+
82+
```json
83+
{
84+
"require":{
85+
"jetrouter": "0.1.2.1"
86+
}
87+
}
88+
```
89+
90+
Finally, make sure to specify the correct path for the "wordpress-plugin" type we assigned
91+
to the JetRouter package in the definition above (note: if you are using
92+
[Bedrock](https://roots.io/bedrock/) this is already taken care of).
93+
94+
```json
95+
{
96+
"extra": {
97+
"installer-paths": {
98+
"path/to/your/wordpress/plugins-directory/{$name}/": ["type:wordpress-plugin"],
99+
}
100+
}
101+
}
102+
```
103+
Run `composer update` from your composer directory and you're good to go!
104+
105+
## Usage
106+
107+
### Initialization & Configuration
108+
In your theme’s `functions.php` file or somewhere in your plugin:
109+
110+
```php
111+
// Import the JetRouter
112+
use JetRouter\Router;
113+
114+
// Router config
115+
$config = [];
116+
117+
// Create the router instance
118+
$r = Router::create($config);
119+
```
120+
121+
#### Available router configuration properties:
122+
123+
* `namespace`: if defined, the namespace prefixes all routes, e.g. if you set the namespace to “my-api” and then define a route as “comments/published”, the actual endpoint will be “/my-api/comments/published/“.
124+
* `outputFormat`: determines the routes handlers output format. Available values: `auto`, `json`, `html`.
125+
126+
The router automatically hooks itself to WordPress, so once you’ve created the router instance with the configuration that suits your needs, all that is left to do is adding routes. After that, the router is ready to dispatch requests.
127+
128+
##### Pro tip on the namespace
129+
130+
If you use the JetRouter to build a json/data api, or if you simply don’t mind having a namespace before your urls, having one will speed up standard WordPress requests, especially if you have many dynamic routes.
131+
132+
This is because, if the router has a namespace, but the http request’s path does not begin with that namespace (like it would be the case with all WordPress urls), the router won’t even try to dispatch the request.
133+
134+
Without a namespace, on the other hand, the router will try to dispatch all requests, building the dynamic routes array every time. *(Note: caching the generated dynamic routes is on my todo list.)*
135+
136+
### Adding Routes
137+
138+
#### The basics
139+
140+
The basic method to add routes is `addMethod`. Example:
141+
142+
```php
143+
$r->addRoute('GET', 'some/resource/path', 'the_route_name', function(){
144+
// the callback fired when a request matches this route
145+
});
146+
```
147+
148+
The `addRoute` method has aliases for each http method (get, post, put, patch, delete, head, options). Example:
149+
150+
```php
151+
$r->get(‘users/new’, ‘route_name’, function(){
152+
// new user form view
153+
});
154+
155+
$r->post('users', 'create_user', function(){
156+
// create user in database
157+
});
158+
```
159+
160+
#### Dynamic routes
161+
162+
Use curly braces to outline parameters, then declare them as arguments of the callback function and the router will take care of everything else. Example:
163+
164+
```php
165+
$r->get('users/{username}', 'get_user_by_username', function($username){
166+
echo "Welcome back, $username!";
167+
});
168+
169+
$->get('schools/{school}/students/{year}', 'get_school_students_by_year', function($school, $year){
170+
echo "Welcome, $school students of $year!";
171+
});
172+
```
173+
174+
#### Parameters regular expressions
175+
By default, all parameters are matched against a very permissive regular expression:
176+
177+
```php
178+
/**
179+
* One or more characters that is not a '/'
180+
*/
181+
const DEFAULT_PARAM_VALUE_REGEX = '[^/]+';
182+
```
183+
184+
You can use your own regular expressions by adding a colon after the parameter name and the regex right after it. Example:
185+
186+
```php
187+
$r->get('schools/{school}/students/{year:[0-9]+}', 'get_school_students_by_year', function($school, $year){
188+
// If $year is not an integer an exception will be thrown
189+
});
190+
```
191+
192+
You can also use one of these regex shortcuts for convenience:
193+
194+
```php
195+
private $regexShortcuts = [
196+
':i}' => ':[0-9]+}', // integer
197+
':a}' => ':[a-zA-Z0-9]+}', // alphanumeric
198+
':s}' => ':[a-zA-Z0-9_\-\.]+}' // alphanumeric, "_", "-" and ".""
199+
];
200+
```
201+
202+
If, for example, we wanted to write the same `get_school_students_by_year` route by using the `:i` shortcut, this is how we would do it:
203+
204+
```php
205+
$r->get('schools/{school}/students/{year:i}', 'get_school_students_by_year', function($school, $year){
206+
// If $year is not an integer an exception will be thrown
207+
});
208+
```
209+
210+
#### Optional parameters
211+
212+
You can make a parameter optional by adding a question mark after the closing curly brace. Example:
213+
214+
```php
215+
$r->get('schools/{school}/students/{year}?’, 'get_school_students_by_year', function($school, $year){
216+
// $year can be empty!
217+
});
218+
```
219+
220+
You can make all parameters optionals, even when they are not the last segment in a route. Example:
221+
222+
```php
223+
$r->get('files/{filter_name}?/{filter_value}?/latest', 'get_latest_files', function($filter_name, $filter_value){
224+
// get files and work with filters if they have a value
225+
});
226+
```
227+
228+
All the example requests below would match the route defined above:
229+
230+
```php
231+
'/files/latest'
232+
'/files/type/pdf/latest'
233+
'/files/author/sformisano/latest'
234+
```
235+
236+
This is not necessarily the best use of optional parameters. The examples here are just to show that, if you have a valid use case, the JetRouter will allow you to setup your routes this way.
237+
238+
### Reverse Routing
239+
240+
Reverse routing is done through the same router object for simplicity’s sake.
241+
242+
Given this simple example route:
243+
244+
```php
245+
$r->addRoute('GET', 'posts/popular', 'popular_posts', function(){});
246+
```
247+
248+
To print out the full path you use the `thePath` method:
249+
250+
```php
251+
$r->thePath('popular_posts'); // prints /posts/popular/
252+
```
253+
254+
You can also get the value returned, rather than printed out, by using the `getThePath` method (trying to follow some WordPress conventions with this):
255+
256+
```php
257+
$r->getThePath('popular_posts'); // returns /posts/popular/
258+
```
259+
260+
#### Dynamic routes reverse routing
261+
262+
Dynamic routes work in the exact same way, you just pass the values of the parameters after the route name in the same order they are defined in when you created the route.
263+
264+
For example, given this route:
265+
266+
```php
267+
$r->get('schools/{school}/students/{year}?’, 'get_school_students_by_year', function($school, $year){
268+
});
269+
```
270+
271+
This is how you would print out a full path to this route:
272+
273+
```php
274+
$r->thePath('get_school_students_by_year', 'caltech', 2005); // prints /schools/caltech/students/2005/
275+
```
276+
277+
As the last parameter is optional you can simply omit it:
278+
279+
```php
280+
$r->thePath('get_school_students_by_year', 'mit'); // prints /schools/mit/students/
281+
```
282+
283+
If the optional parameter you need to omit is not the last parameter, you can simply pass `null` as a value and it will be ignored. Example:
284+
285+
```php
286+
$r->get('{school}?/students/{year}’, 'get_students', function($school, $year){
287+
});
288+
289+
$r->thePath('get_students', null, 1999); // prints /students/1999/
290+
```
291+
292+
I prefer this approach to the more common associative array for arguments as it makes everything simpler (less typing in the vast majority of scenarios) and more explicit (a missing optional parameter is still defined as null, so there’s a 1:1 match between formal parameters and arguments).
293+
294+
### Handler's respond_to Feature
295+
296+
If you ever played around with Ruby on Rails, you're probably familiar with the [respond_to and respond_with blocks](http://www.davidwparker.com/2010/03/09/api-in-rails-respond-to-and-respond-with/). This respond_to feature is a basic, simplistic imitation of that clever way to handle different request types with different behaviours.
297+
298+
If you never tried out Rails, let me make this simple with an example:
299+
300+
You created a public signup form that posts to a `create_user` endpoint created with the JetRouter. You definitely want that form to work with ajax, but you're one of those few developers left who still cares about graceful degradation, so you don't just want to set the `outputFormat` config parameter to `json` and do a `return $data;` of sorts. You need to be able to receive json data with ajax requests, while doing something else (redirects, loading views in error mode etc.) for standard, synchronous requests.
301+
302+
Technically, you *could* manually check for `$_SERVER['HTTP_X_REQUESTED_WITH']` and write some conditional code that works with that, but who wants to do that kind of manual labour for every single endpoint?
303+
304+
JetRouter's respond_to to the rescue! All you have to do is leave the `outputFormat` config parameter to `auto` and have your route handler return an array following this format:
305+
306+
```php
307+
308+
// endpoint doing stuff up here
309+
310+
return [ 'respond_to' => [
311+
'json' => $output // this is whatever came out of this endpoint, to be returned as json!,
312+
'html' => function(){
313+
// this callback will run if this the route handler is dispatching a standard, synchronous request
314+
// do something here, then maybe redirect with wp_redirect or whatever else!
315+
}
316+
] ];
317+
```
318+
319+
**Pro tip:** appending ?json to a request path will force the json output.
320+
321+
## Why I built the JetRouter
322+
323+
* I wanted something fast, easy to setup and with a minimal API. The JetRouter is setup with one line of code (`$r = Router::create();`) and all functionality is readily available through that object.
324+
325+
* I needed extra features not offered by any other small footprint php router I am familiar with, including the ones I credited below.
326+
327+
* The routers I am aware of, even the ones I credited below, are conceived to be used as the core routing system of a website/webapp. This does not make them the best fit for the use case of a WordPress extra routing layer, e.g. they will throw an exception if a route is not found or if there’s a path match but the http method of the request is different. Because this router is an extra layer on top of WordPress, it needs to fail silently in most of these scenarios and give control back to WordPress. WordPress can then dispatch those requests matching, return a 404 or do whatever else it’s supposed to do.
328+
329+
* Finally, building this looked like a fun way to revisit PCRE, which are something I have not worked with in many years.
330+
331+
## Credits
332+
333+
* This router implements [Nikita Popov](https://github.com/nikic)’s group position based, non-chunked approach to regex matching for lightning fast routing. Learn more about it by reading [this great post](http://nikic.github.io/2014/02/18/Fast-request-routing-using-regular-expressions.html) by Nikita himself.
334+
335+
* [Joe Green](https://github.com/mrjgreen) and his [phroute](https://github.com/mrjgreen/phroute) project (also loosely based on Nikita’s work) are to be thanked for:
336+
* the regex used to capture dynamic routes parameters
337+
* the regex shortcuts (a simple yet very elegant idea)
338+
* the basics of the optional parameters and reverse routing implementation
339+
340+
Go check both these authors and their projects, they are awesome.

0 commit comments

Comments
 (0)