The Service:
<?php
namespace TestApp\Services\Ext;
class ExtFilterParser {
private $parsedFilters = array();
private $filters = '';
private $requestParam = 'filter';
public function __toString() {
return $this->filters;
}
public function getParsedFilters() {
return $this->parsedFilters;
}
public function getFilters() {
return $this->filters;
}
public function setFilters($filters) {
$this->filters = $filters;
return $this;
}
public function __construct(\Symfony\Component\HttpFoundation\Request $request) {
$this->filters = $this->pullFilterJsonFromRequest($request);
}
private function pullFilterJsonFromRequest($request) {
$filterJson = '';
$filterFromGet = $request->query->get($this->requestParam);
$filterFromPost = $request->request->get($this->requestParam);
if(empty($filterFromGet) === FALSE) {
$filterJson = $filterFromGet;
} elseif(empty($filterFromPost) === FALSE) {
$filterJson = $filterFromPost;
}
return $filterJson;
}
public function parse() {
$decodedFilters = $this->decodeFilterJson($this->filters);
foreach($decodedFilters as $filter) {
switch($filter->type) {
case 'numeric':
$this->parsedFilters[] = $this->parseComparisonFilter($filter);
break;
case 'date':
$this->parsedFilters[] = $this->parseDateFilter($filter);
break;
case 'list':
$this->parsedFilters[] = $this->parseListFilter($filter);
break;
case 'string':
$this->parsedFilters[] = $this->parseStringFilter($filter);
break;
default:
throw new \UnexpectedValueException(__METHOD__ . " Unknown filter type '$filter->type'");
}
}
return $this;
}
public function parseIntoQueryBuilder(\Doctrine\ORM\QueryBuilder $queryBuilder) {
$this->parse();
foreach($this->parsedFilters as $filter) {
$queryBuilder->andWhere('p.' . $filter['expression'] . ' ' . $filter['value']);
}
return $queryBuilder;
}
private function decodeFilterJson($filterJson) {
$decodedFilters = array();
if(empty($filterJson) === FALSE) {
$decodedFilters = json_decode($filterJson);
if($decodedFilters === NULL) {
throw new \InvalidArgumentException(__METHOD__ . " Expects first parameter to be a valid JSON string");
}
if(!is_array($decodedFilters)) {
$decodedFilters = array($decodedFilters);
}
}
return $decodedFilters;
}
private function translateComparisonOperator($comparisonOperator) {
$operator = '';
switch($comparisonOperator) {
case 'lt':
$operator = '<';
break;
case 'gt':
$operator = '>';
break;
case 'eq':
$operator = '=';
break;
default:
throw new \UnexpectedValueException(__METHOD__ . " Invalid comparison operator '$comparisonOperator'");
}
return $operator;
}
private function parseComparisonFilter($filter) {
$comparisonOperator = $this->translateComparisonOperator($filter->comparison);
if(!is_numeric($filter->value)) {
$filter->value = "'$filter->value'";
}
return array(
'expression' => "$filter->field $comparisonOperator",
'value' => $filter->value
);
}
private function parseDateFilter($filter) {
$value = $filter->value;
if($value == '0000-00-00') {
$value = '';
}
$timestamp = strtotime($value);
if($timestamp !== FALSE) {
$value = date("Y-m-d", $timestamp);
}
$filter->value = $value;
return $this->parseComparisonFilter($filter);
}
private function parseStringFilter($filter) {
return array(
'expression' => "$filter->field LIKE",
'value' => "'%$filter->value%'"
);
}
private function parseListFilter($filter) {
if(!is_array($filter->value)) {
$filter->value = explode(',', $filter->value);
}
return array(
'expression' => "$filter->field IN",
'value' => "('" . implode("','", $filter->value) . "')"
);
}
}
The unit test for the Service:
<?php
namespace TestApp\Tests\Services\Ext;
use TestApp\Services\Ext\ExtFilterParser;
use Symfony\Component\HttpFoundation\Request;
class ExtFilterParserTest extends \PHPUnit_Framework_TestCase {
private $request;
private $extFilterParser;
public function dateDataProvider() {
return array(
array(
'2011-06-30',
'June 30, 2011',
'06/30/2011',
'30/06/2011'
)
);
}
public function setUp() {
$this->request = Request::createFromGlobals();
$this->extFilterParser = new ExtFilterParser($this->request);
}
/**
* @param string $type The filter type to generate
* @param string $field The field the filter applies to
* @param mixed $value The value to filter by
* @param string $comparison (Optional) Comparison Operator
*/
public function generateFilterJson($type, $field, $value, $comparison = '') {
return json_encode(array('type' => $type, 'field' => $field, 'value' => $value, 'comparison' => $comparison));
}
public function testForProperties() {
$className = get_class($this->extFilterParser);
$this->assertClassHasAttribute('parsedFilters', $className);
$this->assertClassHasAttribute('filters', $className);
$this->assertClassHasAttribute('requestParam', $className);
}
public function testPropertyDefaultValues() {
$this->assertAttributeEmpty('parsedFilters', $this->extFilterParser, 'ExtFilterParser::parsedFilters should default to an empty array');
$this->assertAttributeEquals('filter', 'requestParam', $this->extFilterParser, 'ExtFilterParser::requestParam should default to \'filter\'');
$this->assertAttributeEquals('', 'filters', $this->extFilterParser, 'ExtFilterParser::filters should default to an empty string');
}
public function testPropertyDataTypes() {
$this->assertAttributeInternalType('array', 'parsedFilters', $this->extFilterParser);
$this->assertAttributeInternalType('string', 'filters', $this->extFilterParser);
$this->assertAttributeInternalType('string', 'requestParam', $this->extFilterParser);
}
/**
* @expectedException InvalidArgumentException
* @expectedExceptionMessage ExtFilterParser::decodeFilterJson Expects first parameter to be a valid JSON string
*/
public function testParseBadJsonException() {
$this->extFilterParser->setFilters('bad json')->parse();
}
public function test__toString() {
$filterJson = $this->generateFilterJson('string', 'firstName', 'Steve');
$this->extFilterParser->setFilters($filterJson);
$objectAsString = "$this->extFilterParser";
$this->assertEquals($filterJson, $objectAsString);
}
/**
* @expectedException UnexpectedValueException
* @expectedExceptionMessage ExtFilterParser::translateComparisonOperator Invalid comparison operator 'bad'
*/
public function testParseUnknownComparisonOperatorException() {
$this->extFilterParser->setFilters('{"type":"numeric", "field": "foo", "value":"bar", "comparison": "bad"}')->parse();
}
/**
* @expectedException UnexpectedValueException
* @expectedExceptionMessage ExtFilterParser::parse Unknown filter type 'bad'
*/
public function testParseUnknownFilterTypeException() {
$this->extFilterParser->setFilters('{"type":"bad", "field": "foo", "value":"1"}')->parse();
}
public function testParseFromGetRequest() {
$_GET['filter'] = $filter = $this->generateFilterJson('string', 'stringField', 'A');
$this->extFilterParser = new ExtFilterParser(Request::createFromGlobals());
$filter = $this->generateFilterJson('string', 'stringField', 'A');
$parsedFilter = $this->extFilterParser->parse()->getParsedFilters();
$this->assertEquals("stringField LIKE", $parsedFilter[0]['expression']);
$this->assertEquals("'%A%'", $parsedFilter[0]['value']);
$this->testParseStringFilter();
}
public function testParseFromPostRequest() {
$_POST['filter'] = $filter = $this->generateFilterJson('string', 'stringField', 'A');
$this->extFilterParser = new ExtFilterParser(Request::createFromGlobals());
$filter = $this->generateFilterJson('string', 'stringField', 'A');
$parsedFilter = $this->extFilterParser->parse()->getParsedFilters();
$this->assertEquals("stringField LIKE", $parsedFilter[0]['expression']);
$this->assertEquals("'%A%'", $parsedFilter[0]['value']);
$this->testParseStringFilter();
}
/*
* @expectedException UnexpectedValueException
* @expectedExceptionMessage ExtFilterParser::setFilters Unknown filter type 'badValue'
*/
public function testParseUnexpectedFilterTypeException() {
$this->extFilterParser->setFilters('{"type":"badValue", "field": "foo", "value":"1"}');
}
public function testParseStringFilter() {
$filter = $this->generateFilterJson('string', 'stringField', 'A');
$parsedFilter = $this->extFilterParser->setFilters($filter)->parse()->getParsedFilters();
$this->assertEquals("stringField LIKE", $parsedFilter[0]['expression']);
$this->assertEquals("'%A%'", $parsedFilter[0]['value']);
}
/**
* @dataProvider dateDataProvider
* @param string $date The date to be parsed
*/
public function testParseDateFilter($date) {
$filter = $this->generateFilterJson('date', 'dateField', $date, 'lt');
$parsedFilter = $this->extFilterParser->setFilters($filter)->parse()->getParsedFilters();
$this->assertCount(1, $parsedFilter);
$this->assertEquals("dateField <", $parsedFilter[0]['expression']);
$this->assertEquals("'2011-06-30'", $parsedFilter[0]['value']);
}
public function testParseDateFilterEmptyDate() {
$filter = $this->generateFilterJson('date', 'dateField', '0000-00-00', 'gt');
$parsedFilter = $this->extFilterParser->setFilters($filter)->parse()->getParsedFilters();
$this->assertCount(1, $parsedFilter);
$this->assertEquals("dateField >", $parsedFilter[0]['expression']);
$this->assertEquals("''", $parsedFilter[0]['value']);
}
public function testParseNumericFilterLessThan() {
$filter = $this->generateFilterJson('numeric', 'numericField', 1, 'lt');
$parsedFilter = $this->extFilterParser->setFilters($filter)->parse()->getParsedFilters();
$this->assertCount(1, $parsedFilter);
$this->assertEquals("numericField <", $parsedFilter[0]['expression']);
$this->assertEquals("1", $parsedFilter[0]['value']);
}
public function testParseNumericFilterGreaterThan() {
$filter = $this->generateFilterJson('numeric', 'numericField', 1, 'gt');
$parsedFilter = $this->extFilterParser->setFilters($filter)->parse()->getParsedFilters();
$this->assertCount(1, $parsedFilter);
$this->assertEquals("numericField >", $parsedFilter[0]['expression']);
$this->assertEquals("1", $parsedFilter[0]['value']);
}
public function testParseNumericFilterEqualTo() {
$filter = $this->generateFilterJson('numeric', 'numericField', 1, 'eq');
$parsedFilter = $this->extFilterParser->setFilters($filter)->parse()->getParsedFilters();
$this->assertCount(1, $parsedFilter);
$this->assertEquals("numericField =", $parsedFilter[0]['expression']);
$this->assertEquals("1", $parsedFilter[0]['value']);
}
public function testParseListFilterAsArray() {
$filter = $this->generateFilterJson('list', 'listField', array('a', 'b', 'c'));
$parsedFilter = $this->extFilterParser->setFilters($filter)->parse()->getParsedFilters();
$this->assertCount(1, $parsedFilter);
$this->assertEquals("listField IN", $parsedFilter[0]['expression']);
$this->assertEquals("('a','b','c')", $parsedFilter[0]['value']);
}
public function testParseListFilterAsCsv() {
$filter = $this->generateFilterJson('list', 'listField', "a,b,c");
$parsedFilter = $this->extFilterParser->setFilters($filter)->parse()->getParsedFilters();
$this->assertCount(1, $parsedFilter);
$this->assertEquals("listField IN", $parsedFilter[0]['expression']);
$this->assertEquals("('a','b','c')", $parsedFilter[0]['value']);
}
public function testSetFiltersReturnsInstance() {
$instance = $this->extFilterParser->setFilters($this->generateFilterJson('list', 'listField', "a,b,c"));-
$this->assertInstanceOf(get_class($this->extFilterParser), $instance);
}
public function testParseReturnsInstance() {
$instance = $this->extFilterParser->setFilters($this->generateFilterJson('list', 'listField', "a,b,c"))->parse();
$this->assertInstanceOf(get_class($this->extFilterParser), $instance);
}
}
Facts
- I know that the service works (I've used it).
- I know all unit tests pass.
Concerns
- The unit test makes use of and is dependent on the Request Object.
- If I change the scope / configuration of my service via the YAML file that symfony uses (say to add another object to the constructor), these unit tests will all fail even though the code itself may not be "broken".
- I feel like I've too tightly coupled things and made the unit tests not accurately reflect what's going on in my application.