PHP DSL Elements: Making Library APIs Easier to Use

    When developing our internal framework (unfortunately, PHP generally contributes to the constant reinvention of the bicycle), we tried to design library module interfaces in such a way that the client code using these interfaces would be simple, concise and readable.

    Ideally, a specialized module designed to solve a particular problem should form a simplified language that allows the developer to describe the solution or result of solving the problem as close as possible to the terms of the subject area. If at the same time we do not go beyond the framework of the programming language used, we are talking about implementing the so-called internal DSL .



    A lot has been written about the implementation of DSL in various languages, for example, is available on the Fowler websitecatalog of patterns on this topic . The features of any design pattern are largely determined by the implementation language; this is doubly true for DSL patterns. 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 a DSL-style API. It is important that the methods are named as close as possible to the subject area, and not to the software implementation. It sounds corny, but you can find a lot of examples when naming is due, for example, to the implementation of one or another classic design pattern from GoF .

    The use of method chains makes the code more concise and in some cases allows achieving 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. We also usually provide a set of methods for setting the internal properties of an object, which allows you to configure object parameters inside an expression and also makes the code more concise.

    The Builder pattern allows you to more conveniently build systems of nested objects when the parent contains links 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 refers to the parent), since the garbage collector does not work with circular links.

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

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


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

    Based on this class, we write the simplest DSL to describe the configuration of the application.

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


    Now we can create a file config.phpin which to describe the configuration of our application in this form:

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


    You can load the configuration using the call:

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


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

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


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

    1. public function index($page_no = 1) {
    2.     $pager = Data_Pagination::pager($this->db->photo->galleries->count(), $page_no, self::PAGE_LIMIT);
    3.  
    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, you can go even further. By expanding the class a little DSL_Builder, you can describe not only a static structure, but also a set of actions, that is, a certain scenario. For example, you can work with the Google AdWords API like this:

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


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

    Also popular now: