Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions src/FieldsBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
use TheCodingMachine\GraphQLite\Types\MutableObjectType;
use TheCodingMachine\GraphQLite\Types\TypeResolver;
use TheCodingMachine\GraphQLite\Utils\DescriptionResolver;
use TheCodingMachine\GraphQLite\Utils\FieldAccessorPrefixes;
use TheCodingMachine\GraphQLite\Utils\PropertyAccessor;

use function array_diff_key;
Expand All @@ -68,7 +69,6 @@
use function key;
use function reset;
use function rtrim;
use function str_starts_with;
use function trim;

use const PHP_EOL;
Expand All @@ -92,6 +92,7 @@ public function __construct(
private readonly FieldMiddlewareInterface $fieldMiddleware,
private readonly InputFieldMiddlewareInterface $inputFieldMiddleware,
private readonly DescriptionResolver $descriptionResolver = new DescriptionResolver(true),
private readonly FieldAccessorPrefixes $fieldAccessorPrefixes = new FieldAccessorPrefixes(),
)
{
$this->typeMapper = new TypeHandler(
Expand Down Expand Up @@ -846,7 +847,7 @@ private function getMethodFromPropertyName(
if ($reflectionClass->hasMethod($propertyName)) {
$methodName = $propertyName;
} else {
$methodName = PropertyAccessor::findGetter($reflectionClass->getName(), $propertyName);
$methodName = PropertyAccessor::findGetter($reflectionClass->getName(), $propertyName, $this->fieldAccessorPrefixes);
if (! $methodName) {
throw FieldNotFoundException::missingField($reflectionClass->getName(), $propertyName);
}
Expand Down Expand Up @@ -1032,7 +1033,7 @@ private function getInputFieldsByMethodAnnotations(

$docBlockObj = $this->docBlockFactory->create($refMethod);
$methodName = $refMethod->getName();
if (! str_starts_with($methodName, 'set')) {
if (! $this->fieldAccessorPrefixes->hasSetterPrefix($methodName)) {
continue;
}

Expand Down
25 changes: 8 additions & 17 deletions src/NamingStrategy.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,21 @@
use TheCodingMachine\GraphQLite\Annotations\Factory;
use TheCodingMachine\GraphQLite\Annotations\Input;
use TheCodingMachine\GraphQLite\Annotations\TypeInterface;
use TheCodingMachine\GraphQLite\Utils\FieldAccessorPrefixes;

use function implode;
use function lcfirst;
use function str_ends_with;
use function str_replace;
use function str_starts_with;
use function strlen;
use function strrpos;
use function substr;

class NamingStrategy implements NamingStrategyInterface
{
public function __construct(
private readonly FieldAccessorPrefixes $fieldAccessorPrefixes = new FieldAccessorPrefixes(),
) {
}

/**
* Returns the name of the GraphQL interface from a name of a concrete class (when the interface is created
* automatically to manage inheritance)
Expand Down Expand Up @@ -88,27 +91,15 @@ public function getInputTypeName(string $className, Input|Factory $input): strin
*/
public function getFieldNameFromMethodName(string $methodName): string
{
// Let's remove any "get" or "is".
if (str_starts_with($methodName, 'get') && strlen($methodName) > 3) {
return lcfirst(substr($methodName, 3));
}
if (str_starts_with($methodName, 'is') && strlen($methodName) > 2) {
return lcfirst(substr($methodName, 2));
}

return $methodName;
return $this->fieldAccessorPrefixes->stripGetterPrefix($methodName);
}

/**
* Returns the name of a GraphQL input field from the name of the annotated method.
*/
public function getInputFieldNameFromMethodName(string $methodName): string
{
if (str_starts_with($methodName, 'set') && strlen($methodName) > 3) {
return lcfirst(substr($methodName, 3));
}

return $methodName;
return $this->fieldAccessorPrefixes->stripSetterPrefix($methodName);
}

/**
Expand Down
24 changes: 23 additions & 1 deletion src/SchemaFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@
use TheCodingMachine\GraphQLite\Types\InputTypeValidatorInterface;
use TheCodingMachine\GraphQLite\Types\TypeResolver;
use TheCodingMachine\GraphQLite\Utils\DescriptionResolver;
use TheCodingMachine\GraphQLite\Utils\FieldAccessorPrefixes;
use TheCodingMachine\GraphQLite\Utils\NamespacedCache;

use function array_reverse;
Expand Down Expand Up @@ -120,6 +121,8 @@ class SchemaFactory

private NamingStrategyInterface|null $namingStrategy = null;

private FieldAccessorPrefixes|null $fieldAccessorPrefixes = null;

private ClassFinder|FinderInterface|null $finder = null;

private SchemaConfig|null $schemaConfig = null;
Expand Down Expand Up @@ -280,6 +283,23 @@ public function setNamingStrategy(NamingStrategyInterface $namingStrategy): self
return $this;
}

/**
* Configures the method-name prefixes stripped to derive field names and matched when resolving
* property accessors. Getter prefixes map read methods to output fields; setter prefixes map
* write methods to input fields. A prefix is only stripped on a camelCase boundary, so
* "isEnabled" becomes "enabled" while "issue" is left untouched. The defaults preserve
* GraphQLite's historical behavior.
*
* @param list<string> $getters
* @param list<string> $setters
*/
public function stripFieldPrefixes(array $getters = ['get', 'is'], array $setters = ['set']): self
{
$this->fieldAccessorPrefixes = new FieldAccessorPrefixes($getters, $setters);

return $this;
}

public function setSchemaConfig(SchemaConfig $schemaConfig): self
{
$this->schemaConfig = $schemaConfig;
Expand Down Expand Up @@ -398,7 +418,8 @@ public function createSchema(): Schema
PhpDocumentorDocBlockFactory::default(),
);
$descriptionResolver = new DescriptionResolver($this->useDocblockDescriptions);
$namingStrategy = $this->namingStrategy ?: new NamingStrategy();
$fieldAccessorPrefixes = $this->fieldAccessorPrefixes ?? new FieldAccessorPrefixes();
$namingStrategy = $this->namingStrategy ?: new NamingStrategy($fieldAccessorPrefixes);
$typeRegistry = new TypeRegistry();
$classFinder = $this->createClassFinder();
$classFinderComputedCache = $this->devMode ?
Expand Down Expand Up @@ -493,6 +514,7 @@ classBoundCache: $classBoundCache,
$fieldMiddlewarePipe,
$inputFieldMiddlewarePipe,
$descriptionResolver,
$fieldAccessorPrefixes,
);
$parameterizedCallableResolver = new ParameterizedCallableResolver($fieldsBuilder, $callableResolver);

Expand Down
79 changes: 79 additions & 0 deletions src/Utils/FieldAccessorPrefixes.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
<?php

declare(strict_types=1);

namespace TheCodingMachine\GraphQLite\Utils;

use function ctype_upper;
use function lcfirst;
use function str_starts_with;
use function strlen;
use function substr;

/**
* Immutable set of the method-name prefixes GraphQLite treats as property accessors.
*
* Getter prefixes (e.g. "get", "is", "has") map read methods to output fields; setter prefixes
* (e.g. "set") map write methods to input fields. A prefix is only stripped when it sits on a
* camelCase boundary, i.e. the character right after it is uppercase. This keeps genuine accessors
* like "isEnabled" -> "enabled" while leaving ordinary words such as "issue" or "hashKey" untouched.
*/
final class FieldAccessorPrefixes
{
/**
* @param list<string> $getters
* @param list<string> $setters
*/
public function __construct(
public readonly array $getters = ['get', 'is'],
public readonly array $setters = ['set'],
) {
}

/**
* Strips the matching getter prefix from a read method name to derive an output field name.
*/
public function stripGetterPrefix(string $methodName): string
{
return $this->strip($methodName, $this->getters);
}

/**
* Strips the matching setter prefix from a write method name to derive an input field name.
*/
public function stripSetterPrefix(string $methodName): string
{
return $this->strip($methodName, $this->setters);
}

/**
* Whether the method name is a setter, i.e. it carries one of the configured setter prefixes on a
* camelCase boundary. Used to decide which methods define input fields.
*/
public function hasSetterPrefix(string $methodName): bool
{
return $this->strip($methodName, $this->setters) !== $methodName;
}

/** @param list<string> $prefixes */
private function strip(string $methodName, array $prefixes): string
{
foreach ($prefixes as $prefix) {
$length = strlen($prefix);

// The prefix must be present and followed by at least one more character...
if (strlen($methodName) <= $length || ! str_starts_with($methodName, $prefix)) {
continue;
}

// ...and that character must be uppercase, marking a real camelCase accessor boundary.
if (! ctype_upper($methodName[$length])) {
continue;
}

return lcfirst(substr($methodName, $length));
}

return $methodName;
}
}
14 changes: 8 additions & 6 deletions src/Utils/PropertyAccessor.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,9 @@ class PropertyAccessor
/**
* Finds a getter for a property.
*/
public static function findGetter(string $class, string $propertyName): string|null
public static function findGetter(string $class, string $propertyName, FieldAccessorPrefixes $prefixes = new FieldAccessorPrefixes()): string|null
{
foreach (['get', 'is'] as $prefix) {
foreach ($prefixes->getters as $prefix) {
$methodName = self::propertyToMethodName($prefix, $propertyName);

if (self::isPublicMethod($class, $methodName)) {
Expand All @@ -36,12 +36,14 @@ public static function findGetter(string $class, string $propertyName): string|n
/**
* Finds a setter for a property.
*/
public static function findSetter(string $class, string $propertyName): string|null
public static function findSetter(string $class, string $propertyName, FieldAccessorPrefixes $prefixes = new FieldAccessorPrefixes()): string|null
{
$methodName = self::propertyToMethodName('set', $propertyName);
foreach ($prefixes->setters as $prefix) {
$methodName = self::propertyToMethodName($prefix, $propertyName);

if (self::isPublicMethod($class, $methodName)) {
return $methodName;
if (self::isPublicMethod($class, $methodName)) {
return $methodName;
}
}

return null;
Expand Down
24 changes: 24 additions & 0 deletions tests/Fixtures/StripFieldPrefixes/Product.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?php

declare(strict_types=1);

namespace TheCodingMachine\GraphQLite\Fixtures\StripFieldPrefixes;

class Product
{
public function __construct(
private readonly string $name,
private readonly bool $inStock,
) {
}

public function getName(): string
{
return $this->name;
}

public function hasStock(): bool
{
return $this->inStock;
}
}
16 changes: 16 additions & 0 deletions tests/Fixtures/StripFieldPrefixes/ProductController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?php

declare(strict_types=1);

namespace TheCodingMachine\GraphQLite\Fixtures\StripFieldPrefixes;

use TheCodingMachine\GraphQLite\Annotations\Query;

class ProductController
{
#[Query]
public function product(): Product
{
return new Product('Widget', true);
}
}
15 changes: 15 additions & 0 deletions tests/Fixtures/StripFieldPrefixes/ProductType.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?php

declare(strict_types=1);

namespace TheCodingMachine\GraphQLite\Fixtures\StripFieldPrefixes;

use TheCodingMachine\GraphQLite\Annotations\SourceField;
use TheCodingMachine\GraphQLite\Annotations\Type;

#[Type(class: Product::class)]
#[SourceField(name: 'name')]
#[SourceField(name: 'stock')]
class ProductType
{
}
25 changes: 25 additions & 0 deletions tests/Fixtures/StripFieldPrefixesInput/StockAdjustment.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<?php

declare(strict_types=1);

namespace TheCodingMachine\GraphQLite\Fixtures\StripFieldPrefixesInput;

use TheCodingMachine\GraphQLite\Annotations\Field;
use TheCodingMachine\GraphQLite\Annotations\Input;

#[Input]
class StockAdjustment
{
private int $delta = 0;

#[Field]
public function assignDelta(int $delta): void
{
$this->delta = $delta;
}

public function getDelta(): int
{
return $this->delta;
}
}
16 changes: 16 additions & 0 deletions tests/Fixtures/StripFieldPrefixesInput/StockController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?php

declare(strict_types=1);

namespace TheCodingMachine\GraphQLite\Fixtures\StripFieldPrefixesInput;

use TheCodingMachine\GraphQLite\Annotations\Mutation;

class StockController
{
#[Mutation]
public function adjustStock(StockAdjustment $adjustment): int
{
return $adjustment->getDelta();
}
}
12 changes: 11 additions & 1 deletion tests/Fixtures/Types/GetterSetterType.php
Original file line number Diff line number Diff line change
Expand Up @@ -43,4 +43,14 @@ private function setFour(string $value, string $arg): void
{
throw new \RuntimeException('Should not be called');
}
}

public function hasFive(string $arg = ''): bool
{
return $arg === 'foo';
}

public function assignTwo(string $value): void
{
$this->two = $value . ' assigned';
}
}
Loading
Loading