From f684024421b64099c37751a87566e27d136f3051 Mon Sep 17 00:00:00 2001 From: Michael Broshi <94012587+mbroshi-stripe@users.noreply.github.com> Date: Wed, 18 Mar 2026 12:13:13 -0400 Subject: [PATCH 1/3] Add runtime support for V2 int64 string-encoded fields (#2033) * Convert int64 to string * Add Int64 type * Add tests * Incorporate feedback and fix tests --- init.php | 1 + lib/Service/AbstractService.php | 27 +- lib/StripeObject.php | 10 + lib/Util/Int64.php | 128 ++++++++ tests/Stripe/Service/AbstractServiceTest.php | 147 +++++++++ tests/Stripe/StripeObjectTest.php | 67 ++++ tests/Stripe/Util/Int64Test.php | 320 +++++++++++++++++++ 7 files changed, 694 insertions(+), 6 deletions(-) create mode 100644 lib/Util/Int64.php create mode 100644 tests/Stripe/Util/Int64Test.php diff --git a/init.php b/init.php index c809874ea..62368bb26 100644 --- a/init.php +++ b/init.php @@ -18,6 +18,7 @@ require __DIR__ . '/lib/Util/Util.php'; require __DIR__ . '/lib/Util/EventTypes.php'; require __DIR__ . '/lib/Util/EventNotificationTypes.php'; +require __DIR__ . '/lib/Util/Int64.php'; require __DIR__ . '/lib/Util/ObjectTypes.php'; // HttpClient diff --git a/lib/Service/AbstractService.php b/lib/Service/AbstractService.php index 23132a37a..d990bf0ca 100644 --- a/lib/Service/AbstractService.php +++ b/lib/Service/AbstractService.php @@ -70,9 +70,14 @@ private static function formatParams($params) return $params; } - protected function request($method, $path, $params, $opts) + protected function request($method, $path, $params, $opts, $schemas = null) { - return $this->getClient()->request($method, $path, self::formatParams($params), $opts); + $params = self::formatParams($params); + if (null !== $schemas && isset($schemas['request_schema'])) { + $params = \Stripe\Util\Int64::coerceRequestParams($params, $schemas['request_schema']); + } + + return $this->getClient()->request($method, $path, $params, $opts); } protected function requestStream($method, $path, $readBodyChunkCallable, $params, $opts) @@ -80,14 +85,24 @@ protected function requestStream($method, $path, $readBodyChunkCallable, $params return $this->getStreamingClient()->requestStream($method, $path, $readBodyChunkCallable, self::formatParams($params), $opts); } - protected function requestCollection($method, $path, $params, $opts) + protected function requestCollection($method, $path, $params, $opts, $schemas = null) { - return $this->getClient()->requestCollection($method, $path, self::formatParams($params), $opts); + $params = self::formatParams($params); + if (null !== $schemas && isset($schemas['request_schema'])) { + $params = \Stripe\Util\Int64::coerceRequestParams($params, $schemas['request_schema']); + } + + return $this->getClient()->requestCollection($method, $path, $params, $opts); } - protected function requestSearchResult($method, $path, $params, $opts) + protected function requestSearchResult($method, $path, $params, $opts, $schemas = null) { - return $this->getClient()->requestSearchResult($method, $path, self::formatParams($params), $opts); + $params = self::formatParams($params); + if (null !== $schemas && isset($schemas['request_schema'])) { + $params = \Stripe\Util\Int64::coerceRequestParams($params, $schemas['request_schema']); + } + + return $this->getClient()->requestSearchResult($method, $path, $params, $opts); } protected function buildPath($basePath, ...$ids) diff --git a/lib/StripeObject.php b/lib/StripeObject.php index 58eb10986..b53568e2e 100644 --- a/lib/StripeObject.php +++ b/lib/StripeObject.php @@ -296,6 +296,16 @@ public function refreshFrom($values, $opts, $partial = false, $apiMode = 'v1') $values = $values->toArray(); } + // Apply int64_string response coercion on raw values before hydration. + // V2 resource classes declare fieldEncodings() with metadata about which + // fields are int64_string (wire format: JSON string, SDK type: PHP int). + if (\method_exists(static::class, 'fieldEncodings')) { + $encodings = static::fieldEncodings(); + if (!empty($encodings)) { + $values = Util\Int64::coerceResponseValues($values, $encodings); + } + } + // Wipe old state before setting new. This is useful for e.g. updating a // customer, where there is no persistent card parameter. Mark those values // which don't persist as transient diff --git a/lib/Util/Int64.php b/lib/Util/Int64.php new file mode 100644 index 000000000..ff2bccbca --- /dev/null +++ b/lib/Util/Int64.php @@ -0,0 +1,128 @@ + 'object', 'fields' => ['amount' => ['kind' => 'int64_string']]] + * + * @return mixed + */ + public static function coerceRequestParams($params, $schema) + { + if (null === $params) { + return null; + } + + if (!isset($schema['kind'])) { + return $params; + } + + if ('int64_string' === $schema['kind']) { + if (\is_int($params)) { + return (string) $params; + } + + return $params; + } + + if ('array' === $schema['kind'] && isset($schema['items'])) { + if (\is_array($params)) { + $result = []; + foreach ($params as $key => $value) { + $result[$key] = self::coerceRequestParams($value, $schema['items']); + } + + return $result; + } + + return $params; + } + + if ('object' === $schema['kind'] && isset($schema['fields'])) { + if (\is_array($params)) { + $result = $params; + foreach ($schema['fields'] as $field => $fieldSchema) { + if (\array_key_exists($field, $result)) { + $result[$field] = self::coerceRequestParams($result[$field], $fieldSchema); + } + } + + return $result; + } + + return $params; + } + + return $params; + } + + /** + * Coerce inbound response values: convert JSON strings to PHP ints where + * the field encodings indicate an int64_string field. + * + * @param mixed $values + * @param array $encodings e.g. ['amount' => ['kind' => 'int64_string'], 'nested' => ['kind' => 'object', 'fields' => [...]]] + * + * @return mixed + */ + public static function coerceResponseValues($values, $encodings) + { + if (!\is_array($values)) { + return $values; + } + + foreach ($encodings as $field => $encoding) { + if (!\array_key_exists($field, $values)) { + continue; + } + + $value = $values[$field]; + + if (!isset($encoding['kind'])) { + continue; + } + + if ('int64_string' === $encoding['kind']) { + if (\is_string($value) && \is_numeric($value)) { + $values[$field] = (int) $value; + } + } elseif ('array' === $encoding['kind'] && isset($encoding['items'])) { + if (\is_array($value)) { + foreach ($value as $i => $item) { + if (!isset($encoding['items']['kind'])) { + continue; + } + + if ('int64_string' === $encoding['items']['kind']) { + if (\is_string($item) && \is_numeric($item)) { + $values[$field][$i] = (int) $item; + } + } elseif ('object' === $encoding['items']['kind'] && isset($encoding['items']['fields'])) { + if (\is_array($item)) { + $values[$field][$i] = self::coerceResponseValues($item, $encoding['items']['fields']); + } + } + } + } + } elseif ('object' === $encoding['kind'] && isset($encoding['fields'])) { + if (\is_array($value)) { + $values[$field] = self::coerceResponseValues($value, $encoding['fields']); + } + } + } + + return $values; + } +} diff --git a/tests/Stripe/Service/AbstractServiceTest.php b/tests/Stripe/Service/AbstractServiceTest.php index b6474de08..ab23fa60e 100644 --- a/tests/Stripe/Service/AbstractServiceTest.php +++ b/tests/Stripe/Service/AbstractServiceTest.php @@ -92,4 +92,151 @@ public function testFormatParams() self::assertTrue('' === $result['toplevelnull']); self::assertTrue(4 === $result['toplevelnonnull']); } + + public function testRequestCoercesInt64ParamsWhenSchemaProvided() + { + $capturedParams = null; + $mockClient = $this->createMock(\Stripe\StripeClientInterface::class); + $mockClient->expects(self::once()) + ->method('request') + ->with( + self::equalTo('post'), + self::equalTo('/v2/test'), + self::callback(static function ($params) use (&$capturedParams) { + $capturedParams = $params; + + return true; + }), + self::anything() + ) + ->willReturn(\Stripe\StripeObject::constructFrom([])) + ; + + $service = new ConcreteTestService($mockClient); + $schemas = [ + 'request_schema' => [ + 'kind' => 'object', + 'fields' => [ + 'amount' => ['kind' => 'int64_string'], + ], + ], + ]; + $service->publicRequest('post', '/v2/test', ['amount' => 100, 'currency' => 'usd'], [], $schemas); + + self::assertSame('100', $capturedParams['amount']); + self::assertSame('usd', $capturedParams['currency']); + } + + public function testRequestDoesNotCoerceWithoutSchema() + { + $capturedParams = null; + $mockClient = $this->createMock(\Stripe\StripeClientInterface::class); + $mockClient->expects(self::once()) + ->method('request') + ->with( + self::anything(), + self::anything(), + self::callback(static function ($params) use (&$capturedParams) { + $capturedParams = $params; + + return true; + }), + self::anything() + ) + ->willReturn(\Stripe\StripeObject::constructFrom([])) + ; + + $service = new ConcreteTestService($mockClient); + $service->publicRequest('post', '/v2/test', ['amount' => 100], []); + + self::assertSame(100, $capturedParams['amount']); + } + + public function testRequestCollectionCoercesInt64Params() + { + $capturedParams = null; + $mockClient = $this->createMock(\Stripe\StripeClientInterface::class); + $mockClient->expects(self::once()) + ->method('requestCollection') + ->with( + self::anything(), + self::anything(), + self::callback(static function ($params) use (&$capturedParams) { + $capturedParams = $params; + + return true; + }), + self::anything() + ) + ->willReturn(\Stripe\Collection::constructFrom(['data' => []])) + ; + + $service = new ConcreteTestService($mockClient); + $schemas = [ + 'request_schema' => [ + 'kind' => 'object', + 'fields' => [ + 'limit' => ['kind' => 'int64_string'], + ], + ], + ]; + $service->publicRequestCollection('get', '/v2/test', ['limit' => 50], [], $schemas); + + self::assertSame('50', $capturedParams['limit']); + } + + public function testRequestSearchResultCoercesInt64Params() + { + $capturedParams = null; + $mockClient = $this->createMock(\Stripe\StripeClientInterface::class); + $mockClient->expects(self::once()) + ->method('requestSearchResult') + ->with( + self::anything(), + self::anything(), + self::callback(static function ($params) use (&$capturedParams) { + $capturedParams = $params; + + return true; + }), + self::anything() + ) + ->willReturn(\Stripe\SearchResult::constructFrom(['data' => []])) + ; + + $service = new ConcreteTestService($mockClient); + $schemas = [ + 'request_schema' => [ + 'kind' => 'object', + 'fields' => [ + 'amount_gte' => ['kind' => 'int64_string'], + ], + ], + ]; + $service->publicRequestSearchResult('get', '/v2/test', ['amount_gte' => 1000], [], $schemas); + + self::assertSame('1000', $capturedParams['amount_gte']); + } +} + +/** + * @internal + * Concrete subclass that exposes protected methods for testing + */ +final class ConcreteTestService extends AbstractService +{ + public function publicRequest($method, $path, $params, $opts, $schemas = null) + { + return $this->request($method, $path, $params, $opts, $schemas); + } + + public function publicRequestCollection($method, $path, $params, $opts, $schemas = null) + { + return $this->requestCollection($method, $path, $params, $opts, $schemas); + } + + public function publicRequestSearchResult($method, $path, $params, $opts, $schemas = null) + { + return $this->requestSearchResult($method, $path, $params, $opts, $schemas); + } } diff --git a/tests/Stripe/StripeObjectTest.php b/tests/Stripe/StripeObjectTest.php index aacf9c62e..5430b3c39 100644 --- a/tests/Stripe/StripeObjectTest.php +++ b/tests/Stripe/StripeObjectTest.php @@ -597,4 +597,71 @@ public function testDeserializeMetadataWithKeyNamedMetadata() $inner = $obj->metadata; self::assertSame('value', $inner->metadata); } + + public function testRefreshFromCoercesInt64ResponseValues() + { + $obj = StripeObjectWithInt64Fields::constructFrom([ + 'id' => 'obj_123', + 'amount' => '12345', + 'currency' => 'usd', + ]); + + self::assertSame(12345, $obj['amount']); + self::assertSame('usd', $obj['currency']); + } + + public function testRefreshFromCoercesNestedInt64ResponseValues() + { + $obj = StripeObjectWithNestedInt64Fields::constructFrom([ + 'id' => 'obj_123', + 'details' => ['amount' => '999', 'label' => 'test'], + ]); + + self::assertSame(999, $obj['details']['amount']); + self::assertSame('test', $obj['details']['label']); + } + + public function testRefreshFromSkipsCoercionWhenNoFieldEncodings() + { + $obj = StripeObject::constructFrom([ + 'id' => 'obj_123', + 'amount' => '12345', + ]); + + // Without fieldEncodings(), string values stay as strings + self::assertSame('12345', $obj['amount']); + } +} + +/** + * @internal + * Test subclass that declares int64_string field encodings + */ +final class StripeObjectWithInt64Fields extends StripeObject +{ + public static function fieldEncodings() + { + return [ + 'amount' => ['kind' => 'int64_string'], + ]; + } +} + +/** + * @internal + * Test subclass with nested int64_string field encodings + */ +final class StripeObjectWithNestedInt64Fields extends StripeObject +{ + public static function fieldEncodings() + { + return [ + 'details' => [ + 'kind' => 'object', + 'fields' => [ + 'amount' => ['kind' => 'int64_string'], + ], + ], + ]; + } } diff --git a/tests/Stripe/Util/Int64Test.php b/tests/Stripe/Util/Int64Test.php new file mode 100644 index 000000000..134177349 --- /dev/null +++ b/tests/Stripe/Util/Int64Test.php @@ -0,0 +1,320 @@ + 'object', 'fields' => ['amount' => ['kind' => 'int64_string']]]; + self::assertNull(Int64::coerceRequestParams(null, $schema)); + } + + public function testCoerceRequestParamsReturnsParamsWhenSchemaHasNoKind() + { + $params = ['amount' => 42]; + self::assertSame($params, Int64::coerceRequestParams($params, [])); + } + + public function testCoerceRequestParamsConvertsIntToStringForInt64Field() + { + $schema = ['kind' => 'int64_string']; + self::assertSame('42', Int64::coerceRequestParams(42, $schema)); + } + + public function testCoerceRequestParamsPassesThroughStringForInt64Field() + { + $schema = ['kind' => 'int64_string']; + self::assertSame('42', Int64::coerceRequestParams('42', $schema)); + } + + public function testCoerceRequestParamsConvertsObjectFieldsSelectively() + { + $schema = [ + 'kind' => 'object', + 'fields' => [ + 'amount' => ['kind' => 'int64_string'], + ], + ]; + $params = ['amount' => 100, 'currency' => 'usd']; + $result = Int64::coerceRequestParams($params, $schema); + + self::assertSame('100', $result['amount']); + self::assertSame('usd', $result['currency']); + } + + public function testCoerceRequestParamsLeavesUnmatchedFieldsAlone() + { + $schema = [ + 'kind' => 'object', + 'fields' => [ + 'amount' => ['kind' => 'int64_string'], + ], + ]; + $params = ['description' => 'test']; + $result = Int64::coerceRequestParams($params, $schema); + + self::assertSame('test', $result['description']); + } + + public function testCoerceRequestParamsHandlesArrayOfInt64() + { + $schema = [ + 'kind' => 'array', + 'items' => ['kind' => 'int64_string'], + ]; + $params = [1, 2, 3]; + $result = Int64::coerceRequestParams($params, $schema); + + self::assertSame(['1', '2', '3'], $result); + } + + public function testCoerceRequestParamsHandlesArrayOfObjects() + { + $schema = [ + 'kind' => 'array', + 'items' => [ + 'kind' => 'object', + 'fields' => [ + 'amount' => ['kind' => 'int64_string'], + ], + ], + ]; + $params = [ + ['amount' => 100, 'currency' => 'usd'], + ['amount' => 200, 'currency' => 'eur'], + ]; + $result = Int64::coerceRequestParams($params, $schema); + + self::assertSame('100', $result[0]['amount']); + self::assertSame('usd', $result[0]['currency']); + self::assertSame('200', $result[1]['amount']); + self::assertSame('eur', $result[1]['currency']); + } + + public function testCoerceRequestParamsHandlesNestedObjects() + { + $schema = [ + 'kind' => 'object', + 'fields' => [ + 'outer' => [ + 'kind' => 'object', + 'fields' => [ + 'amount' => ['kind' => 'int64_string'], + ], + ], + ], + ]; + $params = ['outer' => ['amount' => 500, 'label' => 'test']]; + $result = Int64::coerceRequestParams($params, $schema); + + self::assertSame('500', $result['outer']['amount']); + self::assertSame('test', $result['outer']['label']); + } + + public function testCoerceRequestParamsPassesThroughNonArrayForObjectSchema() + { + $schema = [ + 'kind' => 'object', + 'fields' => [ + 'amount' => ['kind' => 'int64_string'], + ], + ]; + self::assertSame('not-an-array', Int64::coerceRequestParams('not-an-array', $schema)); + } + + public function testCoerceRequestParamsPassesThroughNonArrayForArraySchema() + { + $schema = [ + 'kind' => 'array', + 'items' => ['kind' => 'int64_string'], + ]; + self::assertSame('not-an-array', Int64::coerceRequestParams('not-an-array', $schema)); + } + + public function testCoerceRequestParamsPassesThroughForUnknownKind() + { + $schema = ['kind' => 'unknown_type']; + $params = ['foo' => 'bar']; + self::assertSame($params, Int64::coerceRequestParams($params, $schema)); + } + + // ——— coerceResponseValues ——— + + public function testCoerceResponseValuesReturnsNonArrayAsIs() + { + $encodings = ['amount' => ['kind' => 'int64_string']]; + self::assertSame('not-an-array', Int64::coerceResponseValues('not-an-array', $encodings)); + } + + public function testCoerceResponseValuesConvertsNumericStringToInt() + { + $encodings = ['amount' => ['kind' => 'int64_string']]; + $values = ['amount' => '12345']; + $result = Int64::coerceResponseValues($values, $encodings); + + self::assertSame(12345, $result['amount']); + } + + public function testCoerceResponseValuesLeavesNonNumericStringAlone() + { + $encodings = ['amount' => ['kind' => 'int64_string']]; + $values = ['amount' => 'not-a-number']; + $result = Int64::coerceResponseValues($values, $encodings); + + self::assertSame('not-a-number', $result['amount']); + } + + public function testCoerceResponseValuesLeavesNonIntFieldsAlone() + { + $encodings = ['amount' => ['kind' => 'int64_string']]; + $values = ['amount' => '100', 'currency' => 'usd']; + $result = Int64::coerceResponseValues($values, $encodings); + + self::assertSame(100, $result['amount']); + self::assertSame('usd', $result['currency']); + } + + public function testCoerceResponseValuesSkipsMissingFields() + { + $encodings = ['amount' => ['kind' => 'int64_string']]; + $values = ['currency' => 'usd']; + $result = Int64::coerceResponseValues($values, $encodings); + + self::assertSame(['currency' => 'usd'], $result); + } + + public function testCoerceResponseValuesSkipsEncodingsWithNoKind() + { + $encodings = ['amount' => []]; + $values = ['amount' => '100']; + $result = Int64::coerceResponseValues($values, $encodings); + + self::assertSame('100', $result['amount']); + } + + public function testCoerceResponseValuesHandlesArrayOfInt64() + { + $encodings = [ + 'amounts' => [ + 'kind' => 'array', + 'items' => ['kind' => 'int64_string'], + ], + ]; + $values = ['amounts' => ['100', '200', '300']]; + $result = Int64::coerceResponseValues($values, $encodings); + + self::assertSame([100, 200, 300], $result['amounts']); + } + + public function testCoerceResponseValuesHandlesArrayOfObjects() + { + $encodings = [ + 'line_items' => [ + 'kind' => 'array', + 'items' => [ + 'kind' => 'object', + 'fields' => [ + 'amount' => ['kind' => 'int64_string'], + ], + ], + ], + ]; + $values = [ + 'line_items' => [ + ['amount' => '100', 'description' => 'Widget'], + ['amount' => '200', 'description' => 'Gadget'], + ], + ]; + $result = Int64::coerceResponseValues($values, $encodings); + + self::assertSame(100, $result['line_items'][0]['amount']); + self::assertSame('Widget', $result['line_items'][0]['description']); + self::assertSame(200, $result['line_items'][1]['amount']); + self::assertSame('Gadget', $result['line_items'][1]['description']); + } + + public function testCoerceResponseValuesHandlesNestedObject() + { + $encodings = [ + 'details' => [ + 'kind' => 'object', + 'fields' => [ + 'amount' => ['kind' => 'int64_string'], + ], + ], + ]; + $values = ['details' => ['amount' => '999', 'label' => 'test']]; + $result = Int64::coerceResponseValues($values, $encodings); + + self::assertSame(999, $result['details']['amount']); + self::assertSame('test', $result['details']['label']); + } + + public function testCoerceResponseValuesHandlesNonArrayValueForArrayEncoding() + { + $encodings = [ + 'amounts' => [ + 'kind' => 'array', + 'items' => ['kind' => 'int64_string'], + ], + ]; + $values = ['amounts' => 'not-an-array']; + $result = Int64::coerceResponseValues($values, $encodings); + + self::assertSame('not-an-array', $result['amounts']); + } + + public function testCoerceResponseValuesHandlesNonArrayValueForObjectEncoding() + { + $encodings = [ + 'details' => [ + 'kind' => 'object', + 'fields' => [ + 'amount' => ['kind' => 'int64_string'], + ], + ], + ]; + $values = ['details' => 'not-an-array']; + $result = Int64::coerceResponseValues($values, $encodings); + + self::assertSame('not-an-array', $result['details']); + } + + public function testCoerceResponseValuesHandlesNegativeNumbers() + { + $encodings = ['amount' => ['kind' => 'int64_string']]; + $values = ['amount' => '-500']; + $result = Int64::coerceResponseValues($values, $encodings); + + self::assertSame(-500, $result['amount']); + } + + public function testCoerceResponseValuesHandlesZero() + { + $encodings = ['amount' => ['kind' => 'int64_string']]; + $values = ['amount' => '0']; + $result = Int64::coerceResponseValues($values, $encodings); + + self::assertSame(0, $result['amount']); + } + + public function testCoerceRequestParamsHandlesZero() + { + $schema = ['kind' => 'int64_string']; + self::assertSame('0', Int64::coerceRequestParams(0, $schema)); + } + + public function testCoerceRequestParamsHandlesNegativeInt() + { + $schema = ['kind' => 'int64_string']; + self::assertSame('-500', Int64::coerceRequestParams(-500, $schema)); + } +} From 2630ffc123904059b4234e7cd4b605f6ee519925 Mon Sep 17 00:00:00 2001 From: Simon <10352679+simonhammes@users.noreply.github.com> Date: Wed, 18 Mar 2026 20:17:36 +0100 Subject: [PATCH 2/3] Ensure that previous_attributes is always an instance of StripeObject (#2011) Fixes #1785 Co-authored-by: simonhammes Co-authored-by: David Brownman <109395161+xavdid-stripe@users.noreply.github.com> --- lib/StripeObject.php | 3 ++- tests/Stripe/StripeObjectTest.php | 14 +++++++++++++- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/lib/StripeObject.php b/lib/StripeObject.php index b53568e2e..6c3b30694 100644 --- a/lib/StripeObject.php +++ b/lib/StripeObject.php @@ -341,7 +341,8 @@ public function updateAttributes($values, $opts = null, $dirty = true, $apiMode // This is necessary in case metadata is empty, as PHP arrays do // not differentiate between lists and hashes, and we consider // empty arrays to be lists. - if (('metadata' === $k) && \is_array($v)) { + // The same applies to the previous_attributes attribute. + if (('metadata' === $k || 'previous_attributes' === $k) && \is_array($v)) { $this->_values[$k] = StripeObject::constructFrom($v, $opts, $apiMode); } else { $this->_values[$k] = Util\Util::convertToStripeObject($v, $opts, $apiMode); diff --git a/tests/Stripe/StripeObjectTest.php b/tests/Stripe/StripeObjectTest.php index 5430b3c39..4ce42c7aa 100644 --- a/tests/Stripe/StripeObjectTest.php +++ b/tests/Stripe/StripeObjectTest.php @@ -423,7 +423,7 @@ public function testSerializeParametersRaisesExceotionOnOtherEmbeddedApiResource } catch (\InvalidArgumentException $e) { self::assertSame( 'Cannot save property `customer` containing an API resource of type Stripe\Customer. ' - . "It doesn't appear to be persisted and is not marked as `saveWithParent`.", + . "It doesn't appear to be persisted and is not marked as `saveWithParent`.", $e->getMessage() ); } catch (\Exception $e) { @@ -598,6 +598,18 @@ public function testDeserializeMetadataWithKeyNamedMetadata() self::assertSame('value', $inner->metadata); } + public function testDeserializeEmptyPreviousAttributes() + { + /** @var mixed $obj */ + $obj = StripeObject::constructFrom([ + 'data' => [ + 'previous_attributes' => [], + ], + ]); + + self::assertInstanceOf(StripeObject::class, $obj->data->previous_attributes); + } + public function testRefreshFromCoercesInt64ResponseValues() { $obj = StripeObjectWithInt64Fields::constructFrom([ From dd4b1659a765b1b65e573b48c5844e84c757ea8a Mon Sep 17 00:00:00 2001 From: David Brownman <109395161+xavdid-stripe@users.noreply.github.com> Date: Wed, 18 Mar 2026 23:13:59 -0700 Subject: [PATCH 3/3] =?UTF-8?q?=E2=9A=A0=EF=B8=8F=20Drop=20support=20for?= =?UTF-8?q?=20PHP=20<=207.2=20(#2038)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * update supported versions * add new version * Update README.md Co-authored-by: Ramya Rao <100975018+ramya-stripe@users.noreply.github.com> --------- Co-authored-by: Ramya Rao <100975018+ramya-stripe@users.noreply.github.com> --- .github/workflows/ci.yml | 8 +++----- README.md | 4 ++-- composer.json | 13 ++++++++----- init.php | 2 ++ lib/version_check.php | 9 +++++++++ 5 files changed, 24 insertions(+), 12 deletions(-) create mode 100644 lib/version_check.php diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cc806c8a4..39b2d9f9f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -111,9 +111,7 @@ jobs: - "0" - "1" php-version: - - "5.6" - - "7.0" - - "7.1" + # https://docs.stripe.com/sdks/versioning?lang=php - "7.2" - "7.3" - "7.4" @@ -122,6 +120,7 @@ jobs: - "8.2" - "8.3" - "8.4" + - "8.5" name: Tests (php@${{ matrix.php-version }}, AUTOLOAD=${{ matrix.autoload }}) steps: @@ -166,8 +165,7 @@ jobs: name: Publish if: >- (github.event_name == 'workflow_dispatch' || github.event_name == 'push') && - startsWith(github.ref, 'refs/tags/v') && - endsWith(github.actor, '-stripe') + startsWith(github.ref, 'refs/tags/v') runs-on: "ubuntu-24.04" permissions: contents: read diff --git a/README.md b/README.md index 0efa1a637..722e958fb 100644 --- a/README.md +++ b/README.md @@ -16,9 +16,9 @@ API. ## Requirements -PHP 5.6.0 and later. +PHP 7.2.0 and later. -Note that per our [language version support policy](https://docs.stripe.com/sdks/versioning?lang=php#stripe-sdk-language-version-support-policy), support for PHP 5.6, 7.0, and 7.1 will be removed in the March 2026 major version. +Note that per our [language version support policy](https://docs.stripe.com/sdks/versioning?lang=php#stripe-sdk-language-version-support-policy), support for PHP 7.2 and 7.3 will be removed soon, so upgrade your runtime if you're able to. Additional PHP versions will be dropped in future major versions, so upgrade to supported versions if possible. diff --git a/composer.json b/composer.json index 5bf2d54b2..e2bd71205 100644 --- a/composer.json +++ b/composer.json @@ -15,7 +15,7 @@ } ], "require": { - "php": ">=5.6.0", + "php": ">=7.2.0", "ext-curl": "*", "ext-json": "*", "ext-mbstring": "*" @@ -28,7 +28,10 @@ "autoload": { "psr-4": { "Stripe\\": "lib/" - } + }, + "files": [ + "lib/version_check.php" + ] }, "autoload-dev": { "psr-4": { @@ -45,9 +48,9 @@ }, "config": { "audit": { - "ignore": { - "PKSA-z3gr-8qht-p93v": "PHPUnit is only a dev dependency. Temporarily ignore PHPUnit security advisory to ensure continued support for PHP 5.6 in CI." - } + "ignore": { + "PKSA-z3gr-8qht-p93v": "PHPUnit is only a dev dependency. Temporarily ignore PHPUnit security advisory to ensure continued support for PHP 5.6 in CI." + } } } } diff --git a/init.php b/init.php index 62368bb26..fee4f7a6b 100644 --- a/init.php +++ b/init.php @@ -1,5 +1,7 @@