-
Notifications
You must be signed in to change notification settings - Fork 2
Diving Deeper
This section will introduce you to some of the more advanced features of Spore.
To follow along with this documentation, you can either modify the code inside the /examples/services/TestService.php
file, or you can create your own "service(s)". You can create your service classes anywhere in the project you like, and you can add them to Spore by simply doing the following:
<?php
require_once "vendor/autoload.php";
require_once "/path/to/services/MyService.php";
use Spore\Spore;
$app = new Spore();
// add an individual service
$app->addService(new MyService());
//
//
// ------ OR ------ //
//
//
// scan recursively for services
$app->addServicesDirectory("/path/to/services/");
$app->run();
A service class is just a regular PHP class, and Spore has no special requirements.
If you would like to disable Spore's default services from being loaded, you can do so by passing a configuration option like so:
<?php
$app = new Spore(array(
"include-examples" => false
));
One of Spore's key features is its annotation-based routing of API requests.
Well, here's the thing… PHP doesn't technically have "annotations" in the traditional sense. However, what it does have is a Reflection
library and so-called DocBlock
comments. You know… these guys:
/**
* I'm a DocBlock (or multiline) comment
*/
I have published another Packagist
library called PHP DocBlock Parser Lite. It's a very simple PHP library that uses the builtin Reflection
API to parse all the DocBlock comments in a set of given classes.
So… An annotation in the sense I'm using it is as follows:
/**
* @name value
*/
Straightforward enough, right? Annotations are a very handy way of adding metadata about a block of code, and that's how this feature of Spore is built.
A route is simply a URL that relates to some "resource". In the case of Spore, a route is defined as a URL which relates to a callback function
which provides data.
In Spore, we call this an auto-route.
Here's an example:
We will use the @url
annotation to define a route, and a @verbs
annotation to define which HTTP methods (verbs) this callback function will allow.
<?php
/**
* @url /hello-world
* @verbs GET
*/
public function sayHello()
{
return "Hello World!";
}
In Spore, Slim's excellent Router
class is overridden to provide a little extra functionality. When a Spore application is started, it will analyze a set of given classes with so-called "auto-routes" (routes with annotations). Spore will then know how to relate the URL http://path/to/spore/hello-world
to the callback function sayHello()
.
Annotation | Description | Acceptable values |
---|---|---|
@name | A name for your auto-route | Any value e.g. myRoute |
@url | A resource URI that relates to a callback function | Anything you like - provided it complies with Slim's URI conventions |
@verbs | A comma-delimited list of acceptable HTTP verbs | GET, POST, PUT, DELETE and any custom verbs |
@auth | A comma-delimited list of authorization roles that may access the related callback | Any value e.g. admin,superuser |
@template | The filename of the template file you'd like to use More info: Templating |
Any value e.g. signup.twig |
@render | The render mode of your template file More info: Templating |
always, nonAJAX, nonXHR, never |
@condition | A pattern that needs to be matched for the auto-route to be called correctly More info: Conditions |
Any value e.g. paramName .+ |
Spore contains a number of useful, configurable properties.
The Spore configuration works off the native Slim config
functionality. Simply use it as you would normally use the Slim configuration.
See the Slim documentation for more information.
Name | Description | Default | Options |
---|---|---|---|
debug | Debug mode | true |
boolean |
content-type | The default content encoding type | application/json |
See Acceptable serialization formats |
gzip | GZIP compression | true |
boolean |
pass-params | Determine whether Request and Response objects should be passed to auto-routes |
true |
See Request and Response section |
include-examples | Include Spore's example services | true |
boolean |
xml-top-node | Change the name of the top-level node in serialized XML data produced by the XMLSerializer
|
"data" |
String |
xml-node | Change the name of the anonymous nodes in serialized XML data produced by the XMLSerializer
|
"element" |
String |
Here's an example of how you can override a configuration value:
<?php
require_once "vendor/autoload.php";
use Spore\Spore;
$app = new Spore(array("debug" => false));
$app->get("/", function () use ($app)
{
return array("message" => "Hello World from Spore", "debugModeEnabled" => $app->config("debug"));
});
$app->run();
Serialization is a very important feature in Spore. Spore enables you to forget about ever having to parse
, encode
or decode
the data you're working with.
It's a fundamental premise of Spore that data should be kept in its most abstract format (native PHP data types) until it's necessary to encode it into an HTTP response. This allows you to work with your data as data, not jumbles of syntax that need to be processed before being worked on.
Spore will make your life easier by deserializing incoming request data and serializing outgoing response data, all-the-while respecting content negotiation settings and default content types.
Spore leverages Slim's Middleware
functionality to deserialize incoming request data based on its content type. The native PHP primatives and objects will be passed to the receiving auto-route for you to work with. Likewise, when you want to send some data back to the client, simply return
the data and Spore will do the rest.
PHP code
<?php
/**
* @url /serialization-example
* @verbs POST,PUT
*/
public function serializationExample(Request $request, Response $response)
{
$incoming = $request->data;
$outgoing = array("3");
return array("incoming" => $incoming, "outgoing" => $outgoing);
}
HTTP Request
POST /projects/spore/serialization-example HTTP/1.1
Host: localhost
Content-Length: 64
Origin: chrome-extension://hgmloofddffdnphfgcellkdfbfbjeloo
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_5) AppleWebKit/537.4 (KHTML, like Gecko) Chrome/22.0.1229.79 Safari/537.4
Content-Type: application/json
Accept: */*
Accept-Encoding: gzip,deflate,sdch
Accept-Language: en-US,en;q=0.8
Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.3
Cookie: splashShown1.5=1; PHPSESSID=e8d6cce60ba5c3471db80bb6b110400e
Pragma: no-cache
Cache-Control: no-cache
{"question":"How many fingers am I holding up?", "type":"nonsensical"}
JSON response
{
"incoming": {
"question": "How many fingers am I holding up?",
"type": "nonsensical"
},
"outgoing": [
"3"
]
}
The important elements to note in the example above are:
- The
Content-Type: application/json
header was used to deserialize the incomingJSON
data - The auto-route has two default parameters:
Request $request
andResponse $response
- more on these later - The
$request->data
property contains the deserialized data passed in the body of the HTTPPOST
request - The data passed back from the
serialization-example
auto-route was serialized back intoJSON
because this is the default content type and theAccept: */*
header means we can return whatever content encoding we like. - If we changed the
Accept
header toAccept: application/xml
then theserialization-example
auto-route would have given us this:
<?xml version="1.0" encoding="UTF-8" ?>
<data>
<incoming>
<question>How many fingers am I holding up?</question>
<type>nonsensical</type>
</incoming>
<outgoing>
<element>3</element>
</outgoing>
</data>
Below is a table of serialization formats that can be used. For more information on how to use these, see the Configuration section of this document.
Name | Content-Type | Incoming | Outgoing |
---|---|---|---|
JSON (default) | application/json | • | • |
XML | application/xml,text/xml | • | • |
CSV | text/csv | • |
You can override the outgoing serialization mechanism by using echo
instead of using return
.
Using the same example as above, here's how you could handle the serialization yourself:
<?php
/**
* @url /serialization-example
* @verbs POST,PUT
*/
public function serializationExample(Request $request, Response $response)
{
$incoming = $request->data;
$outgoing = array("3");
$response->headers["Content-Type"] = "application/json";
echo json_encode(array("incoming" => $incoming, "outgoing" => $outgoing));
}
It is not recommended that you do this though, since your code will become less portable. If you have a function that returns native PHP data, it will be possible to use that code internally (i.e. purely on the back-end without an API) using plain ol' PHP classes, whereas if you use echo
, this will become a lot more difficult.
Overriding the serialization mechanism will not affect any authorization rules already in place.
If you would like to add support for certain encodings, you can do so in the following way:
In this example, I'll be implementing incoming and outgoing YAML serialization support.
Firstly, we will need to tell Spore how to handle a certain Content-Type
being passed to our application. In the case of YAML, its Content-Type
(mime-type) is application/x-yaml
.
First, add the symfony/yaml
dependency to your composer.json
file:
{
"require": {
"dannykopping/spore": "dev-master",
"symfony/yaml": "2.*"
}
}
The symfony/yaml
package contains an excellent and simple library for parsing YAML, by the great Fabien Potencier.
Next, we're going to add YAML support to Spore:
<?php
require_once "vendor/autoload.php";
require_once "data/YAMLSerializer.php";
require_once "data/YAMLDeserializer.php";
use Spore\Spore;
$app = new Spore();
$deserializers = $app->config("deserializers");
$app->config(array(
"deserializers" => array_merge($deserializers, array(
"application/x-yaml" => "YAMLDeserializer"
))
));
$serializers = $app->config("serializers");
$app->config(array(
"serializers" => array_merge($serializers, array(
"application/x-yaml" => "YAMLSerializer"
))
));
$app->run();
Let's break this down a bit…
In Spore, we have two configuration options - one contains an associative array that maps mime-types to deserializers, and the other to serializers. In the example above, we are simply adding an extra option to each to handle the application/x-yaml
mime-type.
Now, we need to create the Serializer and Deserializer classes.
<?php
use Spore\ReST\Data\Base;
use Symfony\Component\Yaml\Yaml;
class YAMLSerializer extends Base
{
public static function parse($data)
{
return Yaml::dump($data);
}
}
<?php
use Spore\ReST\Data\Base;
use Symfony\Component\Yaml\Yaml;
class YAMLDeserializer extends Base
{
public static function parse($data)
{
return Yaml::parse($data);
}
}
Simple, innit?
All you need to do is extend the Base abstract class provided by Spore and you'll be able to create your own serializers and deserializers.
Here is an example of the YAML deserializer and serializer working in the same call:
Request
POST /projects/demo/yml HTTP/1.1
Host: localhost
Content-Length: 137
X-Requested-With: XMLHttpRequest
Content-Type: application/x-yaml
Accept: application/x-yaml
Accept-Encoding: gzip,deflate,sdch
---
characters:
children:
daughters:
- Lisa
- Maggie
son: Bart
parents:
father: Homer
mother: Marge
Response
HTTP/1.1 200 OK
Date: Sat, 13 Oct 2012 16:47:24 GMT
Server: Apache/2.2.14 (Unix) DAV/2 mod_ssl/2.2.14 OpenSSL/0.9.8l PHP/5.3.1 mod_perl/2.0.4 Perl/v5.10.1
X-Powered-By: PHP/5.3.1
Content-Encoding: gzip
Vary: Accept-Encoding
Content-Length: 112
Content-Type: application/x-yaml
characters:
children: { daughters: [Lisa, Maggie], son: Bart }
parents: { father: Homer, mother: Marge }
Authorization should always be a concern when developing an API. You may want to restrict access to certain administrative functions, or put in place conditional restrictions based on session data.
Spore enables you to keep your API auto-routes safe by providing the @auth
annotation and a special Authorization Callback mechanism.
Consider the following example:
<?php
/**
* @url /auth-example
* @verbs POST
* @auth admin,super-user,Chuck Norris
*/
public function somethingImportant(Request $request, Response $response)
{
return "Congrats, you're special!";
}
Using the @auth
annotation alone does not secure your auto-route - you will need to define an Authorization Callback to handle authorization requests.
<?php
require_once "vendor/autoload.php";
use Spore\Spore;
$app = new Spore();
$app->authCallback(function ($roles) use ($app)
{
if(empty($roles))
return true;
$currentRole = "Chuck Norris";
return in_array($currentRole, $roles);
});
$app->run();
The authCallback
function is very simple. All it needs to do is return true
or false
. You can define whatever rules you like in order to validate or invalidate the request. A result of true
means that Spore will continue with the request, while a false
will fire an authorization error.
In the above example, look at the following line:
@auth admin,super-user,Chuck Norris
The @auth
annotation allows you to define a comma-delimited list of acceptable roles that can access this auto-route. A "role" is nothing more than an identifier. In the example above, we will allow any "user" with the "role" of admin
, super-user
or Chuck Norris
to access the function.
If you run the above example, the output will be:
"Congrats, you're special!"
… because in the authCallback
function, we defined our $currentRole
to be "Chuck Norris"
and checked to see if it was in the list of acceptable roles (passed into the callback as the $roles
argument).
If we change the $currentRole
to be "Bob"
- you'll see the following output:
{"message":"You are not authorized to execute this function"}
Shit happens; and we need to handle it elegantly.
If you've used Slim before, you'll know that it has a lovely API for handling errors/exceptions and not found errors rather elegantly.
Spore has - by default - custom error and not found handlers to get you started. Keep in mind that Slim error and not found handlers only work if you have debug mode turned off; I always forget this and report bugs immediately (sorry Josh).
If you look in the Spore.php
file, you will see two functions - errorHandler
and notFoundHandler
:
<?php
/**
* Get the default error callback function
*
* @param \Exception $e
*/
public function errorHandler(Exception $e)
{
$this->contentType($this->config("content-type"));
$data = Serializer::getSerializedData($this, array(
"error" => array(
"message" => $e->getMessage(),
"code" => $e->getCode(),
"file" => $e->getFile(),
"line" => $e->getLine(),
)
));
$this->halt(Status::INTERNAL_SERVER_ERROR, $data);
}
/**
* Get the not found callback function
*/
public function notFoundHandler()
{
$this->contentType($this->config("content-type"));
$data = Serializer::getSerializedData($this, array(
"error" => array(
"message" => "'" . $this->request()->getResourceUri() . "' could not be resolved to a valid API call",
"req" => $this->request()->getIp()
)
));
$this->halt(Status::NOT_FOUND, $data);
}
If you would like to override either of these functions, you can do so like this:
<?php
require_once "vendor/autoload.php";
use Spore\Spore;
use Spore\ReST\Model\Status;
$app = new Spore();
$app->error(function(Exception $e) use ($app)
{
$app->halt(Status::INTERNAL_SERVER_ERROR, "Shit happened ({$e->getMessage()})");
});
$app->notFound(function() use ($app)
{
$app->halt(Status::NOT_FOUND, "Shit not found.");
});
$app->run();
True to form, Spore adds a little extra. Spore also allows you to define an authorization error handler to customize error messages when authorization exceptions occur.
The default authFailedHandler
is rather simple:
<?php
public function authFailedHandler()
{
$this->contentType($this->config("content-type"));
$data = Serializer::getSerializedData($this, array(
"message" => "You are not authorized to execute this function"
));
$this->halt(Status::UNAUTHORIZED, $data);
}
…and you can define your own if you'd like:
<?php
require_once "vendor/autoload.php";
use Spore\Spore;
use Spore\ReST\Model\Status;
$app = new Spore();
$app->authCallback(function($roles) use ($app)
{
return false;
});
$app->authFailedHandler(function() use ($app)
{
$app->halt(Status::FORBIDDEN, "Oi! Not so fast, big guy");
});
$app->run();
In every auto-route, the two default parameters of every callback function are:
\Spore\ReST\Model\Request $request
and \Spore\ReST\Model\Response $response
. These classes are convenience classes meant to help you with common API tasks.
The Request
class will be constructed and passed to the auto-route with several useful properties:
Name | Description |
---|---|
data | The deserialized request body |
params | An associative array of params passed to a Slim route (e.g. /example/:param1/:param2 )/example/abc/123 results in array("param1" => "abc", "param2" => "123")
|
queryParams | An associative array of query string params (e.g. /example?name=danny results in array("name" => "danny") ) |
files | A convenience property containing the value of the $_FILES superglobal |
You can combine some or all of these different request data types. See the example below:
PHP code
<?php
/**
* @url /req-data/:num1/:num2
* @verbs POST
*/
public function reqData(Request $request, Response $response)
{
return $request;
}
HTTP Request
POST /projects/spore/req-data/123/456?hello=spore HTTP/1.1
Host: localhost
Content-Length: 65
Origin: chrome-extension://hgmloofddffdnphfgcellkdfbfbjeloo
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_5) AppleWebKit/537.4 (KHTML, like Gecko) Chrome/22.0.1229.79 Safari/537.4
Content-Type: application/json
Accept: */*
Accept-Encoding: gzip,deflate,sdch
Accept-Language: en-US,en;q=0.8
Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.3
Cookie: splashShown1.5=1; PHPSESSID=e8d6cce60ba5c3471db80bb6b110400e
Pragma: no-cache
Cache-Control: no-cache
{"question":"How many fingers am I holding up?", "type":"nonsensical"}
JSON Response
{
"data": {
"question": "How many fingers am I holding up?",
"type": "nonsensical"
},
"queryParams": {
"hello": "spore"
},
"params": {
"num1": "123",
"num2": "456"
},
"files": []
}
The \Spore\ReST\Model\Request
class also has access to the internal Slim Request
class - and you can access it as follows:
$request->request()
The Response
class will be constructed and passed to the auto-route with several useful properties:
Name | Description |
---|---|
status | The HTTP status code to return. See Spore\ReST\Model\Status for a list of appropriate HTTP statusesNOTE: The Status class does not include any
fictional HTTP codes
|
headers | An associative array of HTTP headers to return |
You can use these properties as follows:
PHP code
<?php
/**
* @url /response-example
* @verbs GET
*/
public function responseExample(Request $request, Response $response)
{
$response->status = Status::PAYMENT_REQUIRED;
$response->headers["Secret-Code"] = "1234";
return "Greetings";
}
HTTP Response
HTTP/1.1 402 Payment Required
Date: Sat, 06 Oct 2012 14:13:04 GMT
Server: Apache/2.2.14 (Unix) DAV/2 mod_ssl/2.2.14 OpenSSL/0.9.8l PHP/5.3.1 mod_perl/2.0.4 Perl/v5.10.1
X-Powered-By: PHP/5.3.1
Secret-Code: 1234
Content-Encoding: gzip
Vary: Accept-Encoding
Content-Length: 31
Content-Type: application/json
Expires: 0
Cache-Control: no-cache
"Greetings"
The \Spore\ReST\Model\Response
class also has access to the internal Slim Response
class - and you can access it as follows:
$response->response()
Well, in that case - Spore has you covered!
If you would rather that Spore make available all the requisite data in some other way than passing parameters by default to your auto-routes, here's how:
Firstly, you will need to add a new configuration option to your Spore application:
<?php
require_once "vendor/autoload.php";
use Spore\Spore;
$app = new Spore(array(
"pass-params" => false // default is true
));
$app->run();
The pass-params
option will tell Spore to not pass any parameters to your auto-routes, but you will still need a way to access the Request
and Response
data, as well as a reference to the current Spore application. You can accomplish this by making your class that contains your auto-route extend the Spore\ReST
D3A0
\BaseService
class.
Once you've done that, you'll be able to access the Request
, Response
& Spore
instances like this:
<?php
class TestService extends BaseService
<?php
/**
* @url /example2
* @verbs GET
*/
public function example2()
{
// set a status code
$this->getResponse()->status = Status::CREATED;
return true;
}
Slim has support for many popular templating engines.
When using Spore, adding templates to your API is really simple and easy.
In this section, we will be using the Twig templating engine to demonstrate the templating functionality in Spore.
In order to use Twig, we will need to a couple dependencies to our composer.json
file:
{
"require": {
"dannykopping/spore": "dev-master",
"slim/extras": "dev-master",
"twig/twig": "dev-master"
}
}
This will include Slim's Extras
project which contains utilities for working with templating engines, as well as the Twig package.
After you've added the dependencies, run:
php composer.phar update
Once you have all the dependencies installed, all you'll need to do is instantiate a Twig
view and add it to your Spore
instance:
<?php
require_once "vendor/autoload.php";
use Spore\Spore;
use Slim\Extras\Views\Twig;
// Setup custom Twig view
$twigView = new Twig();
$app = new Spore(array('view' => $twigView));
You should now have everything you need to get started with the Spore templating functionality. Let's begin with the first example - navigate to http://path/to/spore/example9
and you should see a page returned:
In this example, we'll be using Twig, which is the flexible, fast, and secure template engine for PHP
This page was not requested via AJAX |
If you look inside TestService.php
, you'll see the function example9
:
<?php
/**
* @url /example9
* @verbs GET
* @template example.twig
* @render always
*/
public function example9()
{
return array(
"name" => "Twig",
"description" => "the flexible, fast, and secure template engine for PHP",
"url" => "http://twig.sensiolabs.org/"
);
}
The @template
annotation will tell Spore which template to render when this auto-route is called, and the @render
annotation tells Spore which render-mode to use (see Routing and Annotations). More on the @render
annotation in a minute.
In the example9
function, you'll notice that aside from the @template
annotation - our auto-route looks just like any other. How does the template get rendered and how does it get passed data? Well, when you using the @template
and @render
annotations, the Spore router will handle the rendering of the template and the passing of data for you. If you want to pass some variables to a template, simply return
an array of data from your auto-route callback.
Spore is also built in such a way that auto-routes can be multi-purpose. You may encounter a situation where you'd like an API request to return a JSON response (for example) when called via AJAX, and render a template when called normally without AJAX. Spore has you covered!
All you need to do is set your @render
annotation to nonAJAX
or nonXHR
. Once you do this, the data returned by your auto-route will be encoded and returned when a non-AJAX or non-XHR (XMLHttpRequest
) request is issued, alternatively your template will be rendered and the data passed to the template automatically.
For an example of this, navigate to http://path/to/spore/example10
and be sure to include the following header with your request:
X-Requested-With: XMLHttpRequest
This header tells the server that an AJAX/XHR request is being issued.
NOTE: You can add custom headers to HTTP requests by using tools like the Advanced REST Client Application - an excellent Chrome extension. |
The response from the API request will be a JSON response:
{
"name": "Twig",
"description": "the flexible, fast, and secure template engine for PHP",
"url": "http://twig.sensiolabs.org/",
"ajax": true
}
However, if you send the request without the extra header, you will receive the same HTML response as in the previous example.
In this example, we'll be using Twig, which is the flexible, fast, and secure template engine for PHP
This page was not requested via AJAX |
All of this is made possible by setting the @render
annotation to nonAJAX
:
<?php
/**
* @url /example10
* @verbs GET
* @template example.twig
* @render nonAJAX
*/
public function example10(Request $request)
{
return array(
"name" => "Twig",
"description" => "the flexible, fast, and secure template engine for PHP",
"url" => "http://twig.sensiolabs.org/",
"ajax" => $request->request()->isAjax()
);
}
Slim allows you to assign conditions to route parameters. In Spore, there is annotation-based support for this very handy feature.
All conditions are structured in the following way:
<?php
/**
* @condition parameter pattern
*/
NOTE: The elements in a @condition annotation should always be separated by at least one space or tab
|
- The
parameter
element corresponds to the named parameter in the auto-route URI. - The
pattern
element is the regular expression pattern used to test against the given request * Any regular expression that is PCRE-compliant is acceptable for a condition.
In TestService.php
, take a look at example11:
<?php
/**
* @url /example11/:identifier/:name
* @verbs GET
* @condition identifier [^xX]+
* @condition name [a-z]{3,}
*/
public function example11(Request $request)
{
return sprintf("Congrats %s! Your identifier is %s", $request->params['name'], $request->params['identifier']);
}
As you can see, the auto-route's URI has two parameters: identifier
and name
. In the docblock, you can see two annotations relating to these two parameters.
- The
identifier
parameter's condition in the example states that any request that contains an "x" or an "X" will be invalid. - The
name
parameter's condition in the example states that any request with alphabetical characters is acceptable, which is 3 characters or longer.
You can assign conditions to all, none or selected parameters. They do not have to be defined in the same order as they appear in the URI.