Записки Вредного программиста

enjoy, motherfuckers ;)

PHP замыкания и немного рефлексии

Это будет очень короткая заметка и больше экспериментальная. Вчера вечером смотрел Sinatra (фреймворк на базе Ruby) и меня он покорил своей легкостью. Да вообще мне очень нравится Ruby, обязательно его в скором будущем выучу, а сейчас поговорим о PHP. Заметка сводится к тому, что я попытался реализовать функционал Sinatra на PHP. Таких велосипедов, скажите вы, превеликое множество и несомненно будете правы. Но я нашел в сегодняшнем велосипедостроение очень много для себя полезного. Да и “фреймворк” (фреймворком пока назвать его очень сложно, но начало положено) получился очень маленьким – около 40 строк.

Описание фреймворка

Фреймворк может обрабатывать GET запросы, учитывая маршрутизацию, которая задется регулярным выражением с любым количеством параметров. С выходом 5.3 версии PHP нам стали доступны так называемые замыкания или анонимные функции (кому как нравится). Вот именно и благодаря замыканиям фреймворк получился очень маленьким. Когда придумывал ему название (ведь каждый программист должен назвать свое детище), в интернете натолкнулся на картинку жирафа, так и обрел название мой фреймворк (Jirafa на испанский манер).

Использование

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

Чтобы создать приложение нужно создать класс приложения и назначить на определенные маршруты функции, которые и будет обрабатывать запрос.

1
2
3
4
5
6
$app = new Jirafa();

$app->setRegistry(array(
  'viewObject' => 'may be smarty',
  'dbObject' => new PDO('sqlite:./db/db.db') //'may be pdo or something else'
));

Метод setRegistry() используется мною для того, чтобы передать во все функции необходимые параметры, это могут быть и шаблонизаторы, и PDO объекты, да вообще все что угодно можно передать. А дальше несколько строк кода для демонстрации.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
$app->get('/', function() use ($registry) { // показываем главную
                  return '<br>1hi ';
               })
   ->get('404', function($registry) { // отображается, если ни один маршрут не подошел
                  var_dump($registry);
                  return '<h1>Error 404</h1>';
               })
   ->get('(\w+).html', function ($alias, $registry) { // example.com/static.html
                  $pdo = $registry['dbObject'];
                  $html = 'Error';
                  try {
                     $row = $pdo->query("SELECT * FROM content WHERE alias='{$alias}'")->fetch();
                     $html = "<h1>{$row['title']}</h1><p>{$row['content']}</p>";
                  } catch (PDOException $e) {
                     return $e->getMessage();
                  }
                  return $html;
               })

Я считаю, что все должно быть предельно понятно, поэтому вернемся к классу фреймворка (на данный момент “фреймворк состоит из одного файла, двух, если считать .htaccess)

Исходный код фреймворка

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
class Jirafa
{
  protected $_app = array();
  protected $_registry;

  public function get() {
      $args = func_get_args();
      $this->_app[array_shift($args)] = array_shift($args);

      return $this;
  }

  public function render() {
      $uri = ($_SERVER['REQUEST_URI'] === '/') ? '/' : substr($_SERVER['REQUEST_URI'], 1);
      // iterate over all paths
      foreach($this->_app as $path => $func) {
          // if $uri and regular expression are matched
          if (preg_match('@^' . $path . '\/*$@', $uri, $matches)) {
              var_dump($path);
              array_shift($matches);
              $countInPath = count($matches);

              $reflection = new ReflectionFunction($func);
              $params = $reflection->getParameters();

              $array = array();
              foreach($params as $param)
              $array[$param->getName()] = array_shift($matches);

              //if (count($array) !== $countInPath + 1)
              //goto error;

              echo call_user_func_array($func, array_merge($array, array('registry' => $this->_registry)));
              return;
          }
      }
      //error:
      echo call_user_func_array($this->_app['404'], array('registry' => $this->_registry));
  }

  public function setRegistry($registry) {
      $this->_registry = $registry;
  }
}

Метод get() в нем очень простой, он записывает обрабатываемый маршрут и параметры, с которыми данный метод был вызван (понадобится нам в дальнейшем для заполнения параметров в функции). Для удобство возвращаем свой же объект, чтобы сократить код приложения.

Метод render() находит соответствие сохраненных методом get() маршрутов и $SERVER[‘REQUEST_URI’]. Если соответствие найдено, то мы с помощью рефлексии (Reflection) получаем данные нашего замыкания (как называются переменные и их количество). Далее с помощью замечательного метода call_user_func_array(), позволяющего вызывать функцию или метод с переменным количество аргументов, мы вызываем функцию нашего исключения, передавая ей в качестве параметров, помимо запрашиваемых, еще и переменную $this->registry, которая, как я уже упоминал выше, может содержать вспомогательную информацию, необходимую для работы приложения.

Заключение

В данной заметки мы вскользь познакомились с замыканиями и рефлексией. Изобрели еще один маленький велосипед с квадратными колесами, которые, надеюсь, поднял вам настроение и прибавил вам немного практики и мыслей по поводу дальнейшего использования полезных конструкция замечательного языка PHP. Удачи вам, веб-девелоперы :)

Комментарии