Skip to content

Commit b17f342

Browse files
chetanupareChetan Uparejustlevine
authored
feat: Add phone format properties to resolve issue #441 (#475)
* feat: Add phoneFormatProperties field to resolve issue #441 - Create new PhoneFormatProperties object type with label, mask, regex, instruction, and type fields - Add phoneFormatProperties field to FieldWithPhoneFormat interface with resolver - Support custom phone formats via gform_phone_formats filter - Update tests to include new phoneFormatProperties field - Register PhoneFormatProperties type in TypeRegistry Resolves: #441 * fix: Address coding standards and add changelog entry - Fix PHPCS violations in FieldWithPhoneFormat.php - Add proper inline comment punctuation - Add phpcs:ignore for third-party gform_phone_formats hook - Add changelog entry for phoneFormatProperties feature * test: Add international phone format coverage - Add dedicated PhoneFieldInternationalFormatTest - Normalize phone format properties for null/false values - Adjust phone format expectations for international mask/regex * fix: Use callable descriptions for phone format properties * chore: cleanup --------- Co-authored-by: Chetan Upare <chetan.upare@neosoftmail.com> Co-authored-by: Dovid Levine <david@axepress.dev>
1 parent dd0292c commit b17f342

File tree

7 files changed

+339
-3
lines changed

7 files changed

+339
-3
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
## [Unreleased]
44

5+
- feat: Deprecate `PhoneField.phoneFormat` in favor of `PhoneField.phoneFormatType`, and add new `GfPhoneFormat` type. Props @chetanupare, @justlevine.
6+
57
- fix: Correctly resolve `AddressField.defaultState`, `AddressField.defaultProvince` based on the `addressType`. H/t @byanko-bot
68
- fix: Set the default `AddressField.inputs.label` for the State/Province and Zip/Postal inputs based on the `addressType`. H/t @byanko-bot
79
- chore: Update Composer and NPM deps.

src/Registry/TypeRegistry.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -293,6 +293,8 @@ public static function objects(): array {
293293
WPObject\FormField\FormFields::class,
294294
// Field Error.
295295
WPObject\FieldError::class,
296+
// Phone Format.
297+
WPObject\PhoneFormat::class,
296298
// Submission Confirmation.
297299
WPObject\SubmissionConfirmation::class,
298300
// GF Settings.

src/Type/WPInterface/FieldSetting/FieldWithPhoneFormat.php

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,10 @@
1010

1111
namespace WPGraphQL\GF\Type\WPInterface\FieldSetting;
1212

13+
use GF_Field;
14+
use WPGraphQL\GF\Model\FormField;
1315
use WPGraphQL\GF\Type\Enum\PhoneFieldFormatEnum;
16+
use WPGraphQL\GF\Type\WPObject\PhoneFormat;
1417

1518
/**
1619
* Class - FieldWithPhoneFormat
@@ -35,9 +38,47 @@ class FieldWithPhoneFormat extends AbstractFieldSetting {
3538
*/
3639
public static function get_fields(): array {
3740
return [
38-
'phoneFormat' => [
41+
'phoneFormatType' => [
3942
'type' => PhoneFieldFormatEnum::$type,
4043
'description' => static fn () => __( 'Determines the allowed format for phones. If the phone value does not conform with the specified format, the field will fail validation.', 'wp-graphql-gravity-forms' ),
44+
'resolve' => static fn ( FormField $field ) => $field->gfField->phoneFormat,
45+
],
46+
'phoneFormat' => [
47+
'type' => PhoneFieldFormatEnum::$type,
48+
'deprecationReason' => static fn () => __( 'Use `phoneFormatType` instead. The GraphQL type for this field will change in the next breaking release.', 'wp-graphql-gravity-forms' ),
49+
'description' => static fn () => __( 'Determines the allowed format for phones. If the phone value does not conform with the specified format, the field will fail validation.', 'wp-graphql-gravity-forms' ),
50+
],
51+
'_phoneFormatExperimental' => [
52+
'type' => PhoneFormat::$type,
53+
'description' => static fn () => __( 'The phone format properties. Experimental', 'wp-graphql-gravity-forms' ),
54+
'deprecationReason' => static fn () => __( 'The `phoneFormat` field has been renamed to `phoneFormatType`. The `_phoneFormatExperimental` field will be replaced in a future release.', 'wp-graphql-gravity-forms' ),
55+
'resolve' => static function ( $field ) {
56+
if ( empty( $field->phoneFormat ) ) {
57+
return null;
58+
}
59+
60+
$gf_field = $field->gfField ?? null;
61+
62+
// Ensure the field has the get_phone_format() method.
63+
if ( ! $gf_field instanceof GF_Field || ! method_exists( $gf_field, 'get_phone_format' ) ) {
64+
return null;
65+
}
66+
67+
$format = $gf_field->get_phone_format();
68+
69+
if ( ! is_array( $format ) ) {
70+
return null;
71+
}
72+
73+
// Normalize values: convert false to null for nullable GraphQL fields.
74+
return [
75+
'label' => isset( $format['label'] ) && false !== $format['label'] ? (string) $format['label'] : null,
76+
'mask' => isset( $format['mask'] ) && false !== $format['mask'] ? (string) $format['mask'] : null,
77+
'regex' => isset( $format['regex'] ) && false !== $format['regex'] ? (string) $format['regex'] : null,
78+
'instruction' => isset( $format['instruction'] ) && false !== $format['instruction'] ? (string) $format['instruction'] : null,
79+
'type' => $field->phoneFormat,
80+
];
81+
},
4182
],
4283
];
4384
}

src/Type/WPObject/PhoneFormat.php

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
<?php
2+
/**
3+
* Object Type - PhoneFormat
4+
*
5+
* @package WPGraphQL\GF\Type\WPObject
6+
* @since 0.13.4
7+
*/
8+
9+
declare( strict_types = 1 );
10+
11+
namespace WPGraphQL\GF\Type\WPObject;
12+
13+
use WPGraphQL\GF\Type\Enum\PhoneFieldFormatEnum;
14+
15+
/**
16+
* Class - PhoneFormat
17+
*/
18+
class PhoneFormat extends AbstractObject {
19+
/**
20+
* Type registered in WPGraphQL.
21+
*
22+
* @var string
23+
*/
24+
public static string $type = 'GfPhoneFormat';
25+
26+
/**
27+
* {@inheritDoc}
28+
*/
29+
public static function get_description(): string {
30+
return __( 'Properties of a phone number format, including label, mask, regex, instruction and type.', 'wp-graphql-gravity-forms' );
31+
}
32+
33+
/**
34+
* {@inheritDoc}
35+
*/
36+
public static function get_fields(): array {
37+
return [
38+
'label' => [
39+
'type' => 'String',
40+
'description' => static fn () => __( 'The display label for the phone format.', 'wp-graphql-gravity-forms' ),
41+
],
42+
'mask' => [
43+
'type' => 'String',
44+
'description' => static fn () => __( 'The input mask for the phone format (e.g., "(999) 999-9999").', 'wp-graphql-gravity-forms' ),
45+
],
46+
'regex' => [
47+
'type' => 'String',
48+
'description' => static fn () => __( 'The regex pattern for validating the phone format.', 'wp-graphql-gravity-forms' ),
49+
],
50+
'instruction' => [
51+
'type' => 'String',
52+
'description' => static fn () => __( 'The instruction text displayed to users for this phone format.', 'wp-graphql-gravity-forms' ),
53+
],
54+
'type' => [
55+
'type' => PhoneFieldFormatEnum::$type,
56+
'description' => static fn () => __( 'The internal type identifier for the phone format.', 'wp-graphql-gravity-forms' ),
57+
],
58+
];
59+
}
60+
}

tests/_support/Helper/GFHelpers/ExpectedFormFields.php

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -482,7 +482,24 @@ public function pen_size_setting( GF_Field $field, array &$properties ): void {
482482
}
483483

484484
public function phone_format_setting( GF_Field $field, array &$properties ): void {
485-
$properties[] = $this->expectedField( 'phoneFormat', ! empty( $field->phoneFormat ) ? GFHelpers::get_enum_for_value( Enum\PhoneFieldFormatEnum::$type, $field->phoneFormat ) : self::IS_NULL );
485+
$properties[] = $this->expectedField( 'phoneFormatType', ! empty( $field->phoneFormat ) ? GFHelpers::get_enum_for_value( Enum\PhoneFieldFormatEnum::$type, $field->phoneFormat ) : self::IS_NULL );
486+
487+
// Add _phoneFormatExperimental field.
488+
if ( ! empty( $field->phoneFormat ) ) {
489+
$properties[] = $this->expectedField( '_phoneFormatExperimental.label', self::NOT_NULL );
490+
$properties[] = $this->expectedField( '_phoneFormatExperimental.type', self::NOT_NULL );
491+
if ( 'international' === $field->phoneFormat ) {
492+
$properties[] = $this->expectedField( '_phoneFormatExperimental.mask', self::IS_NULL );
493+
$properties[] = $this->expectedField( '_phoneFormatExperimental.regex', self::IS_NULL );
494+
$properties[] = $this->expectedField( '_phoneFormatExperimental.instruction', self::IS_NULL );
495+
} else {
496+
$properties[] = $this->expectedField( '_phoneFormatExperimental.mask', self::NOT_NULL );
497+
$properties[] = $this->expectedField( '_phoneFormatExperimental.regex', self::NOT_NULL );
498+
$properties[] = $this->expectedField( '_phoneFormatExperimental.instruction', self::NOT_NULL );
499+
}
500+
} else {
501+
$properties[] = $this->expectedField( '_phoneFormatExperimental', self::IS_NULL );
502+
}
486503
}
487504

488505
public function placeholder_setting( GF_Field $field, array &$properties ): void {
Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
1+
<?php
2+
/**
3+
* Tests for Phone field format properties.
4+
*
5+
* @package WPGraphQL\GF\Tests\WPUnit
6+
*/
7+
8+
use Tests\WPGraphQL\GF\TestCase\GFGraphQLTestCase;
9+
10+
/**
11+
* Class PhoneFieldFormatTest
12+
*/
13+
class PhoneFieldFormatTest extends GFGraphQLTestCase {
14+
/**
15+
* The ID of the form created for testing.
16+
*
17+
* @var int
18+
*/
19+
private $form_id;
20+
21+
/**
22+
* {@inheritDoc}
23+
*/
24+
public function setUp(): void {
25+
parent::setUp();
26+
27+
wp_set_current_user( $this->admin->ID );
28+
$this->clearSchema();
29+
}
30+
31+
/**
32+
* {@inheritDoc}
33+
*/
34+
public function tearDown(): void {
35+
$this->factory->form->delete( $this->form_id );
36+
37+
parent::tearDown();
38+
}
39+
40+
/**
41+
* Helper method to create a form with a phone field with the given properties.
42+
*
43+
* @param array $props The properties for the phone field.
44+
*
45+
* @return int The ID of the created form.
46+
*/
47+
private function create_phone_form( array $props ): int {
48+
$field_helper = $this->tester->getPropertyHelper( 'PhoneField', $props );
49+
$field = $this->factory->field->create( $field_helper->values );
50+
51+
return $this->factory->form->create(
52+
array_merge(
53+
[ 'fields' => [ $field ] ],
54+
$this->tester->getFormDefaultArgs()
55+
)
56+
);
57+
}
58+
59+
/**
60+
* Test that the standard phone format returns all properties.
61+
*/
62+
public function testStandardPhoneFormat(): void {
63+
$this->form_id = $this->create_phone_form( [ 'phoneFormat' => 'standard' ] );
64+
65+
$query = '
66+
query ($id: ID!) {
67+
gfForm(id: $id, idType: DATABASE_ID) {
68+
formFields {
69+
nodes {
70+
... on PhoneField {
71+
phoneFormatType
72+
_phoneFormatExperimental {
73+
label
74+
mask
75+
regex
76+
instruction
77+
type
78+
}
79+
}
80+
}
81+
}
82+
}
83+
}
84+
';
85+
86+
$variables = [ 'id' => $this->form_id ];
87+
$response = $this->graphql( compact( 'query', 'variables' ) );
88+
89+
$this->assertArrayNotHasKey( 'errors', $response );
90+
$this->assertEquals( 'STANDARD', $response['data']['gfForm']['formFields']['nodes'][0]['phoneFormatType'] );
91+
92+
$properties = $response['data']['gfForm']['formFields']['nodes'][0]['_phoneFormatExperimental'];
93+
$this->assertNotNull( $properties );
94+
$this->assertNotNull( $properties['label'] );
95+
$this->assertNotNull( $properties['mask'] );
96+
$this->assertNotNull( $properties['regex'] );
97+
$this->assertNotNull( $properties['instruction'] );
98+
$this->assertEquals( 'STANDARD', $properties['type'] );
99+
}
100+
101+
/**
102+
* Test that the international phone format returns null for mask, regex, and instruction.
103+
*/
104+
public function testInternationalPhoneFormat(): void {
105+
$this->form_id = $this->create_phone_form( [ 'phoneFormat' => 'international' ] );
106+
107+
$query = '
108+
query ($id: ID!) {
109+
gfForm(id: $id, idType: DATABASE_ID) {
110+
formFields {
111+
nodes {
112+
... on PhoneField {
113+
phoneFormatType
114+
_phoneFormatExperimental {
115+
label
116+
mask
117+
regex
118+
instruction
119+
type
120+
}
121+
}
122+
}
123+
}
124+
}
125+
}
126+
';
127+
128+
$variables = [ 'id' => $this->form_id ];
129+
$response = $this->graphql( compact( 'query', 'variables' ) );
130+
131+
$this->assertArrayNotHasKey( 'errors', $response );
132+
$this->assertEquals( 'INTERNATIONAL', $response['data']['gfForm']['formFields']['nodes'][0]['phoneFormatType'] );
133+
134+
$properties = $response['data']['gfForm']['formFields']['nodes'][0]['_phoneFormatExperimental'];
135+
$this->assertNotNull( $properties );
136+
$this->assertNotNull( $properties['label'] );
137+
$this->assertNull( $properties['mask'] );
138+
$this->assertNull( $properties['regex'] );
139+
$this->assertNull( $properties['instruction'] );
140+
$this->assertEquals( 'INTERNATIONAL', $properties['type'] );
141+
}
142+
143+
/**
144+
* Test that a custom phone format added via filter is supported.
145+
*/
146+
public function testCustomPhoneFormat(): void {
147+
$custom_format = [
148+
'label' => 'UK Format',
149+
'mask' => '+44 9999 999999',
150+
'regex' => '/^\+44\s?\d{4}\s?\d{6}$/',
151+
'instruction' => '+44 XXXX XXXXXX',
152+
];
153+
154+
add_filter(
155+
'gform_phone_formats',
156+
static function ( $formats ) use ( $custom_format ) {
157+
$formats['uk'] = $custom_format;
158+
return $formats;
159+
}
160+
);
161+
162+
// Use the property helper but override phoneFormat after creation.
163+
$field_helper = $this->tester->getPropertyHelper( 'PhoneField' );
164+
$field = $this->factory->field->create( $field_helper->values );
165+
$field->phoneFormat = 'uk';
166+
167+
$this->form_id = $this->factory->form->create(
168+
array_merge(
169+
[ 'fields' => [ $field ] ],
170+
$this->tester->getFormDefaultArgs()
171+
)
172+
);
173+
174+
$query = '
175+
query ($id: ID!) {
176+
gfForm(id: $id, idType: DATABASE_ID) {
177+
formFields {
178+
nodes {
179+
... on PhoneField {
180+
_phoneFormatExperimental {
181+
label
182+
mask
183+
regex
184+
instruction
185+
}
186+
}
187+
}
188+
}
189+
}
190+
}
191+
';
192+
193+
$variables = [ 'id' => $this->form_id ];
194+
$response = $this->graphql( compact( 'query', 'variables' ) );
195+
196+
$this->assertArrayNotHasKey( 'errors', $response );
197+
198+
$properties = $response['data']['gfForm']['formFields']['nodes'][0]['_phoneFormatExperimental'];
199+
$this->assertNotNull( $properties );
200+
$this->assertEquals( $custom_format['label'], $properties['label'] );
201+
$this->assertEquals( $custom_format['mask'], $properties['mask'] );
202+
$this->assertEquals( $custom_format['regex'], $properties['regex'] );
203+
$this->assertEquals( $custom_format['instruction'], $properties['instruction'] );
204+
205+
remove_all_filters( 'gform_phone_formats' );
206+
}
207+
}

tests/wpunit/PhoneFieldTest.php

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,14 @@ public function field_query(): string {
115115
shouldErase
116116
shouldExport
117117
}
118-
phoneFormat
118+
phoneFormatType
119+
_phoneFormatExperimental {
120+
label
121+
mask
122+
regex
123+
instruction
124+
type
125+
}
119126
placeholder
120127
shouldAllowDuplicates
121128
size

0 commit comments

Comments
 (0)