forked from silverstripe/silverstripe-graphql
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Needed a "setter" functionality, and in fact it has little to do with actual "resolving" in GraphQL sense
- Loading branch information
Showing
5 changed files
with
297 additions
and
62 deletions.
There are no files selected for viewing
This file was deleted.
Oops, something went wrong.
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} |