⬆️ ⬇️

REST architecture web application controller layer

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:





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:

  1. for large applications there is a risk of getting a large complex system of heterogeneous rules, heavy in support;
  2. the rule system is static, it is difficult to influence the result of dispatching directly in its process;
  3. 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?



We have implemented a simplified version of the standard, taking into account the PHP specifics:



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:



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:



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:



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:

  1. Determine the required presentation format (by request headers or document extension);
  2. View resource descriptions and match the path of each with the beginning of the URL;
  3. 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;
  4. Resource found, looking for a method. We remove from the URL the same part of the resource path;
  5. 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;
  6. If there is neither a method nor a sublocator matching the URL pattern and presentation format - 404;
  7. 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;
  8. 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.
  9. The result of sublocator execution is a new resource. We search the resource list for the description of the resource of the corresponding class.
  10. If the class of the resource in the description is missing - 404;
  11. 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?



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.

Source: https://habr.com/ru/post/83442/



All Articles