diff --git a/.github/workflows/phpstan.yml b/.github/workflows/phpstan.yml new file mode 100644 index 0000000..7b666df --- /dev/null +++ b/.github/workflows/phpstan.yml @@ -0,0 +1,30 @@ +name: PHPStan + +on: + push: + branches: + - 'v1' + - 'v2' + - 'v3' + pull_request: + branches: + - '*' + +jobs: + phpstan: + name: phpstan + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '8.3' + coverage: none + + - name: Install composer dependencies + uses: ramsey/composer-install@v2 + + - name: Run PHPStan + run: ./vendor/bin/phpstan analyse --error-format=github diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 1883a67..2b35931 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -20,7 +20,7 @@ jobs: fail-fast: true matrix: os: [ubuntu-latest, windows-latest] - php: [8.1, 8.2] + php: [8.1, 8.2, 8.3] laravel: [10.*] stability: [prefer-lowest, prefer-stable] include: diff --git a/composer.json b/composer.json index 9579d9f..45de03e 100644 --- a/composer.json +++ b/composer.json @@ -1,10 +1,10 @@ { "name": "saloonphp/laravel-plugin", - "description": "Laravel plugin for Saloon", + "description": "The official Laravel plugin for Saloon", "license": "MIT", "type": "library", "keywords": [ - "sammyjo20", + "saloonphp", "saloon", "sdk", "api", @@ -21,15 +21,14 @@ "require": { "php": "^8.1", "illuminate/console": "^10.0", - "illuminate/http": "^10.0", "illuminate/support": "^10.0", - "saloonphp/saloon": "^3.0" + "saloonphp/saloon": "^3.5" }, "require-dev": { - "friendsofphp/php-cs-fixer": "^3.5", - "orchestra/testbench": "^v7.30 || ^v8.10", - "pestphp/pest": "^v1.23", - "phpstan/phpstan": "^1.9" + "friendsofphp/php-cs-fixer": "^3.48", + "orchestra/testbench": "^8.21", + "pestphp/pest": "^2.32", + "phpstan/phpstan": "^1.10.56" }, "minimum-stability": "stable", "autoload": { diff --git a/src/Casts/EncryptedOAuthAuthenticatorCast.php b/src/Casts/EncryptedOAuthAuthenticatorCast.php index a85c26c..581690f 100644 --- a/src/Casts/EncryptedOAuthAuthenticatorCast.php +++ b/src/Casts/EncryptedOAuthAuthenticatorCast.php @@ -24,10 +24,8 @@ public function get($model, string $key, $value, array $attributes): ?OAuthAuthe /** * Prepare the given value for storage. - * - * @return mixed|void */ - public function set($model, string $key, $value, array $attributes) + public function set($model, string $key, $value, array $attributes): ?string { if (is_null($value)) { return null; diff --git a/src/Casts/OAuthAuthenticatorCast.php b/src/Casts/OAuthAuthenticatorCast.php index dd0ed16..1aeb8ba 100644 --- a/src/Casts/OAuthAuthenticatorCast.php +++ b/src/Casts/OAuthAuthenticatorCast.php @@ -24,10 +24,8 @@ public function get($model, string $key, $value, array $attributes): ?OAuthAuthe /** * Prepare the given value for storage. - * - * @return mixed|void */ - public function set($model, string $key, $value, array $attributes) + public function set($model, string $key, $value, array $attributes): ?string { if (is_null($value)) { return null; diff --git a/src/Console/Commands/ListCommand.php b/src/Console/Commands/ListCommand.php index 2ad7cd2..feec856 100644 --- a/src/Console/Commands/ListCommand.php +++ b/src/Console/Commands/ListCommand.php @@ -26,7 +26,7 @@ class ListCommand extends Command /** * Execute the console command. */ - public function handle() + public function handle(): int { $this->newLine(); @@ -63,43 +63,84 @@ public function handle() $this->newLine(); } - return Command::SUCCESS; + return self::SUCCESS; } - + /** + * @return array + */ protected function getIntegrations(): array { - return glob(config('saloon.integrations_path').'/*') ?? []; + if (! $files = glob(config('saloon.integrations_path').'/*')) { + return []; + } + + return $files; } + /** + * @return array + */ protected function getIntegrationConnectors(string $integration): array { - return glob($integration . '/*Connector.php') ?? []; + if (! $files = glob($integration . '/*Connector.php')) { + return []; + } + + return $files; } + /** + * @return array + */ protected function getIntegrationRequests(string $integration): array { - return glob($integration . '/Requests/*') ?? []; + if (! $files = glob($integration . '/Requests/*')) { + return []; + } + + return $files; } + /** + * @return array + */ protected function getIntegrationPlugins(string $integration): array { - return glob($integration . '/Plugins/*') ?? []; + if (! $files = glob($integration . '/Plugins/*')) { + return []; + } + + return $files; } + /** + * @return array + */ protected function getIntegrationResponses(string $integration): array { - return glob($integration . '/Responses/*') ?? []; + if (! $files = glob($integration . '/Responses/*')) { + return []; + } + + return $files; } + /** + * @return array + */ protected function getIntegrationAuthenticators(string $integration): array { - return glob($integration . '/Auth/*') ?? []; + if (! $files = glob($integration . '/Auth/*')) { + return []; + } + + return $files; } - protected function getIntegrationOutput(string $integration) + protected function getIntegrationOutput(string $integration): void { - return $this->components->twoColumnDetail( + $this->components->twoColumnDetail( '' . Str::afterLast($integration, '/') . '', sprintf( 'Authenticators: %s / Connectors: %s / Requests: %s / Plugins: %s / Responses: %s', @@ -112,22 +153,22 @@ protected function getIntegrationOutput(string $integration) ); } - protected function getIntegrationAuthenticatorOutput(string $authenticator) + protected function getIntegrationAuthenticatorOutput(string $authenticator): void { - return $this->components->twoColumnDetail( + $this->components->twoColumnDetail( 'Authenticator ... ' . Str::afterLast($authenticator, '/') ); } - protected function getIntegrationConnectorOutput(string $connector) + protected function getIntegrationConnectorOutput(string $connector): void { - return $this->components->twoColumnDetail( + $this->components->twoColumnDetail( 'Connector ....... ' . Str::afterLast($connector, '/'), '' . $this->getIntegrationConnectorBaseUrl($connector) . '' ); } - protected function getIntegrationRequestOutput(string $request) + protected function getIntegrationRequestOutput(string $request): void { $requestMethod = Str::afterLast($this->getIntegrationRequestMethod($request), ':'); @@ -138,7 +179,7 @@ protected function getIntegrationRequestOutput(string $request) default => 'magenta' }; - return $this->components->twoColumnDetail( + $this->components->twoColumnDetail( 'Request ......... ' . Str::afterLast($request, '/'), ' ' . $this->getIntegrationRequestEndpoint($request) . '' . @@ -147,29 +188,45 @@ protected function getIntegrationRequestOutput(string $request) ); } - protected function getIntegrationPluginOutput(string $plugin) + + protected function getIntegrationPluginOutput(string $plugin): void { - return $this->components->twoColumnDetail( + $this->components->twoColumnDetail( 'Plugin .......... ' . Str::afterLast($plugin, '/') ); } - protected function getIntegrationResponseOutput(string $response) + + protected function getIntegrationResponseOutput(string $response): void { - return $this->components->twoColumnDetail( + $this->components->twoColumnDetail( 'Response ........ ' . Str::afterLast($response, '/') ); } protected function getIntegrationRequestMethod(string $request): string { - return Str::match('/\$method\s*=\s*(.*?);/', file_get_contents($request)); + $contents = file_get_contents($request); + + if ($contents === false) { + $contents = ''; + } + + return Str::match('/\$method\s*=\s*(.*?);/', $contents); } + protected function getIntegrationRequestEndpoint(string $request): string { + $contents = file_get_contents($request); + + if ($contents === false) { + $contents = ''; + } + $regex = '/public\s+function\s+resolveEndpoint\(\):\s+string\s*\{\s*return\s+(.*?);/s'; - $match = Str::match($regex, file_get_contents($request)); + + $match = Str::match($regex, $contents); $matchSegments = explode('/', $match); foreach ($matchSegments as $key => $matchSegment) { @@ -184,10 +241,17 @@ protected function getIntegrationRequestEndpoint(string $request): string return str_replace('\'', '', implode('/', $matchSegments)); } + protected function getIntegrationConnectorBaseUrl(string $connector): string { + $contents = file_get_contents($connector); + + if ($contents === false) { + $contents = ''; + } + $regex = '/public\s+function\s+resolveBaseUrl\(\):\s+string\s*\{\s*return\s+\'(.*?)\';\s*/s'; - $matches = Str::match($regex, file_get_contents($connector)); + $matches = Str::match($regex, $contents); return Str::after($matches, '://'); } diff --git a/src/Console/Commands/MakeConnector.php b/src/Console/Commands/MakeConnector.php index d0c2ed7..e9cafe0 100644 --- a/src/Console/Commands/MakeConnector.php +++ b/src/Console/Commands/MakeConnector.php @@ -45,7 +45,12 @@ protected function resolveStubName(): string : 'saloon.connector.stub'; } - protected function getOptions() + /** + * Get the options for making a connector + * + * @return array> + */ + protected function getOptions(): array { return [ ['oauth', null, InputOption::VALUE_NONE, 'Whether the connector should include the OAuth boilerplate'], diff --git a/src/Console/Commands/MakeRequest.php b/src/Console/Commands/MakeRequest.php index 74020fd..c4bc5ee 100644 --- a/src/Console/Commands/MakeRequest.php +++ b/src/Console/Commands/MakeRequest.php @@ -6,6 +6,7 @@ use Saloon\Enums\Method; use Illuminate\Support\Arr; +use InvalidArgumentException; use function Laravel\Prompts\select; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Input\InputInterface; @@ -49,6 +50,11 @@ class MakeRequest extends MakeCommand */ protected $stub = 'saloon.request.stub'; + /** + * Get the options for making a request + * + * @return array> + */ protected function getOptions(): array { return [ @@ -82,8 +88,14 @@ protected function afterPromptingForMissingArguments(InputInterface $input, Outp */ protected function buildClass($name): MakeRequest|string { + $method = $this->option('method') ?? 'GET'; + + if (! is_string($method)) { + throw new InvalidArgumentException('The method option must be a string.'); + } + $stub = $this->files->get($this->getStub()); - $stub = $this->replaceMethod($stub, $this->option('method') ?? 'GET'); + $stub = $this->replaceMethod($stub, $method); return $this->replaceNamespace($stub, $name)->replaceClass($stub, $name); } diff --git a/src/Facades/Saloon.php b/src/Facades/Saloon.php index cdc000c..47039e6 100644 --- a/src/Facades/Saloon.php +++ b/src/Facades/Saloon.php @@ -5,10 +5,12 @@ namespace Saloon\Laravel\Facades; use Saloon\Http\Response; +use Illuminate\Support\Facades\Facade; use Saloon\Laravel\Http\Faking\MockClient; -use Illuminate\Support\Facades\Facade as BaseFacade; /** + * @deprecated You should use MockClient::global() instead. This facade will be removed in Saloon v4. + * * @see \Saloon\Laravel\Saloon * * @method static MockClient fake(array $responses) @@ -25,7 +27,7 @@ * @method static \Saloon\Http\Response[] getRecordedResponses() * @method static \Saloon\Http\Response getLastRecordedResponse() */ -class Saloon extends BaseFacade +class Saloon extends Facade { /** * Register the Saloon facade diff --git a/src/Http/Faking/MockClient.php b/src/Http/Faking/MockClient.php index 6d0988e..556862c 100644 --- a/src/Http/Faking/MockClient.php +++ b/src/Http/Faking/MockClient.php @@ -6,6 +6,9 @@ use Saloon\Http\Faking\MockClient as BaseMockClient; +/** + * @deprecated You should use MockClient::global() instead. This class will be removed in Saloon v4. + */ class MockClient extends BaseMockClient { /** @@ -18,7 +21,6 @@ class MockClient extends BaseMockClient * * @param array<\Saloon\Http\Faking\MockResponse|\Saloon\Http\Faking\Fixture|callable> $responses * @return $this - * @throws \Saloon\Exceptions\InvalidMockResponseCaptureMethodException */ public function startMocking(array $responses = []): self { diff --git a/src/Saloon.php b/src/Saloon.php index 135746b..7c8dec9 100644 --- a/src/Saloon.php +++ b/src/Saloon.php @@ -5,8 +5,11 @@ namespace Saloon\Laravel; use Saloon\Http\Response; -use Saloon\Laravel\Http\Faking\MockClient; +use Saloon\Http\Faking\MockClient; +/** + * @deprecated You should use MockClient::global() instead. This class will be removed in Saloon v4. + */ class Saloon { /** @@ -30,25 +33,22 @@ class Saloon * Start mocking! * * @param array<\Saloon\Http\Faking\MockResponse|\Saloon\Http\Faking\Fixture|callable> $responses - * @throws \Saloon\Exceptions\InvalidMockResponseCaptureMethodException */ public static function fake(array $responses): MockClient { - return MockClient::resolve()->startMocking($responses); + return MockClient::global($responses); } /** - * Retrieve the mock client from the container + * Retrieve the global mock client */ public static function mockClient(): MockClient { - return MockClient::resolve(); + return MockClient::global(); } /** * Assert that a given request was sent. - * - * @throws \ReflectionException */ public static function assertSent(string|callable $value): void { @@ -57,8 +57,6 @@ public static function assertSent(string|callable $value): void /** * Assert that a given request was not sent. - * - * @throws \ReflectionException */ public static function assertNotSent(string|callable $value): void { @@ -69,7 +67,6 @@ public static function assertNotSent(string|callable $value): void * Assert JSON data was sent * * @param array $data - * @throws \ReflectionException */ public static function assertSentJson(string $request, array $data): void { diff --git a/src/SaloonServiceProvider.php b/src/SaloonServiceProvider.php index 6752c86..fe02381 100644 --- a/src/SaloonServiceProvider.php +++ b/src/SaloonServiceProvider.php @@ -16,6 +16,7 @@ use Saloon\Laravel\Console\Commands\MakeConnector; use Saloon\Laravel\Http\Middleware\MockMiddleware; use Saloon\Laravel\Http\Middleware\RecordResponse; +use Saloon\Http\Faking\MockClient as BaseMockClient; use Saloon\Laravel\Http\Middleware\SendRequestEvent; use Saloon\Laravel\Http\Middleware\SendResponseEvent; use Saloon\Laravel\Console\Commands\MakeAuthenticator; @@ -24,10 +25,8 @@ class SaloonServiceProvider extends ServiceProvider { /** * Register any application services. - * - * @return void */ - public function register() + public function register(): void { $this->mergeConfigFrom( __DIR__ . '/../config/saloon.php', @@ -37,8 +36,6 @@ public function register() /** * Handle the booting of the service provider. - * - * @throws \Saloon\Exceptions\DuplicatePipeNameException */ public function boot(): void { @@ -69,6 +66,10 @@ public function boot(): void Saloon::$registeredDefaults = true; } + + // Destroy global mock client to prevent leaky tests + + BaseMockClient::destroyGlobal(); } /**