This guide helps you migrate your GraPHPinator application from version 1.x to version 2.0.
Action Required: Upgrade to PHP 8.2 or higher.
# Check your PHP version
php -v
Version 2.0 requires PHP 8.2+, while version 1.x required PHP 8.1+.
The most significant change is in how custom scalar types are implemented.
final class EmailAddressType extends ScalarType
{
protected const NAME = 'EmailAddress';
public function validateNonNullValue(mixed $rawValue) : bool
{
return \is_string($rawValue)
&& (bool) \filter_var($rawValue, \FILTER_VALIDATE_EMAIL);
}
}
final class EmailAddressType extends ScalarType
{
protected const NAME = 'EmailAddress';
public function validateAndCoerceInput(mixed $rawValue) : mixed
{
if (!\is_string($rawValue)) {
return null; // Invalid value
}
if (!(bool) \filter_var($rawValue, \FILTER_VALIDATE_EMAIL)) {
return null; // Invalid value
}
return $rawValue; // Valid and coerced value
}
public function coerceOutput(mixed $rawValue) : string|int|float|bool
{
return $rawValue; // Coerce to JSON-serializable type
}
}
validateNonNullValue() methodvalidateAndCoerceInput() method:
null for invalid values (instead of returning false)coerceOutput() method:
string, int, float, or boolSimple validation without coercion:
public function validateAndCoerceInput(mixed $rawValue) : mixed
{
if (/* validation check */) {
return $rawValue; // Return as-is if valid
}
return null; // Invalid
}
public function coerceOutput(mixed $rawValue) : string|int|float|bool
{
return $rawValue; // Return as-is if already serializable
}
With input coercion (string normalization):
public function validateAndCoerceInput(mixed $rawValue) : mixed
{
if (!\is_string($rawValue) && !\is_int($rawValue)) {
return null; // Accept both string and int
}
return (string) $rawValue; // Always return as string
}
public function coerceOutput(mixed $rawValue) : string
{
return (string) $rawValue;
}
Advanced: Converting to PHP objects
One of the most powerful features in v2.0 is the ability to convert scalar values into PHP objects:
public function validateAndCoerceInput(mixed $rawValue) : ?\DateTimeImmutable
{
if (!\is_string($rawValue)) {
return null;
}
try {
return new \DateTimeImmutable($rawValue); // Convert to object
} catch (\Exception $e) {
return null; // Invalid date string
}
}
public function coerceOutput(mixed $rawValue) : string
{
\assert($rawValue instanceof \DateTimeImmutable);
return $rawValue->format(\DateTimeInterface::ATOM); // Convert back to string
}
Now your resolvers can work directly with \DateTimeImmutable objects instead of strings! See the typesystem documentation for more details.
Tip: The graphpinator-extra-types package includes many ready-to-use scalar types with object coercion (DateTime, Date, Email, UUID, etc.).
Version 2.0 validates that resolver function return types match field type declarations.
If your resolvers have incorrect type hints, schema initialization will fail with exceptions:
FieldResolverNullabilityMismatch - Return type nullability doesn’t match fieldFieldResolverNotIterable - List field doesn’t return iterableFieldResolverVoidReturnType - Resolver returns void// ❌ INCORRECT - field is notNull but return type allows null
ResolvableField::create(
'name',
Container::String()->notNull(),
function (UserDto $user) : ?string {
return $user->name;
},
)
// ✅ CORRECT - return type matches field
ResolvableField::create(
'name',
Container::String()->notNull(),
function (UserDto $user) : string {
return $user->name;
},
)
// ❌ INCORRECT - list field without iterable return
ResolvableField::create(
'friends',
$friendType->list(),
function (UserDto $user) : \Generator {
foreach ($user->friends as $friend) {
yield $friend;
}
},
)
// ✅ CORRECT - list field with iterable return
ResolvableField::create(
'friends',
$friendType->list(),
function (UserDto $user) : iterable {
foreach ($user->friends as $friend) {
yield $friend;
}
},
)
Some classes have been moved to different namespaces.
If you’re using IDE auto-import features, this should be handled automatically.
composer update infinityloop-dev/graphpinator
vendor/bin/phpunit
Try to instantiate your schema in a test:
public function testSchemaInitialization() : void
{
$schema = new YourSchema($container);
$this->assertInstanceOf(Schema::class, $schema);
}
This will catch most schema validation errors.
For each custom scalar type:
public function testEmailScalarInput() : void
{
$scalar = new EmailAddressType();
// Test valid input
$this->assertEquals('test@example.com', $scalar->validateAndCoerceInput('test@example.com'));
// Test invalid input
$this->assertNull($scalar->validateAndCoerceInput('invalid'));
$this->assertNull($scalar->validateAndCoerceInput(123));
}
public function testEmailScalarOutput() : void
{
$scalar = new EmailAddressType();
$this->assertEquals('test@example.com', $scalar->coerceOutput('test@example.com'));
}
After migration, you’ll benefit from:
If you encounter issues during migration: