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?

7 thoughts on “Building a Silex application from one Behat/Gherkin feature file

      1. When you use mink you win a lot of function with it.

        You can run

        $ bin/behat -dl

        Example here http://pastebin.com/c3bwecvd

        You receive all functions you can use directly from the .feature’s files, getting values, and go to urls, without Context and more cool tools.

        You still using Context files If you want, but not always is necessary and not all need to be programmed in contexts’s files.

Leave a comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.