diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 938918c..c8a32fb 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -10,19 +10,19 @@ jobs: strategy: fail-fast: true matrix: - php: [ 8.0, 8.1 ] + php: [ 8.3 ] name: PHP ${{ matrix.php }} on Linux steps: - name: Checkout Code - uses: actions/checkout@v2 + uses: actions/checkout@v4 - name: Setup PHP uses: shivammathur/setup-php@v2 with: php-version: ${{ matrix.php }} - extensions: dom, curl, libxml, mbstring, zip, http + extensions: dom, curl, fileinfo, json, libxml, mbstring, zip, http tools: composer:v2 coverage: none @@ -37,7 +37,7 @@ jobs: strategy: fail-fast: true matrix: - php: [ 8.0, 8.1 ] + php: [ 8.3 ] name: PHP ${{ matrix.php }} on Windows @@ -48,13 +48,13 @@ jobs: git config --global core.autocrlf false git config --global core.eol lf - name: Checkout code - uses: actions/checkout@v2 + uses: actions/checkout@v4 - name: Setup PHP uses: shivammathur/setup-php@v2 with: php-version: ${{ matrix.php }} - extensions: dom, curl, libxml, mbstring, zip, http + extensions: dom, curl, fileinfo, json, libxml, mbstring, zip, http tools: composer:v2 coverage: none ini-values: memory_limit=512M diff --git a/.gitignore b/.gitignore index fe26886..6e221fb 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ .idea/ .phpunit.result.cache composer.lock +/.phpunit.cache diff --git a/LICENSE b/LICENSE index ee6331d..2eba56c 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2020 Steve McDougall +Copyright (c) 2023 Steve McDougall Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 5e930cc..d081acd 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,5 @@ # PHP SDK -

- -![](./banner.png) - -

- [![Latest Version][badge-release]][packagist] [![PHP Version][badge-php]][php] @@ -21,7 +15,7 @@ [downloads]: https://packagist.org/packages/juststeveking/php-sdk -A framework for building simple to use SDKs in PHP 8.0 and above. +A framework for building SDKs in PHP. ## Installation @@ -35,203 +29,150 @@ The purpose of this package is to provide a consistent and interoperable way to ## Usage -Working with this library is relatively simple, and an example can be found in the [demo](./demo) and [examples](./examples) directories. - -The basic concept is that you will need to provide: - -- PSR-17 Request and Response Factory. -- PSR-7 Messages - -Inside this library we are using a PSR-18 implementation allowing you to connect the pieces together under the hood and provide SDK functionality using a replaceable set of components. - -I highly recommend either: - -- [nyholm/psr7](https://github.com/Nyholm/psr7/) -- [slim/psr7](https://github.com/slimphp/Slim-Psr7) -- [symfony/http-client](https://github.com/symfony/http-client) -- [laminas/diactoros](https://github.com/laminas/laminas-diactoros) - -To handle the Http PSRs as they are lightweight and designed to be simple and PSR compliant. - -### Building the SDK - -To begin with we need to be able to build our SDK, to do this we can either use the `constructor` or use the static `build` method.: - -#### SDK constructor - -To create an SDK instance; simply pass through a uri, a Http Client that uses auto-discovery to find the available PSR-18 client, an authentication strategy, and an instance of the `Container`. - -```php -use JustSteveKing\HttpAuth\Strategies\BasicStrategy; -use JustSteveKing\HttpSlim\HttpClient; -use JustSteveKing\UriBuilder\Uri; -use JustSteveKing\PhpSdk\SDK; -use PHPFox\Container\Container; - -$sdk = new SDK( - uri: Uri::fromString('https://www.domain.com'), - client: HttpClient::build(), - strategy: new BasicStrategy( - authString: base64_encode("username:password") - ), - container: Container::getInstance(), -); -``` - -#### SDK build +To get started with this library, you need to start by extending the `Client` class. Let's walk through building an SDK. -To use the static build method, the only requirement is to pass through a uri. If you want to set a custom Authentication Strategy you can also pass this through otherwise it will default to a Null Strategy. +### Create your SDK class ```php -use JustSteveKing\UriBuilder\Uri; -use JustSteveKing\PhpSdk\SDK; +use JustSteveKing\Sdk\Client; -$sdk = SDK::build( - uri: 'https://www.domain.com', -); +final class YourSDK extends Client +{ + // +} ``` -### Adding Resources to our SDK - -Each Resource you add to your SDK requires 2 things: - -- Implements `ResourceContract` -- Extends `AbstractResource` - -Your resource should look like this: +Once this is in place, you can start adding your resources to the class. Let's add a `projects` method for a projects resource. ```php -use JustSteveKing\PhpSdk\Contracts\ResourceContract; -use JustSteveKing\PhpSdk\Resources\AbstractResource; +use JustSteveKing\Sdk\Client; +use JustSteveKing\Sdk\Tests\Stubs\Resources\ProjectResource; -class TestResource extends AbstractResource implements ResourceContract +final class YourSDK extends Client { - protected string $path = '/test'; - - public static function name(): string + public function projects() { - return 'tests'; + return new ProjectResource( + client: $this, + ); } } - ``` -The Path property allows you to set the uri path for this resource, and the static name method is how this resource is stored on the container. +We return a new instance of our resource classes, passing through your SDK as a `client`. This is so that each resource is able to talk through the client to send requests. -To add this resource to the SDK, you can use the add method: +Now, let's look at how to structure a resource. ```php -$sdk->add( - name: TestResource::name(), - resource: TestResource::class, -); +final class ProjectResource +{ + // +} ``` -Internally this will add the resource onto container and inject the SDK into the constructor, allowing you to access the Http Client and other aspects of the SDK. +To save time, there are a collection of traits that you can use on your resources. -### Calling a Resource +- `CanAccessClient` - which will add the default constructor required for a resource. +- `CanCreateDataObjects` - which will allow you to create DataObjects from API responses. +- `CanCreateRequests` - which will allow you to create HTTP requests and payloads using PSR-17 Factories. -Now that you have added a resource to the SDK, you are able to call it using the PHP magic __get method: +Let's look at an example of a full resource class. ```php -$response = $sdk->tests->get(); -``` - -This will return a nice PSR-7 response for you to work with inside your SDK code. - -### API +use Exception; +use JustSteveKing\Sdk\Concerns\Resources; +use JustSteveKing\Tools\Http\Enums\Method; +use Ramsey\Collection\Collection; +use Throwable; -The below documents the API of the PHP-SDK: +final class ProjectResource +{ + use Resources\CanAccessClient; + use Resources\CanCreateDataObjects; + use Resources\CanCreateRequests; -#### SDK class + public function all(): Collection + { + $request = $this->request( + method: Method::GET, + uri: '/projects', + ); -Your own SDK class should extend the base SDK class for easier integration. + try { + $response = $this->client->send( + request: $request, + ); + } catch (Throwable $exception) { + throw new Exception( + message: 'Failed to list test.', + code: $exception->getCode(), + previous: $exception, + ); + } + + return (new Collection( + collectionType: Project::class, + data: array_map( + callback: static fn(array $data): Project => Project::make( + data: $data, + ), + array: (array) json_decode( + json: $response->getBody()->getContents(), + associative: true, + flags: JSON_THROW_ON_ERROR, + ), + ), + )); + } +} +``` -- `__construct(URI $uri, HttpClient $client, Container $container, null|StrategyInterface $strategy)` **The SDK constructor.** -- `static build(string $uri, null|StrategyInterface $strategy = null, null|Container = null): SDK` **This static build method allows the defaults to be set for you to get an SDK quickly.** -- `add(string $name, string $resource): self` **The add method allows you to add resources onto the SDK container, and checks that the resource being passed extends the AbstractResource and implements the ResourceContract.** -- `uri(): Uri` **Return the setup instance of UriBuilder that is being used, to allow you to manipulate the URI string.** -- `client(): HttpClient` **Return the setup instance of the HttpClient that is being used, to allow you to enforce any changes that are required.** -- `strategy(): StrategyInterface` **Returns the setup instance of the Authentication Strategy that is being used, to allow you to export the auth header array.** -- `container(): Container` **Returns the setup instance of the Container, to allow you to bind make and work with the container directly should it be required.** -- `__get(string $name): ResourceContract` **Returns a build instance of the called Resource if it has been added to the container.** +We start by creating a request, and then try to get a response by sending it through the client. -#### AbstractResource class +Once we have a response, we create a `Collection` thanks to a package by Ben Ramsey. We pass through the type of each item we expect it to be, +then the data as an array. To create the data we map over the response content and statically create a new Data Object. -Your resources must all extend the Abstract Resource class. +This allows us to keep our code clean, concise, and testable. -- `__construct(SDK $sdk, null|string $path = null, string $authHeader = 'Bearer', array $with = [], array $relations = [], bool $strictRelations = false, null|string $load = null)` **The Resource constructor.** -- `sdk(): SDK` **Return the setup instance of the SDK that has been passed through to the resource.** -- `getWith(): array` **Return an array of relations to sideload onto the request.** -- `getLoad(): string|null` **Return a string or null if a specific resource identifier has been passed in.** -- `load(string|int $identifier): self` **The load method allows you to set a specific resource identifier to look for on an API.** -- `uri(Uri $uri): self` **The uri method allows you to completely override the URI Builder on the SDK.** -- `client(HttpClient $http): self` **The client method allows you to completely override the Http Client on the SDK.** -- `strategy(StrategyInterface $strategy): self` **The strategy method allows you to completely override the Authentication Strategy on the SDK.** -- `loadPath(): self` **Loads the path from the resource into to URI builder on the SDK.** -- `get(): ResponseInterface` **Performs a GET request to the resource path, to return a list of resources.** -- `find(string|int $identifier): ResponseInterface` **Performs a GET request to the resource path with an identifier appended to it, to return a single resource.** -- `create(array $data): ResponseInterface` **Performs a POST request to the resource path, to create a new single resource.** -- `update($identifier, array $data, string $method = 'patch'): ResponseInterface` **Performs either a PATCH or PUT request to the resource path with an identifier appended to it, to update a single resource.** -- `delete(string|int $identifier): ResponseInterface` **Performs a DELETE request to the resource path with an identifier appended to it, to remove a resource.** -- `where(string $key, $value): self` **Builds the query parameters in a famility query builder style syntax.** +## Testing -#### ResourceContract Interface +To run the test: -Your resources must implement the ResourceContract interface. +```bash +composer run test +``` -- `static name(): string` **Returns a string representation of the resource name, to allow it to be bound the the SDK container.** +## Static analysis +To run the static analysis checks: -It is highly recommended that you use all of these internally in your API to give you the ability to control the process. +```bash +composer run stan +``` -### Building an SDK +## Code Style -To build an SDK, you can simply extend the SDK like so: +To run the code style fixer: -```php -use Demo\Resources\Server; -use JustSteveKing\HttpAuth\Strategies\NullStrategy; -use JustSteveKing\HttpSlim\HttpClient; -use JustSteveKing\PhpSdk\SDK; -use JustSteveKing\UriBuilder\Uri; -use PHPFox\Container\Container; - -class MySDK extends SDK -{ - public function __construct() - { - parent::__construct( - uri: Uri::fromString( - uri: 'https://www.domain.tld', - ), - client: HttpClient::build(), - container: Container::getInstance(), - strategy: new NullStrategy()), - ); - } +```bash +composer run pint +``` - public static function boot(): MySDK - { - $client = new MySDK(); +## Refactoring - $client->add( - name: TestResource::name(), - resource: TestResource::class, - ); +To run the rector code refactoring: - return $client; - } -} +```bash +composer run refactor ``` -## Testing +## Special Thanks -TO run the test: +Without the following packages and people, this framework would have been a lot harder to build. -```bash -composer run test -``` +- [The PHP League - Object Mapper](https://github.com/thephpleague/object-mapper) +- [Ben Ramsey - Collection](https://github.com/ramsey/collection) +- [Larry Garfield - Serde](https://github.com/crell/serde) ## Credits @@ -240,4 +181,4 @@ composer run test ## LICENSE -The MIT LIcense (MIT). Please see [License File](./LICENSE) for more information. +The MIT License (MIT). Please see [License File](./LICENSE) for more information. diff --git a/banner.png b/banner.png deleted file mode 100644 index f963ea5..0000000 Binary files a/banner.png and /dev/null differ diff --git a/composer.json b/composer.json index f0a4791..b3440f5 100644 --- a/composer.json +++ b/composer.json @@ -1,57 +1,95 @@ { "name": "juststeveking/php-sdk", - "description": "A base sdk for your PHP SDKs", + "description": "A framework for building SDKs in PHP.", + "type": "library", "keywords": [ - "http", - "sdk", - "psr18", - "psr-18" + "php","sdk","sdk-framework","psr-18" ], - "type": "library", "license": "MIT", "authors": [ { - "role": "developer", "name": "Steve McDougall", "email": "juststevemcd@gmail.com", - "homepage": "https://www.juststeveking.uk/" + "homepage": "https://www.juststeveking.uk/", + "role": "Developer" + } + ], + "support": { + "issues": "https://github.com/juststeveking/php-sdk/issues" + }, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/JustSteveKing" } ], "require": { - "php": "^8.0|^8.1", - "juststeveking/http-auth-strategies": "^v1.2", - "juststeveking/http-slim": "^2.0", - "juststeveking/uri-builder": "^2.0", - "phpfox/container": "^0.3.0" + "php": "^8.3", + "ext-json": "*", + "ext-fileinfo": "*", + "crell/serde": "^1.0.1", + "juststeveking/sdk-tools": "^0.0.5", + "league/object-mapper": "dev-main", + "nyholm/psr7": "^1.8.1", + "php-http/client-common": "^2.7.1", + "php-http/discovery": "^1.19.2", + "psr/http-client": "^1.0.3", + "psr/http-client-implementation": "*", + "psr/http-factory-implementation": "*", + "psr/http-message": "^2.0", + "ramsey/collection": "^2.0", + "symfony/http-client": "^7.0" }, "require-dev": { - "nyholm/psr7": "^1.4", - "pestphp/pest": "^1.21", - "symfony/http-client": "^v6.0", - "symfony/var-dumper": "^v6.0" + "laravel/pint": "^1.13.7", + "php-http/mock-client": "^1.6", + "phpstan/phpstan": "^1.10.50", + "phpunit/phpunit": "^10.5.3", + "rector/rector": "^0.18.13", + "roave/security-advisories": "dev-latest" + }, + "provide": { + "psr-discovery/http-client-implementations": "^1.0" }, + "suggest": {}, "autoload": { "psr-4": { - "JustSteveKing\\PhpSdk\\": "src/" + "JustSteveKing\\Sdk\\": "src/" } }, "autoload-dev": { "psr-4": { - "JustSteveKing\\PhpSdk\\Tests\\": "tests/", - "Demo\\": "demo/" + "JustSteveKing\\Sdk\\Tests\\": "tests/" } }, - "scripts": { - "test": "./vendor/bin/pest" - }, - "minimum-stability": "dev", + "minimum-stability": "stable", "prefer-stable": true, "config": { - "sort-packages": true, - "preferred-install": "dist", - "optimize-autoloader": true, "allow-plugins": { - "pestphp/pest-plugin": true - } + "php-http/discovery": true + }, + "optimize-autoloader": true, + "sort-packages": true, + "classmap-authoritative": true + }, + "scripts": { + "pint": [ + "./vendor/bin/pint" + ], + "refactor": [ + "./vendor/bin/rector --debug" + ], + "stan": [ + "./vendor/bin/phpstan analyse --memory-limit=3g" + ], + "test": [ + "./vendor/bin/phpunit --testdox" + ] + }, + "scripts-descriptions": { + "pint": "Run Laravel Pint on the codebase.", + "refactor": "Run Rector on the codebase.", + "stan": "Run PhpStan on the codebase.", + "test": "Run PhpUnit on the testsuite." } } diff --git a/demo/Forge.php b/demo/Forge.php deleted file mode 100644 index 556f345..0000000 --- a/demo/Forge.php +++ /dev/null @@ -1,48 +0,0 @@ -add( - name: Server::name(), - resource: Server::class, - ); - - return $client; - } -} diff --git a/demo/Resources/Post.php b/demo/Resources/Post.php deleted file mode 100644 index eb4f329..0000000 --- a/demo/Resources/Post.php +++ /dev/null @@ -1,21 +0,0 @@ -with( - with: ['databases'], - ); - - if (!is_null($identifier)) { - $this->load( - identifier: $identifier, - ); - } - - return $this; - } - - /** - * @return string - */ - public static function name(): string - { - return 'servers'; - } -} diff --git a/examples/client.php b/examples/client.php deleted file mode 100644 index edff79d..0000000 --- a/examples/client.php +++ /dev/null @@ -1,32 +0,0 @@ -add( - name: Post::name(), - resource: Post::class, -); - -$response = $sdk->posts->get(); // Http Response (psr-7) diff --git a/examples/forge.php b/examples/forge.php deleted file mode 100644 index c4b432d..0000000 --- a/examples/forge.php +++ /dev/null @@ -1,15 +0,0 @@ -servers->with(['databases'])->load('418503')->find("407126")->getBody()->getContents(); - -// Shortcut implementation -$databases = $forge->servers->databases('418503')->find("407126")->getBody()->getContents(); diff --git a/phpstan.neon.dist b/phpstan.neon.dist new file mode 100644 index 0000000..a328a75 --- /dev/null +++ b/phpstan.neon.dist @@ -0,0 +1,9 @@ +parameters: + paths: + - src/ + + level: 9 + + checkGenericClassInNonGenericObjectType: false + + ignoreErrors: diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 7adcf61..a680ad0 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,29 +1,24 @@ - - - - ./src - - - - ./tests + + tests + + + + src + + diff --git a/pint.json b/pint.json new file mode 100644 index 0000000..4a66495 --- /dev/null +++ b/pint.json @@ -0,0 +1,81 @@ +{ + "preset": "per", + "rules": { + "align_multiline_comment": true, + "array_indentation": true, + "array_syntax": true, + "blank_line_after_namespace": true, + "blank_line_after_opening_tag": true, + "combine_consecutive_issets": true, + "combine_consecutive_unsets": true, + "concat_space": { + "spacing": "one" + }, + "declare_parentheses": true, + "declare_strict_types": true, + "explicit_string_variable": true, + "final_class": false, + "fully_qualified_strict_types": true, + "global_namespace_import": { + "import_classes": true, + "import_constants": true, + "import_functions": true + }, + "is_null": true, + "lambda_not_used_import": true, + "logical_operators": true, + "mb_str_functions": true, + "method_chaining_indentation": true, + "modernize_strpos": true, + "new_with_braces": true, + "no_empty_comment": true, + "not_operator_with_space": true, + "ordered_traits": true, + "protected_to_private": true, + "simplified_if_return": true, + "strict_comparison": true, + "ternary_to_null_coalescing": true, + "trim_array_spaces": true, + "use_arrow_functions": true, + "void_return": true, + "yoda_style": true, + "array_push": true, + "assign_null_coalescing_to_coalesce_equal": true, + "explicit_indirect_variable": true, + "method_argument_space": { + "on_multiline": "ensure_fully_multiline" + }, + "modernize_types_casting": true, + "no_superfluous_elseif": true, + "no_useless_else": true, + "nullable_type_declaration_for_default_null_value": true, + "ordered_imports": { + "sort_algorithm": "alpha" + }, + "ordered_class_elements": { + "order": [ + "use_trait", + "case", + "constant", + "constant_public", + "constant_protected", + "constant_private", + "property_public", + "property_protected", + "property_private", + "construct", + "destruct", + "magic", + "phpunit", + "method_abstract", + "method_public_static", + "method_public", + "method_protected_static", + "method_protected", + "method_private_static", + "method_private" + ], + "sort_algorithm": "none" + } + } +} diff --git a/rector.php b/rector.php new file mode 100644 index 0000000..5c49137 --- /dev/null +++ b/rector.php @@ -0,0 +1,27 @@ +paths([ + __DIR__ . '/src', + ]); + + $rectorConfig->rules([ + InlineConstructorDefaultToPropertyRector::class, + ]); + + $rectorConfig->sets([ + LevelSetList::UP_TO_PHP_83, + SetList::CODE_QUALITY, + SetList::DEAD_CODE, + SetList::EARLY_RETURN, + SetList::TYPE_DECLARATION, + SetList::PRIVATIZATION, + ]); +}; diff --git a/src/Client.php b/src/Client.php new file mode 100644 index 0000000..dbf03e0 --- /dev/null +++ b/src/Client.php @@ -0,0 +1,85 @@ + $plugins + */ + public function setup(array $plugins = []): ClientContract + { + $this->http = new PluginClient( + client: Psr18ClientDiscovery::find(), + plugins: $plugins, + ); + + return $this; + } + + /** + * Return the URL for the API. + */ + public function url(): string + { + return $this->url; + } + + /** + * Set the HTTP Client for the SDK. + */ + public function client(ClientInterface $client): ClientContract + { + $this->http = $client; + + return $this; + } + + /** + * Send an API Request. + * + * @throws ClientSetupException|ClientExceptionInterface + */ + public function send(RequestInterface $request): ResponseInterface + { + if ( ! $this->http instanceof ClientInterface) { + throw new ClientSetupException( + message: 'You have not setup the client correctly, you need to set the HTTP Client using `setup` or `client`.', + ); + } + + return $this->http->sendRequest( + request: $request, + ); + } +} diff --git a/src/Concerns/DataObjects/CanCreateInstances.php b/src/Concerns/DataObjects/CanCreateInstances.php new file mode 100644 index 0000000..d8af57d --- /dev/null +++ b/src/Concerns/DataObjects/CanCreateInstances.php @@ -0,0 +1,23 @@ + $class + * @param array $payload + */ + public static function create(string $class, array $payload): DataObjectContract + { + return (new ObjectMapperUsingReflection())->hydrateObject( + className: $class, + payload: $payload, + ); + } +} diff --git a/src/Concerns/Resources/CanAccessClient.php b/src/Concerns/Resources/CanAccessClient.php new file mode 100644 index 0000000..e1b504f --- /dev/null +++ b/src/Concerns/Resources/CanAccessClient.php @@ -0,0 +1,19 @@ + $class + */ + public function make(string $class, string $body): DataObjectContract + { + return $this->serializer->deserialize( + serialized: $body, + from: 'json', + to: $class, + ); + } +} diff --git a/src/Concerns/Resources/CanCreateRequests.php b/src/Concerns/Resources/CanCreateRequests.php new file mode 100644 index 0000000..8980bab --- /dev/null +++ b/src/Concerns/Resources/CanCreateRequests.php @@ -0,0 +1,38 @@ +createRequest( + method: $method->value, + uri: "{$this->client->url()}{$uri}", + ); + } + + /** + * Create a new Stream for sending data to the API using PSR-17 discovery. + */ + public function payload(string $payload): StreamInterface + { + return Psr17FactoryDiscovery::findStreamFactory()->createStream( + content: $payload, + ); + } +} diff --git a/src/Contracts/ClientContract.php b/src/Contracts/ClientContract.php new file mode 100644 index 0000000..da3b8e1 --- /dev/null +++ b/src/Contracts/ClientContract.php @@ -0,0 +1,40 @@ + $plugins + */ + public function setup(array $plugins = []): ClientContract; + /** + * Return the URL for the API. + */ + public function url(): string; + + /** + * Set the HTTP Client for the SDK Client. + */ + public function client(ClientInterface $client): ClientContract; + + /** + * Send an API Request. + * + * @throws ClientSetupException|ClientExceptionInterface + */ + public function send(RequestInterface $request): ResponseInterface; +} diff --git a/src/Contracts/DataObjectContract.php b/src/Contracts/DataObjectContract.php new file mode 100644 index 0000000..840e7b9 --- /dev/null +++ b/src/Contracts/DataObjectContract.php @@ -0,0 +1,24 @@ + $class + * @param array $payload + */ + public static function create(string $class, array $payload): DataObjectContract; + + /** + * Create a new DataObject. + * + * @param array $data + */ + public static function make(array $data): DataObjectContract; +} diff --git a/src/Contracts/ResourceContract.php b/src/Contracts/ResourceContract.php deleted file mode 100644 index 82db4e2..0000000 --- a/src/Contracts/ResourceContract.php +++ /dev/null @@ -1,13 +0,0 @@ -sdk; - } - - /** - * @return array - */ - public function getWith(): array - { - return $this->with; - } - - /** - * @param array $with - * @return $this - */ - public function with(array $with): self - { - if ($this->strictRelations) { - foreach ($with as $resource) { - if (! in_array($resource, $this->relations)) { - throw new RuntimeException( - message: "Cannot sideload {$resource} as it has not been registered as an available resource", - ); - } - } - } - - $this->with = $with; - - return $this; - } - - /** - * @return string|null - */ - public function getLoad(): string|null - { - return $this->load; - } - - /** - * @param string|int $identifier - * @return $this - */ - public function load(string|int $identifier): self - { - $this->load = (string) $identifier; - - return $this; - } - - /** - * @param Uri $uri - * - * @return $this - */ - public function uri(Uri $uri): self - { - $this->sdk()->uri = $uri; - - return $this; - } - - /** - * @param HttpClient $http - * - * @return $this - */ - public function client(HttpClient $http): self - { - $this->sdk()->client = $http; - - return $this; - } - - /** - * @param StrategyInterface $strategy - * - * @return $this - */ - public function strategy(StrategyInterface $strategy): self - { - $this->sdk()->strategy = $strategy; - - return $this; - } - - /** - * @return $this - */ - public function loadPath(): self - { - $this->sdk()->uri()->addPath( - path: $this->path, - ); - - return $this; - } - - /** - * @return ResponseInterface - * @throws \Psr\Http\Client\ClientExceptionInterface - * @throws \Throwable - */ - public function get(): ResponseInterface - { - return $this->loadPath()->sdk()->client()->get( - uri: $this->sdk()->uri()->toString(), - headers: $this->sdk()->strategy()->getHeader( - prefix: $this->authHeader, - ) - ); - } - - /** - * @param string|int $identifier - * @return ResponseInterface - * @throws \Psr\Http\Client\ClientExceptionInterface - * @throws \Throwable - */ - public function find(string|int $identifier): ResponseInterface - { - $this->loadPath()->sdk()->uri()->addPath( - path: "{$this->sdk()->uri()->path()}/{$identifier}", - ); - - if (! is_null($this->with)) { - $this->sdk()->uri()->addPath( - path: "{$this->sdk()->uri()->path()}/" . implode("/", $this->with), - ); - } - - if (! is_null($this->load)) { - $this->sdk()->uri()->addPath( - path: "{$this->sdk()->uri()->path()}/{$this->load}", - ); - } - - return $this->sdk()->client()->get( - uri: $this->sdk()->uri()->toString(), - headers: $this->sdk()->strategy()->getHeader( - prefix: $this->authHeader), - ); - } - - /** - * @param array $data - * @return ResponseInterface - * @throws \Psr\Http\Client\ClientExceptionInterface - * @throws \Throwable - */ - public function create(array $data): ResponseInterface - { - $this->loadPath(); - - if (! is_null($this->with)) { - $this->sdk()->uri()->addPath( - path: "{$this->sdk()->uri()->path()}/" . implode("/", $this->with), - ); - } - - return $this->sdk()->client()->post( - uri: $this->sdk()->uri()->toString(), - body: $data, - headers: $this->sdk()->strategy()->getHeader( - prefix: $this->authHeader, - ), - ); - } - - /** - * @param $identifier - * @param array $data - * @param string $method - * @return ResponseInterface - */ - public function update($identifier, array $data, string $method = 'patch'): ResponseInterface - { - $this->loadPath()->sdk()->uri()->addPath( - path: "{$this->sdk()->uri()->path()}/{$identifier}", - ); - - if (! is_null($this->with)) { - $this->sdk()->uri()->addPath( - path: "{$this->sdk()->uri()->path()}/" . implode("/", $this->with), - ); - } - - return $this->sdk()->client()->{$method}( - uri: $this->sdk()->uri()->toString(), - data: $data, - headers: $this->sdk()->strategy()->getHeader( - prefix: $this->authHeader, - ), - ); - } - - /** - * @param string|int $identifier - * @return ResponseInterface - * @throws \Psr\Http\Client\ClientExceptionInterface - * @throws \Throwable - */ - public function delete(string|int $identifier): ResponseInterface - { - $this->loadPath()->sdk()->uri()->addPath( - path: "{$this->sdk()->uri()->path()}/{$identifier}" - ); - - if (! is_null($this->with)) { - $this->sdk()->uri()->addPath( - path: "{$this->sdk()->uri()->path()}/" . implode("/", $this->with) - ); - } - - return $this->sdk()->client()->delete( - uri: $this->sdk()->uri()->toString(), - headers: $this->sdk()->strategy()->getHeader( - prefix: $this->authHeader, - ) - ); - } - - /** - * @param string $key - * @param mixed $value - * @return $this - */ - public function where(string $key, $value): self - { - $this->sdk()->uri()->addQueryParam( - key: $key, - value: $value - ); - - return $this; - } -} diff --git a/src/SDK.php b/src/SDK.php deleted file mode 100644 index 5a8eb96..0000000 --- a/src/SDK.php +++ /dev/null @@ -1,125 +0,0 @@ -implementsInterface(interface: ResourceContract::class) && - ! $reflection->isSubclassOf(class: AbstractResource::class) - ) { - throw new InvalidArgumentException( - message: "[$resource] needs to implement ResourceContract and extend the AbstractResource class.", - ); - } - } catch (Throwable) {} - - $this->container()->bind( - abstract: $name, - concrete: fn() => new $resource( - sdk: $this, - ), - ); - - return $this; - } - - /** - * @return Uri - */ - public function uri(): Uri - { - return $this->uri; - } - - /** - * @return HttpClient - */ - public function client(): HttpClient - { - return $this->client; - } - - /** - * @return StrategyInterface - */ - public function strategy(): StrategyInterface - { - return $this->strategy; - } - - /** - * @return Container - */ - public function container(): Container - { - return $this->container; - } - - /** - * @param string $name - * @return ResourceContract - * @throws BindingResolutionException|InvalidArgumentException - */ - public function __get(string $name): ResourceContract - { - if (! $this->container()->contains(abstract: $name)) { - throw new InvalidArgumentException( - message: "Resource [$name] has not been registered on the SDK.", - ); - } - - return $this->container()->make( - abstract: $name, - ); - } -} diff --git a/tests/Feature/ClientTest.php b/tests/Feature/ClientTest.php new file mode 100644 index 0000000..336d2b6 --- /dev/null +++ b/tests/Feature/ClientTest.php @@ -0,0 +1,39 @@ +createMockClient( + fixture: 'tests/list', + ); + + + $response = $client->tests()->list(); + + $this->assertInstanceOf( + expected: Collection::class, + actual: $response, + ); + + foreach ($response->toArray() as $engine) { + $this->assertInstanceOf( + expected: TestDataObject::class, + actual: $engine, + ); + } + } +} diff --git a/tests/Fixtures/tests/list.json b/tests/Fixtures/tests/list.json new file mode 100644 index 0000000..547291e --- /dev/null +++ b/tests/Fixtures/tests/list.json @@ -0,0 +1,10 @@ +[ + { + "id": "1234-1234-1234-1234", + "number": 1234 + }, + { + "id": "2345-2345-2345-2345", + "number": 2345 + } +] diff --git a/tests/PackageTestCase.php b/tests/PackageTestCase.php new file mode 100644 index 0000000..2efc967 --- /dev/null +++ b/tests/PackageTestCase.php @@ -0,0 +1,73 @@ +addResponse( + response: new Response( + status: 200, + headers: [], + body: json_encode( + value: $this->fixture( + path: $fixture, + ), + ), + ) + ); + + return (new MockSDK( + apiToken: '1234', + url: 'https://api.mock-server.com' + ))->client( + client: $mockClient, + ); + } + + /** + * @throws JsonException|InvalidArgumentException + */ + protected function fixture(string $path): array + { + $filename = __DIR__ . "/Fixtures/{$path}.json"; + + if ( ! file_exists(filename: $filename)) { + throw new InvalidArgumentException( + message: 'Failed to fetch fixture.', + ); + } + + return json_decode( + json: file_get_contents( + filename: $filename, + ), + associative: true, + flags: JSON_THROW_ON_ERROR, + ); + } +} diff --git a/tests/Pest.php b/tests/Pest.php deleted file mode 100644 index 5949c61..0000000 --- a/tests/Pest.php +++ /dev/null @@ -1,45 +0,0 @@ -in('Feature'); - -/* -|-------------------------------------------------------------------------- -| Expectations -|-------------------------------------------------------------------------- -| -| When you're writing tests, you often need to check that values meet certain conditions. The -| "expect()" function gives you access to a set of "expectations" methods that you can use -| to assert different things. Of course, you may extend the Expectation API at any time. -| -*/ - -expect()->extend('toBeOne', function () { - return $this->toBe(1); -}); - -/* -|-------------------------------------------------------------------------- -| Functions -|-------------------------------------------------------------------------- -| -| While Pest is very powerful out-of-the-box, you may have some testing code specific to your -| project that you don't want to repeat in every file. Here you can also expose helpers as -| global functions to help you to reduce the number of lines of code in your test files. -| -*/ - -function something() -{ - // .. -} diff --git a/tests/SDKTest.php b/tests/SDKTest.php deleted file mode 100644 index 5aebb15..0000000 --- a/tests/SDKTest.php +++ /dev/null @@ -1,86 +0,0 @@ -toBeInstanceOf(SDK::class); -}); - -it('can build an sdk instance using the constructor', function () { - expect( - new SDK( - uri: Uri::fromString('https://www.juststeveking.uk'), - client: HttpClient::build(), - container: Container::getInstance(), - strategy: new BasicStrategy( - authString: 'test', - ) - ) - )->toBeInstanceOf(SDK::class); -}); - -it('can access properties of the sdk', function () { - $sdk = SDK::build( - uri: 'https://www.juststeveking.uk' - ); - - expect( - $sdk->container() - )->toBeInstanceOf(Container::class); - - expect( - $sdk->strategy() - )->toBeInstanceOf(StrategyInterface::class); - - expect( - $sdk->uri() - )->toBeInstanceOf(Uri::class); - - expect( - $sdk->client() - )->toBeInstanceOf(HttpClient::class); -}); - -it('can add resources to the SDK', function () { - $sdk = SDK::build( - uri: 'https://www.juststeveking.uk', - ); - - $sdk->add( - name: ProjectResource::name(), - resource: ProjectResource::class - ); - - expect( - $sdk->container()->make( - abstract: ProjectResource::name(), - ), - )->toBeInstanceOf(ProjectResource::class); -}); - -it('can forward calls through to attached resource', function () { - $sdk = SDK::build( - uri: 'https://www.juststeveking.uk', - ); - - $sdk->add( - name: ProjectResource::name(), - resource: ProjectResource::class - ); - - expect( - $sdk->projects - )->toBeInstanceOf(ProjectResource::class); -}); diff --git a/tests/Stubs/DataObjects/TestDataObject.php b/tests/Stubs/DataObjects/TestDataObject.php new file mode 100644 index 0000000..6c2fe8a --- /dev/null +++ b/tests/Stubs/DataObjects/TestDataObject.php @@ -0,0 +1,26 @@ +request( + method: Method::GET, + uri: '/test', + ); + + try { + $response = $this->client->send( + request: $request, + ); + } catch (Throwable $exception) { + throw new Exception( + message: 'Failed to list test.', + code: $exception->getCode(), + previous: $exception, + ); + } + + return (new Collection( + collectionType: TestDataObject::class, + data: array_map( + callback: static fn(array $data): TestDataObject => TestDataObject::make( + data: $data, + ), + array: (array) json_decode( + json: $response->getBody()->getContents(), + associative: true, + flags: JSON_THROW_ON_ERROR, + ), + ), + )); + } +} diff --git a/tests/Stubs/ToDoResource.php b/tests/Stubs/ToDoResource.php deleted file mode 100644 index f0ac5ed..0000000 --- a/tests/Stubs/ToDoResource.php +++ /dev/null @@ -1,12 +0,0 @@ -assertInstanceOf( + expected: ClientContract::class, + actual: $this->newClient(), + ); + } + + #[Test] + public function it_can_setup_the_http_client(): void + { + $client = $this->newClient(); + + $this->assertNull( + actual: $client->http, + ); + + $client->setup(); + + $this->assertInstanceOf( + expected: PluginClient::class, + actual: $client->http, + ); + } + + #[Test] + public function it_can_set_a_new_http_client(): void + { + $client = $this->newClient()->setup(); + + $this->assertInstanceOf( + expected: PluginClient::class, + actual: $client->http, + ); + + $this->assertInstanceOf( + expected: MockClient::class, + actual: $client->client( + client: new MockClient(), + )->http, + ); + } +}