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?
Great !
sick, messed up 🙂 nice man thanks for sharing
It was raining outside 🙂
Very interesting. Forking, studying and improving it right now 😉
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.