📜 ⬆️ ⬇️

DSL elements in PHP: making library APIs easier to use

When developing our internal framework (unfortunately, PHP in general is very conducive to the constant reinvention of the bicycle), we tried to design the interfaces of the library modules in such a way so that the client code using these interfaces is simple, concise and readable.

Ideally, a specialized module designed to solve a particular task should form a kind of simplified language that allows the developer to describe the solution or the result of solving the problem as close as possible to the terms of the subject area. In this case, if we do not go beyond the framework of the programming language used, we are talking about the implementation of the so-called internal DSL .


')
A lot has been written about DSL implementation in various languages, for example, a catalog of patterns on this topic is available on the Fowler website. The features of any design pattern are largely determined by the implementation language, for DSL patterns this is doubly true. Unfortunately, the range of possibilities that PHP can provide is extremely limited. Nevertheless, using two standard templates - Method Chaining and Expression Builder , you can achieve a more convenient and readable API.

Proper naming of classes and methods is half the battle when developing DSL-style APIs. It is important that the methods be named as close as possible to the subject area, and not to the software implementation. It sounds trite, but you can find a lot of examples when the naming is caused, for example, by the implementation of a classic design pattern from GoF .

The use of call chains (method chaining) makes the code more concise and in some cases allows to achieve the effect of a specialized DSL. When developing library modules, we try to follow the rule: if the method does not return a functionally necessary result, let it return $this . Also, we usually provide a set of methods for setting the internal properties of an object, which allows you to customize the parameters of an object inside an expression and also makes the code more concise.

The Pattern Builder allows you to more conveniently build systems of nested objects, when the parent object contains references to children, those in turn, to their children, and so on. Note that in PHP it is advisable to avoid bidirectional links (the parent object refers to the child, and the child to the parent), since the garbage collector does not work with circular references.

To create such systems, we will create a very simple base class:

  1. <? php
  2. class DSL_Builder {
  3. protected $ parent ;
  4. protected $ object ;
  5. public function __construct ( $ parent , $ object ) {
  6. $ this -> parent = $ parent ;
  7. $ this -> object = $ object ;
  8. }
  9. public function __get ( $ property ) {
  10. switch ( $ property ) {
  11. case 'end' :
  12. return $ this -> parent? $ this -> parent: $ this -> object;
  13. case 'object' :
  14. return $ this -> $ property ;
  15. default :
  16. throw new Core_MissingPropertyException ( $ property );
  17. }
  18. }
  19. public function __set ( $ property , $ value ) { throw new Core_ReadOnlyObjectException ( $ this ); }
  20. public function __isset ( $ property ) {
  21. switch ( $ property ) {
  22. case 'object' :
  23. return isset ( $ this -> $ property );
  24. default :
  25. return false ;
  26. }
  27. }
  28. public function __unset ( $ property ) { throw new Core_ReadOnlyObjectException ( $ this ); }
  29. public function __call ( $ method , $ args ) {
  30. method_exists ( $ this -> object, $ method )?
  31. call_user_func_array ( array ( $ this -> object, $ method ), $ args ):
  32. $ this -> object-> $ method = $ args [ 0 ];
  33. return $ this ;
  34. }
  35. }
  36. ?>


Objects of this class set up the target object, the link to which is stored in the $object field, delegating to it a method call and setting properties. Of course, the builder can define its own set of methods for more complex configuration of the target object. In this case, the end pseudo-property allows you to return to the parent object builder, and so on.

On the basis of this class, we write the simplest DSL to describe the application configuration.

  1. <? php
  2. class Config_DSL_Builder extends DSL_Builder {
  3. public function __construct (Config_DSL_Builder $ parent = null , stdClass $ object = null ) {
  4. parent :: __ construct ( $ parent , Core :: if_null ( $ object , new stdClass ()));
  5. }
  6. public function load ( $ file ) {
  7. ob_start ();
  8. include ( $ file );
  9. ob_end_clean ();
  10. return $ this ;
  11. }
  12. public function begin ( $ name ) {
  13. Return new Config_DSL_Builder ( $ this , $ this -> object-> $ name = new stdClass ());
  14. }
  15. public function __get ( $ property ) {
  16. return (strpos ( $ property , 'begin_' ) === 0 )?
  17. $ this -> begin (substr ( $ property , 6 )):
  18. parent :: __ get ( $ property );
  19. }
  20. public function __call ( $ method , $ args ) {
  21. $ this -> object-> $ method = $ args [ 0 ];
  22. return $ this ;
  23. }
  24. }
  25. ?>


Now we can create a config.php in which we describe the configuration of our application in the following form:

  1. <? php
  2. $ this ->
  3. begin_db->
  4. dsn ( 'mysql: // user: password @ localhost / db' ) ->
  5. end->
  6. begin_cache->
  7. dsn ( 'dummy: //' ) ->
  8. default_timeout ( 300 ) ->
  9. timeouts ( array (
  10. 'front / index' => 300 ,
  11. 'news / most_popular' => 300 ,
  12. 'news / category' => 300 )) ->
  13. end->
  14. begin_site->
  15. begin_from->
  16. top_limit ( 7 ) ->
  17. end->
  18. begin_news->
  19. most_popular_limit ( 5 ) ->
  20. end->
  21. end;
  22. ?>


You can load the configuration by calling:

  1. <? php
  2. $ config = Config_DSL :: Builder () -> load ( 'config.php' );
  3. ?>


Of course, business is not limited only to configs. For example, we describe the structure of a REST application like this:

  1. <? php
  2. WS_REST_DSL :: Application () ->
  3. media_type ( 'html' , 'text / html' , true ) ->
  4. media_type ( 'rss' , 'application / xhtml + xml' ) ->
  5. begin_resource ( 'gallery' , 'App.Photo.Gallery' , 'galleries / {id: \ d +}' ) ->
  6. for_format ( 'html' ) ->
  7. get_for ( '{page_no: \ d +}' , 'index' ) ->
  8. post_for ( 'vote' , 'vote' ) ->
  9. index () ->
  10. end->
  11. end->
  12. begin_resource ( 'index' , 'App.Photo.Index' ) ->
  13. for_format ( 'rss' ) ->
  14. get ( 'index_rss' ) ->
  15. get_for ( 'top' , 'top_rss' ) ->
  16. end->
  17. for_format ( 'html' ) ->
  18. get_for ( '{page_no: \ d +}' , 'index' ) ->
  19. index () ->
  20. end->
  21. end->
  22. end;
  23. ?>


Using fast DSL-style APIs allows you to get short and readable code, for example, in the application controller methods:

  1. <? php
  2. public function index ( $ page_no = 1 ) {
  3. $ pager = Data_Pagination :: pager ( $ this -> db-> photo-> galleries-> count (), $ page_no , self :: PAGE_LIMIT);
  4. return $ this -> html ( 'index' ) ->
  5. with ( array (
  6. 'top' => $ this -> db-> photo-> galleries-> most_important () -> select (),
  7. 'pager' => $ pager ,
  8. 'galleries' => $ this -> db-> photo-> galleries->
  9. published () ->
  10. paginate_with ( $ pager ) ->
  11. select ()));
  12. }
  13. ?>


In some relatively rare cases, one can go even further. DSL_Builder slightly expanding the DSL_Builder class, it is possible to describe not only a static structure, but also a set of actions, that is, some script. For example, with the Google AdWords API you can work like this:

  1. <? php
  2. Service_Google_AdWords_DSL :: Script () ->
  3. for_campaign ( $ campaign_id ) ->
  4. for_ad_group ( $ group_id ) ->
  5. for_each ( 'text' , 'keyword1' , 'keyword2' , 'keyword3' ) ->
  6. add_keyword_criteria () ->
  7. bind ( 'text' ) ->
  8. end->
  9. end->
  10. add_ad () ->
  11. with ( 'headline' , 'headline' ,
  12. 'displayUrl' , 'www.techart.ru' ,
  13. 'destinationUrl' , 'http://www.techart.ru/' ,
  14. 'description1' , 'desc1' ,
  15. 'description2' , 'desc2' ) ->
  16. format ( "Ad Created" ) ->
  17. end->
  18. end->
  19. end->
  20. for_each_campaign () ->
  21. format ( "Campaign:% d,% s \ n" , 'campaign.id' , 'campaign.name' ) ->
  22. dump ( 'campaign' ) ->
  23. for_each_ad_group () ->
  24. format ( "Ad group:% d,% s \ n" , 'ad_group.id' , 'ad_group.name' ) ->
  25. for_each_criteria () ->
  26. format ( "Criteria:% d,% s \ n" , 'criteria.id' , 'criteria.text' ) ->
  27. end->
  28. end->
  29. end->
  30. end->
  31. run_for (Service_Google_AdWords :: Client () ->
  32. useragent ( 'user agent' ) ->
  33. email ( 'email@domain.com' ));
  34. ?>


Of course, this approach should be used within reasonable limits, but sometimes it gives a very good result.

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


All Articles