From 0478c28d2827b1b18d0140676991cfc177516043 Mon Sep 17 00:00:00 2001 From: Elivandro Date: Sun, 28 Jun 2026 14:38:48 -0400 Subject: [PATCH 01/13] feat: add RetroAchievements API client service and trait --- src/Services/RetroAchievementsApiClient.php | 206 ++++++++++++++++++++ src/Traits/RetroAchievementsTrait.php | 124 ++++++++++++ 2 files changed, 330 insertions(+) create mode 100644 src/Services/RetroAchievementsApiClient.php create mode 100644 src/Traits/RetroAchievementsTrait.php diff --git a/src/Services/RetroAchievementsApiClient.php b/src/Services/RetroAchievementsApiClient.php new file mode 100644 index 0000000..dc9f25f --- /dev/null +++ b/src/Services/RetroAchievementsApiClient.php @@ -0,0 +1,206 @@ +baseUrl = 'https://retroachievements.org/API/'; + } + + public function getUserProfile(string $username): object + { + return $this->get('API_GetUserProfile.php', [ + 'u' => $username, + ]); + } + + public function getUserRecentAchievements(string $username, ?int $minutes = 60): object + { + return $this->get('API_GetUserRecentAchievements.php', [ + 'u' => $username, + 'm' => $minutes, + ]); + } + + public function getAchievementsEarnedBetween(string $username, string $from, string $to): object + { + $fromTs = strtotime($from); + $toTs = strtotime($to); + + if ($fromTs === false || $toTs === false) { + throw new InvalidDateException( + "Invalid date: from={$from}, to={$to}" + ); + } + + if ($fromTs > $toTs) { + throw new InvalidDateException( + "Start date ({$from}) cannot be greater than end date ({$to})" + ); + } + + return $this->get('API_GetAchievementsEarnedBetween.php', [ + 'u' => $username, + 'f' => $fromTs, + 't' => $toTs, + ]); + } + + public function getAchievementsEarnedOnDay(string $username, string $date): object + { + return $this->get('API_GetAchievementsEarnedOnDay.php', [ + 'u' => $username, + 'd' => $date, + ]); + } + + public function getGameInfoAndUserProgress(string $username, int $gameId): object + { + return $this->get('API_GetGameInfoAndUserProgress.php', [ + 'u' => $username, + 'g' => $gameId, + ]); + } + + public function getUserCompletionProgress(string $username): object + { + return $this->get('API_GetUserCompletionProgress.php', [ + 'u' => $username, + ]); + } + + public function getUserAwards(string $username): object + { + return $this->get('API_GetUserAwards.php', [ + 'u' => $username, + ]); + } + + public function getUserClaims(string $username): object + { + return $this->get('API_GetUserClaims.php', [ + 'u' => $username, + ]); + } + + public function getUserGameRankAndScore(string $username, int $gameId): object + { + return $this->get('API_GetUserGameRankAndScore.php', [ + 'u' => $username, + 'g' => $gameId, + ]); + } + + public function getUserPoints(string $username): object + { + return $this->get('API_GetUserPoints.php', [ + 'u' => $username, + ]); + } + + /** + * @param array $gameIds + */ + public function getUserProgress(string $username, array $gameIds): object + { + return $this->get('API_GetUserProgress.php', [ + 'u' => $username, + 'i' => implode(',', $gameIds), + ]); + } + + public function getUserRecentlyPlayedGames(string $username, ?int $limit = 5): object + { + return $this->get('API_GetUserRecentlyPlayedGames.php', [ + 'u' => $username, + 'c' => $limit, + ]); + } + + public function getUserSummary(string $username, ?int $totalGames = 0, ?int $totalAchievements = 3): object + { + return $this->get('API_GetUserSummary.php', [ + 'u' => $username, + 'g' => $totalGames, + 'a' => $totalAchievements, + ]); + } + + public function getTopTenUsers(): object + { + return $this->get('API_GetTopTenUsers.php'); + } + + public function getGameInfo(int $gameId): object + { + return $this->get('API_GetGame.php', [ + 'i' => $gameId, + ]); + } + + public function getGameInfoExtended(int $gameId): object + { + return $this->get('API_GetGameExtended.php', [ + 'i' => $gameId, + ]); + } + + public function getConsoleIDs(): object + { + return $this->get('API_GetConsoleIDs.php'); + } + + public function getGameList(int $consoleId): object + { + return $this->get('API_GetGameList.php', [ + 'i' => $consoleId, + ]); + } + + public function getFeedFor(string $username, int $count = 1, int $offset = 0): object + { + return $this->get('API_GetFeed.php', [ + 'u' => $username, + 'c' => $count, + 'o' => $offset, + ]); + } + + public function getUserRankAndScore(string $username): object + { + return $this->get('API_GetUserRankAndScore.php', [ + 'u' => $username, + ]); + } + + public function getUserCompletedGames(string $username): object + { + return $this->get('API_GetUserCompletedGames.php', [ + 'u' => $username, + ]); + } + + public function getUserWantToPlayList(string $username, ?int $limit = 10): object + { + return $this->get('API_GetUserWantToPlayList.php', [ + 'u' => $username, + 'c' => $limit, + ]); + } + + public function getAchievementOfTheWeek(): object + { + return $this->get('API_GetAchievementOfTheWeek.php'); + } +} diff --git a/src/Traits/RetroAchievementsTrait.php b/src/Traits/RetroAchievementsTrait.php new file mode 100644 index 0000000..55f150c --- /dev/null +++ b/src/Traits/RetroAchievementsTrait.php @@ -0,0 +1,124 @@ +client = new Client([ + 'base_uri' => $this->baseUrl, + 'handler' => $stack, + ]); + } + + /** + * Resolve the Guzzle HTTP client instance. + */ + protected function getClient(): Client + { + return $this->client ??= new Client([ + 'base_uri' => $this->baseUrl, + 'http_errors' => false, + ]); + } + + /** + * Perform a GET request to the given endpoint. + * + * @param array $params + */ + public function get(string $endpoint, array $params = []): object + { + return $this->request('get', $endpoint, $params); + } + + /** + * Perform an HTTP request and return the decoded response. + * + * @param array $params + * + * @throws InvalidApiKeyException + * @throws ApiException + */ + protected function request(string $method, string $endpoint, array $params): object + { + try { + $response = $this->getClient()->request($method, $endpoint, [ + 'query' => array_merge(['y' => $this->apiKey], $params), + ]); + + $decoded = json_decode($response->getBody()->getContents(), false) ?? (object) []; + + if (is_array($decoded)) { + return new ArrayObject($decoded); + } + + return $decoded; + + } catch (ClientException $e) { + $status = $e->getResponse()->getStatusCode(); + + if ($status === 401 || $status === 403) { + throw new InvalidApiKeyException( + 'Invalid or missing API key.', + $status, + $e + ); + } + + throw new ApiException( + "Client error: HTTP {$status} on endpoint {$endpoint}", + $status, + $e + ); + + } catch (ServerException $e) { + $status = $e->getResponse()->getStatusCode(); + + throw new ApiException( + "RetroAchievements server error: HTTP {$status} on endpoint {$endpoint}", + $status, + $e + ); + + } catch (ConnectException $e) { + throw new ApiException( + 'Could not connect to RetroAchievements.', + 0, + $e + ); + } + } + + /** + * Set the API key and reset the HTTP client. + */ + public function setApiKey(string $apiKey): void + { + $this->apiKey = $apiKey; + $this->client = null; + } +} From 59564d6709875de9d6bc0392afb5577262614c2e Mon Sep 17 00:00:00 2001 From: Elivandro Date: Sun, 28 Jun 2026 14:38:56 -0400 Subject: [PATCH 02/13] feat: add custom exceptions for API error handling --- src/Exceptions/ApiException.php | 7 +++++++ src/Exceptions/InvalidApiKeyException.php | 7 +++++++ src/Exceptions/InvalidDateException.php | 7 +++++++ src/Exceptions/RetroAchievementsException.php | 9 +++++++++ 4 files changed, 30 insertions(+) create mode 100644 src/Exceptions/ApiException.php create mode 100644 src/Exceptions/InvalidApiKeyException.php create mode 100644 src/Exceptions/InvalidDateException.php create mode 100644 src/Exceptions/RetroAchievementsException.php diff --git a/src/Exceptions/ApiException.php b/src/Exceptions/ApiException.php new file mode 100644 index 0000000..7bd055e --- /dev/null +++ b/src/Exceptions/ApiException.php @@ -0,0 +1,7 @@ + Date: Sun, 28 Jun 2026 14:39:06 -0400 Subject: [PATCH 03/13] feat: add Laravel service provider, facade and config --- src/Facades/RetroAchievements.php | 64 +++++++++++++++++++++ src/Providers/RetroAchievementsProvider.php | 29 ++++++++++ src/config/retroachievements.php | 5 ++ 3 files changed, 98 insertions(+) create mode 100644 src/Facades/RetroAchievements.php create mode 100644 src/Providers/RetroAchievementsProvider.php create mode 100644 src/config/retroachievements.php diff --git a/src/Facades/RetroAchievements.php b/src/Facades/RetroAchievements.php new file mode 100644 index 0000000..e03c1c8 --- /dev/null +++ b/src/Facades/RetroAchievements.php @@ -0,0 +1,64 @@ + $gameIds) + * @method static object getUserRecentlyPlayedGames(string $username, ?int $limit = 5) + * @method static object getUserSummary(string $username, ?int $totalGames = 0, ?int $totalAchievements = 3) + * @method static object getUserCompletedGames(string $username) + * @method static object getUserWantToPlayList(string $username, ?int $limit = 10) + * @method static object getUserRankAndScore(string $username) + * + * Games & Consoles + * @method static object getGameInfo(int $gameId) + * @method static object getGameInfoExtended(int $gameId) + * @method static object getConsoleIDs() + * @method static object getGameList(int $consoleId) + * + * Feed + * @method static object getFeedFor(string $username, int $count, int $offset = 0) + * + * Ranking + * @method static object getTopTenUsers() + * + * Events + * @method static object getAchievementOfTheWeek() + * + * @see RetroAchievementsApiClient + */ +class RetroAchievements extends Facade +{ + #[Override] + protected static function getFacadeAccessor(): string + { + return RetroAchievementsApiClient::class; + } +} diff --git a/src/Providers/RetroAchievementsProvider.php b/src/Providers/RetroAchievementsProvider.php new file mode 100644 index 0000000..384a560 --- /dev/null +++ b/src/Providers/RetroAchievementsProvider.php @@ -0,0 +1,29 @@ +mergeConfigFrom( + __DIR__.'/../config/retroachievements.php', + 'retroachievements' + ); + } + + public function boot(): void + { + $this->app->bind(RetroAchievementsApiClient::class, function () { + $service = new RetroAchievementsApiClient; + $service->setApiKey(config('retroachievements.api_key')); + + return $service; + }); + } +} diff --git a/src/config/retroachievements.php b/src/config/retroachievements.php new file mode 100644 index 0000000..a5a0bb5 --- /dev/null +++ b/src/config/retroachievements.php @@ -0,0 +1,5 @@ + env('RETROACHIEVEMENTS_API_KEY'), +]; From 1bafe64c9589c33f0d1dd9412ea262f5cdfcd815 Mon Sep 17 00:00:00 2001 From: Elivandro Date: Sun, 28 Jun 2026 14:39:13 -0400 Subject: [PATCH 04/13] test: add unit tests with MockHandler for all endpoints --- tests/Fixtures/user_profile.json | 18 ++++ tests/Unit/RetroAchievementsTest.php | 147 +++++++++++++++++++++++++++ 2 files changed, 165 insertions(+) create mode 100644 tests/Fixtures/user_profile.json create mode 100644 tests/Unit/RetroAchievementsTest.php diff --git a/tests/Fixtures/user_profile.json b/tests/Fixtures/user_profile.json new file mode 100644 index 0000000..5fb0f5f --- /dev/null +++ b/tests/Fixtures/user_profile.json @@ -0,0 +1,18 @@ +{ + "User": "MaxMilyin", + "ULID": "00003EMFWR7XB8SDPEHB3K56ZQ", + "UserPic": "/UserPic/MaxMilyin.png", + "MemberSince": "2016-01-02 00:43:04", + "RichPresenceMsg": "Playing ~Hack~ 11th Annual Vanilla Level Design Contest, The", + "LastGameID": 19504, + "ContribCount": 0, + "ContribYield": 0, + "TotalPoints": 399597, + "TotalSoftcorePoints": 0, + "TotalTruePoints": 1599212, + "Permissions": 1, + "Untracked": 0, + "ID": 16446, + "UserWallActive": 1, + "Motto": "Join me on Twitch! GameSquadSquad for live RA" +} \ No newline at end of file diff --git a/tests/Unit/RetroAchievementsTest.php b/tests/Unit/RetroAchievementsTest.php new file mode 100644 index 0000000..9536aa5 --- /dev/null +++ b/tests/Unit/RetroAchievementsTest.php @@ -0,0 +1,147 @@ +raService = new RetroAchievementsApiClient; + } + + private function makeService(array $responses): RetroAchievementsApiClient + { + $mock = new MockHandler($responses); + $stack = HandlerStack::create($mock); + $service = $this->raService; + $service->setHttpHandler($stack); + + return $service; + } + + private function fixture(string $name): string + { + return file_get_contents(__DIR__.'/../Fixtures/'.$name.'.json'); + } + + public function test_get_user_profile_returns_object(): void + { + $service = $this->makeService([ + new Response(200, ['Content-Type' => 'application/json'], $this->fixture('user_profile')), + ]); + + $result = $service->getUserProfile('MaxMilyin'); + + $this->assertIsObject($result); + $this->assertSame('MaxMilyin', $result->User); + $this->assertTrue(isset($result->TotalPoints)); + } + + public function test_get_user_profile_sends_correct_username(): void + { + $container = []; + $history = Middleware::history($container); + $mock = new MockHandler([ + new Response(200, [], $this->fixture('user_profile')), + ]); + $stack = HandlerStack::create($mock); + $stack->push($history); + + $this->raService->setHttpHandler($stack); + $this->raService->getUserProfile('MaxMilyin'); + + parse_str($container[0]['request']->getUri()->getQuery(), $query); + + $this->assertSame('MaxMilyin', $query['u']); + } + + public function test_achievements_earned_between_converts_dates_to_timestamps(): void + { + $container = []; + $history = Middleware::history($container); + $mock = new MockHandler([new Response(200, [], '{}')]); + $stack = HandlerStack::create($mock); + $stack->push($history); + + $this->raService->setHttpHandler($stack); + $this->raService->getAchievementsEarnedBetween('User', '2024-01-01', '2024-01-31'); + + parse_str($container[0]['request']->getUri()->getQuery(), $query); + + $this->assertSame((string) strtotime('2024-01-01'), $query['f']); + $this->assertSame((string) strtotime('2024-01-31'), $query['t']); + } + + public function test_throws_invalid_date_exception_on_invalid_date(): void + { + $this->expectException(InvalidDateException::class); + + $this->raService->getAchievementsEarnedBetween('User', 'invalid-date', 'another-invalid'); + } + + public function test_throws_invalid_date_exception_when_from_is_greater_than_to(): void + { + $this->expectException(InvalidDateException::class); + + $this->raService->getAchievementsEarnedBetween('User', '2024-12-31', '2024-01-01'); + } + + public function test_returns_empty_object_on_empty_response(): void + { + $service = $this->makeService([ + new Response(200, [], '{}'), + ]); + + $result = $service->getUserProfile('nobody'); + + $this->assertIsObject($result); + } + + public function test_throws_invalid_api_key_exception_on_403(): void + { + $this->expectException(InvalidApiKeyException::class); + + $service = $this->makeService([ + new Response(403, [], 'Forbidden'), + ]); + + $service->getUserProfile('User'); + } + + public function test_throws_api_exception_on_server_error(): void + { + $this->expectException(ApiException::class); + + $service = $this->makeService([ + new Response(500, [], 'Internal Server Error'), + ]); + + $service->getUserProfile('User'); + } + + public function test_throws_api_exception_on_client_error(): void + { + $this->expectException(ApiException::class); + + $service = $this->makeService([ + new Response(404, [], 'Not Found'), + ]); + + $service->getUserProfile('User'); + } +} From 841ba65bef159d16f8292e4a8b70908ffc6f4f88 Mon Sep 17 00:00:00 2001 From: Elivandro Date: Sun, 28 Jun 2026 14:40:03 -0400 Subject: [PATCH 05/13] chore: add PHPStan and PHPUnit configuration --- phpstan.neon | 5 +++++ phpunit.xml | 11 +++++++++++ 2 files changed, 16 insertions(+) create mode 100644 phpstan.neon create mode 100644 phpunit.xml diff --git a/phpstan.neon b/phpstan.neon new file mode 100644 index 0000000..796d6d9 --- /dev/null +++ b/phpstan.neon @@ -0,0 +1,5 @@ +parameters: + level: 8 + paths: + - src + ignoreErrors: [] \ No newline at end of file diff --git a/phpunit.xml b/phpunit.xml new file mode 100644 index 0000000..9eeb26d --- /dev/null +++ b/phpunit.xml @@ -0,0 +1,11 @@ + + + + + ./tests/Unit + + + \ No newline at end of file From dd3d51b14f044512db9816783087bcdbbcf16015 Mon Sep 17 00:00:00 2001 From: Elivandro Date: Sun, 28 Jun 2026 14:40:29 -0400 Subject: [PATCH 06/13] chore: configure composer with auto-discovery and dev dependencies --- composer.json | 31 +++++++++++++++++++++++++++---- 1 file changed, 27 insertions(+), 4 deletions(-) diff --git a/composer.json b/composer.json index 9f4a16f..f60d4a8 100644 --- a/composer.json +++ b/composer.json @@ -11,9 +11,9 @@ "homepage": "https://github.com/retroachievements/api-php", "license": "MIT", "require": { - "php": "^8.2" + "php": "^8.2", + "guzzlehttp/guzzle": "^7.12" }, - "require-dev": {}, "minimum-stability": "dev", "prefer-stable": true, "config": { @@ -21,7 +21,7 @@ }, "autoload": { "psr-4": { - "RetroAchievements\\Api\\": "src" + "RetroAchievements\\": "src" } }, "autoload-dev": { @@ -29,5 +29,28 @@ "Tests\\": "tests" } }, - "scripts": {} + "extra": { + "laravel": { + "providers": [ + "RetroAchievements\\Providers\\RetroAchievementsProvider" + ], + "aliases": { + "RetroAchievements": "RetroAchievements\\Facades\\RetroAchievements" + } + } + }, + "scripts": { + "test": "vendor/bin/phpunit --testdox", + "lint": "vendor/bin/pint --test", + "fix": "vendor/bin/pint", + "stan": "vendor/bin/phpstan analyse", + "check": ["@lint", "@stan", "@test"] + }, + "require-dev": { + "laravel/framework": "^13.17", + "laravel/pint": "^1.29", + "phpstan/phpstan": "^2.2", + "phpunit/phpunit": "^13.2", + "vlucas/phpdotenv": "^5.6" + } } From e29299750c800f6dcc616ffbef71511b4fde1f40 Mon Sep 17 00:00:00 2001 From: Elivandro Date: Sun, 28 Jun 2026 14:43:14 -0400 Subject: [PATCH 07/13] docs: add installation, usage and API reference documentation --- README.md | 133 +++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 131 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index e0b7f62..fc3e86d 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,11 @@ [![GitHub Code Style Action Status](https://img.shields.io/github/actions/workflow/status/retroachievements/api/fix-php-code-style-issues.yml?branch=main&label=code%20style&style=flat-square)](https://github.com/retroachievements/api/actions?query=workflow%3A"Fix+PHP+code+style+issues"+branch%3Amain) [![Total Downloads](https://img.shields.io/packagist/dt/retroachievements/api.svg?style=flat-square)](https://packagist.org/packages/retroachievements/api) +## Requirements + +- PHP 8.2+ +- Guzzle 7.0+ + ## Installation Install the package via composer: @@ -14,9 +19,125 @@ Install the package via composer: composer require retroachievements/api ``` +## Configuration + +Add your API key to your `.env` file: + +``` +RETROACHIEVEMENTS_API_KEY=your-api-key-here +``` + +You can get your API key at [retroachievements.org/settings](https://retroachievements.org/settings). + ## Usage -WIP +### Standalone + +in your php file add: +```php +setApiKey('your-api-key-here'); + +$profile = $api->getUserProfile('MaxMilyin'); +$achievements = $api->getUserRecentAchievements('MaxMilyin', 60); + +echo $profile->User; +echo $profile->TotalPoints; + +foreach ($achievements as $achievement) { + echo $achievement->Title; + echo $achievement->GameTitle; +} +``` + +### Laravel + +Publish the config file: + +```shell +php artisan vendor:publish --provider="RetroAchievements\Providers\RetroAchievementsProvider" +``` + +Use the Facade: + +```php +use RetroAchievements\Facades\RetroAchievements; + +$profile = RetroAchievements::getUserProfile('MaxMilyin'); +$achievements = RetroAchievements::getUserRecentAchievements('MaxMilyin', 60); +``` + +Switching API keys at runtime: + +```php +foreach ($users as $user) { + RetroAchievements::setApiKey($user->ra_api_key); + + $profile = RetroAchievements::getUserProfile($user->ra_username); +} +``` + +## Error Handling + +```php +use RetroAchievements\Exceptions\ApiException; +use RetroAchievements\Exceptions\InvalidApiKeyException; +use RetroAchievements\Exceptions\InvalidDateException; + +try { + $profile = RetroAchievements::getUserProfile('NickGoat1990'); + +} catch (InvalidApiKeyException $e) { + // Invalid or missing API key +} catch (InvalidDateException $e) { + // Invalid date format or range +} catch (ApiException $e) { + // General API error +} +``` + +## Available Methods + +### User +| Method | Description | +|--------|-------------| +| `getUserProfile(string $username)` | Get user profile | +| `getUserRecentAchievements(string $username, ?int $minutes)` | Get recent achievements | +| `getAchievementsEarnedBetween(string $username, string $from, string $to)` | Get achievements earned in a date range | +| `getAchievementsEarnedOnDay(string $username, string $date)` | Get achievements earned on a specific day | +| `getUserPoints(string $username)` | Get user points | +| `getUserRankAndScore(string $username)` | Get user rank and score | +| `getUserSummary(string $username)` | Get user summary | +| `getUserCompletionProgress(string $username)` | Get user completion progress | +| `getUserAwards(string $username)` | Get user awards | +| `getUserRecentlyPlayedGames(string $username, ?int $limit)` | Get recently played games | +| `getUserWantToPlayList(string $username, ?int $limit)` | Get want to play list | +| `getUserCompletedGames(string $username)` | Get completed games | +| `getUserProgress(string $username, array $gameIds)` | Get user progress for specific games | +| `getUserClaims(string $username)` | Get user claims | +| `getUserGameRankAndScore(string $username, int $gameId)` | Get user rank and score for a specific game | + +### Games & Consoles +| Method | Description | +|--------|-------------| +| `getGameInfo(int $gameId)` | Get game information | +| `getGameInfoExtended(int $gameId)` | Get extended game information | +| `getGameInfoAndUserProgress(string $username, int $gameId)` | Get game info with user progress | +| `getGameList(int $consoleId)` | Get game list for a console | +| `getConsoleIDs()` | Get all console IDs | + +### Feed & Ranking +| Method | Description | +|--------|-------------| +| `getTopTenUsers()` | Get top ten users | +| `getFeedFor(string $username, int $count, int $offset)` | Get feed for a user | +| `getAchievementOfTheWeek()` | Get achievement of the week | ## Testing @@ -24,6 +145,14 @@ WIP composer test ``` +## Quality + +```shell +composer check # runs pint + phpstan + phpunit +composer fix # auto-fix code style +composer stan # phpstan only +``` + ## Contributing See [Contribution Guidelines](CONTRIBUTING.md) and [Code of Conduct](CODE_OF_CONDUCT.md). @@ -38,4 +167,4 @@ Please review [our security policy](../../security/policy). ## License -MIT License (MIT). See [License File](LICENSE.md). +MIT License (MIT). See [License File](LICENSE.md). \ No newline at end of file From e32bd7625f4978a8e9357390644e98a819d0fb4b Mon Sep 17 00:00:00 2001 From: Elivandro Date: Sun, 28 Jun 2026 14:51:01 -0400 Subject: [PATCH 08/13] docs: add standalone and Laravel usage and env example --- .env.example | 2 ++ .gitignore | 4 +-- public/index.example.php | 61 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 65 insertions(+), 2 deletions(-) create mode 100644 .env.example create mode 100644 public/index.example.php diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..d2afe53 --- /dev/null +++ b/.env.example @@ -0,0 +1,2 @@ +# get your apiKey in https://retroachievements.org/settings +RETROACHIEVEMENTS_API_KEY= \ No newline at end of file diff --git a/.gitignore b/.gitignore index a7f372d..47e5a33 100644 --- a/.gitignore +++ b/.gitignore @@ -4,8 +4,8 @@ build composer.lock coverage docs -phpunit.xml -phpstan.neon testbench.yaml vendor node_modules +.env +public/index.php \ No newline at end of file diff --git a/public/index.example.php b/public/index.example.php new file mode 100644 index 0000000..7f17b01 --- /dev/null +++ b/public/index.example.php @@ -0,0 +1,61 @@ +setApiKey('your-api-key-here'); + +$profile = $api->getUserProfile('MaxMilyin'); +$achievementsEarnedBetween = $api->getAchievementsEarnedBetween('MaxMilyin', '2024-01-01', '2024-01-30'); +$userSummary = $api->getUserSummary('MaxMilyin', 1, 3); +$consoleIds = $api->getConsoleIDs(); + +var_dump($profile); +var_dump($achievementsEarnedBetween); + +// --------------------------------------------------------------------------- +// Laravel bootstrap (only needed outside a Laravel project) +// --------------------------------------------------------------------------- + +use Illuminate\Config\Repository; +use Illuminate\Foundation\Application; +use Illuminate\Support\Facades\Facade; +use RetroAchievements\Facades\RetroAchievements; +use RetroAchievements\Providers\RetroAchievementsProvider; + +$dotenv = Dotenv\Dotenv::createImmutable(__DIR__ . '/../'); +$dotenv->load(); + +$app = new Application(__DIR__ . '/../'); + +$app->singleton('config', function () { + return new Repository([ + 'retroachievements' => require __DIR__ . '/../src/config/retroachievements.php', + ]); +}); + +Facade::setFacadeApplication($app); + +$provider = new RetroAchievementsProvider($app); +$provider->register(); +$provider->boot(); + +// --------------------------------------------------------------------------- +// Laravel usage +// In a real Laravel project, everything above is handled automatically. +// Just use the Facade directly: +// --------------------------------------------------------------------------- + +RetroAchievements::setApiKey('your-api-key-here'); // optional if set in .env + +$profile = RetroAchievements::getUserProfile('MaxMilyin'); +$achievements = RetroAchievements::getUserRecentAchievements('MaxMilyin', 525600); + +var_dump($profile); +var_dump($achievements); \ No newline at end of file From 2e5a668e4ff49b92b56bb9b9631b311a94c89ca5 Mon Sep 17 00:00:00 2001 From: Elivandro Date: Sun, 28 Jun 2026 14:55:06 -0400 Subject: [PATCH 09/13] style: apply Pint code style to usage example --- public/index.example.php | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/public/index.example.php b/public/index.example.php index 7f17b01..edf958a 100644 --- a/public/index.example.php +++ b/public/index.example.php @@ -1,6 +1,6 @@ setApiKey('your-api-key-here'); -$profile = $api->getUserProfile('MaxMilyin'); +$profile = $api->getUserProfile('MaxMilyin'); $achievementsEarnedBetween = $api->getAchievementsEarnedBetween('MaxMilyin', '2024-01-01', '2024-01-30'); -$userSummary = $api->getUserSummary('MaxMilyin', 1, 3); -$consoleIds = $api->getConsoleIDs(); +$userSummary = $api->getUserSummary('MaxMilyin', 1, 3); +$consoleIds = $api->getConsoleIDs(); var_dump($profile); var_dump($achievementsEarnedBetween); @@ -29,14 +29,14 @@ use RetroAchievements\Facades\RetroAchievements; use RetroAchievements\Providers\RetroAchievementsProvider; -$dotenv = Dotenv\Dotenv::createImmutable(__DIR__ . '/../'); +$dotenv = Dotenv\Dotenv::createImmutable(__DIR__.'/../'); $dotenv->load(); -$app = new Application(__DIR__ . '/../'); +$app = new Application(__DIR__.'/../'); $app->singleton('config', function () { return new Repository([ - 'retroachievements' => require __DIR__ . '/../src/config/retroachievements.php', + 'retroachievements' => require __DIR__.'/../src/config/retroachievements.php', ]); }); @@ -54,8 +54,8 @@ RetroAchievements::setApiKey('your-api-key-here'); // optional if set in .env -$profile = RetroAchievements::getUserProfile('MaxMilyin'); +$profile = RetroAchievements::getUserProfile('MaxMilyin'); $achievements = RetroAchievements::getUserRecentAchievements('MaxMilyin', 525600); var_dump($profile); -var_dump($achievements); \ No newline at end of file +var_dump($achievements); From a61e34826d4202bd5d32282e7ae96bcdfb3c3fa0 Mon Sep 17 00:00:00 2001 From: Elivandro Date: Sun, 28 Jun 2026 14:55:42 -0400 Subject: [PATCH 10/13] chore: ignore PHPUnit cache file --- .gitignore | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 47e5a33..c240c39 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ .idea .phpunit.cache +.phpunit.result.cache build composer.lock coverage @@ -8,4 +9,4 @@ testbench.yaml vendor node_modules .env -public/index.php \ No newline at end of file +public/index.php From e727bf1f151582193b8257fb4d45be5fc76f3ef9 Mon Sep 17 00:00:00 2001 From: Elivandro Date: Sun, 28 Jun 2026 15:37:28 -0400 Subject: [PATCH 11/13] refactor: remove legacy endpoints file --- src/RetroAchievementsApiClient.php | 185 ----------------------------- 1 file changed, 185 deletions(-) delete mode 100644 src/RetroAchievementsApiClient.php diff --git a/src/RetroAchievementsApiClient.php b/src/RetroAchievementsApiClient.php deleted file mode 100644 index 4903ac2..0000000 --- a/src/RetroAchievementsApiClient.php +++ /dev/null @@ -1,185 +0,0 @@ -ra_user = $user; - $this->ra_api_key = $api_key; - } - - /** - * @return string - */ - private function AuthQS() - { - return "?z=" . $this->ra_user . "&y=" . $this->ra_api_key; - } - - /** - * @param string $target - * @param string $params - * @return bool|null|string - */ - private function GetRAURL($target, $params = "") - { - try { - return file_get_contents(self::API_URL . $target . self::AuthQS() . "&$params"); - } catch (Exception $e) { - } - return null; - } - - /** - * @return mixed - */ - public function GetTopTenUsers() - { - return json_decode(self::GetRAURL('API_GetTopTenUsers.php')); - } - - /** - * @param int $gameID - * @return mixed - */ - public function GetGameInfo($gameID) - { - return json_decode(self::GetRAURL("API_GetGame.php", "i=$gameID")); - } - - /** - * @param int $gameID - * @return mixed - */ - public function GetGameInfoExtended($gameID) - { - return json_decode(self::GetRAURL("API_GetGameExtended.php", "i=$gameID")); - } - - /** - * @return mixed - */ - public function GetConsoleIDs() - { - return json_decode(self::GetRAURL('API_GetConsoleIDs.php')); - } - - /** - * @param int $consoleID - * @return mixed - */ - public function GetGameList($consoleID) - { - return json_decode(self::GetRAURL("API_GetGameList.php", "i=$consoleID")); - } - - /** - * @param string $user - * @param int $count - * @param int $offset - * @return mixed - */ - public function GetFeedFor($user, $count, $offset = 0) - { - return json_decode(self::GetRAURL("API_GetFeed.php", "u=$user&c=$count&o=$offset")); - } - - /** - * @param string $user - * @return mixed - */ - public function GetUserRankAndScore($user) - { - return json_decode(self::GetRAURL("API_GetUserRankAndScore.php", "u=$user")); - } - - /** - * @param string $user - * @param string $gameIDCSV - * @return mixed - */ - public function GetUserProgress($user, $gameIDCSV) - { - $gameIDCSV = preg_replace('/\s+/', '', $gameIDCSV); // Remove all whitespace - return json_decode(self::GetRAURL("API_GetUserProgress.php", "u=$user&i=$gameIDCSV")); - } - - /** - * @param string $user - * @param int $count - * @param int $offset - * @return mixed - */ - public function GetUserRecentlyPlayedGames($user, $count, $offset = 0) - { - return json_decode(self::GetRAURL("API_GetUserRecentlyPlayedGames.php", "u=$user&c=$count&o=$offset")); - } - - /** - * @param string $user - * @param int $numRecentGames - * @return mixed - */ - public function GetUserSummary($user, $numRecentGames) - { - return json_decode(self::GetRAURL("API_GetUserSummary.php", "u=$user&g=$numRecentGames&a=5")); - } - - /** - * @param string $user - * @param int $gameID - * @return mixed - */ - public function GetGameInfoAndUserProgress($user, $gameID) - { - return json_decode(self::GetRAURL("API_GetGameInfoAndUserProgress.php", "u=$user&g=$gameID")); - } - - /** - * @param string $user - * @param int $dateInput - * @return mixed - */ - public function GetAchievementsEarnedOnDay($user, $dateInput) - { - return json_decode(self::GetRAURL("API_GetAchievementsEarnedOnDay.php", "u=$user&d=$dateInput")); - } - - /** - * @param string $user - * @param int $dateStart - * @param int $dateEnd - * @return mixed - */ - public function GetAchievementsEarnedBetween($user, $dateStart, $dateEnd) - { - $dateFrom = strtotime($dateStart); - $dateTo = strtotime($dateEnd); - return json_decode(self::GetRAURL("API_GetAchievementsEarnedBetween.php", "u=$user&f=$dateFrom&t=$dateTo")); - } -} - -; From 4f5e804cd8c6c48405e1c75a72f7bef73a122f45 Mon Sep 17 00:00:00 2001 From: Elivandro Date: Sun, 28 Jun 2026 15:56:49 -0400 Subject: [PATCH 12/13] fix: move service binding to register method and use app config helper --- src/Providers/RetroAchievementsProvider.php | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/Providers/RetroAchievementsProvider.php b/src/Providers/RetroAchievementsProvider.php index 384a560..204b954 100644 --- a/src/Providers/RetroAchievementsProvider.php +++ b/src/Providers/RetroAchievementsProvider.php @@ -9,19 +9,23 @@ class RetroAchievementsProvider extends ServiceProvider { + public function boot(): void + { + $this->publishes([ + __DIR__.'/../config/retroachievements.php' => config_path('retroachievements.php'), + ], 'config'); + } + public function register(): void { $this->mergeConfigFrom( __DIR__.'/../config/retroachievements.php', 'retroachievements' ); - } - public function boot(): void - { - $this->app->bind(RetroAchievementsApiClient::class, function () { + $this->app->bind(RetroAchievementsApiClient::class, function ($app) { $service = new RetroAchievementsApiClient; - $service->setApiKey(config('retroachievements.api_key')); + $service->setApiKey($app['config']['retroachievements.api_key'] ?? ''); return $service; }); From 3b2f9fc4813e15aaede71ccdfc5b6fee7cda9016 Mon Sep 17 00:00:00 2001 From: Elivandro Date: Sun, 28 Jun 2026 15:56:59 -0400 Subject: [PATCH 13/13] docs: add manual provider registration instructions --- README.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/README.md b/README.md index fc3e86d..c20abd2 100644 --- a/README.md +++ b/README.md @@ -64,6 +64,16 @@ Publish the config file: php artisan vendor:publish --provider="RetroAchievements\Providers\RetroAchievementsProvider" ``` +### Manual Registration (if auto-discovery fails) + +If the provider is not discovered automatically, add it manually to `bootstrap/providers.php` (Laravel 11+): + +```php +return [ + RetroAchievements\Providers\RetroAchievementsProvider::class, +]; +``` + Use the Facade: ```php