In this article, I would like to share the experience of developing a controller layer in our web framework. What we want from this layer:
- abstraction of HTTP request and response, compensation for the inconvenience of the embedded implementation;
- the ability to build request handlers from separate modules (middleware);
- most importantly: URL scheduling, simple scheduling rule set structure;
- REST as the most versatile architecture.
Basically, I would like to focus on the last two points, so the rest is very superficial, especially since these are obvious things.
HTTP request and response abstraction
Unlike the vast majority of web frameworks in various languages, PHP does not have a normal interface for working with the request object (at least in the average configuration). There are many special variables that contain fragmented and not always uniformly structured information (which is only worth the difference in the data structure, for example, in
$_POST
and
$_FILES
).
Therefore, in the best traditions of PHP, we are rapidly inventing a bicycle in the form of an object of the
Net_HTTP_Request
class. Its main features are: convenient access to the parameters and fields of the request header, uploads as file objects, etc.
')
1 <?php <br>
2 $ id = $ request [ ' id ' ] ;<br>
3 $ auth = $ request -> header [ ' Authorization ' ] <br>
4 foreach ( $ request [ ' upload ' ] as $ line ) { /* ... */ } <br>
5 $ stores = $ request [ ' upload ' ] -> copy_to ( $ permanent_path ) ;<br>
6 ?> <br>
Similarly, a response is represented by an object of the
Net_HTTP_Response
class, which provides convenient work with response headers, body as a string, iterator or file object, etc.
1 <?php <br>
2 $ file = IO_FS :: File ( $ path ) ;<br>
3 return Net_HTTP :: Response () - > <br>
4 status ( Net_HTTP :: OK ) - > <br>
5 content_type ( MIME :: type_for_file ( $ file )) - > <br>
6 body ( $ file ) ;<br>
7 ?> <br>
Request handlers, middleware
Request processing is performed by service objects that implement the standard interface:
1 <?php <br>
2 interface WS_ServiceInterface { <br>
3 public function run ( WS_Environment $ env ) ;<br>
4 } <br>
5 ?> <br>
The result of the
run()
method execution is an object of the
Net_HTTP_Response
class. The
WS_Environment
class
WS_Environment
designed to exchange information between service objects; each service can create, read, or write the value of an environment parameter. By default, the environment contains the
$env->request
element of the
$env->request
class.
If the service in the process of processing calls another service, modifying the request, response, or environment, we get the so-called middleware component, the name is entered into use by
the WSGI standard . We have a set of trivial middleware-services that implement the configuration of the application, connect to the database, caching, authorization, and most interesting, dispatching.
In general, it was an obvious preamble, we turn, finally, to the ambulla.
REST based dispatching
In many existing frameworks, dispatching is built on the basis of matching the calls of various application methods to regular expressions applied to the URL string. The need to support REST has led to the emergence of various REST-oriented add-ons over this scheme, which, first of all, provide an analysis of the HTTP request method.
Dispatching based on a set of regular expressions has several drawbacks, first of all:
- for large applications there is a risk of getting a large complex system of heterogeneous rules, heavy in support;
- the rule system is static, it is difficult to influence the result of dispatching directly in its process;
- Difficult to dispatch for nested resources with an arbitrary level of nesting.
In the process of choosing the dispatching scheme for the library, we looked at the various existing options and most of all we liked the approach proposed in the
Java standard JAX-RS (JSR-311).
Why did we choose this standard?
- plain;
- Transparently transfers the REST model to the level of the application code;
- allows you to conveniently work with nested resources;
- does not impose restrictions on classes that implement resources.
We have implemented a simplified version of the standard, taking into account the PHP specifics:
- to describe the resources using its own DSL, rather than annotations;
- we abandoned the possibility of an arbitrary order of resource determination, which significantly simplified the algorithm.
Our request manager is implemented as a service object (
WS_ServiceInteface
). Its
run()
method delegates processing to a user-defined resource object created by the dispatcher based on the description of the application’s resource set.
A resource is just an object of an arbitrary class. It is not necessary to inherit it from any standard parent class, it does not need to implement any standard interface. In this case, in a user application, it is most likely that it makes sense to introduce a hierarchy of inheritance in order to avoid duplication of code, but the framework does not require this.
A resource class implements a set of methods that can be divided into three types:
- helper methods intended for use within the class;
- HTTP methods that process various types of requests and return response objects;
- sublocators generating instances of nested resource classes.
Application resources
A set of resources forms the application. The application contains a description of its resources, for the construction of the description is used internal DSL.
To make a decision on calling a resource method, you must have a complete description of the available resources and their methods. The code of one or several resources is actually executed, so there is no need to load all classes. Thus, the description of the resources that make up the application should be separated from their actual implementation.
Example application schema:
1 <?php <br>
2 $ companies = WS_REST_DSL :: Application () - > <br>
3 begin_resource ( ' company ', ' App.WS.Company ', ' company/{name:[a-zA-Z][a-zA-Z-]+} ' ) - > <br>
4 sublocator ( ' blog ' ) - > // - <br>
5 sublocator ( ' vacancies ' ) - > // - <br>
6 for_format ( ' html ' ) - > <br>
7 index () - > // /company/Techart/ - <br>
8 end - > <br>
9 end - > <br>
10 begin_resource ( ' blog ', ' App.WS.Blog ', null ) - > <br>
11 sublocator ( ' entry ', ' {\d+:id} ' ) - > <br>
12 for_format ( ' html ' ) - > <br>
13 get_for ( ' {page_no:\d+} ', ' index ' ) - > // /company/Techart/blog/5.html - <br>
14 post () - > // - create() <br>
15 index () - > // /company/Techart/blog/ - <br>
16 end - > <br>
17 for_format ( ' rss ' ) - > <br>
18 get ( ' index_rss ' ) - > // /company/Techart/blog/index.rss - RSS- <br>
19 end - > <br>
20 end - > <br>
21 begin_resource ( ' entry ', ' App.WS.Entry ', null ) - > <br>
22 for_format ( ' html ' ) - > <br>
23 index () - > // /company/Techart/blog/82715/ - <br>
24 get_for ( ' print ', ' print_version ' ) - > // /company/Techart/blog/82715/print.html - <br>
25 put () - > // , - update() <br>
26 delete () - > // , - delete() <br>
27 end - > <br>
28 end - > <br>
29 begin_resource ( ' vacancies ', ' App.WS.Job ', null ) - > <br>
30 for_format ( ' html ' ) - > <br>
31 index () - > // /company/Techart/vacancies/ - <br>
32 end - > <br>
33 end - > <br>
34 end ;<br>
35 ?> <br>
In this scheme, the resource is described by three parameters:
- name - the name of the resource, an auxiliary parameter, you can still ignore;
- classname - the name of the class that implements the resource;
- path - URL pattern corresponding to the resource.
The URL pattern is a regular expression (where do without them!) With named parameters.
HTTP methods
HTTP methods perform processing of requests of various types and generate a response. Method Description Parameters:
- name - the name of the method;
- http_mask - the mask that defines the combination of http-methods that the resource method processes;
- path - URL pattern corresponding to the method;
- formats - a list of formats of representations that forms the method.
A resource class constructor and resource methods can have an arbitrary set of arguments. If the URL pattern contains parameters whose names match the arguments of the methods, the parameter values are automatically substituted when the constructor or methods of the resource are called. In addition, there are several predefined standard parameters, such as
$env
,
$request
and
$format
. If the argument name is not included in the parameter set of the template and is not a predefined parameter,
null
substituted.
For each method, you can specify a list of presentation formats. The definition of the requested format is made by the HTTP request headers or by the extension of the requested document. For each format, you can provide a separate method, or perform processing in one method, using the
$format
parameter. Formats can be specified both for individual methods and for whole resources.
Sublokatory
Creating an instance of a class corresponding to a nested resource can be dynamically performed during processing. For this, so-called sub-resource locators are used. The method is a sublokator if it is present in the resource description, but no method is specified in the HTTP mask. The sublokator does not process the request; instead, it creates an instance of the nested resource class. Thus, the decision to create a resource can be made at the time of processing the request, depending on certain external conditions.
Resource classes
For the example above, the skeleton of several classes of resources might look like this:
1 <?php <br>
2 // - <br>
3 class App_WS_Resource { <br>
4 protected $ env ;<br>
5 protected $ db ;<br>
6 <br>
7 public function __construct ( WS_Environment $ env ) { <br>
8 $ this -> env = $ env ;<br>
9 $ this -> db = $ env -> db;<br>
10 } <br>
11 } <br>
12 <br>
13 // blog <br>
14 class App_WS_Blog extends App_WS_Resource { <br>
15 <br>
16 public function index ( $ page_no = 1 ) { /* $page_no URL */ } <br>
17 <br>
18 public function index_rss () { /* RSS- */ } <br>
19 <br>
20 public function entry ( $ id ) { <br>
21 // - <br>
22 // <br>
23 if ( $ entry = $ this -> db -> blog -> entries [ $ id ]) <br>
24 return new App_WS_Entry ( $ this -> env, $ entry ) ; <br>
25 } <br>
26 <br>
27 public function create () { /* */ } <br>
28 } <br>
29 <br>
30 // entry <br>
31 class App_WS_Entry extends App_WS_Resource { <br>
32 protected $ entry ;<br>
33 <br>
34 public function __construct ( WS_Environment $ env , App_DB_Entry $ entry ) { <br>
35 parent :: __construct ( $ env ) ;<br>
36 $ this -> entry = $ entry ;<br>
37 } <br>
38 <br>
39 public function index () { /* */ } <br>
40 public function print_version () { /* */ } <br>
41 public function update () { /* */ } <br>
42 public function delete () { /* */ } <br>
43 } <br>
Dispatch algorithm
The dispatch algorithm, which is a simplified version of the algorithm described in
the JAX-RS standard , looks like this:
- Determine the required presentation format (by request headers or document extension);
- View resource descriptions and match the path of each with the beginning of the URL;
- If you have not found a resource for which the URL matches the pattern and the format is included in the list of supported - 404;
- Resource found, looking for a method. We remove from the URL the same part of the resource path;
- We look through all the descriptions of the resource methods by matching the URL pattern of the method with the beginning of the rest of the URL;
- If there is neither a method nor a sublocator matching the URL pattern and presentation format - 404;
- If a method is found with the appropriate URL pattern, format, and HTTP mask, we create an object of the resource class and call the method, performing parameter substitution. The result of the execution is the object representing the response, the work is completed;
- If a sublocator with the corresponding URL pattern is found, we create an object of the resource class and call the method, performing parameter substitution.
- The result of sublocator execution is a new resource. We search the resource list for the description of the resource of the corresponding class.
- If the class of the resource in the description is missing - 404;
- Delete the matched line from the beginning of the URL and return to step 4, and so on until the http method is found.
To simplify the algorithm, we assume that the addresses / resource / and /resource/index.html (for the html format) are equivalent, and we also limit the maximum number of iterations of the algorithm.
To improve performance, you can, firstly, cache resource descriptions, and secondly, break large applications into separate smaller ones by linking them to URL subdirectories or subdomains of the main application domain.
Result
What do we gain by implementing the proposed scheme?
- We can fully utilize all the advantages of the REST model, regardless of the type of application (traditional web application, web service, AJAX application);
- the application code is separated from the implementation mechanism as much as possible; this greatly simplifies testing and gives the developer more freedom in designing an application model object;
- dispatching rules and, as a result, resource classes have a fairly homogeneous structure, which facilitates design and further support. If you wish (we are all going, we are not going to get it together), we can develop a graphical notation to describe the structure of the application and visualize it, say, using graphviz .
- working with nested resources of arbitrary depth of nesting is trivial.
In general, we are very pleased with the proposed scheme, in our opinion, it practically does not impose restrictions on the developer and is very simple to implement. An interesting and unexpected result was a very simple integration with our primitive ORM layer. Having implemented only two very simple resource classes, we received a transparent mapping of HTTP requests in the ORM layer instruction, for example
GET /api/news/stories/most_popular
$db->news->stories->most_popular->select()
POST /api/news/stories/
$db->news->stories->insert($story)
PUT /api/news/stories/15
$db->news->stories->update($db->news->stories[15]);
and so on.
Well, to the heap, our
materials of the internal seminar on REST , suddenly anyone is interested.