Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Release 0.9.0 #101

Merged
merged 13 commits into from
Oct 30, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Changed

## [0.8.5]
## [0.9.0] - 2023-10-30

### Added
- Adds generic types to Promise types in PHPDocs

### Changed
- Ensure changes to nested BackedModel or array<BackedModel> properties in the backing store makes the entire backing store value dirty.

## [0.8.5] - 2023-10-17

### Changed
- Disabled auto-suggestion of PSR implementations by OpenTelemetry SDK. [#95](https://github.com/microsoft/kiota-abstractions-php/pull/95)
Expand Down
4 changes: 2 additions & 2 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,12 @@
},
"require": {
"php": "^7.4 || ^8.0",
"php-http/promise": "^1.1.0",
"php-http/promise": "^1.2.0",
"doctrine/annotations": "^1.13 || ^2.0",
"open-telemetry/sdk": "^0.0.17",
"open-telemetry/api": "^0.0.17",
"ramsey/uuid": "^3 || ^4",
"stduritemplate/stduritemplate": "^0.0.43",
"stduritemplate/stduritemplate": "^0.0.44 || ^0.0.46",
"psr/http-message": "^1.1 || ^2.0"
},
"require-dev": {
Expand Down
2 changes: 1 addition & 1 deletion src/Authentication/AccessTokenProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ interface AccessTokenProvider
*
* @param string $url the target URI to get an access token for
* @param array<string, mixed> $additionalAuthenticationContext
* @return Promise
* @return Promise<string>
*/
public function getAuthorizationTokenAsync(string $url, array $additionalAuthenticationContext = []): Promise;

Expand Down
4 changes: 2 additions & 2 deletions src/Authentication/AnonymousAuthenticationProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,13 @@ class AnonymousAuthenticationProvider implements AuthenticationProvider {
/**
* @param RequestInformation $request Request information
* @param array<string, mixed> $additionalAuthenticationContext
* @return Promise
* @return Promise<RequestInformation>
*/
public function authenticateRequest(
RequestInformation $request,
array $additionalAuthenticationContext = []
): Promise
{
return new FulfilledPromise(null);
return new FulfilledPromise($request);
}
}
2 changes: 1 addition & 1 deletion src/Authentication/AuthenticationProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ interface AuthenticationProvider {
/**
* @param RequestInformation $request
* @param array<string, mixed> $additionalAuthenticationContext
* @return Promise
* @return Promise<RequestInformation>
*/
public function authenticateRequest(
RequestInformation $request,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@

use Http\Promise\FulfilledPromise;
use Http\Promise\Promise;
use League\Uri\Contracts\UriException;
use Microsoft\Kiota\Abstractions\RequestInformation;

/**
Expand Down Expand Up @@ -58,7 +57,7 @@ public function getAccessTokenProvider(): AccessTokenProvider
/**
* @param RequestInformation $request
* @param array<string, mixed> $additionalAuthenticationContext
* @return Promise
* @return Promise<RequestInformation>
*/
public function authenticateRequest(
RequestInformation $request,
Expand All @@ -78,9 +77,9 @@ public function authenticateRequest(
if ($token) {
$request->addHeader(self::$authorizationHeaderKey, "Bearer {$token}");
}
return null;
return $request;
});
}
return new FulfilledPromise(null);
return new FulfilledPromise($request);
}
}
2 changes: 1 addition & 1 deletion src/Constants.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,5 @@

final class Constants
{
public const VERSION = '0.8.5';
public const VERSION = '0.9.0';
}
21 changes: 19 additions & 2 deletions src/NativeResponseHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,32 @@
*/
class NativeResponseHandler implements ResponseHandler
{

/**
* @var ResponseInterface|null
*/
private ?ResponseInterface $nativeResponse = null;

/**
* Returns the PSR-7 Response
*
* @return ResponseInterface|null
*/
public function getResponse(): ?ResponseInterface
{
return $this->nativeResponse;
}

/**
* Returns a promise that resolves to the raw PSR-7 response
*
* @param ResponseInterface $response
* @param array<string, array{string, string}>|null $errorMappings
* @return Promise
* @return Promise<null>
*/
public function handleResponseAsync(ResponseInterface $response, ?array $errorMappings = null): Promise
{
return new FulfilledPromise($response);
$this->nativeResponse = $response;
return new FulfilledPromise(null);
}
}
25 changes: 16 additions & 9 deletions src/RequestAdapter.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,22 @@
use Microsoft\Kiota\Abstractions\Serialization\SerializationWriterFactory;
use Microsoft\Kiota\Abstractions\Store\BackingStoreFactory;
use Microsoft\Kiota\Abstractions\Serialization\Parsable;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\StreamInterface;

/** Service responsible for translating abstract Request Info into concrete native HTTP requests. */
/**
* Service responsible for translating abstract Request Info into concrete native HTTP requests.
*/
interface RequestAdapter {
/**
* Executes the HTTP request specified by the given RequestInformation and returns the deserialized response model.
* @template T of Parsable
* @template V of Parsable
* @param RequestInformation $requestInfo the request info to execute.
* @param array{class-string<T>,string} $targetCallable the model to deserialize the response into.
* @param array<string, array{class-string<T>, string}>|null $errorMappings
* @return Promise with the deserialized response model.
* @param array<string, array{class-string<V>, string}>|null $errorMappings
* @return Promise<T|null> with the deserialized response model.
*/
public function sendAsync(
RequestInformation $requestInfo,
Expand All @@ -39,10 +45,11 @@ public function getParseNodeFactory(): ParseNodeFactory;
/**
* Executes the HTTP request specified by the given RequestInformation and returns the deserialized response model collection.
* @template T of Parsable
* @template V of Parsable
* @param RequestInformation $requestInfo the request info to execute.
* @param array{class-string<T>,string} $targetCallable the callable representing object creation logic.
* @param array<string, array{class-string<T>, string}>|null $errorMappings
* @return Promise with the deserialized response model collection.
* @param array<string, array{class-string<V>, string}>|null $errorMappings
* @return Promise<array<T|null>|null> with the deserialized response model collection.
*/
public function sendCollectionAsync(
RequestInformation $requestInfo,
Expand All @@ -56,7 +63,7 @@ public function sendCollectionAsync(
* @param RequestInformation $requestInfo
* @param string $primitiveType e.g. int, bool
* @param array<string, array{class-string<T>, string}>|null $errorMappings
* @return Promise
* @return Promise<mixed|StreamInterface>
*/
public function sendPrimitiveAsync(
RequestInformation $requestInfo,
Expand All @@ -70,7 +77,7 @@ public function sendPrimitiveAsync(
* @param RequestInformation $requestInfo
* @param string $primitiveType e.g. int, bool
* @param array<string, array{class-string<T>, string}>|null $errorMappings
* @return Promise
* @return Promise<array<mixed>|null>
*/
public function sendPrimitiveCollectionAsync(
RequestInformation $requestInfo,
Expand All @@ -83,7 +90,7 @@ public function sendPrimitiveCollectionAsync(
* @template T of Parsable
* @param RequestInformation $requestInfo
* @param array<string, array{class-string<T>, string}>|null $errorMappings
* @return Promise
* @return Promise<null>
*/
public function sendNoContentAsync(RequestInformation $requestInfo, ?array $errorMappings = null): Promise;
/**
Expand All @@ -108,7 +115,7 @@ public function getBaseUrl(): string;
* Converts RequestInformation object to an authenticated(containing auth header) PSR-7 Request Object.
*
* @param RequestInformation $requestInformation
* @return Promise
* @return Promise<RequestInterface>
*/
public function convertToNative(RequestInformation $requestInformation): Promise;
}
8 changes: 5 additions & 3 deletions src/ResponseHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,16 @@

use Http\Promise\Promise;
use Psr\Http\Message\ResponseInterface;
use Microsoft\Kiota\Abstractions\Serialization\Parsable;

interface ResponseHandler {
/**
* Callback method that is invoked when a response is received.
* @template T of Parsable
* @param ResponseInterface $response The native response object.
* @param array<string, array<string, string>>|null $errorMappings map of error status codes to exception models
* to deserialize to
* @return Promise A Promise that represents the asynchronous operation and contains the deserialized response.
* @param array<string, array{class-string<T>, string}>|null $errorMappings
* map of error status codes to exception models to deserialize to
* @return Promise<mixed> A Promise that contains the deserialized response.
*/
public function handleResponseAsync(ResponseInterface $response, ?array $errorMappings = null): Promise;
}
57 changes: 51 additions & 6 deletions src/Store/InMemoryBackingStore.php
Original file line number Diff line number Diff line change
Expand Up @@ -38,17 +38,27 @@ public function get(string $key) {
public function set(string $key, $value): void
{
$oldValue = $this->store[$key] ?? null;
$valueToAdd = is_array($value) ? [$this->isInitializationCompleted, $value, count($value)] : [$this->isInitializationCompleted, $value];
$valueToAdd = is_array($value) ?
[$this->isInitializationCompleted, $value, count($value)] : [$this->isInitializationCompleted, $value];

// Dirty track changes if $value is a model and its properties change
if (!array_key_exists($key, $this->store)) {
if ($value instanceof BackedModel && $value->getBackingStore()) {
$value->getBackingStore()->subscribe(fn ($propertyKey, $oldVal, $newVal) => $this->set($key, $value));
$value->getBackingStore()->subscribe(function ($propertyKey, $oldVal, $newVal) use ($key, $value) {
// Mark all properties as dirty
$value->getBackingStore()->setIsInitializationCompleted(false);
$this->set($key, $value);
});
}
if (is_array($value)) {
array_map(function ($item) use ($key, $value) {
if ($item instanceof BackedModel && $item->getBackingStore()) {
$item->getBackingStore()->subscribe(fn ($propertyKey, $oldVal, $newVal) => $this->set($key, $value));
$item->getBackingStore()->subscribe
(function ($propertyKey, $oldVal, $newVal) use ($key, $value, $item) {
// Mark all properties as dirty
$item->getBackingStore()->setIsInitializationCompleted(false);
$this->set($key, $value);
});
}
}, $value);
}
Expand Down Expand Up @@ -113,6 +123,20 @@ public function clear(): void {
*/
public function setIsInitializationCompleted(bool $value): void {
$this->isInitializationCompleted = $value;
// Update existing values in store
foreach ($this->store as $key => $storedValue) {
$storedValue[0] = !$storedValue[0];
if ($storedValue[1] instanceof BackedModel && $storedValue[1]->getBackingStore()) {
$storedValue[1]->getBackingStore()->setIsInitializationCompleted($value);
}
if (is_array($storedValue[1])) {
array_map(function ($item) use ($value) {
if ($item instanceof BackedModel && $item->getBackingStore()) {
$item->getBackingStore()->setIsInitializationCompleted($value);
}
}, $storedValue[1]);
}
}
}

/**
Expand Down Expand Up @@ -174,9 +198,30 @@ private function getValueFromWrapper(array $wrapper) {
*/
private function checkCollectionSizeChanged(string $key): void {
$wrapper = $this->store[$key] ?? null;
if ($wrapper && is_array($wrapper[1])) {
if (sizeof($wrapper[1]) != $wrapper[2]) {
$this->set($key, $wrapper[1]);
if ($wrapper) {
if (is_array($wrapper[1])) {
array_map(function ($item) {
if ($item instanceof BackedModel && $item->getBackingStore()) {
// Call get() on nested properties so that this method may be called recursively
// to ensure collections are consistent
array_map(
fn ($itemKey) => $item->getBackingStore()->get($itemKey),
array_keys($item->getBackingStore()->enumerate())
);
}
}, $wrapper[1]);

if (sizeof($wrapper[1]) != $wrapper[2]) {
$this->set($key, $wrapper[1]);
}
}
if ($wrapper[1] instanceof BackedModel && $wrapper[1]->getBackingStore()) {
// Call get() on nested properties so that this method may be called recursively
// to ensure collections are consistent
array_map(
fn ($itemKey) => $wrapper[1]->getBackingStore()->get($itemKey),
array_keys($wrapper[1]->getBackingStore()->enumerate())
);
}
}
}
Expand Down
48 changes: 41 additions & 7 deletions tests/Store/InMemoryBackingStoreTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -149,33 +149,67 @@ public function testChangesToBackedModelCollection(): void
$this->assertEquals(2, sizeof($this->backingStore->get('key')));
$this->assertEquals(250, $this->backingStore->get('key')[1]->getAge());
}


public function testChangesToBackedModelAsBackingStoreValueMakesEntireModelDirty(): void
{
$nestedBackedModel = new SampleBackedModel(10, 'name');

$this->backingStore->set('user', $nestedBackedModel);
$this->backingStore->setIsInitializationCompleted(true);

$nestedBackedModel->setAge(5);

$this->backingStore->setReturnOnlyChangedValues(true);
$nestedBackedModel->getBackingStore()->setReturnOnlyChangedValues(true);

$this->assertInstanceOf(BackedModel::class, $this->backingStore->get('user'));
$this->assertEquals(2, sizeof(array_keys($this->backingStore->get('user')->getBackingStore()->enumerate())));
}

public function testChangesToBackedModelCollectionAsBackingStoreValueMakesEntireModelDirty(): void
{
$nestedBackedModel = new SampleBackedModel(10, 'name');
$anotherNestedBackedModel = new SampleBackedModel(100, 'FirstName');

$this->backingStore->set('user', [$nestedBackedModel, $anotherNestedBackedModel]);
$this->backingStore->setIsInitializationCompleted(true);

$nestedBackedModel->setAge(5);

$this->backingStore->setReturnOnlyChangedValues(true);
$nestedBackedModel->getBackingStore()->setReturnOnlyChangedValues(true);

$this->assertIsArray($this->backingStore->get('user'));
$this->assertEquals(2, sizeof($this->backingStore->get('user')));
$this->assertEquals(2, sizeof(array_keys($this->backingStore->get('user')[0]->getBackingStore()->enumerate())));
$this->assertEquals(2, sizeof(array_keys($this->backingStore->get('user')[1]->getBackingStore()->enumerate())));
}
}

class SampleBackedModel implements BackedModel
{
private int $age;
private ?string $name = null;
private BackingStore $backingStore;

public function __construct(int $age, ?string $name = null)
public function __construct(?int $age = null, ?string $name = null)
{
$this->backingStore = BackingStoreFactorySingleton::getInstance()->createBackingStore();
$this->setAge($age);
$this->setName($name);
}

/**
* @return int
* @return int|null
*/
public function getAge(): int
public function getAge(): ?int
{
return $this->backingStore->get("age");
}

/**
* @param int $age
* @param int|null $age
*/
public function setAge(int $age): void
public function setAge(?int $age): void
{
$this->backingStore->set("age", $age);
}
Expand Down