Skip to content
Draft
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
8 changes: 7 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@
"require-dev": {
"roave/security-advisories": "dev-latest",
"orchestra/testbench": "^9.1",
"mockery/mockery": "^1.6"
"mockery/mockery": "^1.6",
"phpstan/phpstan": "^2.1"
},
"autoload": {
"psr-4": {
Expand All @@ -31,5 +32,10 @@
"EXACTSports\\Spotify\\SpotifyServiceProvider"
]
}
},
"scripts": {
"stan": [
"vendor/bin/phpstan analyse --configuration=phpstan.neon.dist"
]
}
}
3,109 changes: 1,366 additions & 1,743 deletions composer.lock

Large diffs are not rendered by default.

5 changes: 5 additions & 0 deletions phpstan.neon.dist
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
parameters:
level: 5
paths:
- src
treatPhpDocTypesAsCertain: false
10 changes: 7 additions & 3 deletions src/Client/SpotifyClient.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
use EXACTSports\Spotify\Request\TopItemsRequest;
use EXACTSports\Spotify\Response\BaseSpotifyResponse;
use EXACTSports\Spotify\Response\TracksResponse;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Auth;

class SpotifyClient
{
Expand All @@ -42,7 +44,7 @@ public function searchTracks(SearchTrackRequestDto $requestDto): TracksResponse
} catch (SpotifyTokenExpiredException) {
$data = (new SearchTrackRequest($requestDto, $this->getHeaders($this->getUser(), true)))->execute()->getData();
}
return new TracksResponse(\Arr::get($data, 'tracks.items', []));
return new TracksResponse(Arr::get($data, 'tracks.items', []));
}

public function getArtist(string $id): BaseSpotifyResponse
Expand Down Expand Up @@ -83,8 +85,10 @@ public function addTrackToPlaylist(TrackToPlaylistDto $trackToPlaylistDto): Base

private function getUser(): SpotifyUserInterface
{
$user = \Auth::user();
/**@var SpotifyUserInterface $user * */
$user = Auth::user();
if (!$user instanceof SpotifyUserInterface) {
throw new SpotifyConnectionException("User doesn't implement SpotifyUserInterface");
}
return $user;
}

Expand Down
8 changes: 5 additions & 3 deletions src/Facade/SpotifyHttpClient.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@

namespace EXACTSports\Spotify\Facade;

use EXACTSports\Spotify\Response\BaseSpotifyResponse;
use EXACTSports\Spotify\Response\ResponseInterface;
use Illuminate\Support\Facades\Facade;

/**
* @method static getApiCall(string $endpoint, array $headers)
* @method static postApiCall(string $endpoint, array $headers, array $bodyParams)
* @method static postAccountCall(string $endpoint, array $headers, array $bodyParams)
* @method static ResponseInterface getApiCall(string $endpoint, array $headers)
* @method static ResponseInterface postApiCall(string $endpoint, array $headers, array $bodyParams = [])
* @method static ResponseInterface postAccountCall(string $endpoint, array $headers, array $bodyParams = [])
*/
class SpotifyHttpClient extends Facade
{
Expand Down
1 change: 1 addition & 0 deletions src/HttpClientService.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ public function getApiCall(string $endpoint, array $headers): ResponseInterface
return new BaseSpotifyResponse($content);
} catch (Exception $e) {
$this->handleException($e);
return new BaseSpotifyResponse([]);
}
}

Expand Down
10 changes: 9 additions & 1 deletion src/Request/AddTrackToPlaylistRequest.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
use EXACTSports\Spotify\Facade\SpotifyHttpClient;
use EXACTSports\Spotify\Request\Dto\TrackToPlaylistDto;
use EXACTSports\Spotify\Response\BaseSpotifyResponse;
use EXACTSports\Spotify\Response\ResponseInterface;

class AddTrackToPlaylistRequest implements RequestInterface
{
Expand All @@ -16,11 +17,18 @@ public function __construct(private TrackToPlaylistDto $trackToPlaylistDto, priv

public function execute(): BaseSpotifyResponse
{
return SpotifyHttpClient::postApiCall(
$response = SpotifyHttpClient::postApiCall(
'v1/playlists/'.$this->trackToPlaylistDto->playlistId.'/tracks',
$this->headers->toArray(),
$this->trackToPlaylistDto->toArray()
);
if ($response instanceof BaseSpotifyResponse) {
return $response;
} elseif ($response instanceof ResponseInterface) {
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since BaseSpotifyResponse implements ResponseInterface, we can change these if statements to just if $response instanceof ResponseInterface

return new BaseSpotifyResponse($response->getData());
} else {
return new BaseSpotifyResponse([]);
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should throw an exception here rather than an empty response

}

}
}
10 changes: 9 additions & 1 deletion src/Request/CreateNewPlaylistRequest.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
use EXACTSports\Spotify\Facade\SpotifyHttpClient;
use EXACTSports\Spotify\Request\Dto\NewPlaylistDto;
use EXACTSports\Spotify\Response\BaseSpotifyResponse;
use EXACTSports\Spotify\Response\ResponseInterface;

class CreateNewPlaylistRequest implements RequestInterface
{
Expand All @@ -16,11 +17,18 @@ public function __construct(private NewPlaylistDto $newPlaylistDto, private Spot

public function execute(): BaseSpotifyResponse
{
return SpotifyHttpClient::postApiCall(
$response = SpotifyHttpClient::postApiCall(
'v1/users/' . $this->newPlaylistDto->spotifyId . '/playlists',
$this->headers->toArray(),
$this->newPlaylistDto->toArray()
);
if ($response instanceof BaseSpotifyResponse) {
return $response;
} elseif ($response instanceof ResponseInterface) {
return new BaseSpotifyResponse($response->getData());
} else {
return new BaseSpotifyResponse([]);
}
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same deal as AddTrackTo....


}
}
10 changes: 9 additions & 1 deletion src/Request/GetArtistRequest.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
use EXACTSports\Spotify\Client\SpotifyHeaders;
use EXACTSports\Spotify\Facade\SpotifyHttpClient;
use EXACTSports\Spotify\Response\BaseSpotifyResponse;
use EXACTSports\Spotify\Response\ResponseInterface;

class GetArtistRequest implements RequestInterface
{
Expand All @@ -17,6 +18,13 @@ public function __construct(
public function execute(): BaseSpotifyResponse
{
$endpoint = 'v1/artists/' . $this->id;
return SpotifyHttpClient::getApiCall($endpoint, $this->headers->toArray());
$response = SpotifyHttpClient::getApiCall($endpoint, $this->headers->toArray());
if ($response instanceof BaseSpotifyResponse) {
return $response;
} elseif ($response instanceof ResponseInterface) {
return new BaseSpotifyResponse($response->getData());
} else {
return new BaseSpotifyResponse([]);
}
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as the comment for the other if statements

}
}
11 changes: 10 additions & 1 deletion src/Request/GetTrackRequest.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
use EXACTSports\Spotify\Client\SpotifyHeaders;
use EXACTSports\Spotify\Facade\SpotifyHttpClient;
use EXACTSports\Spotify\Response\BaseSpotifyResponse;
use EXACTSports\Spotify\Response\ResponseInterface;

class GetTrackRequest implements RequestInterface
{
Expand All @@ -17,6 +18,14 @@ public function __construct(
public function execute(): BaseSpotifyResponse
{
$endpoint = 'v1/tracks/' . $this->id;
return SpotifyHttpClient::getApiCall($endpoint, $this->headers->toArray());
$response = SpotifyHttpClient::getApiCall($endpoint, $this->headers->toArray());

if ($response instanceof BaseSpotifyResponse) {
return $response;
} elseif ($response instanceof ResponseInterface) {
return new BaseSpotifyResponse($response->getData());
} else {
return new BaseSpotifyResponse([]);
}
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same

}
}
4 changes: 3 additions & 1 deletion src/Request/RefreshTokenRequest.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
use EXACTSports\Spotify\Facade\SpotifyHttpClient;
use EXACTSports\Spotify\Contracts\SpotifyUserInterface;
use EXACTSports\Spotify\Response\RefreshTokenResponse;
use EXACTSports\Spotify\Response\ResponseInterface;

final readonly class RefreshTokenRequest implements RequestInterface
{
Expand All @@ -32,7 +33,8 @@ public function execute(): RefreshTokenResponse

try {
$response = SpotifyHttpClient::postAccountCall('api/token', $headers->toArray(), $formParams);
$newAccessToken = $response->getData()['access_token'] ?? null;
$responseData = $response instanceof ResponseInterface ? $response->getData() : [];
$newAccessToken = $responseData['access_token'] ?? null;
$this->user->renewSpotifyToken($newAccessToken);
return new RefreshTokenResponse($newAccessToken);
} catch (\Throwable $throwable) {
Expand Down
10 changes: 9 additions & 1 deletion src/Request/SearchTrackRequest.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
use EXACTSports\Spotify\Facade\SpotifyHttpClient;
use EXACTSports\Spotify\Request\Dto\SearchTrackRequestDto;
use EXACTSports\Spotify\Response\BaseSpotifyResponse;
use EXACTSports\Spotify\Response\ResponseInterface;

class SearchTrackRequest implements RequestInterface
{
Expand All @@ -20,6 +21,13 @@ public function execute(): BaseSpotifyResponse
$endpoint = 'v1/search?query=' . $this->requestDto->search .
'&type=track&limit=' . $this->requestDto->limit .
'&include_external=' . $this->requestDto->includeExternal;
return SpotifyHttpClient::getApiCall($endpoint, $this->headers->toArray());
$response = SpotifyHttpClient::getApiCall($endpoint, $this->headers->toArray());
if ($response instanceof BaseSpotifyResponse) {
return $response;
} elseif ($response instanceof ResponseInterface) {
return new BaseSpotifyResponse($response->getData());
} else {
return new BaseSpotifyResponse([]);
}
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same

}
}
10 changes: 9 additions & 1 deletion src/Request/TopItemsRequest.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
use EXACTSports\Spotify\Facade\SpotifyHttpClient;
use EXACTSports\Spotify\Request\Dto\TopItemsRequestDto;
use EXACTSports\Spotify\Response\BaseSpotifyResponse;
use EXACTSports\Spotify\Response\ResponseInterface;

class TopItemsRequest implements RequestInterface
{
Expand All @@ -20,6 +21,13 @@ public function execute(): BaseSpotifyResponse
$endpoint = 'v1/me/top/tracks?limit=' . $this->requestDto->limit .
'&offset=' . $this->requestDto->offset .
'&time_range=' . $this->requestDto->timeRange;
return SpotifyHttpClient::getApiCall($endpoint, $this->headers->toArray());
$response = SpotifyHttpClient::getApiCall($endpoint, $this->headers->toArray());
if ($response instanceof BaseSpotifyResponse) {
return $response;
} elseif ($response instanceof ResponseInterface) {
return new BaseSpotifyResponse($response->getData());
} else {
return new BaseSpotifyResponse([]);
}
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same

}
}
25 changes: 21 additions & 4 deletions src/Request/Traits/SpotifyResponseCodeExceptionTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
use EXACTSports\Spotify\Exceptions\SpotifyBadResponseException;
use EXACTSports\Spotify\Exceptions\SpotifyTokenExpiredException;
use EXACTSports\Spotify\Exceptions\SpotifyUnauthorizedException;
use GuzzleHttp\Exception\RequestException;

trait SpotifyResponseCodeExceptionTrait
{
Expand All @@ -15,10 +16,26 @@ trait SpotifyResponseCodeExceptionTrait
*/
public function handleException(\Exception $exception)
{
match ($exception->getCode()) {
401 => throw new SpotifyTokenExpiredException($exception->getMessage()),
403 => throw new SpotifyUnauthorizedException($exception->getMessage()),
default => throw new SpotifyBadResponseException($exception->getMessage())
$statusCode = null;
if ($exception instanceof RequestException && $exception->hasResponse()) {
$statusCode = $exception->getResponse()->getStatusCode();
} else {
// Fallback for non-Guzzle exceptions or Guzzle exceptions without a response
// or if getCode() is more appropriate for other exception types.
$statusCode = $exception->getCode();
}

$message = $exception->getMessage();

match ($statusCode) {
401 => throw new SpotifyTokenExpiredException($message, $statusCode, $exception),
403 => throw new SpotifyUnauthorizedException($message, $statusCode, $exception),
// It's good practice to include the status code in the default message if it's an HTTP error
default => throw new SpotifyBadResponseException(
"Spotify API request failed with status code: {$statusCode}. Message: {$message}",
is_int($statusCode) ? $statusCode : 0, // Ensure code is an int for the exception constructor
$exception
)
};
}
}
4 changes: 2 additions & 2 deletions src/Response/BaseSpotifyResponse.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,12 @@

final readonly class BaseSpotifyResponse implements ResponseInterface
{
public function __construct(private mixed $data)
public function __construct(private array $data)
{

}

public function getData(): mixed
public function getData(): array
{
return $this->data;
}
Expand Down
5 changes: 5 additions & 0 deletions src/Response/RefreshTokenResponse.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,9 @@ public function getToken(): string
{
return $this->token;
}

public function getData(): array
{
return ['access_token' => $this->token];
}
}
7 changes: 6 additions & 1 deletion src/Response/ResponseInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,10 @@

interface ResponseInterface
{

/**
* Get the response data
*
* @return array
*/
public function getData(): array;
}
7 changes: 6 additions & 1 deletion src/Response/TracksResponse.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@ public function __construct(private array $tracks = [])

public function getTracks(): array
{
return $this->tracks;
return $this->tracks;
}

public function getData(): array
{
return $this->tracks;
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

two methods returning same thing?

}
}
3 changes: 2 additions & 1 deletion src/SpotifyService.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
use EXACTSports\Spotify\Request\Dto\TrackToPlaylistDto;
use EXACTSports\Spotify\Response\BaseSpotifyResponse;
use EXACTSports\Spotify\Response\TracksResponse;
use Illuminate\Support\Facades\Auth;

class SpotifyService
{
Expand Down Expand Up @@ -100,7 +101,7 @@ public function createNewPlaylist(NewPlaylistDto $newPlaylistDto): BaseSpotifyRe
*/
private function validateUserInterface(): void
{
$user = \Auth::user();
$user = Auth::user();
if (!$user instanceof SpotifyUserInterface) {
throw new MissingSpotifyConfigurationException("User doesn't implement SpotifyUserInterface");
}
Expand Down