diff --git a/src/Contracts/Hydratable.php b/src/Contracts/Hydratable.php index 5ec2d0b7..53563a5e 100644 --- a/src/Contracts/Hydratable.php +++ b/src/Contracts/Hydratable.php @@ -4,7 +4,7 @@ interface Hydratable { - public static function shouldAutoHydrate(bool $shouldAutoHydrate = true): void; + public static function setAutoHydrate(bool $shouldAutoHydrate = true): void; /** * @return mixed diff --git a/src/EndpointCollection/EndpointCollection.php b/src/EndpointCollection/EndpointCollection.php index 4825f403..11bf8e47 100644 --- a/src/EndpointCollection/EndpointCollection.php +++ b/src/EndpointCollection/EndpointCollection.php @@ -16,7 +16,7 @@ public function __construct(Connector $connector) /** * Default hydration decision to true to maintain legacy compatibility. */ - $connector::shouldAutoHydrate(); + $connector::setAutoHydrate(); } /** diff --git a/src/Helpers/Arr.php b/src/Helpers/Arr.php index 511457c2..588e8644 100644 --- a/src/Helpers/Arr.php +++ b/src/Helpers/Arr.php @@ -26,6 +26,45 @@ public static function get(array $array, string $keys, $default = null) return $value; } + /** + * Get and remove an item from an array using "dot" notation. + * + * @param array $array + * @param string $key + * @param mixed $default + * @return mixed + */ + public static function pull(array &$array, string $key, $default = null) + { + $value = static::get($array, $key, $default); + + static::forget($array, $key); + + return $value; + } + + /** + * Remove an item from an array using "dot" notation. + * + * @param array $array + * @param string $key + */ + public static function forget(array &$array, string $key): void + { + $keys = explode('.', $key); + $last = array_pop($keys); + $array = &$array; + + foreach ($keys as $segment) { + if (!is_array($array) || !array_key_exists($segment, $array)) { + break; + } + $array = &$array[$segment]; + } + + unset($array[$last]); + } + /** * Checks if the given key/s exist in the provided array. * diff --git a/src/Resources/CursorCollection.php b/src/Resources/CursorCollection.php index 68280ee0..31b55b1c 100644 --- a/src/Resources/CursorCollection.php +++ b/src/Resources/CursorCollection.php @@ -4,15 +4,43 @@ use Generator; use Mollie\Api\Http\Requests\DynamicGetRequest; +use Mollie\Api\Http\Requests\ResourceHydratableRequest; abstract class CursorCollection extends ResourceCollection { + private bool $autoHydrate = false; + + public function setAutoHydrate(bool $shouldAutoHydrate = true): void + { + $this->autoHydrate = $shouldAutoHydrate; + } + + public function shouldAutoHydrate(): bool + { + if ($this->response === null) { + return $this->autoHydrate; + } + + $request = $this->response->getRequest(); + + /** + * Don't try to hydrate when the request + * already has auto-hydration enabled. The + * Hydrate Middleware will take care of that. + */ + if ($request instanceof ResourceHydratableRequest && $request->shouldAutoHydrate()) { + return false; + } + + return $this->autoHydrate; + } + /** * Return the next set of resources when available * * @throws \Mollie\Api\Exceptions\ApiException */ - final public function next(): ?CursorCollection + public function next(): ?CursorCollection { if (! $this->hasNext()) { return null; @@ -26,7 +54,7 @@ final public function next(): ?CursorCollection * * @throws \Mollie\Api\Exceptions\ApiException */ - final public function previous(): ?CursorCollection + public function previous(): ?CursorCollection { if (! $this->hasPrevious()) { return null; @@ -37,9 +65,13 @@ final public function previous(): ?CursorCollection private function fetchCollection(string $url): CursorCollection { - return $this + $response = $this ->connector ->send(new DynamicGetRequest($url, static::class)); + + return $this->shouldAutoHydrate() + ? $response->toResource() + : $response; } /** @@ -66,6 +98,8 @@ public function getAutoIterator(bool $iterateBackwards = false): LazyCollection $page = $this; return new LazyCollection(function () use ($page, $iterateBackwards): Generator { + $page->setAutoHydrate(); + while (true) { foreach ($page as $item) { yield $item; diff --git a/src/Traits/HandlesAutoHydration.php b/src/Traits/HandlesAutoHydration.php index 26736eee..0c868529 100644 --- a/src/Traits/HandlesAutoHydration.php +++ b/src/Traits/HandlesAutoHydration.php @@ -8,7 +8,7 @@ trait HandlesAutoHydration { protected static $hydrationSettingResolver = null; - public static function shouldAutoHydrate(bool $shouldAutoHydrate = true): void + public static function setAutoHydrate(bool $shouldAutoHydrate = true): void { static::$hydrationSettingResolver = static function () use ($shouldAutoHydrate) { ResourceHydratableRequest::hydrate($shouldAutoHydrate); diff --git a/tests/Fixtures/SequenceMockResponse.php b/tests/Fixtures/SequenceMockResponse.php index f7d2bed1..7e4fad96 100644 --- a/tests/Fixtures/SequenceMockResponse.php +++ b/tests/Fixtures/SequenceMockResponse.php @@ -30,4 +30,9 @@ public function pop(): MockResponse return $response; } + + public function isEmpty(): bool + { + return count($this->responses) === 0; + } } diff --git a/tests/Helpers/ArrTest.php b/tests/Helpers/ArrTest.php index 4d65138f..22f2af87 100644 --- a/tests/Helpers/ArrTest.php +++ b/tests/Helpers/ArrTest.php @@ -17,6 +17,25 @@ public function get(): void $this->assertEquals('default', Arr::get($array, 'foo.baz', 'default')); } + /** @test */ + public function pull(): void + { + $array = ['foo' => ['bar' => 'baz']]; + + $this->assertEquals('baz', Arr::pull($array, 'foo.bar')); + $this->assertEquals(['foo' => []], $array); + } + + /** @test */ + public function forget(): void + { + $array = ['foo' => ['bar' => 'baz']]; + + Arr::forget($array, 'foo.bar'); + + $this->assertEquals(['foo' => []], $array); + } + /** @test */ public function has(): void { diff --git a/tests/Http/Adapter/MockMollieHttpAdapter.php b/tests/Http/Adapter/MockMollieHttpAdapter.php index a54a2d5b..789e09cd 100644 --- a/tests/Http/Adapter/MockMollieHttpAdapter.php +++ b/tests/Http/Adapter/MockMollieHttpAdapter.php @@ -29,15 +29,13 @@ public function __construct(array $expectedResponses = []) */ public function sendRequest(PendingRequest $pendingRequest): Response { - if (! Arr::has($this->expectedResponses, $requestClass = get_class($pendingRequest->getRequest()))) { + $requestClass = get_class($pendingRequest->getRequest()); + + if (! Arr::has($this->expectedResponses, $requestClass)) { throw new \RuntimeException('The request class '.$requestClass.' is not expected.'); } - $mockedResponse = $this->expectedResponses[$requestClass]; - - if ($mockedResponse instanceof SequenceMockResponse) { - $mockedResponse = $mockedResponse->pop(); - } + $mockedResponse = $this->getResponse($requestClass); return new Response( $mockedResponse->createPsrResponse(), @@ -46,6 +44,30 @@ public function sendRequest(PendingRequest $pendingRequest): Response ); } + /** + * Get the mocked response and remove it from the expected responses. + * + * @param string $requestClass + * @return MockResponse + */ + private function getResponse(string $requestClass): MockResponse + { + $mockedResponse = Arr::get($this->expectedResponses, $requestClass); + + if (!($mockedResponse instanceof SequenceMockResponse)) { + Arr::forget($this->expectedResponses, $requestClass); + return $mockedResponse; + } + + $response = $mockedResponse->pop(); + + if ($mockedResponse->isEmpty()) { + Arr::forget($this->expectedResponses, $requestClass); + } + + return $response; + } + /** * {@inheritDoc} */ diff --git a/tests/Http/Requests/CreatePaymentRequestTest.php b/tests/Http/Requests/CreatePaymentRequestTest.php new file mode 100644 index 00000000..14f0700c --- /dev/null +++ b/tests/Http/Requests/CreatePaymentRequestTest.php @@ -0,0 +1,39 @@ + new MockResponse(201, 'payment'), + ]); + + $payload = new CreatePaymentPayload( + 'Test payment', + new Money('EUR', '10.00'), + 'https://example.org/redirect', + 'https://example.org/webhook' + ); + + $request = new CreatePaymentRequest($payload); + + /** @var Response */ + $response = $client->send($request); + + $this->assertTrue($response->successful()); + $this->assertInstanceOf(Payment::class, $response->toResource()); + } +} diff --git a/tests/Http/Requests/GetPaginatedPaymentsRequestTest.php b/tests/Http/Requests/GetPaginatedPaymentsRequestTest.php new file mode 100644 index 00000000..6517fd92 --- /dev/null +++ b/tests/Http/Requests/GetPaginatedPaymentsRequestTest.php @@ -0,0 +1,65 @@ + new MockResponse(200, 'payment-list'), + ]); + + $request = new GetPaginatedPaymentsRequest(); + + /** @var Response */ + $response = $client->send($request); + + $this->assertTrue($response->successful()); + + /** @var PaymentCollection */ + $payments = $response->toResource(); + // Assert response was properly handled + $this->assertInstanceOf(PaymentCollection::class, $payments); + $this->assertGreaterThan(0, $payments->count()); + + foreach ($payments as $payment) { + $this->assertInstanceOf(Payment::class, $payment); + $this->assertEquals('payment', $payment->resource); + } + } + + /** @test */ + public function it_can_iterate_over_payments() + { + $client = new MockClient([ + GetPaginatedPaymentsRequest::class => new MockResponse(200, 'payment-list'), + DynamicGetRequest::class => new MockResponse(200, 'payment-list'), + DynamicGetRequest::class => new MockResponse(200, 'empty-list', 'payments'), + ]); + + $request = (new GetPaginatedPaymentsRequest())->useIterator(); + + /** @var Response */ + $response = $client->send($request); + $this->assertTrue($response->successful()); + + /** @var LazyCollection */ + $payments = $response->toResource(); + + foreach ($payments as $payment) { + $this->assertInstanceOf(Payment::class, $payment); + } + } +} diff --git a/tests/Resources/CursorCollectionTest.php b/tests/Resources/CursorCollectionTest.php index 60b476ae..3500064c 100644 --- a/tests/Resources/CursorCollectionTest.php +++ b/tests/Resources/CursorCollectionTest.php @@ -13,13 +13,6 @@ class CursorCollectionTest extends TestCase { - protected function setUp(): void - { - parent::setUp(); - - MockClient::shouldAutoHydrate(); - } - /** @test */ public function can_get_next_collection_result_when_next_link_is_available() { @@ -37,6 +30,8 @@ public function can_get_next_collection_result_when_next_link_is_available() ]) ); + $collection->setAutoHydrate(); + $this->assertTrue($collection->hasNext()); $nextPage = $collection->next(); @@ -54,6 +49,8 @@ public function test_will_return_null_if_no_next_result_is_available() (object) [] ); + $collection->setAutoHydrate(); + $this->assertFalse($collection->hasNext()); $this->assertNull($collection->next()); } @@ -74,6 +71,8 @@ public function test_can_get_previous_collection_result_when_previous_link_is_av ]) ); + $collection->setAutoHydrate(); + $this->assertTrue($collection->hasPrevious()); $previousPage = $collection->previous(); @@ -91,6 +90,8 @@ public function test_will_return_null_if_no_previous_result_is_available() (object) [] ); + $collection->setAutoHydrate(); + $this->assertFalse($collection->hasPrevious()); $this->assertNull($collection->previous()); } @@ -105,6 +106,8 @@ public function test_auto_paginator_returns_lazy_collection() (object) [] ); + $collection->setAutoHydrate(); + $this->assertInstanceOf(LazyCollection::class, $collection->getAutoIterator()); } diff --git a/tests/TestCase.php b/tests/TestCase.php index 66d6a386..3a0f5f90 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -11,6 +11,6 @@ protected function setUp(): void { parent::setUp(); - MockClient::shouldAutoHydrate(false); + MockClient::setAutoHydrate(false); } }