Note: I'm migrating from gonzalo123.com to here. When I finish I'll swap the DNS to here. The "official" blog will be always gonzalo123.com

      Building a Silex application from one Behat/Gherkin feature file

      Last days I’ve playing with Behat. Behat is a behavior driven development (BDD) framework based on Ruby’s Cucumber. Basically with Behat we defenie features within one feature file. I’m not going to crate a Behat tutorial (you can read more about Behat here). Behat use Gherkin to write the features files. When I was playing with Behat I had one idea. The idea is simple: Can we use Gherking to build a Silex application?. It was a good excuse to study Gherking, indeed ;).

      Here comes the feature file:

      Feature: application API
        Scenario: List users
          Given url "/api/users/list.json"
          And request method is "GET"
          Then instance "\Api\Users"
          And execute function "listUsers"
          And format output into json
       
        Scenario: Get user info
          Given url "/api/user/{userName}.json"
          And request method is "GET"
          Then instance "\Api\User"
          And execute function "info"
          And format output into json
       
        Scenario: Update user information
          Given url "/api/user/{userName}.json"
          And request method is "POST"
          Then instance "\Api\User"
          And execute function "update"
          And format output into json
      

      Our API use this simple library:

      <?php
       
      namespace Api;
      use Symfony\Component\HttpFoundation\Request;
       
      class User
      {
          private $request;
       
          public function __construct(Request $request)
          {
              $this->request = $request;
          }
       
          public function info()
          {
              switch ($this->request->get('userName')) {
                  case 'gonzalo':
                      return array('name' => 'Gonzalo', 'surname' => 'Ayuso');
                  case 'peter':
                      return array('name' => 'Peter', 'surname' => 'Parker');
              }
          }
       
          public function update()
          {
              return array('infoUpdated');
          }
      }
      
      <?php
       
      namespace Api;
      use Symfony\Component\HttpFoundation\Request;
       
      class Users
      {
          public function listUsers()
          {
              return array('gonzalo', 'peter');
          }
      }
      

      The idea is simple. Parse the feature file with behat/gherkin component and create a silex application. And here comes the “magic”. This is a simple working prototype, just an experiment for a rainy sunday.

      <?php
      include __DIR__ . '/../vendor/autoload.php';
      define(FEATURE_PATH, __DIR__ . '/api.feature');
       
      use Behat\Gherkin\Lexer,
              Behat\Gherkin\Parser,
              Behat\Gherkin\Keywords\ArrayKeywords,
              Behat\Gherkin\Node\FeatureNode,
              Behat\Gherkin\Node\ScenarioNode,
              Symfony\Component\HttpFoundation\Request,
              Silex\Application;
       
      $keywords = new ArrayKeywords([
          'en' => [
              'feature' => 'Feature',
              'background' => 'Background',
              'scenario' => 'Scenario',
              'scenario_outline' => 'Scenario Outline',
              'examples' => 'Examples',
              'given' => 'Given',
              'when' => 'When',
              'then' => 'Then',
              'and' => 'And',
              'but' => 'But'
          ],
      ]);
       
      function getMatch($subject, $pattern) {
          preg_match($pattern, $subject, $matches);
          return isset($matches[1]) ? $matches[1] : NULL;
      }
       
      $app = new Application();
       
      function getScenarioConf($scenario) {
          $silexConfItem = [];
       
          /** @var $scenario  ScenarioNode */
          foreach ($scenario->getSteps() as $step) {
              $route = getMatch($step->getText(), '/^url "([^"]*)"$/');
       
              if (!is_null($route)) {
                  $silexConfItem['route'] = $route;
              }
       
              $requestMethod = getMatch($step->getText(), '/^request method is "([^"]*)"$/');
              if (!is_null($requestMethod)) {
                  $silexConfItem['requestMethod'] = strtoupper($requestMethod);
              }
       
              $instance = getMatch($step->getText(), '/^instance "([^"]*)"$/');
              if (!is_null($instance)) {
                  $silexConfItem['className'] = $instance;
              }
       
              $method = getMatch($step->getText(), '/^execute function "([^"]*)"$/');
              if (!is_null($method)) {
                  $silexConfItem['method'] = $method;
              }
       
              if ($step->getText() == 'format output into json') {
                  $silexConfItem['jsonEncode'] = TRUE;
              }
          }
          return $silexConfItem;
      }
       
      /** @var $features FeatureNode */
      $features = (new Parser(new Lexer($keywords)))->parse(file_get_contents(FEATURE_PATH), FEATURE_PATH);
       
      foreach ($features->getScenarios() as $scenario) {
          $silexConfItem = getScenarioConf($scenario);
          $app->match($silexConfItem['route'], function (Request $request) use ($app, $silexConfItem) {
                  function getConstructorParams($rClass, $request) {
                      $parameters =[];
                      foreach ($rClass->getMethod('__construct')->getParameters() as $parameter) {
                          if ('Symfony\Component\HttpFoundation\Request' == $parameter->getClass()->name) {
                              $parameters[$parameter->getName()] = $request;
                          }
                      }
                      return $parameters;
                  }
       
                  $rClass = new ReflectionClass($silexConfItem['className']);
                  $obj = ($rClass->hasMethod('__construct')) ?
                          $rClass->newInstanceArgs(getConstructorParams($rClass, $request)) :
                          new $silexConfItem['className'];
       
                  $output = $obj->{$silexConfItem['method']}();
       
                  return ($silexConfItem['jsonEncode'] === TRUE) ? $app->json($output, 200) : $output;
              }
          )->method($silexConfItem['requestMethod']);
      }
       
      $app->run();
      

      You can see the source code in github. What do you think?

      comments powered by Disqus