Skip to content

Commit

Permalink
Refactored field resolvers
Browse files Browse the repository at this point in the history
Needed a "setter" functionality, and in fact it has little to do with actual "resolving" in GraphQL sense
  • Loading branch information
chillu committed Sep 24, 2016
1 parent 00bcecd commit 1f1f931
Show file tree
Hide file tree
Showing 5 changed files with 297 additions and 62 deletions.
44 changes: 0 additions & 44 deletions src/Resolver/DataObjectLowerCamelResolver.php

This file was deleted.

18 changes: 0 additions & 18 deletions src/Resolver/IResolver.php

This file was deleted.

163 changes: 163 additions & 0 deletions src/Util/CaseInsensitiveFieldAccessor.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
<?php

namespace Chillu\GraphQL\Util;

use SilverStripe\ORM\DataObject;
use SilverStripe\Core\ClassInfo;
use SilverStripe\View\ViewableData;
use InvalidArgumentException;

/**
* Infer original field name casing from case insensitive field comparison.
* Useful counterpart to {@link \Convert::upperCamelToLowerCamel()}.
*
* SilverStripe is using a mix of case sensitive and case insensitive checks,
* due to the nature of PHP (case sensitive for properties and array keys,
* case insensitive for methods).
*
* Caution: Assumes fields have been whitelisted through GraphQL type definitions already.
* Does not perform any canView() checks or further validation.
*
* @see http://www.php.net/manual/en/functions.user-defined.php
* @see http://php.net/manual/en/function.array-change-key-case.php
*/
class CaseInsensitiveFieldAccessor {

const HAS_METHOD = 'HAS_METHOD';
const HAS_FIELD = 'HAS_FIELD';
const HAS_SETTER = 'HAS_SETTER';
const DATAOBJECT = 'DATAOBJECT';

/**
* @param ViewableData $object The parent resolved object
* @param string $fieldName Name of the field/getter/method
* @param array $opts Map of which lookups to use (class constants to booleans).
* Example: [ViewableDataCaseInsensitiveFieldMapper::HAS_METHOD => true]
* @return mixed
*/
public function getValue(ViewableData $object, $fieldName, $opts = [])
{
$opts = $opts ?: [];
$opts = array_merge([
self::HAS_METHOD => true,
self::HAS_FIELD => true,
self::HAS_SETTER => false,
self::DATAOBJECT => true,
], $opts);

$objectFieldName = $this->getObjectFieldName($object, $fieldName, $opts);

if(!$objectFieldName) {
throw new InvalidArgumentException(sprintf(
'Field name or method "%s" does not exist on %s',
$fieldName,
(string)$object
));
}

// Correct case for methods (e.g. canView)
if($object->hasMethod($objectFieldName)) {
return $object->{$objectFieldName}();
}

// Correct case (and getters)
if($object->hasField($objectFieldName)) {
return $object->{$objectFieldName};
}

return null;
}

/**
* @param ViewableData $object The parent resolved object
* @param string $fieldName Name of the field/getter/method
* @param mixed $value
* @param array $opts Map of which lookups to use (class constants to booleans).
* Example: [ViewableDataCaseInsensitiveFieldMapper::HAS_METHOD => true]
* @return mixed
*/
public function setValue(ViewableData $object, $fieldName, $value, $opts = [])
{
$opts = $opts ?: [];
$opts = [
self::HAS_METHOD => true,
self::HAS_FIELD => true,
self::HAS_SETTER => true,
self::DATAOBJECT => true,
] + $opts;

$objectFieldName = $this->getObjectFieldName($object, $fieldName, $opts);

if(!$objectFieldName) {
throw new InvalidArgumentException(sprintf(
'Field name "%s" does not exist on %s',
$fieldName,
(string)$object
));
}

// Correct case for methods (e.g. canView)
if($object->hasMethod($objectFieldName)) {
$object->{$objectFieldName}($value);
}

// Correct case (and getters)
if($object->hasField($objectFieldName)) {
$object->{$objectFieldName} = $value;
}

// Infer casing
if($object instanceof DataObject) {
$object->setField($objectFieldName, $value);
}

return null;
}

/**
* @param $object The object to resolve a name on
* @param string $fieldName Name in different casing
* @param array $opts Map of which lookups to use (class constants to booleans).
* Example: [ViewableDataCaseInsensitiveFieldMapper::HAS_METHOD => true]
* @return null|string Name in actual casing on $object
*/
protected function getObjectFieldName(ViewableData $object, $fieldName, $opts = [])
{
$optFn = function($type) use(&$opts) {
return (in_array($type, $opts) && $opts[$type] === true);
};

// Correct case (and getters)
if($optFn(self::HAS_FIELD) && $object->hasField($fieldName)) {
return $fieldName;
}

// Infer casing from DataObject fields
if($optFn(self::DATAOBJECT) && $object instanceof DataObject) {
$parents = ClassInfo::ancestry($object, true);
foreach($parents as $parent) {
$fields = DataObject::database_fields($parent);
foreach($fields as $objectFieldName => $fieldClass) {
if(strcasecmp($objectFieldName, $fieldName) === 0) {
return $objectFieldName;
}
}
}
}

// Setters
// TODO Support for Object::$extra_methods (case sensitive array key check)
$setterName = "set" . ucfirst($fieldName);
if($optFn(self::HAS_SETTER) && $object->hasMethod($setterName)) {
return $setterName;
}

// Correct case for methods (e.g. canView) - method_exists() is case insensitive
if($optFn(self::HAS_METHOD) && $object->hasMethod($fieldName)) {
return $fieldName;
}

return null;
}

}
18 changes: 18 additions & 0 deletions tests/Fake/DataObjectFake.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,14 @@

class DataObjectFake extends DataObject implements TestOnly
{
private static $db = [
'MyField' => 'Varchar'
];

public $customSetterFieldResult;

public $customSetterMethodResult;

public function getCustomGetter()
{
return 'customGetterValue';
Expand All @@ -16,4 +24,14 @@ public function customMethod()
{
return 'customMethodValue';
}

public function setCustomSetterField($val)
{
$this->customSetterFieldResult = $val;
}

public function customSetterMethod($val)
{
$this->customSetterMethodResult = $val;
}
}
116 changes: 116 additions & 0 deletions tests/Util/CaseInsensitiveFieldAccessorTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
<?php

namespace Chillu\GraphQL\Tests\Util;

use Chillu\GraphQL\Util\CaseInsensitiveFieldAccessor;
use Chillu\GraphQL\Tests\DataObjectFake;
use SilverStripe\Dev\SapphireTest;

class CaseInsensitiveFieldAccessorTest extends SapphireTest
{

public function testGetValueWithOriginalCasing()
{
$fake = new DataObjectFake([
'MyField' => 'myValue'
]);
$mapper = new CaseInsensitiveFieldAccessor();
$this->assertEquals('myValue', $mapper->getValue($fake, 'MyField'));
}

public function testGetValueWithDifferentCasing()
{
$fake = new DataObjectFake([
'MyField' => 'myValue'
]);
$mapper = new CaseInsensitiveFieldAccessor();
$this->assertEquals('myValue', $mapper->getValue($fake, 'myfield'));
}

public function testGetValueWithCustomGetter()
{
$fake = new DataObjectFake([]);
$mapper = new CaseInsensitiveFieldAccessor();
$this->assertEquals('customGetterValue', $mapper->getValue($fake, 'customGetter'));
}

public function testGetValueWithMethod()
{
$fake = new DataObjectFake([]);
$mapper = new CaseInsensitiveFieldAccessor();
$this->assertEquals('customMethodValue', $mapper->getValue($fake, 'customMethod'));
}

/**
* @expectedException \InvalidArgumentException
*/
public function testGetValueWithUnknownFieldThrowsException()
{
$fake = new DataObjectFake([]);
$mapper = new CaseInsensitiveFieldAccessor();
$mapper->getValue($fake, 'unknownField');
}

/**
* @expectedException \InvalidArgumentException
*/
public function testGetValueWithCustomOpts()
{
$fake = new DataObjectFake([
'MyField' => 'myValue'
]);
$mapper = new CaseInsensitiveFieldAccessor();
$opts = [
// only check for methods
CaseInsensitiveFieldAccessor::HAS_FIELD => false,
CaseInsensitiveFieldAccessor::DATAOBJECT => false,
];
$mapper->getValue($fake, 'MyField', $opts);
}

public function testSetValueWithOriginalCasing()
{
$fake = new DataObjectFake([
'MyField' => 'myValue'
]);
$mapper = new CaseInsensitiveFieldAccessor();
$mapper->setValue($fake, 'MyField', 'myNewValue');
$this->assertEquals('myNewValue', $fake->MyField);
}

public function testSetValueWithDifferentCasing()
{
$fake = new DataObjectFake([
'MyField' => 'myValue'
]);
$mapper = new CaseInsensitiveFieldAccessor();
$mapper->setValue($fake, 'myfield', 'myNewValue');
$this->assertEquals('myNewValue', $fake->MyField);
}

public function testSetValueWithCustomGetter()
{
$fake = new DataObjectFake([]);
$mapper = new CaseInsensitiveFieldAccessor();
$mapper->setValue($fake, 'customsetterfield', 'myNewValue');
$this->assertEquals('myNewValue', $fake->customSetterFieldResult);
}

public function testSetValueWithMethod()
{
$fake = new DataObjectFake([]);
$mapper = new CaseInsensitiveFieldAccessor();
$mapper->setValue($fake, 'customsettermethod', 'myNewValue');
$this->assertEquals('myNewValue', $fake->customSetterMethodResult);
}

/**
* @expectedException \InvalidArgumentException
*/
public function testSetValueWithUnknownFieldThrowsException()
{
$fake = new DataObjectFake([]);
$mapper = new CaseInsensitiveFieldAccessor();
$mapper->setValue($fake, 'unknownField', true);
}
}

0 comments on commit 1f1f931

Please sign in to comment.