Skip to content

Commit 8078bc2

Browse files
Lappihuanoojacobooeldr0ndependabot[bot]
authored
Bring input setters on par with type getters (#466)
* Fixed incorrect mapping causing input types to be undiscovered during recursive type mapping * Treat "Input" as a "type" for AnnotationReader * Use TypeInterface for return typing * Further interface implementation * Default to true, regardless of name attribute - following @type implementation * Ensure the mapping has the type name as well * CS fixes * Fixed tests * Handle Input type processing in RecursiveTypeMapper * Default value is null * Resolve PHPStan error * Resolved PHPStan errors * Revert default logic to avoid default attribute requirements * CS fixes * Added additional clarity to docs on @input annotation * Removed superfluous annotation name * Remove temporary phpunit group annotation * Add experimental setter equivalent to getter functionality for fields setters are not yet called if Field Attribute is only on class property * add .idea * add missing naming strategy for input type * added simple inputType unit test with field annotation on setter instead of property * Add TrickyProduct Model for e2e test * exclude fields from type if the method begins with set* * Add Constructor and Getter * Add Query and Mutations for TrickyProduct * remove redundant field annotation from model * call setter even if property is already set via constructor * add autowired service to setter * add UseInputType annotation * add test for model with autowire on setter * Bump codecov/codecov-action from 2.1.0 to 3.0.0 (#459) Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 2.1.0 to 3.0.0. - [Release notes](https://github.com/codecov/codecov-action/releases) - [Changelog](https://github.com/codecov/codecov-action/blob/master/CHANGELOG.md) - [Commits](codecov/codecov-action@v2.1.0...v3.0.0) --- updated-dependencies: - dependency-name: codecov/codecov-action dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Default to true, regardless of name attribute - following @type implementation * Resolved PHPStan errors * Revert default logic to avoid default attribute requirements * cloned fild middleware for inputs cloned QueryFieldDescriptor for input fields changed InputType to handle InputFields * reused InputTypeProperty for property based argument resolution added Input Resolver for Property and MagicProperty fixed up default and isUpdate behaviour cleaned up description having ugly new lines on all field properties if its pulled from the dockblock * added clones of authorization and security middlewares for inputs * fix authorization input middleware to not throw an error on inspection but only on field resolution * add test for model with autowire on setter and security * phpstan cleanup * fix typings arround input field middleware * fix default values in InputFieldDescriptor so the middleware tests can run using it * fix strange intersection type issue on InputFieldDescriptor * fix strange intersection type issue on InputFieldDescriptor (1) * fix strange intersection type issue on InputFieldDescriptor (2) * satisfy phpstan with typing that phpstorm doesn't like.. * satisfy phpstan with typing that phpstorm doesn't like.. * satisfy phpstan with typing that phpstorm doesn't like.. * make sure input type is using callable * allow "inputType" property on "Field" Attribute to overwrite nullability of "update" property on "Input" Attribut * making sure "set" prefixed methods are only skipped for fields, previously query and mutations would have been skipped too. * minor cleanup and additional codecoverage * changed field resolution for input types to be properly lazy. should prevent crash on circular * removing unused code * reverted duplicate field exception to not throw and just overwrite previously definded fields. this makes multiple field definitions behave the same as before * add test for model with array field * fixed return of resolve to actually return the value so it can be asserted to be the correct type * Added test for Input construction and property hydration * fixed inputFields property should be initialized as empty array * freeze and getFields has to be called on InputType in the test * made test not fail * handle constructor set properties with more care. instanciation happens with properly resolved parameters now * detect constructor hydrated fields early and prevent middleware functionallity on those. * minor cleanup * fixed all coding style issues pointed out in review * Added comments and resolved remaining requested changes * CS fixes * CS fixes * Test removing Symfony v4 for PHP 8 support * Require ecodev/graphql-upload 6.1.3 for PHP 8.1 support * Do not ignore platform-reqs with Composer on CI * Allow 6.1 of ecodev/graphql-upload for PHP 7.4 and 8.0 support Co-authored-by: Jacob Thomason <jacob@thomason.xxx> Co-authored-by: Rico Good <rgo@exigo.org> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: eldr0n <38736006+eldr0n@users.noreply.github.com>
1 parent 4fcbb74 commit 8078bc2

39 files changed

+2398
-146
lines changed

.github/workflows/continuous_integration.yml

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -56,12 +56,6 @@ jobs:
5656
- name: "Install dependencies with composer"
5757
run: |
5858
composer update ${{ matrix.install-args }} --no-interaction --no-progress --prefer-dist
59-
if: ${{ matrix.php-version != '8.0' }}
60-
61-
- name: "Install dependencies with composer. Ignoring platform reqs to bypass a problem with ecodev/graphql-upload available only with latest Webonyx on PHP8."
62-
run: |
63-
composer update ${{ matrix.install-args }} --no-interaction --no-progress --prefer-dist --ignore-platform-reqs
64-
if: ${{ matrix.php-version == '8.0' }}
6559
6660
- name: "Run tests with phpunit/phpunit"
6761
run: "vendor/bin/phpunit"

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
/src/Tests/
55
/phpunit.xml
66
/.php_cs/cache
7+
/.idea
78

89
node_modules
910

composer.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@
3232
"require-dev": {
3333
"beberlei/porpaginas": "^1.2",
3434
"doctrine/coding-standard": "^9.0",
35-
"ecodev/graphql-upload": "^4.0 || ^5.0 || ^6.0",
35+
"ecodev/graphql-upload": "^6.1",
3636
"laminas/laminas-diactoros": "^2",
3737
"mouf/picotainer": "^1.1",
3838
"myclabs/php-enum": "^1.6.6",

src/Annotations/Exceptions/IncompatibleAnnotationsException.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,9 @@ public static function cannotUseFailWithAndHide(): self
1212
{
1313
return new self('You cannot use "FailWith" and "HideIfUnauthorized" annotations in the same method. These annotations are mutually exclusive.');
1414
}
15+
16+
public static function middlewareAnnotationsUnsupported(): self
17+
{
18+
return new self('You cannot use Middleware annotations for properties that are hydrated via constructor.');
19+
}
1520
}

src/FieldsBuilder.php

Lines changed: 270 additions & 27 deletions
Large diffs are not rendered by default.

src/InputField.php

Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace TheCodingMachine\GraphQLite;
6+
7+
use GraphQL\Error\ClientAware;
8+
use GraphQL\Type\Definition\InputObjectField;
9+
use GraphQL\Type\Definition\InputType;
10+
use GraphQL\Type\Definition\ListOfType;
11+
use GraphQL\Type\Definition\NonNull;
12+
use GraphQL\Type\Definition\ResolveInfo;
13+
use GraphQL\Type\Definition\Type;
14+
use GraphQL\Type\Definition\NullableType;
15+
use TheCodingMachine\GraphQLite\Exceptions\GraphQLAggregateException;
16+
use TheCodingMachine\GraphQLite\Middlewares\MissingAuthorizationException;
17+
use TheCodingMachine\GraphQLite\Middlewares\ResolverInterface;
18+
use TheCodingMachine\GraphQLite\Middlewares\SourceResolverInterface;
19+
use TheCodingMachine\GraphQLite\Parameters\InputTypeParameter;
20+
use TheCodingMachine\GraphQLite\Parameters\InputTypeProperty;
21+
use TheCodingMachine\GraphQLite\Parameters\MissingArgumentException;
22+
use TheCodingMachine\GraphQLite\Parameters\ParameterInterface;
23+
use TheCodingMachine\GraphQLite\Parameters\SourceParameter;
24+
25+
/**
26+
* A GraphQL input field that maps to a PHP method automatically.
27+
*
28+
* @internal
29+
*/
30+
class InputField extends InputObjectField
31+
{
32+
/** @var Callable */
33+
private $resolve;
34+
35+
/** @var bool */
36+
private $forConstructorHydration = false;
37+
38+
/**
39+
* @param InputType|(NullableType&Type) $type
40+
* @param array<string, ParameterInterface> $arguments Indexed by argument name.
41+
* @param ResolverInterface $originalResolver A pointer to the resolver being called (but not wrapped by any field middleware)
42+
* @param callable $resolver The resolver actually called
43+
* @param mixed|null $defaultValue the default value set for this field
44+
* @param array<string, mixed> $additionalConfig
45+
*/
46+
public function __construct(string $name, $type, array $arguments, ?ResolverInterface $originalResolver, ?callable $resolver, ?string $comment, bool $isUpdate,bool $hasDefaultValue, $defaultValue, array $additionalConfig = [])
47+
{
48+
$config = [
49+
'name' => $name,
50+
'type' => $type,
51+
'description' => $comment
52+
];
53+
54+
if (!(!$hasDefaultValue || $isUpdate)) {
55+
$config['defaultValue'] = $defaultValue;
56+
}
57+
58+
if ($originalResolver !== null && $resolver !== null){
59+
$this->resolve = function ($source, array $args, $context, ResolveInfo $info) use ($arguments, $originalResolver, $resolver) {
60+
if ($originalResolver instanceof SourceResolverInterface) {
61+
$originalResolver->setObject($source);
62+
}
63+
$toPassArgs = $this->paramsToArguments($arguments, $source, $args, $context, $info, $resolver);
64+
$result = $resolver(...$toPassArgs);
65+
66+
try {
67+
$this->assertInputType($result);
68+
} catch (TypeMismatchRuntimeException $e) {
69+
$e->addInfo($this->name, $originalResolver->toString());
70+
throw $e;
71+
}
72+
73+
return $result;
74+
};
75+
} else {
76+
$this->forConstructorHydration = true;
77+
$this->resolve = function ($source, array $args, $context, ResolveInfo $info) use ($arguments) {
78+
$result = $arguments[$this->name]->resolve($source, $args, $context, $info);
79+
$this->assertInputType($result);
80+
return $result;
81+
};
82+
}
83+
84+
85+
$config += $additionalConfig;
86+
parent::__construct($config);
87+
}
88+
89+
/**
90+
* @return Callable
91+
*/
92+
public function getResolve()
93+
{
94+
return $this->resolve;
95+
}
96+
97+
/**
98+
*
99+
* @param mixed $input
100+
*/
101+
private function assertInputType($input): void
102+
{
103+
$type = $this->removeNonNull($this->getType());
104+
if (! $type instanceof ListOfType) {
105+
return;
106+
}
107+
108+
ResolveUtils::assertInnerInputType($input, $type);
109+
}
110+
111+
private function removeNonNull(Type $type): Type
112+
{
113+
if ($type instanceof NonNull) {
114+
return $type->getWrappedType();
115+
}
116+
117+
return $type;
118+
}
119+
120+
public function forConstructorHydration(): bool
121+
{
122+
return $this->forConstructorHydration;
123+
}
124+
125+
/**
126+
* @param bool $isNotLogged False if the user is logged (and the error is a 403), true if the error is unlogged (the error is a 401)
127+
*
128+
* @return InputField
129+
*/
130+
public static function unauthorizedError(InputFieldDescriptor $fieldDescriptor, bool $isNotLogged): self
131+
{
132+
$callable = static function () use ($isNotLogged): void {
133+
if ($isNotLogged) {
134+
throw MissingAuthorizationException::unauthorized();
135+
}
136+
throw MissingAuthorizationException::forbidden();
137+
};
138+
139+
$fieldDescriptor->setResolver($callable);
140+
141+
return self::fromDescriptor($fieldDescriptor);
142+
}
143+
144+
private static function fromDescriptor(InputFieldDescriptor $fieldDescriptor): self
145+
{
146+
return new self(
147+
$fieldDescriptor->getName(),
148+
$fieldDescriptor->getType(),
149+
$fieldDescriptor->getParameters(),
150+
$fieldDescriptor->getOriginalResolver(),
151+
$fieldDescriptor->getResolver(),
152+
$fieldDescriptor->getComment(),
153+
$fieldDescriptor->isUpdate(),
154+
$fieldDescriptor->hasDefaultValue(),
155+
$fieldDescriptor->getDefaultValue(),
156+
);
157+
}
158+
159+
public static function fromFieldDescriptor(InputFieldDescriptor $fieldDescriptor): self
160+
{
161+
$arguments = $fieldDescriptor->getParameters();
162+
if ($fieldDescriptor->isInjectSource() === true) {
163+
$arguments = ['__graphqlite_source' => new SourceParameter()] + $arguments;
164+
}
165+
$fieldDescriptor->setParameters($arguments);
166+
167+
return self::fromDescriptor($fieldDescriptor);
168+
}
169+
170+
/**
171+
* Casts parameters array into an array of arguments ready to be passed to the resolver.
172+
*
173+
* @param ParameterInterface[] $parameters
174+
* @param array<string, mixed> $args
175+
* @param mixed $context
176+
*
177+
* @return array<int, mixed>
178+
*/
179+
private function paramsToArguments(array $parameters, ?object $source, array $args, $context, ResolveInfo $info, callable $resolve): array
180+
{
181+
$toPassArgs = [];
182+
$exceptions = [];
183+
foreach ($parameters as $parameter) {
184+
try {
185+
$toPassArgs[] = $parameter->resolve($source, $args, $context, $info);
186+
} catch (MissingArgumentException $e) {
187+
throw MissingArgumentException::wrapWithFieldContext($e, $this->name, $resolve);
188+
} catch (ClientAware $e) {
189+
$exceptions[] = $e;
190+
}
191+
}
192+
GraphQLAggregateException::throwExceptions($exceptions);
193+
194+
return $toPassArgs;
195+
}
196+
}

0 commit comments

Comments
 (0)