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?

About these ads

About Gonzalo Ayuso

Web Architect specialized in Open Source technologies. PHP, Python, JQuery, Dojo, PostgreSQL, CouchDB and node.js but always learning.

Posted on October 22, 2012, in php, Technology and tagged , , , , . Bookmark the permalink. 7 Comments.

  1. sick, messed up :) nice man thanks for sharing

  2. Very interesting. Forking, studying and improving it right now ;)

  3. Why you don’t use Mink to avoid the use of ContextClass?

    • I don’t understand your proposal. How can I fit Mink here?

      I must admit something first: Studding Mink is in my to-do-list yet. My knowledge about Mink is a quick view of the http://mink.behat.org/. :)

      • 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 Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

Follow

Get every new post delivered to your Inbox.

Join 869 other followers

%d bloggers like this: