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?