Pull to refresh

Элементы DSL на PHP: как сделать библиотечные API удобнее в использовании

Reading time 8 min
Views 8.3K
При разработке нашего внутреннего фреймворка (к сожалению, PHP вообще очень способствует постоянному переизобретению велосипеда), мы старались таким образом проектировать интерфейсы библиотечных модулей, чтобы клиентский код, использующий эти интерфейсы, получался простым, лаконичным и читаемым.

В идеале специализированный модуль, предназначенный для решения той или иной задачи, должен формировать некий упрощенный язык, позволяющий разработчику описывать решение или результат решения задачи максимально близко к терминам предметной области. В случае, если при этом мы не выходим за рамки используемого языка программирования, речь идет о реализации так называемого internal DSL.



Про реализацию DSL на различных языках написано очень много, например, на сайте Фаулера доступен каталог паттернов на эту тему. Особенности любого паттерна проектирования в значительной степени определяются языком реализации, для DSL-паттернов это верно вдвойне. К сожалению, диапазон возможностей, которые может предоставить PHP, крайне ограничен. Тем не менее, используя два типовых шаблона — Method Chaining и Expression Builder, можно добиться более удобного и читаемого API.

Правильное именование классов и методов — половина дела при разработке API в стиле DSL. Важно, чтобы методы именовались максимально близко к предметной области, а не к программной реализации. Звучит банально, но можно найти массу примеров, когда именование обусловлено, например, реализацией того или иного классического паттерна проектирования из GoF.

Использование цепочек вызовов (method chaining) делает код более лаконичным и в ряде случаев позволяет добиться эффекта специализированного DSL. При разработке библиотечных модулей мы стараемся следовать правилу: если метод не возвращает функционально необходимый результат, пусть вернет $this. Также обычно мы обеспечиваем набор методов для установки внутренних свойств объекта, что позволяет осуществлять настройку параметров объекта внутри выражения и также делает код более лаконичным.

Паттерн Builder позволяет более удобно строить системы вложенных объектов, когда родительский объект содержит ссылки на дочерние, те, в свою очередь, на свои дочерние и так далее. Отметим, что в PHP желательно избегать двунаправленных ссылок (родительский объект ссылается на дочерний, а дочерний на родительский), так как сборщик мусора не работает с циклическими ссылками.

Для создания таких систем создадим очень простой базовый класс:

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


Объекты этого класса выполняют настройку целевого объекта, ссылка на который хранится в поле $object, делегируя ему вызов методов и установку свойств. Разумеется, объект-builder может определять и набор собственных методов для более сложной настройки целевого объекта. При этом псевдосвойство end позволяет вернуться к построителю родительского объекта и так далее.

Напишем на основе этого класса простейший DSL для описания конфигурации приложения.

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


Теперь мы можем создать файл config.php в котором описать конфигурацию нашего приложения в таком виде:

  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. ?>


Загрузить конфигурацию можно с помощью вызова:

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


Разумеется, дело не ограничивается только конфигами. Например, мы описываем структуру REST-приложения вот таким образом:

  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.  
  23.   end;
  24. ?>


Использование быстрых API в стиле DSL позволяет получить короткий и хорошо читаемый код, например, в методах контроллера приложений:

  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.  
  5.     return $this->html('index')->
  6.       with(array(
  7.         'top' => $this->db->photo->galleries->most_important()->select(),
  8.         'pager' => $pager,
  9.         'galleries' => $this->db->photo->galleries->
  10.                                published()->
  11.                                paginate_with($pager)->
  12.                                select()));
  13.   }
  14. ?>


В некоторых, относительно редких, случаях можно пойти еще дальше. Немного расширив класс DSL_Builder, можно описывать не только статическую структуру, но и набор действий, то есть некоторый сценарий. Например, с Google AdWords API можно работать вот так:

  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. ?>


Конечно, использовать такой подход нужно в разумных пределах, но иногда он дает очень хороший результат.
Tags:
Hubs:
+15
Comments 33
Comments Comments 33

Articles

Information

Website
www.techart.ru
Registered
Founded
Employees
101–200 employees
Location
Россия