Skip to content

Instantly share code, notes, and snippets.

@jubianchi
Created August 7, 2012 12:18
Show Gist options
  • Save jubianchi/3284862 to your computer and use it in GitHub Desktop.
Save jubianchi/3284862 to your computer and use it in GitHub Desktop.
Conditionnal configuration validation
<?php
use \mageekguy\atoum;
define('COVERAGE_TITLE', 'Config');
define('COVERAGE_DIRECTORY', './coverage');
define('COVERAGE_WEB_PATH', 'http://%host%/coverage');
if(false === is_dir(COVERAGE_DIRECTORY))
{
mkdir(COVERAGE_DIRECTORY, 0777, true);
}
$coverageField = new atoum\report\fields\runner\coverage\html(COVERAGE_TITLE, COVERAGE_DIRECTORY);
$coverageField->setRootUrl(COVERAGE_WEB_PATH);
$stdOutWriter = new atoum\writers\std\out();
$cliReport = new atoum\reports\realtime\cli();
$cliReport
->addWriter($stdOutWriter)
->addField($coverageField, array(atoum\runner::runStop))
;
$xunitFileWriter = new atoum\writers\file('atoum.xml');
$xunit = new atoum\reports\asynchronous\xunit();
$xunit->addWriter($xunitFileWriter);
$cloverFileWriter = new atoum\writers\file('atoum.coverage.xml');
$clover = new atoum\reports\asynchronous\clover();
$clover->addWriter($cloverFileWriter);
$runner->addReport($cliReport);
$runner->addReport($clover);
$runner->addReport($xunit);
<?php
namespace jubianchi\Config;
class Chart extends Table implements Type {
const TYPE_NAME = 'chart';
}
<?php
namespace tests\unit\jubianchi\Config;
use mageekguy\atoum;
use jubianchi\Config\Chart as TestedClass;
require_once dirname(__DIR__) . '/../../../vendor/autoload.php';
require_once dirname(__DIR__) . '/../../../vendor/mageekguy/atoum/scripts/runner.php';
class Chart extends atoum\test {
public function testConstants() {
$this
->string(TestedClass::TYPE_NAME)->isEqualTo('chart')
->string(TestedClass::CLASS_ATTR)->isEqualTo('class')
->string(TestedClass::COLS_ATTR)->isEqualTo('columns')
->string(TestedClass::ROWS_ATTR)->isEqualTo('rows')
;
}
public function testGetType() {
$this
->if($object = new TestedClass())
->then
->string($object->getType())->isEqualTo(TestedClass::TYPE_NAME)
;
}
}
{
"name": "sandbox",
"type": "script",
"description": "PHP Sandbox",
"authors": [
{
"name": "Julien Bianchi",
"email": "[email protected]",
"homepage": "http://jubianchi.fr"
}
],
"autoload": {
"psr-0": { "jubianchi": "src/" }
},
"minimum-stability": "dev",
"require": {
"php": ">=5.3.2",
"symfony/config": "*"
},
"require-dev": {
"mageekguy/atoum": "dev-master"
}
}
{
"hash": "709c1a146f21274a7a6253c4a07e795e",
"packages": [
{
"package": "symfony/config",
"version": "dev-master",
"source-reference": "v2.1.0-RC1",
"commit-date": "1343512949"
}
],
"packages-dev": [
{
"package": "mageekguy/atoum",
"version": "dev-master",
"source-reference": "8ff37c48b558a33ce9cdfa252937e8ca08713364",
"commit-date": "1343308831"
}
],
"aliases": [
],
"minimum-stability": "dev",
"stability-flags": {
"mageekguy/atoum": 20
}
}
<?php
namespace jubianchi\Config;
class Configuration {
const ROOT_NAME = 'layouts';
const TYPE_ATTR = 'type';
}
<?php
namespace tests\unit\jubianchi\Config;
use mageekguy\atoum;
use jubianchi\Config\Configuration as TestedClass;
require_once dirname(__DIR__) . '/../../../vendor/autoload.php';
require_once dirname(__DIR__) . '/../../../vendor/mageekguy/atoum/scripts/runner.php';
class Configuration extends atoum\test {
public function testConstants() {
$this
->string(TestedClass::ROOT_NAME)->isEqualTo('layouts')
->string(TestedClass::TYPE_ATTR)->isEqualTo('type')
;
}
}
<?php
use jubianchi\Config\Configuration,
jubianchi\Config\Table,
jubianchi\Config\Listing,
jubianchi\Config\Chart;
require_once __DIR__ . '/vendor/autoload.php';
$config = array(
'layouts' => array(
'tableau' => array(
Configuration::TYPE_ATTR => Table::TYPE_NAME,
Table::CLASS_ATTR => 'formated_table',
Table::ROWS_ATTR => array('measures'),
Table::COLS_ATTR => array('dim1', 'dim2')
),
'liste' => array(
Configuration::TYPE_ATTR => Listing::TYPE_NAME,
Listing::CLASS_ATTR => 'formated_table',
Listing::ITEMS_ATTR => array('measures')
),
'graphique' => array(
Configuration::TYPE_ATTR => Chart::TYPE_NAME,
Chart::CLASS_ATTR => 'formated_table',
Chart::ROWS_ATTR => array('measures'),
Chart::COLS_ATTR => array('dim1', 'dim2')
)
)
);
$validator = new \jubianchi\Config\Validator();
$validator->validate($config, new \jubianchi\Config\TypeFinder());
<?php
namespace jubianchi\Config;
class Listing implements Type {
const TYPE_NAME = 'listing';
const CLASS_ATTR = 'class';
const ITEMS_ATTR = 'items';
/**
* @static
*
* @return string
*/
public static function getType() {
return static::TYPE_NAME;
}
/**
* @static
*
* @return array
*/
public static function getFields() {
return array(static::CLASS_ATTR, static::ITEMS_ATTR);
}
/**
* @static
*
* @param \Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition $root
*/
public static function build(\Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition $root) {
$root
->children()
->scalarNode(static::CLASS_ATTR)->isRequired()->end()
->arrayNode(static::ITEMS_ATTR)
->isRequired()
->prototype('scalar')->end()
->end()
->end()
;
}
}
<?php
namespace tests\unit\jubianchi\Config;
use mageekguy\atoum;
use jubianchi\Config\Listing as TestedClass;
require_once dirname(__DIR__) . '/../../../vendor/autoload.php';
require_once dirname(__DIR__) . '/../../../vendor/mageekguy/atoum/scripts/runner.php';
class Listing extends atoum\test {
public function testConstants() {
$this
->string(TestedClass::TYPE_NAME)->isEqualTo('listing')
->string(TestedClass::CLASS_ATTR)->isEqualTo('class')
->string(TestedClass::ITEMS_ATTR)->isEqualTo('items')
;
}
public function testGetType() {
$this
->if($object = new TestedClass())
->then
->string($object->getType())->isEqualTo(TestedClass::TYPE_NAME)
;
}
public function testGetFields() {
$this
->if($object = new TestedClass())
->then
->array($object->getFields())->isIdenticalTo(array(TestedClass::CLASS_ATTR, TestedClass::ITEMS_ATTR))
;
}
public function testBuild() {
$this
->if($object = new TestedClass())
->and($builder = new \mock\Symfony\Component\Config\Definition\Builder\NodeBuilder())
->and($root = new \mock\Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition(uniqid()))
->and($node = new \mock\Symfony\Component\Config\Definition\Builder\NodeDefinition(uniqid()))
->and($builder->getMockController()->scalarNode = function() use($node) { return $node; })
->and($builder->getMockController()->arrayNode = function() use($root) { return $root; })
->and($root->getMockController()->children = function() use($builder) { return $builder; })
->and($root->getMockController()->end = function() use($builder) { return $builder; })
->and($node->getMockController()->end = function() use($builder) { return $builder; })
->and($node->getMockController()->isRequired = function() use($node) { return $node; })
->then
->variable($object->build($root))->isNull()
->mock($root)
->call('children')->once()
;
}
}
<?php
namespace jubianchi\Config;
class Table implements Type {
const TYPE_NAME = 'table';
const CLASS_ATTR = 'class';
const ROWS_ATTR = 'rows';
const COLS_ATTR = 'columns';
/**
* @static
*
* @return string
*/
public static function getType() {
return static::TYPE_NAME;
}
/**
* @static
*
* @return array
*/
public static function getFields() {
return array(static::CLASS_ATTR, static::ROWS_ATTR, static::COLS_ATTR);
}
/**
* @static
*
* @param \Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition $root
*/
public static function build(\Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition $root) {
$root
->children()
->scalarNode(static::CLASS_ATTR)->isRequired()->end()
->arrayNode(static::ROWS_ATTR)
->isRequired()
->prototype('scalar')->end()
->end()
->arrayNode(static::COLS_ATTR)
->isRequired()
->prototype('scalar')->end()
->end()
->end()
;
}
}
<?php
namespace tests\unit\jubianchi\Config;
use mageekguy\atoum;
use jubianchi\Config\Table as TestedClass;
require_once dirname(__DIR__) . '/../../../vendor/autoload.php';
require_once dirname(__DIR__) . '/../../../vendor/mageekguy/atoum/scripts/runner.php';
class Table extends atoum\test {
public function testConstants() {
$this
->string(TestedClass::TYPE_NAME)->isEqualTo('table')
->string(TestedClass::CLASS_ATTR)->isEqualTo('class')
->string(TestedClass::COLS_ATTR)->isEqualTo('columns')
->string(TestedClass::ROWS_ATTR)->isEqualTo('rows')
;
}
public function testGetType() {
$this
->if($object = new TestedClass())
->then
->string($object->getType())->isEqualTo(TestedClass::TYPE_NAME)
;
}
public function testGetFields() {
$this
->if($object = new TestedClass())
->then
->array($object->getFields())->isIdenticalTo(array(TestedClass::CLASS_ATTR, TestedClass::ROWS_ATTR, TestedClass::COLS_ATTR))
;
}
public function testBuild() {
$this
->if($object = new TestedClass())
->and($builder = new \mock\Symfony\Component\Config\Definition\Builder\NodeBuilder())
->and($root = new \mock\Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition(uniqid()))
->and($node = new \mock\Symfony\Component\Config\Definition\Builder\NodeDefinition(uniqid()))
->and($builder->getMockController()->scalarNode = function() use($node) { return $node; })
->and($builder->getMockController()->arrayNode = function() use($root) { return $root; })
->and($root->getMockController()->children = function() use($builder) { return $builder; })
->and($root->getMockController()->end = function() use($builder) { return $builder; })
->and($node->getMockController()->end = function() use($builder) { return $builder; })
->and($node->getMockController()->isRequired = function() use($node) { return $node; })
->then
->variable($object->build($root))->isNull()
->mock($root)
->call('children')->once()
;
}
}
<?php
namespace jubianchi\Config;
interface Type {
/**
* @static
* @abstract
*
* @return string
*/
static function getType();
/**
* @static
* @abstract
*
* @return array
*/
static function getFields();
/**
* @static
* @abstract
*
* @param \Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition $root
*/
static function build(\Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition $root);
}
<?php
namespace jubianchi\Config;
use mageekguy\atoum\adapter;
class TypeFinder {
private $adapter;
public function setAdapter(adapter $adapter) {
$this->adapter = $adapter;
return $this;
}
public function getAdapter() {
if(null === $this->adapter) {
$this->setAdapter(new adapter());
}
return $this->adapter;
}
/**
* This should be changed to a better strategy!!!
* * Use a classmap
* * Use Symfony2 tagged services
*
* @return array
*/
public function getValidTypes() {
$classes = $this->getAdapter()->get_declared_classes();
$types = array();
foreach($classes as $class) {
if($this->getAdapter()->is_subclass_of($class, '\\jubianchi\\Config\\Type')) {
$types[$class] = $class::getType();
}
}
return $types;
}
/**
* @param string $type
*
* @return mixed|null
*/
public function getClassFromType($type) {
$class = array_search($type, $this->getValidTypes());
return false !== $class ? $class : null;
}
/**
* @return array
*/
public function getValidFields() {
$fields = array();
foreach($this->getValidTypes() as $class => $type) {
$fields = array_unique(array_merge($class::getFields(), $fields));
}
return $fields;
}
}
<?php
namespace tests\unit\jubianchi\Config;
use mageekguy\atoum;
use jubianchi\Config\TypeFinder as TestedClass;
require_once dirname(__DIR__) . '/../../../vendor/autoload.php';
require_once dirname(__DIR__) . '/../../../vendor/mageekguy/atoum/scripts/runner.php';
/**
* @ignore on
*/
class BazType implements \jubianchi\Config\Type {
static function getType()
{
return 'baz';
}
static function getFields()
{
return array('foo', 'bar');
}
static function build(\Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition $root)
{
}
}
class TypeFinder extends atoum\test {
public function testSetAdapter() {
$this
->if($object = new TestedClass())
->and($adapter = new \mageekguy\atoum\adapter())
->then
->object($object->setAdapter($adapter))->isIdenticalTo($object)
->object($object->getAdapter())->isIdenticalTo($adapter)
;
}
public function testGetAdapter() {
$this
->if($object = new TestedClass())
->then
->object($object->getAdapter())->isInstanceOf('\\mageekguy\\atoum\\adapter')
;
}
public function testGetValidTypes() {
$this
->if($object = new TestedClass())
->and($adapter = new \mageekguy\atoum\test\adapter())
->and($object->setAdapter($adapter))
->and($adapter->get_declared_classes = array())
->then
->array($object->getValidTypes())->isEqualTo(array())
->adapter($adapter)
->call('get_declared_classes')->once()
->if($adapter->get_declared_classes = array('Foo\\Bar', 'Bar\\Foo'))
->then
->array($object->getValidTypes())->isEqualTo(array())
->adapter($adapter)
->call('is_subclass_of')->withArguments('Foo\\Bar', '\\jubianchi\\Config\\Type')->once()
->call('is_subclass_of')->withArguments('Bar\\Foo', '\\jubianchi\\Config\\Type')->once()
->if($object = new TestedClass())
->then
->array($object->getValidTypes())->isEqualTo(array(
'tests\\unit\\jubianchi\\Config\\BazType' => 'baz'
))
;
}
public function testGetClassFromType() {
$this
->if($object = new TestedClass())
->then
->string($object->getClassFromType('baz'))->isEqualTo('tests\\unit\\jubianchi\\Config\\BazType')
->variable($object->getClassFromType(uniqid()))->isNull()
;
}
public function testGetValidFields() {
$this
->if($object = new TestedClass())
->then
->array($object->getValidFields())->isEqualTo(array('foo', 'bar'))
->if($object = new TestedClass())
->and($adapter = new \mageekguy\atoum\test\adapter())
->and($object->setAdapter($adapter))
->and($adapter->get_declared_classes = array())
->then
->array($object->getValidFields())->isEqualTo(array())
;
}
}
<?php
namespace jubianchi\Config;
use Symfony\Component\Config\Definition\Builder\TreeBuilder,
Symfony\Component\Config\Definition\Processor,
Symfony\Component\Config\Definition\Exception\InvalidConfigurationException,
jubianchi\Config\TypeFinder;
class Validator {
/** @var \Symfony\Component\Config\Definition\Builder\TreeBuilder */
private $builder;
/** @var \Symfony\Component\Config\Definition\Builder\NodeDefinition */
private $root;
/** @var \Symfony\Component\Config\Definition\Builder\NodeDefinition */
private $prototype;
/**
* @return \Symfony\Component\Config\Definition\Builder\TreeBuilder
*/
public function getNewBuilder() {
return new TreeBuilder();
}
/**
* @return \Symfony\Component\Config\Definition\Processor
*/
public function getNewProcessor() {
return new Processor();
}
/**
* @return \Symfony\Component\Config\Definition\Builder\TreeBuilder
*/
public function getBuilder() {
if(null === $this->builder) {
$this->builder = $this->getNewBuilder();
}
return $this->builder;
}
/**
* @return \Symfony\Component\Config\Definition\Builder\NodeDefinition
*/
public function getRoot() {
if(null === $this->root) {
$this->root = $this->getBuilder()->root(Configuration::ROOT_NAME);
}
return $this->root;
}
/**
* @return \Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition
*/
public function getPrototype() {
if(null === $this->prototype) {
$this->prototype = $this->getRoot()->prototype('array');
}
return $this->prototype;
}
/**
* @param TypeFinder $finder
* @throws \RuntimeException
* @throws \Symfony\Component\Config\Definition\Exception\InvalidConfigurationException
*/
protected function build(TypeFinder $finder) {
$prototype = $this->getPrototype();
try {
$prototype
->children()
->enumNode(Configuration::TYPE_ATTR)->values($finder->getValidTypes())->end()
->end()
;
} catch(\InvalidArgumentException $exception) {
throw new \RuntimeException('No valid types found', $exception->getCode(), $exception);
}
foreach($finder->getValidFields() as $field) {
$prototype->append(new \Symfony\Component\Config\Definition\Builder\VariableNodeDefinition($field));
}
$self = $this;
$prototype
->validate()
->ifTrue(function($v) { return is_array($v); })
->then(function($v) use($self, $finder) {
$builder = $self->getNewBuilder();
$root = $builder->root($v[Configuration::TYPE_ATTR]);
$root->children()->scalarNode(Configuration::TYPE_ATTR)->isRequired()->end();
$validator = $finder->getClassFromType($v[Configuration::TYPE_ATTR]);
if(null === $validator) {
throw new InvalidConfigurationException(
sprintf(
'No validator defined for type "%s"',
$v[Configuration::TYPE_ATTR]
)
);
}
$validator::build($root);
$self->process(array($v[Configuration::TYPE_ATTR] => $v), $builder);
})
->end()
;
}
/**
* @param array $config
* @param TypeFinder $finder
*/
public function validate(array $config, TypeFinder $finder) {
$this->build($finder);
$this->process($config, $this->getBuilder());
}
/**
* @param array $config
* @param \Symfony\Component\Config\Definition\Builder\TreeBuilder $builder
*/
public function process(array $config, TreeBuilder $builder) {
$this->getNewProcessor()->process($builder->buildTree(), $config);
}
}
<?php
namespace tests\unit\jubianchi\Config;
use mageekguy\atoum;
use jubianchi\Config\Validator as TestedClass;
require_once dirname(__DIR__) . '/../../../vendor/autoload.php';
require_once dirname(__DIR__) . '/../../../vendor/mageekguy/atoum/scripts/runner.php';
/**
* @ignore on
*/
class FooType implements \jubianchi\Config\Type {
static function getType()
{
return 'foo';
}
static function getFields()
{
return array('foo', 'bar');
}
static function build(\Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition $root)
{
}
}
/**
* @ignore on
*/
class BarType implements \jubianchi\Config\Type {
static function getType()
{
return 'bar';
}
static function getFields()
{
return array('foo', 'bar');
}
static function build(\Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition $root)
{
}
}
class Validator extends atoum\test {
public function testGetNewBuilder() {
$this
->if($object = new TestedClass())
->then
->object($builder = $object->getNewBuilder())->isInstanceOf('\\Symfony\\Component\\Config\\Definition\\Builder\\TreeBuilder')
->object($object->getNewBuilder())->isNotIdenticalTo($builder)
;
}
public function testGetNewProcessor() {
$this
->if($object = new TestedClass())
->then
->object($processor = $object->getNewProcessor())->isInstanceOf('\\Symfony\\Component\\Config\\Definition\\Processor')
->object($object->getNewProcessor())->isNotIdenticalTo($processor)
;
}
public function testGetBuilder() {
$this
->if($object = new \mock\jubianchi\Config\Validator())
->then
->object($builder = $object->getBuilder())->isInstanceOf('\\Symfony\\Component\\Config\\Definition\\Builder\\TreeBuilder')
->object($object->getBuilder())->isIdenticalTo($builder)
->mock($object)
->call('getNewBuilder')->once()
;
}
public function testGetRoot() {
$this
->if($object = new \mock\jubianchi\Config\Validator())
->and($object->getMockController()->getBuilder = $builder = new \mock\Symfony\Component\Config\Definition\Builder\TreeBuilder())
->then
->object($root = $object->getRoot())->isInstanceOf('\\Symfony\\Component\\Config\\Definition\\Builder\\ArrayNodeDefinition')
->object($object->getRoot())->isIdenticalTo($root)
->mock($object)
->call('getBuilder')->once()
->mock($builder)
->call('root')->withArguments(\jubianchi\Config\Configuration::ROOT_NAME)
;
}
public function testGetPrototype() {
$this
->if($object = new \mock\jubianchi\Config\Validator())
->and($object->getMockController()->getRoot = $root = new \mock\Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition(uniqid()))
->then
->object($prototype = $object->getPrototype())->isInstanceOf('\\Symfony\\Component\\Config\\Definition\\Builder\\NodeDefinition')
->object($object->getPrototype())->isIdenticalTo($prototype)
->mock($object)
->call('getRoot')->once()
->mock($root)
->call('prototype')->withArguments('array')
;
}
public function testValidate() {
$this
->if($object = new \mock\jubianchi\Config\Validator())
->and($object->getMockController()->getBuilder = $builder = new \mock\Symfony\Component\Config\Definition\Builder\TreeBuilder())
->and($finder = new \mock\jubianchi\Config\TypeFinder())
->and($finder->getMockController()->getValidTypes = array(
'tests\\unit\\jubianchi\Config\\FooType' => 'foo',
'tests\\unit\\jubianchi\Config\\BarType' => 'bar'
))
->then
->variable($object->validate(array(), $finder))->isNull()
->mock($object)
->call('process')->withArguments(array(), $builder)->once()
->if($object->getMockController()->getPrototype = $prototype = new \mock\Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition(uniqid()))
->and($finder->getMockController()->getValidFields = array('foo', 'bar'))
->and($prototype->getMockController()->append = function() use($prototype) { return $prototype; })
->then
->variable($object->validate(array(), $finder))->isNull()
->mock($prototype)
->call('append')->withArguments(new \Symfony\Component\Config\Definition\Builder\VariableNodeDefinition('foo'))->once()
->call('append')->withArguments(new \Symfony\Component\Config\Definition\Builder\VariableNodeDefinition('bar'))->once()
->if($finder->getMockController()->getValidTypes = array())
->then
->exception(function() use($object, $finder) {
$object->validate(array(), $finder);
})
->isInstanceOf('\\RuntimeException')
->hasMessage('No valid types found')
;
}
public function testProcess() {
$this
->if($object = new \mock\jubianchi\Config\Validator())
->and($object->getMockController()->getNewProcessor = $processor = new \mock\Symfony\Component\Config\Definition\Processor())
->and($builder = new \mock\Symfony\Component\Config\Definition\Builder\TreeBuilder())
->and($builder->root(uniqid()))
->then
->variable($object->process(array(), $builder))->isNull()
->mock($processor)
->call('process')->withArguments($builder->buildTree(), array())->once()
;
}
}
@jubianchi
Copy link
Author

# Init
$ composer.phar install --dev

# Launch unit tests suite
$ vendor/bin/atoum -d tests/

# Test
$ php index.php

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment