├── .github ├── dependabot.yml └── workflows │ ├── code-quality.yml │ ├── code-style.yml │ ├── codecov.yml │ ├── deploy-docs.yml │ ├── tests.yml │ └── type-coverage.yml ├── .gitignore ├── LICENSE ├── README.md ├── composer.json ├── config └── config.php ├── docs ├── 01-installation.md ├── 02-getting-started.md ├── 03-select.md ├── 04-search.md ├── 05-fuzzy-search.md ├── 06-where-clauses.md ├── 07-order-limit-offset.md ├── 08-cache.md ├── 09-relationships.md ├── 10-fetch-results.md ├── 11-properties.md ├── 12-images.md ├── 90-webhooks.md ├── art │ ├── .gitkeep │ └── cover.png └── index.md ├── phpstan-baseline.neon ├── phpstan.neon ├── phpunit.xml ├── pint.json ├── rector.php ├── routes └── web.php ├── src ├── ApiHelper.php ├── Builder.php ├── Client.php ├── Console │ ├── CreateWebhook.php │ ├── DeleteWebhook.php │ ├── ListWebhooks.php │ ├── PublishCommand.php │ └── ReactivateWebhook.php ├── Enums │ ├── AgeRating │ │ ├── Category.php │ │ └── Rating.php │ ├── AgeRatingContentDescription │ │ └── Category.php │ ├── Character │ │ ├── Gender.php │ │ └── Species.php │ ├── Company │ │ ├── ChangeDateCategory.php │ │ └── StartDateCategory.php │ ├── CompanyWebsite │ │ └── Category.php │ ├── ExternalGame │ │ ├── Category.php │ │ └── Media.php │ ├── Game │ │ ├── Category.php │ │ └── Status.php │ ├── GameVersionFeature │ │ └── Category.php │ ├── GameVersionFeatureValue │ │ └── IncludedFeature.php │ ├── Image │ │ └── Size.php │ ├── Platform │ │ └── Category.php │ ├── PlatformVersionReleaseDate │ │ ├── Category.php │ │ └── Region.php │ ├── PlatformWebsite │ │ └── Category.php │ ├── Popularity │ │ └── Source.php │ ├── ReleaseDate │ │ ├── Category.php │ │ └── Region.php │ ├── TagNumbers.php │ ├── Webhook │ │ ├── Category.php │ │ └── Method.php │ └── Website │ │ └── Category.php ├── Events │ ├── AgeRatingContentDescriptionCreated.php │ ├── AgeRatingContentDescriptionDeleted.php │ ├── AgeRatingContentDescriptionUpdated.php │ ├── AgeRatingCreated.php │ ├── AgeRatingDeleted.php │ ├── AgeRatingUpdated.php │ ├── AlternativeNameCreated.php │ ├── AlternativeNameDeleted.php │ ├── AlternativeNameUpdated.php │ ├── ArtworkCreated.php │ ├── ArtworkDeleted.php │ ├── ArtworkUpdated.php │ ├── CharacterCreated.php │ ├── CharacterDeleted.php │ ├── CharacterMugShotCreated.php │ ├── CharacterMugShotDeleted.php │ ├── CharacterMugShotUpdated.php │ ├── CharacterUpdated.php │ ├── CollectionCreated.php │ ├── CollectionDeleted.php │ ├── CollectionMembershipCreated.php │ ├── CollectionMembershipDeleted.php │ ├── CollectionMembershipTypeCreated.php │ ├── CollectionMembershipTypeDeleted.php │ ├── CollectionMembershipTypeUpdated.php │ ├── CollectionMembershipUpdated.php │ ├── CollectionRelationCreated.php │ ├── CollectionRelationDeleted.php │ ├── CollectionRelationTypeCreated.php │ ├── CollectionRelationTypeDeleted.php │ ├── CollectionRelationTypeUpdated.php │ ├── CollectionRelationUpdated.php │ ├── CollectionTypeCreated.php │ ├── CollectionTypeDeleted.php │ ├── CollectionTypeUpdated.php │ ├── CollectionUpdated.php │ ├── CompanyCreated.php │ ├── CompanyDeleted.php │ ├── CompanyLogoCreated.php │ ├── CompanyLogoDeleted.php │ ├── CompanyLogoUpdated.php │ ├── CompanyUpdated.php │ ├── CompanyWebsiteCreated.php │ ├── CompanyWebsiteDeleted.php │ ├── CompanyWebsiteUpdated.php │ ├── CoverCreated.php │ ├── CoverDeleted.php │ ├── CoverUpdated.php │ ├── Event.php │ ├── EventCreated.php │ ├── EventDeleted.php │ ├── EventLogoCreated.php │ ├── EventLogoDeleted.php │ ├── EventLogoUpdated.php │ ├── EventNetworkCreated.php │ ├── EventNetworkDeleted.php │ ├── EventNetworkUpdated.php │ ├── EventUpdated.php │ ├── ExternalGameCreated.php │ ├── ExternalGameDeleted.php │ ├── ExternalGameUpdated.php │ ├── FranchiseCreated.php │ ├── FranchiseDeleted.php │ ├── FranchiseUpdated.php │ ├── GameCreated.php │ ├── GameDeleted.php │ ├── GameEngineCreated.php │ ├── GameEngineDeleted.php │ ├── GameEngineLogoCreated.php │ ├── GameEngineLogoDeleted.php │ ├── GameEngineLogoUpdated.php │ ├── GameEngineUpdated.php │ ├── GameLocalizationCreated.php │ ├── GameLocalizationDeleted.php │ ├── GameLocalizationUpdated.php │ ├── GameModeCreated.php │ ├── GameModeDeleted.php │ ├── GameModeUpdated.php │ ├── GameTimeToBeatCreated.php │ ├── GameTimeToBeatDeleted.php │ ├── GameTimeToBeatUpdated.php │ ├── GameUpdated.php │ ├── GameVersionCreated.php │ ├── GameVersionDeleted.php │ ├── GameVersionFeatureCreated.php │ ├── GameVersionFeatureDeleted.php │ ├── GameVersionFeatureUpdated.php │ ├── GameVersionFeatureValueCreated.php │ ├── GameVersionFeatureValueDeleted.php │ ├── GameVersionFeatureValueUpdated.php │ ├── GameVersionUpdated.php │ ├── GameVideoCreated.php │ ├── GameVideoDeleted.php │ ├── GameVideoUpdated.php │ ├── GenreCreated.php │ ├── GenreDeleted.php │ ├── GenreUpdated.php │ ├── InvolvedCompanyCreated.php │ ├── InvolvedCompanyDeleted.php │ ├── InvolvedCompanyUpdated.php │ ├── KeywordCreated.php │ ├── KeywordDeleted.php │ ├── KeywordUpdated.php │ ├── LanguageCreated.php │ ├── LanguageDeleted.php │ ├── LanguageSupportCreated.php │ ├── LanguageSupportDeleted.php │ ├── LanguageSupportTypeCreated.php │ ├── LanguageSupportTypeDeleted.php │ ├── LanguageSupportTypeUpdated.php │ ├── LanguageSupportUpdated.php │ ├── LanguageUpdated.php │ ├── MultiplayerModeCreated.php │ ├── MultiplayerModeDeleted.php │ ├── MultiplayerModeUpdated.php │ ├── NetworkTypeCreated.php │ ├── NetworkTypeDeleted.php │ ├── NetworkTypeUpdated.php │ ├── PlatformCreated.php │ ├── PlatformDeleted.php │ ├── PlatformFamilyCreated.php │ ├── PlatformFamilyDeleted.php │ ├── PlatformFamilyUpdated.php │ ├── PlatformLogoCreated.php │ ├── PlatformLogoDeleted.php │ ├── PlatformLogoUpdated.php │ ├── PlatformUpdated.php │ ├── PlatformVersionCompanyCreated.php │ ├── PlatformVersionCompanyDeleted.php │ ├── PlatformVersionCompanyUpdated.php │ ├── PlatformVersionCreated.php │ ├── PlatformVersionDeleted.php │ ├── PlatformVersionReleaseDateCreated.php │ ├── PlatformVersionReleaseDateDeleted.php │ ├── PlatformVersionReleaseDateUpdated.php │ ├── PlatformVersionUpdated.php │ ├── PlatformWebsiteCreated.php │ ├── PlatformWebsiteDeleted.php │ ├── PlatformWebsiteUpdated.php │ ├── PlayerPerspectiveCreated.php │ ├── PlayerPerspectiveDeleted.php │ ├── PlayerPerspectiveUpdated.php │ ├── PopularityTypeCreated.php │ ├── PopularityTypeDeleted.php │ ├── PopularityTypeUpdated.php │ ├── RegionCreated.php │ ├── RegionDeleted.php │ ├── RegionUpdated.php │ ├── ReleaseDateCreated.php │ ├── ReleaseDateDeleted.php │ ├── ReleaseDateStatusCreated.php │ ├── ReleaseDateStatusDeleted.php │ ├── ReleaseDateStatusUpdated.php │ ├── ReleaseDateUpdated.php │ ├── ScreenshotCreated.php │ ├── ScreenshotDeleted.php │ ├── ScreenshotUpdated.php │ ├── ThemeCreated.php │ ├── ThemeDeleted.php │ ├── ThemeUpdated.php │ ├── WebsiteCreated.php │ ├── WebsiteDeleted.php │ └── WebsiteUpdated.php ├── Exceptions │ ├── AuthenticationException.php │ ├── InvalidParamsException.php │ ├── InvalidWebhookMethodException.php │ ├── InvalidWebhookSecretException.php │ ├── MissingEndpointException.php │ ├── ModelNotFoundException.php │ ├── PropertyDoesNotExist.php │ ├── ServiceException.php │ ├── ServiceUnavailableException.php │ ├── UnauthorizedException.php │ └── WebhookSecretMissingException.php ├── IGDBLaravelServiceProvider.php ├── Models │ ├── AgeRating.php │ ├── AgeRatingContentDescription.php │ ├── AlternativeName.php │ ├── Artwork.php │ ├── Character.php │ ├── CharacterMugShot.php │ ├── Collection.php │ ├── CollectionMembership.php │ ├── CollectionMembershipType.php │ ├── CollectionRelation.php │ ├── CollectionRelationType.php │ ├── CollectionType.php │ ├── Company.php │ ├── CompanyLogo.php │ ├── CompanyWebsite.php │ ├── Cover.php │ ├── Event.php │ ├── EventLogo.php │ ├── EventNetwork.php │ ├── ExternalGame.php │ ├── Franchise.php │ ├── Game.php │ ├── GameEngine.php │ ├── GameEngineLogo.php │ ├── GameLocalization.php │ ├── GameMode.php │ ├── GameTimeToBeat.php │ ├── GameVersion.php │ ├── GameVersionFeature.php │ ├── GameVersionFeatureValue.php │ ├── GameVideo.php │ ├── Genre.php │ ├── Image.php │ ├── InvolvedCompany.php │ ├── Keyword.php │ ├── Language.php │ ├── LanguageSupport.php │ ├── LanguageSupportType.php │ ├── Model.php │ ├── MultiplayerMode.php │ ├── NetworkType.php │ ├── Platform.php │ ├── PlatformFamily.php │ ├── PlatformLogo.php │ ├── PlatformVersion.php │ ├── PlatformVersionCompany.php │ ├── PlatformVersionReleaseDate.php │ ├── PlatformWebsite.php │ ├── PlayerPerspective.php │ ├── PopularityPrimitive.php │ ├── PopularityType.php │ ├── Region.php │ ├── ReleaseDate.php │ ├── ReleaseDateStatus.php │ ├── Screenshot.php │ ├── Search.php │ ├── Theme.php │ ├── Webhook.php │ └── Website.php └── Traits │ ├── DateCasts.php │ ├── HasLimits.php │ ├── HasNestedWhere.php │ ├── HasSearch.php │ ├── HasSelect.php │ ├── HasWhere.php │ ├── HasWhereBetween.php │ ├── HasWhereDate.php │ ├── HasWhereHas.php │ ├── HasWhereIn.php │ ├── HasWhereLike.php │ ├── Operators.php │ └── ValuePreparer.php └── tests ├── ApiHelperTest.php ├── BuilderTest.php ├── ConsoleTest.php ├── EventsTest.php ├── ImageTest.php ├── ModelTest.php ├── Pest.php ├── TestCase.php └── WebhookTest.php /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "composer" 9 | directory: "/" 10 | schedule: 11 | interval: "daily" 12 | assignees: 13 | - "marcreichel" 14 | commit-message: 15 | prefix: "⬆️" 16 | prefix_development: "⬆️" 17 | include_scope: false 18 | -------------------------------------------------------------------------------- /.github/workflows/code-quality.yml: -------------------------------------------------------------------------------- 1 | name: PHPStan 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | phpstan: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v2 14 | - name: Install dependencies 15 | run: composer install --prefer-dist --no-progress --dev 16 | - run: composer stan -- --error-format=github 17 | -------------------------------------------------------------------------------- /.github/workflows/code-style.yml: -------------------------------------------------------------------------------- 1 | name: Pint 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | pint: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v2 14 | - name: Install dependencies 15 | run: composer install --prefer-dist --no-progress --dev 16 | - run: composer pint 17 | -------------------------------------------------------------------------------- /.github/workflows/codecov.yml: -------------------------------------------------------------------------------- 1 | name: Code coverage 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | build: 10 | runs-on: ${{ matrix.os }} 11 | 12 | strategy: 13 | fail-fast: true 14 | matrix: 15 | os: [ubuntu-latest] 16 | php: ['8.2', '8.3', 8.4, '8.4'] 17 | laravel: ['11.*', '12.*'] 18 | stability: [prefer-lowest] 19 | include: 20 | - laravel: 11.* 21 | testbench: 9.* 22 | - laravel: 12.* 23 | testbench: 10.* 24 | 25 | steps: 26 | - uses: actions/checkout@v2 27 | 28 | - name: Setup PHP 29 | uses: shivammathur/setup-php@v2 30 | with: 31 | php-version: ${{ matrix.php }} 32 | extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, bcmath, soap, intl, gd, exif, iconv, imagick, fileinfo 33 | coverage: xdebug 34 | 35 | - name: Setup problem matchers 36 | run: | 37 | echo "::add-matcher::${{ runner.tool_cache }}/php.json" 38 | echo "::add-matcher::${{ runner.tool_cache }}/phpunit.json" 39 | 40 | - name: Install dependencies 41 | run: | 42 | composer require "laravel/framework:${{ matrix.laravel }}" "orchestra/testbench:${{ matrix.testbench }}" --no-interaction --no-update 43 | composer update --${{ matrix.stability }} --prefer-dist --no-interaction 44 | 45 | - run: composer test:coverage 46 | 47 | - name: Upload 48 | uses: codecov/codecov-action@v1 49 | with: 50 | files: ./build/clover.xml 51 | -------------------------------------------------------------------------------- /.github/workflows/deploy-docs.yml: -------------------------------------------------------------------------------- 1 | name: Deploy docs 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | paths: [ docs/**/* ] 7 | workflow_dispatch: 8 | 9 | jobs: 10 | deploy: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Deploy documentation 14 | uses: wei/curl@master 15 | with: 16 | args: -X GET ${{ secrets.DEPLOYMENT_URL }} 17 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | 11 | jobs: 12 | run-tests: 13 | runs-on: ubuntu-latest 14 | 15 | strategy: 16 | fail-fast: true 17 | matrix: 18 | php: [8.4, 8.3, 8.2] 19 | laravel: ['11.*', '12.*'] 20 | stability: [prefer-stable] 21 | include: 22 | - laravel: 11.* 23 | testbench: 9.* 24 | - laravel: 12.* 25 | testbench: 10.* 26 | 27 | name: PHP ${{ matrix.php }} - Laravel ${{ matrix.laravel }} - ${{ matrix.stability }} 28 | 29 | steps: 30 | - name: Checkout code 31 | uses: actions/checkout@v2 32 | 33 | - name: Setup PHP 34 | uses: shivammathur/setup-php@v2 35 | with: 36 | php-version: ${{ matrix.php }} 37 | extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, bcmath, soap, intl, gd, exif, iconv, imagick, fileinfo 38 | coverage: xdebug 39 | 40 | - name: Setup problem matchers 41 | run: | 42 | echo "::add-matcher::${{ runner.tool_cache }}/php.json" 43 | echo "::add-matcher::${{ runner.tool_cache }}/phpunit.json" 44 | 45 | - name: Install dependencies 46 | run: | 47 | composer require "laravel/framework:${{ matrix.laravel }}" "orchestra/testbench:${{ matrix.testbench }}" --no-interaction --no-update 48 | composer update --${{ matrix.stability }} --prefer-dist --no-interaction 49 | 50 | - run: composer test 51 | -------------------------------------------------------------------------------- /.github/workflows/type-coverage.yml: -------------------------------------------------------------------------------- 1 | name: Type Coverage 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | type-coverage: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v4 14 | - name: Setup PHP 15 | uses: shivammathur/setup-php@v2 16 | with: 17 | php-version: 8.4 18 | coverage: xdebug 19 | - name: Install dependencies 20 | run: composer install --prefer-dist --no-progress --dev 21 | - run: composer test:type-coverage 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | vendor/ 3 | build/ 4 | report/ 5 | composer.lock 6 | .phpunit.cache 7 | .phpunit.result.cache 8 | cghooks.lock 9 | coverage.xml 10 | 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Marc Reichel 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

Laravel IGDB Wrapper

2 | 3 |

4 | This is a Laravel wrapper for version 4 of the IGDB API (Apicalypse) 5 | including webhook handling. 6 |

7 | 8 |

9 | 10 | Packagist Version 11 | 12 | 13 | Packagist Downloads 14 | 15 | 16 | Tests 17 | 18 | 19 | Pint 20 | 21 | 22 | PHPStan 23 | 24 | 25 | CodeFactor 26 | 27 | 28 | codecov 29 | 30 | 31 | License 32 | 33 |

34 | 35 | ![Cover](docs/art/cover.png) 36 | 37 | ## Basic installation 38 | 39 | You can install this package via composer using: 40 | 41 | ```bash 42 | composer require marcreichel/igdb-laravel 43 | ``` 44 | 45 | The package will automatically register its service provider. 46 | 47 | To publish the config file to `config/igdb.php` run: 48 | 49 | ```bash 50 | php artisan igdb:publish 51 | ``` 52 | 53 | This is the default content of the config file: 54 | 55 | ```php 56 | return [ 57 | /* 58 | * These are the credentials you got from https://dev.twitch.tv/console/apps 59 | */ 60 | 'credentials' => [ 61 | 'client_id' => env('TWITCH_CLIENT_ID', ''), 62 | 'client_secret' => env('TWITCH_CLIENT_SECRET', ''), 63 | ], 64 | 65 | /* 66 | * This package caches queries automatically (for 1 hour per default). 67 | * Here you can set how long each query should be cached (in seconds). 68 | * 69 | * To turn cache off set this value to 0 70 | */ 71 | 'cache_lifetime' => env('IGDB_CACHE_LIFETIME', 3600), 72 | 73 | /** 74 | * The prefix used to cache the results. 75 | * 76 | * E.g.: `[CACHE_PREFIX].75170fc230cd88f32e475ff4087f81d9` 77 | */ 78 | 'cache_prefix' => 'igdb_cache', 79 | 80 | /* 81 | * Path where the webhooks should be handled. 82 | */ 83 | 'webhook_path' => 'igdb-webhook/handle', 84 | 85 | /* 86 | * The webhook secret. 87 | * 88 | * This needs to be a string of your choice in order to use the webhook 89 | * functionality. 90 | */ 91 | 'webhook_secret' => env('IGDB_WEBHOOK_SECRET', null), 92 | ]; 93 | ``` 94 | 95 | ## Documentation 96 | 97 | You will find the full documentation on [the dedicated documentation site](https://marcreichel.dev/docs/igdb-laravel). 98 | 99 | ## Testing 100 | 101 | Run the tests with: 102 | 103 | ```bash 104 | composer test 105 | ``` 106 | 107 | ## Contribution 108 | 109 | Pull requests are welcome :) 110 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "marcreichel/igdb-laravel", 3 | "description": "A Laravel wrapper for version 4 of the IGDB API (Apicalypse) including webhook handling", 4 | "keywords": [ 5 | "laravel", 6 | "api-wrapper", 7 | "igdb", 8 | "igdb-api", 9 | "apicalypse", 10 | "wrapper" 11 | ], 12 | "type": "library", 13 | "minimum-stability": "stable", 14 | "require": { 15 | "php": "^8.2", 16 | "ext-json": "*", 17 | "illuminate/support": "^11.0|^12.0", 18 | "guzzlehttp/guzzle": "~6.0|~7.0", 19 | "nesbot/carbon": "^2.53.1|^3.0" 20 | }, 21 | "require-dev": { 22 | "orchestra/testbench": "^9.0|^10.0", 23 | "nunomaduro/collision": "^8.0", 24 | "roave/security-advisories": "dev-latest", 25 | "larastan/larastan": "^3.0.2", 26 | "laravel/pint": "^1.13", 27 | "pestphp/pest": "^3.7.4", 28 | "pestphp/pest-plugin-type-coverage": "^3.2.3", 29 | "rector/rector": "^2.0.7" 30 | }, 31 | "license": "MIT", 32 | "authors": [ 33 | { 34 | "name": "Marc Reichel", 35 | "email": "mail@marcreichel.de" 36 | } 37 | ], 38 | "scripts": { 39 | "pint": "./vendor/bin/pint --test -v", 40 | "test": "./vendor/bin/pest --parallel", 41 | "stan": "./vendor/bin/phpstan --memory-limit=2G", 42 | "test:coverage": [ 43 | "@putenv XDEBUG_MODE=coverage", 44 | "@test --coverage --min=90 --coverage-clover build/clover.xml" 45 | ], 46 | "test:coverage-html": [ 47 | "@putenv XDEBUG_MODE=coverage", 48 | "@test --coverage --min=90 --coverage-html build/coverage" 49 | ], 50 | "test:type-coverage": [ 51 | "@putenv XDEBUG_MODE=coverage", 52 | "@test -- --type-coverage --min=100" 53 | ] 54 | }, 55 | "autoload": { 56 | "psr-4": { 57 | "MarcReichel\\IGDBLaravel\\": "src" 58 | } 59 | }, 60 | "autoload-dev": { 61 | "psr-4": { 62 | "MarcReichel\\IGDBLaravel\\Tests\\": "tests" 63 | } 64 | }, 65 | "extra": { 66 | "laravel": { 67 | "providers": [ 68 | "MarcReichel\\IGDBLaravel\\IGDBLaravelServiceProvider" 69 | ] 70 | } 71 | }, 72 | "config": { 73 | "allow-plugins": { 74 | "pestphp/pest-plugin": true 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /config/config.php: -------------------------------------------------------------------------------- 1 | [ 10 | 'client_id' => env('TWITCH_CLIENT_ID', ''), 11 | 'client_secret' => env('TWITCH_CLIENT_SECRET', ''), 12 | ], 13 | 14 | /** 15 | * This package caches queries automatically (for 1 hour per default). 16 | * Here you can set how long each query should be cached (in seconds). 17 | * 18 | * To turn cache off set this value to 0. 19 | */ 20 | 'cache_lifetime' => env('IGDB_CACHE_LIFETIME', 3600), 21 | 22 | /** 23 | * The prefix used to cache the results. 24 | * 25 | * E.g.: `[CACHE_PREFIX].75170fc230cd88f32e475ff4087f81d9` 26 | */ 27 | 'cache_prefix' => 'igdb_cache', 28 | 29 | /** 30 | * Path where the webhooks should be handled. 31 | */ 32 | 'webhook_path' => 'igdb-webhook/handle', 33 | 34 | /** 35 | * The webhook secret. 36 | * 37 | * This needs to be a string of your choice in order to use the webhook 38 | * functionality. 39 | */ 40 | 'webhook_secret' => env('IGDB_WEBHOOK_SECRET'), 41 | ]; 42 | -------------------------------------------------------------------------------- /docs/01-installation.md: -------------------------------------------------------------------------------- 1 | # Installation 2 | 3 | ## Create Twitch Developer App 4 | 5 | [Create](https://dev.twitch.tv/console/apps/create) a new Twitch Developer App and set the `Client ID` and 6 | `Client Secret` as described below. 7 | 8 | ## Install the package 9 | 10 | You can install this package via composer using: 11 | 12 | ```bash 13 | // torchlight! {"lineNumbers": false} 14 | composer require marcreichel/igdb-laravel 15 | ``` 16 | 17 | The package will automatically register its service provider. 18 | 19 | To publish the config file to `config/igdb.php` run: 20 | 21 | ```bash 22 | // torchlight! {"lineNumbers": false} 23 | php artisan igdb:publish 24 | ``` 25 | 26 | This is the default content of the config file: 27 | 28 | ```php 29 | // torchlight! {"lineNumbers": false} 30 | [ 37 | 'client_id' => env('TWITCH_CLIENT_ID', ''), 38 | 'client_secret' => env('TWITCH_CLIENT_SECRET', ''), 39 | ], 40 | 41 | /* 42 | * This package caches queries automatically (for 1 hour per default). 43 | * Here you can set how long each query should be cached (in seconds). 44 | * 45 | * To turn cache off set this value to 0 46 | */ 47 | 'cache_lifetime' => env('IGDB_CACHE_LIFETIME', 3600), 48 | 49 | /** 50 | * The prefix used to cache the results. 51 | * 52 | * E.g.: `[CACHE_PREFIX].75170fc230cd88f32e475ff4087f81d9` 53 | */ 54 | 'cache_prefix' => 'igdb_cache', 55 | 56 | /* 57 | * Path where the webhooks should be handled. 58 | */ 59 | 'webhook_path' => 'igdb-webhook/handle', 60 | 61 | /* 62 | * The webhook secret. 63 | * 64 | * This needs to be a string of your choice in order to use the webhook 65 | * functionality. 66 | */ 67 | 'webhook_secret' => env('IGDB_WEBHOOK_SECRET'), 68 | ]; 69 | ``` 70 | -------------------------------------------------------------------------------- /docs/02-getting-started.md: -------------------------------------------------------------------------------- 1 | # Getting started 2 | 3 | If you're familiar with the [Eloquent System](https://laravel.com/docs/eloquent) and 4 | the [Query Builder](https://laravel.com/docs/queries) of Laravel you will love this package as it uses a similar 5 | approach. 6 | 7 | ## Models 8 | 9 | Each endpoint of the API is mapped to its own model. 10 | 11 | To get a list of games you simply call something like this: 12 | 13 | ```php 14 | // torchlight! {"lineNumbers": false} 15 | use MarcReichel\IGDBLaravel\Models\Game; 16 | 17 | $games = Game::where('name', 'Fortnite')->get(); 18 | ``` 19 | 20 | [Here](https://github.com/marcreichel/igdb-laravel/tree/main/src/Models) you can find a list of all available Models. 21 | 22 | When you use one of these models the query results will be mapped into the used model automatically. 23 | 24 | _This method is used in the examples in the documentation._ 25 | 26 | ## Query Builder 27 | 28 | You can also use the Query Builder (which is used under the hood) directly if you want to: 29 | 30 | ```php 31 | // torchlight! {"lineNumbers": false} 32 | use MarcReichel\IGDBLaravel\Builder as IGDB; 33 | 34 | $igdb = new IGDB('games'); // 'games' is the endpoint 35 | 36 | $games = $igdb->get(); 37 | ``` 38 | 39 | -------------------------------------------------------------------------------- /docs/03-select.md: -------------------------------------------------------------------------------- 1 | # Select (Fields) 2 | 3 | Select which fields should be in the response. If you want to have all available fields in the response you can also 4 | skip this method as the query builder will select `*` by default. (**Attention**: This is the opposite behaviour from 5 | the Apicalypse API) 6 | 7 | ```php 8 | // torchlight! {"lineNumbers": false} 9 | use MarcReichel\IGDBLaravel\Models\Game; 10 | 11 | $games = Game::select(['*'])->get(); 12 | 13 | $games = Game::select(['name', 'first_release_date'])->get(); 14 | ``` 15 | -------------------------------------------------------------------------------- /docs/04-search.md: -------------------------------------------------------------------------------- 1 | # Search 2 | 3 | ```php 4 | // torchlight! {"lineNumbers": false} 5 | use MarcReichel\IGDBLaravel\Models\Game; 6 | 7 | $games = Game::search('Fortnite')->get(); 8 | ``` 9 | 10 | ## Searchable models 11 | 12 | - `Character` 13 | - `Collection` 14 | - `Game` 15 | - `Platform` 16 | - `Theme` 17 | -------------------------------------------------------------------------------- /docs/05-fuzzy-search.md: -------------------------------------------------------------------------------- 1 | # Fuzzy Search 2 | 3 | The fuzzy search (since v3.1.0) acts like a "where like" chain under the hood. 4 | 5 | ```php 6 | // torchlight! {"lineNumbers": false} 7 | use MarcReichel\IGDBLaravel\Models\Game; 8 | 9 | $games = Game::fuzzySearch( 10 | // fields to search in 11 | [ 12 | 'name', 13 | 'involved_companies.company.name', // you can search for nested values as well 14 | ], 15 | // the query to search for 16 | 'Call of Duty', 17 | // enable/disable case sensitivity (disabled by default) 18 | false, 19 | )->get(); 20 | ``` 21 | 22 | **Attention**: Keep in mind you have to do the sorting of the results yourself. They are not ordered by relevance or 23 | any other way. 24 | -------------------------------------------------------------------------------- /docs/06-where-clauses.md: -------------------------------------------------------------------------------- 1 | # Where clauses 2 | 3 | ## Simple where clause 4 | 5 | ```php 6 | // torchlight! {"lineNumbers": false} 7 | use MarcReichel\IGDBLaravel\Models\Game; 8 | 9 | $games = Game::where('first_release_date', '>=', now()->subMonth()) 10 | ->get(); 11 | ``` 12 | 13 | > **Please note**: `Carbon` objects are supported since v3.4.0. 14 | 15 | For convenience, if you want to verify that a column is equal to a given value, you may pass the value directly as the 16 | second argument to the `where` method: 17 | 18 | ```php 19 | // torchlight! {"lineNumbers": false} 20 | use MarcReichel\IGDBLaravel\Models\Game; 21 | 22 | $games = Game::where('name', 'Fortnite')->get(); 23 | 24 | // this is the same as 25 | 26 | $games = Game::where('name', '=', 'Fortnite')->get(); 27 | ``` 28 | 29 | ## OR statements 30 | 31 | You may chain where constraints together as well as add `or` clauses to the query. The `orWhere` method accepts the same 32 | arguments as the where method: 33 | 34 | ```php 35 | // torchlight! {"lineNumbers": false} 36 | use MarcReichel\IGDBLaravel\Models\Game; 37 | 38 | $games = Game::where('name', 'Fortnite') 39 | ->orWhere('name', 'Borderlands 2') 40 | ->get(); 41 | ``` 42 | 43 | ## Additional Where Clauses 44 | 45 | ### whereBetween 46 | 47 | The `whereBetween` method verifies that a fields's value is between two values: 48 | 49 | ```php 50 | // torchlight! {"lineNumbers": false} 51 | use MarcReichel\IGDBLaravel\Models\Game; 52 | 53 | $games = Game::whereBetween('first_release_date', now()->subYear(), now()) 54 | ->get(); 55 | ``` 56 | 57 | > **Please note**: `Carbon` objects are supported since v3.4.0. 58 | 59 | ### whereNotBetween 60 | 61 | The `whereNotBetween` method verifies that a field's value lies outside of two 62 | values: 63 | 64 | ```php 65 | // torchlight! {"lineNumbers": false} 66 | use MarcReichel\IGDBLaravel\Models\Game; 67 | 68 | $games = Game::whereNotBetween('first_release_date', now()->subYear(), now()) 69 | ->get(); 70 | ``` 71 | 72 | > **Please note**: `Carbon` objects are supported since v3.4.0. 73 | 74 | ### whereIn 75 | 76 | The `whereIn` method verifies that a given field's value is contained within the 77 | given array: 78 | 79 | ```php 80 | // torchlight! {"lineNumbers": false} 81 | use MarcReichel\IGDBLaravel\Models\Game; 82 | 83 | $games = Game::whereIn('category', [0,4])->get(); 84 | ``` 85 | 86 | ### whereNotIn 87 | 88 | The `whereNotIn` method verifies that the given field's value is **not** 89 | contained in the given array: 90 | 91 | ```php 92 | // torchlight! {"lineNumbers": false} 93 | use MarcReichel\IGDBLaravel\Models\Game; 94 | 95 | $games = Game::whereNotIn('category', [0,4])->get(); 96 | ``` 97 | 98 | ### whereInAll / whereNotInAll / whereInExact / whereNotInExact 99 | 100 | Alternatively you could use one of these methods to match against **all** or **exactly** the given array. 101 | 102 | ### whereNull 103 | 104 | The `whereNull` method verifies that the value of the given field is `NULL`: 105 | 106 | ```php 107 | // torchlight! {"lineNumbers": false} 108 | use MarcReichel\IGDBLaravel\Models\Game; 109 | 110 | $games = Game::whereNull('first_release_date')->get(); 111 | ``` 112 | 113 | ### whereNotNull 114 | 115 | The `whereNotNull` method verifies that the field's value is **not** `NULL`: 116 | 117 | ```php 118 | // torchlight! {"lineNumbers": false} 119 | use MarcReichel\IGDBLaravel\Models\Game; 120 | 121 | $games = Game::whereNotNull('first_release_date')->get(); 122 | ``` 123 | 124 | ### whereDate 125 | 126 | The `whereDate` method may be used to compare a field's value against a date: 127 | 128 | ```php 129 | // torchlight! {"lineNumbers": false} 130 | use MarcReichel\IGDBLaravel\Models\Game; 131 | 132 | $games = Game::whereDate('first_release_date', '2019-01-01') 133 | ->get(); 134 | ``` 135 | 136 | ### whereYear 137 | 138 | The `whereYear` method may be used to compare a fields's value against a specific 139 | year: 140 | 141 | ```php 142 | // torchlight! {"lineNumbers": false} 143 | use MarcReichel\IGDBLaravel\Models\Game; 144 | 145 | $games = Game::whereYear('first_release_date', 2019) 146 | ->get(); 147 | ``` 148 | 149 | ### whereHas / whereHasNot 150 | 151 | These methods have the same syntax as `whereNull` and `whereNotNull` and literally 152 | do the exact same thing. 153 | 154 | ## Parameter Grouping 155 | 156 | ```php 157 | // torchlight! {"lineNumbers": false} 158 | use MarcReichel\IGDBLaravel\Models\Game; 159 | 160 | $games = Game::where('name', 'Fortnite') 161 | ->orWhere(function($query) { 162 | $query->where('aggregated_rating', '>=', 90) 163 | ->where('aggregated_rating_count', '>=', 3000); 164 | }) 165 | ->get(); 166 | ``` 167 | -------------------------------------------------------------------------------- /docs/07-order-limit-offset.md: -------------------------------------------------------------------------------- 1 | # Ordering, Limit, & Offset 2 | 3 | ## orderBy 4 | 5 | The `orderBy` method allows you to sort the result of the query by a given field. 6 | The first argument to the `orderBy` method should be the field you wish to sort 7 | by, while the second argument controls the direction of the sort and may be either 8 | `asc` or `desc`: 9 | 10 | ```php 11 | // torchlight! {"lineNumbers": false} 12 | use MarcReichel\IGDBLaravel\Models\Game; 13 | 14 | $games = Game::orderBy('first_release_date', 'asc')->get(); 15 | ``` 16 | 17 | ## skip / take (limit / offset) 18 | 19 | To limit the number of results returned from the query, or to skip a given 20 | number of results in the query, you may use the `skip` and `take` methods (`take` is limited to a maximum of 500): 21 | 22 | ```php 23 | // torchlight! {"lineNumbers": false} 24 | use MarcReichel\IGDBLaravel\Models\Game; 25 | 26 | $games = Game::skip(10)->take(5)->get(); 27 | ``` 28 | 29 | Alternatively, you may use the `limit` and `offset` methods: 30 | 31 | ```php 32 | // torchlight! {"lineNumbers": false} 33 | use MarcReichel\IGDBLaravel\Models\Game; 34 | 35 | $games = Game::offset(10)->limit(5)->get(); 36 | ``` 37 | -------------------------------------------------------------------------------- /docs/08-cache.md: -------------------------------------------------------------------------------- 1 | # Cache 2 | 3 | You can overwrite the default cache time for one specific query. So you can for 4 | example turn off caching for a query: 5 | 6 | ```php 7 | // torchlight! {"lineNumbers": false} 8 | use MarcReichel\IGDBLaravel\Models\Game; 9 | 10 | $games = Game::cache(0)->get(); 11 | ``` 12 | -------------------------------------------------------------------------------- /docs/09-relationships.md: -------------------------------------------------------------------------------- 1 | # Relationships (Extends) 2 | 3 | To extend your result use the `with`-method: 4 | 5 | ```php 6 | // torchlight! {"lineNumbers": false} 7 | use MarcReichel\IGDBLaravel\Models\Game; 8 | 9 | $game = Game::with(['cover', 'artworks'])->get(); 10 | ``` 11 | 12 | By default, every field (`*`) of the relationship is selected. 13 | If you want to define the fields of the relationship yourself you have to define 14 | the relationship as the array-key and the fields as an array: 15 | 16 | ```php 17 | // torchlight! {"lineNumbers": false} 18 | use MarcReichel\IGDBLaravel\Models\Game; 19 | 20 | $game = Game::with(['cover' => ['url', 'image_id']])->get(); 21 | ``` 22 | -------------------------------------------------------------------------------- /docs/10-fetch-results.md: -------------------------------------------------------------------------------- 1 | # Fetch results 2 | 3 | ## Get 4 | 5 | To finally get results for the query, simply call `get`: 6 | 7 | ```php 8 | // torchlight! {"lineNumbers": false} 9 | use MarcReichel\IGDBLaravel\Models\Game; 10 | 11 | $games = Game::get(); 12 | ``` 13 | 14 | ## All 15 | 16 | If you just want to get "all" results (limited to a maximum of 500) 17 | just call the `all`-Method directly on your model: 18 | 19 | ```php 20 | // torchlight! {"lineNumbers": false} 21 | use MarcReichel\IGDBLaravel\Models\Game; 22 | 23 | $games = Game::all(); 24 | ``` 25 | 26 | ## First 27 | 28 | If you only want one result call the `first`-method after your query: 29 | 30 | ```php 31 | // torchlight! {"lineNumbers": false} 32 | use MarcReichel\IGDBLaravel\Models\Game; 33 | 34 | $game = Game::first(); 35 | ``` 36 | 37 | ## Find 38 | 39 | If you know the Identifier of the model you can simply call the `find`-method 40 | with the identifier as a parameter: 41 | 42 | ```php 43 | // torchlight! {"lineNumbers": false} 44 | use MarcReichel\IGDBLaravel\Models\Game; 45 | 46 | $game = Game::find(1905); 47 | ``` 48 | 49 | ### FindOrFail 50 | 51 | `find` returns `null` if no result were found. If you want to throw an Exception 52 | instead use `findOrFail`. This will throw an 53 | `MarcReichel\IGDBLaravel\Exceptions\ModelNotFoundException` if no result were 54 | found. 55 | -------------------------------------------------------------------------------- /docs/11-properties.md: -------------------------------------------------------------------------------- 1 | # Reading properties 2 | 3 | ## Model-based approach 4 | 5 | If you used the Model-based approach you can simply get a property: 6 | 7 | ```php 8 | // torchlight! {"lineNumbers": false} 9 | use MarcReichel\IGDBLaravel\Models\Game; 10 | 11 | $game = Game::find(1905); 12 | 13 | if ($game) { 14 | echo $game->name; // Will output "Fortnite" 15 | } 16 | ``` 17 | 18 | If you want to access a property which does not exist `null` is returned: 19 | 20 | ```php 21 | // torchlight! {"lineNumbers": false} 22 | use MarcReichel\IGDBLaravel\Models\Game; 23 | 24 | $game = Game::find(1905); 25 | 26 | if ($game) { 27 | echo $game->foo; // Will output nothing 28 | } 29 | ``` 30 | 31 | ## Query Builder-based approach 32 | 33 | If you used the Query Builder itself you must check if a property exists 34 | yourself. 35 | -------------------------------------------------------------------------------- /docs/12-images.md: -------------------------------------------------------------------------------- 1 | # Images 2 | 3 | Since version 3.5.0 it is possible to generate image URLs for 4 | the [different available sizes](https://api-docs.igdb.com/#images). 5 | 6 | This is supported for: 7 | 8 | - `Artwork` 9 | - `CharacterMugShot` 10 | - `CompanyLogo` 11 | - `Cover` 12 | - `EventLogo` 13 | - `GameEngineLogo` 14 | - `PlatformLogo` 15 | - `Screenshot` 16 | 17 | ## Basic Usage 18 | 19 | ### Default image 20 | 21 | ```php 22 | // torchlight! {"lineNumbers": false} 23 | use MarcReichel\IGDBLaravel\Enums\Image\Size; 24 | use MarcReichel\IGDBLaravel\Models\Game; 25 | 26 | $game = Game::where('name', 'Fortnite') 27 | ->with(['cover']) 28 | ->first(); 29 | 30 | $game->cover->getUrl(); 31 | ``` 32 | 33 | ### Other sizes 34 | 35 | As the first parameter the method receives your desired image size. Simply use the available enum values 36 | of the `MarcReichel\IGDBLaravel\Enums\Image\Size` enum. 37 | 38 | ```php 39 | // torchlight! {"lineNumbers": false} 40 | use MarcReichel\IGDBLaravel\Enums\Image\Size; 41 | use MarcReichel\IGDBLaravel\Models\Game; 42 | 43 | $game = Game::where('name', 'Fortnite') 44 | ->with(['cover']) 45 | ->first(); 46 | 47 | $game->cover->getUrl(Size::COVER_BIG); 48 | ``` 49 | 50 | ### Retina images 51 | 52 | If you want to get retina images simply set the second parameter to `true`. 53 | 54 | ```php 55 | // torchlight! {"lineNumbers": false} 56 | use MarcReichel\IGDBLaravel\Enums\Image\Size; 57 | use MarcReichel\IGDBLaravel\Models\Game; 58 | 59 | $game = Game::where('name', 'Fortnite') 60 | ->with(['cover']) 61 | ->first(); 62 | 63 | $game->cover->getUrl(Size::COVER_BIG, true); 64 | ``` 65 | -------------------------------------------------------------------------------- /docs/90-webhooks.md: -------------------------------------------------------------------------------- 1 | # Webhooks 2 | 3 | Since version 2.3.0 of this package you can create webhooks and handle their requests with ease. 🎉 4 | 5 | ## Initial Setup 6 | 7 | ### Configuration 8 | 9 | Inside your `config/igdb.php` file you need to have a `webhook_path` and `webhook_secret` of your choice like so: 10 | 11 | ```php 12 | // torchlight! {"lineNumbers": false} 13 | // torchlight! {"summaryCollapsedIndicator": "⌄"} 14 | [ 22 | 'client_id' => env('TWITCH_CLIENT_ID', ''), 23 | 'client_secret' => env('TWITCH_CLIENT_SECRET', ''), 24 | ], 25 | 26 | /* 27 | * This package caches queries automatically (for 1 hour per default). 28 | * Here you can set how long each query should be cached (in seconds). 29 | * 30 | * To turn cache off set this value to 0 31 | */ 32 | 'cache_lifetime' => env('IGDB_CACHE_LIFETIME', 3600), // [tl! collapse:end] 33 | 34 | /* 35 | * Path where the webhooks should be handled. 36 | */ 37 | 'webhook_path' => 'igdb-webhook/handle', 38 | 39 | /* 40 | * The webhook secret. 41 | * 42 | * This needs to be a string of your choice in order to use the webhook 43 | * functionality. 44 | */ 45 | 'webhook_secret' => env('IGDB_WEBHOOK_SECRET'), 46 | ]; 47 | ``` 48 | 49 | _**Please note**: You only need to add this part to your config if you have upgraded from a prior version of this 50 | package. New installations have this configured automatically._ 51 | 52 | And then set a secret inside your `.env` file: 53 | 54 | ```dotenv 55 | // torchlight! {"lineNumbers": false} 56 | IGDB_WEBHOOK_SECRET=yoursecret 57 | ``` 58 | 59 | > Make sure your `APP_URL` (inside your `.env`) is something different than `localhost` or `127.0.0.1`. Otherwise webhooks can 60 | > not be created. 61 | 62 | That's it! 63 | 64 | ## Create a webhook 65 | 66 | Let's say we want to be informed whenever a new game is created on https://igdb.com. 67 | 68 | First of all we need to inform IGDB that we want to be informed. 69 | 70 | For this we create a webhook like so (for example inside a controller): 71 | 72 | ```php 73 | // torchlight! {"lineNumbers": false} 74 | use MarcReichel\IGDBLaravel\Enums\Webhook\Method; 75 | use MarcReichel\IGDBLaravel\Models\Game; 76 | use Illuminate\Routing\Controller; 77 | 78 | class ExampleController extends Controller 79 | { 80 | public function createWebhook() 81 | { 82 | Game::createWebhook(Method::CREATE) 83 | } 84 | } 85 | ``` 86 | 87 | ## Listen for events 88 | 89 | Now that we have created our webhook we can listen for a specific event - in our case when a game is created. 90 | 91 | For this we create a Laravel EventListener or for sake of simplicity we just listen for an event inside the `boot()` 92 | method of our `app/providers/EventServiceProvider.php`: 93 | 94 | ```php 95 | // torchlight! {"lineNumbers": false} 96 | use MarcReichel\IGDBLaravel\Events\GameCreated; 97 | use Illuminate\Support\Facades\Event; 98 | 99 | public function boot() 100 | { 101 | Event::listen(function (GameCreated $event) { 102 | // $event->data holds the (unexpanded!) data (of the game in this case) 103 | }); 104 | } 105 | ``` 106 | 107 | [Here](https://github.com/marcreichel/igdb-laravel/tree/main/src/Events) you can find a list of all available events. 108 | 109 | Further information on how to set up event listeners can be found on 110 | the [official docs](https://laravel.com/docs/events). 111 | 112 | ## Manage webhooks via CLI 113 | 114 | ### List your webhooks 115 | 116 | ```bash 117 | // torchlight! {"lineNumbers": false} 118 | $ php artisan igdb:webhooks 119 | ``` 120 | 121 | ### Create a webhook 122 | 123 | ```bash 124 | // torchlight! {"lineNumbers": false} 125 | $ php artisan igdb:webhooks:create {model?} {--method=} 126 | ``` 127 | 128 | You can also just call `php artisan igdb:webhooks:create` without any arguments. The command will then ask for the 129 | required data interactively. 130 | 131 | The `model` parameter needs to be the (studly cased) class name of a model (e.g. `Game`). 132 | 133 | The `--method` option needs to be one of `create`, `update` or `delete` accordingly for which event you want to listen. 134 | 135 | ### Reactivate a webhook 136 | 137 | ```bash 138 | // torchlight! {"lineNumbers": false} 139 | $ php artisan igdb:webhooks:reactivate {id} 140 | ``` 141 | 142 | For `{id}` insert the id of the (inactive) webhook. 143 | 144 | ### Delete a webhook 145 | 146 | ```bash 147 | // torchlight! {"lineNumbers": false} 148 | $ php artisan igdb:webhooks:delete {id?} {--A|all} 149 | ``` 150 | 151 | You may provide the `id` of a webhook to delete it or use the `-A`/`--all` flag to delete all your registered webhooks. 152 | -------------------------------------------------------------------------------- /docs/art/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marcreichel/igdb-laravel/59632c0c76a20731dd40a4399c1ed0a8cea8aa0c/docs/art/.gitkeep -------------------------------------------------------------------------------- /docs/art/cover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marcreichel/igdb-laravel/59632c0c76a20731dd40a4399c1ed0a8cea8aa0c/docs/art/cover.png -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # Introduction 2 | 3 | {.inline-images} 4 | [![Packagist Version](https://img.shields.io/packagist/v/marcreichel/igdb-laravel?style=for-the-badge)](https://packagist.org/packages/marcreichel/igdb-laravel) 5 | [![Packagist Downloads](https://img.shields.io/packagist/dt/marcreichel/igdb-laravel?style=for-the-badge)](https://packagist.org/packages/marcreichel/igdb-laravel) 6 | [![tests](https://img.shields.io/github/actions/workflow/status/marcreichel/igdb-laravel/tests.yml?event=push&style=for-the-badge&logo=github&label=tests)](https://github.com/marcreichel/igdb-laravel/actions/workflows/tests.yml) 7 | [![Pint](https://img.shields.io/github/actions/workflow/status/marcreichel/igdb-laravel/code-style.yml?event=push&style=for-the-badge&logo=github&label=Code-Style)](https://github.com/marcreichel/igdb-laravel/actions/workflows/pint.yml) 8 | [![PHPStan](https://img.shields.io/github/actions/workflow/status/marcreichel/igdb-laravel/code-quality.yml?event=push&style=for-the-badge&logo=github&label=Code-Quality)](https://github.com/marcreichel/igdb-laravel/actions/workflows/code-quality.yml) 9 | [![CodeFactor](https://img.shields.io/codefactor/grade/github/marcreichel/igdb-laravel?style=for-the-badge&logo=codefactor&label=Codefactor)](https://www.codefactor.io/repository/github/marcreichel/igdb-laravel) 10 | [![CodeCov](https://img.shields.io/codecov/c/github/marcreichel/igdb-laravel?token=m6FOB0CyPE&style=for-the-badge&logo=codecov)](https://codecov.io/gh/marcreichel/igdb-laravel) 11 | [![GitHub](https://img.shields.io/github/license/marcreichel/igdb-laravel?style=for-the-badge)](https://packagist.org/packages/marcreichel/igdb-laravel) 12 | 13 | ![Cover](art/cover.png){style="width: 100%"} 14 | 15 | This is a Laravel wrapper for version 4 of the [IGDB API](https://api-docs.igdb.com/) (Apicalypse) including [webhook handling](90-webhooks.md). 16 | 17 | It handles authentication and caching of the IGDB API automatically. 18 | 19 | ## Example 20 | 21 | ```php 22 | // torchlight! {"lineNumbers": false} 23 | use MarcReichel\IGDBLaravel\Models\Game; 24 | 25 | $game = Game::where('name', 'Fortnite')->first(); 26 | ``` 27 | -------------------------------------------------------------------------------- /phpstan-baseline.neon: -------------------------------------------------------------------------------- 1 | parameters: 2 | ignoreErrors: 3 | - 4 | message: "#^Unable to resolve the template type TMapValue in call to method Illuminate\\\\Support\\\\Collection\\<\\(int\\|string\\),mixed\\>\\:\\:map\\(\\)$#" 5 | count: 1 6 | path: src/Models/Model.php 7 | 8 | - 9 | message: "#^Cannot call method assertExitCode\\(\\) on Illuminate\\\\Testing\\\\PendingCommand\\|int\\.$#" 10 | count: 12 11 | path: tests/ConsoleTest.php 12 | 13 | - 14 | message: "#^Cannot call method expectsQuestion\\(\\) on Illuminate\\\\Testing\\\\PendingCommand\\|int\\.$#" 15 | count: 7 16 | path: tests/ConsoleTest.php 17 | -------------------------------------------------------------------------------- /phpstan.neon: -------------------------------------------------------------------------------- 1 | includes: 2 | - ./vendor/larastan/larastan/extension.neon 3 | - phpstan-baseline.neon 4 | 5 | parameters: 6 | paths: 7 | - src 8 | - tests 9 | 10 | # The level 9 is the highest level 11 | level: 8 12 | ignoreErrors: 13 | - '#Call to an undefined static method MarcReichel\\IGDBLaravel\\Models\\Game::foo\(\).#' 14 | - '#Unable to resolve the template type TValue in call to function collect#' 15 | - '#Unable to resolve the template type TKey in call to function collect#' 16 | - identifier: missingType.iterableValue 17 | - identifier: missingType.generics 18 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | tests 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | ./src 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /pint.json: -------------------------------------------------------------------------------- 1 | { 2 | "preset": "psr12", 3 | "rules": { 4 | "align_multiline_comment": true, 5 | "array_indentation": true, 6 | "array_push": true, 7 | "array_syntax": { 8 | "syntax": "short" 9 | }, 10 | "assign_null_coalescing_to_coalesce_equal": true, 11 | "binary_operator_spaces": true, 12 | "blank_line_before_statement": true, 13 | "cast_spaces": true, 14 | "clean_namespace": true, 15 | "combine_consecutive_issets": true, 16 | "combine_consecutive_unsets": true, 17 | "compact_nullable_typehint": true, 18 | "concat_space": { 19 | "spacing": "one" 20 | }, 21 | "declare_strict_types": true, 22 | "fully_qualified_strict_types": true, 23 | "function_to_constant": true, 24 | "general_phpdoc_annotation_remove": { 25 | "annotations": [ 26 | "author", 27 | "package", 28 | "since" 29 | ], 30 | "case_sensitive": false 31 | }, 32 | "get_class_to_class_keyword": true, 33 | "is_null": true, 34 | "lambda_not_used_import": true, 35 | "logical_operators": true, 36 | "method_chaining_indentation": true, 37 | "modernize_types_casting": true, 38 | "multiline_whitespace_before_semicolons": true, 39 | "no_empty_comment": true, 40 | "no_empty_phpdoc": true, 41 | "no_empty_statement": true, 42 | "no_extra_blank_lines": { 43 | "tokens": ["attribute", "break", "case", "continue", "curly_brace_block", "default", "extra", "parenthesis_brace_block", "return", "square_brace_block", "switch", "throw", "use", "use_trait"] 44 | }, 45 | "no_multiline_whitespace_around_double_arrow": true, 46 | "no_short_bool_cast": true, 47 | "no_singleline_whitespace_before_semicolons": true, 48 | "no_superfluous_elseif": true, 49 | "no_superfluous_phpdoc_tags": true, 50 | "no_trailing_comma_in_singleline": true, 51 | "no_unneeded_control_parentheses": true, 52 | "no_useless_concat_operator": true, 53 | "no_useless_else": true, 54 | "no_useless_nullsafe_operator": true, 55 | "no_useless_return": true, 56 | "no_whitespace_before_comma_in_array": true, 57 | "nullable_type_declaration": true, 58 | "object_operator_without_whitespace": true, 59 | "ordered_imports": { 60 | "imports_order": [ 61 | "class", 62 | "function", 63 | "const" 64 | ], 65 | "sort_algorithm": "alpha" 66 | }, 67 | "ordered_interfaces": true, 68 | "ordered_types": { 69 | "null_adjustment": "always_last" 70 | }, 71 | "phpdoc_align": { 72 | "align": "left" 73 | }, 74 | "phpdoc_indent": true, 75 | "phpdoc_no_useless_inheritdoc": true, 76 | "phpdoc_order": true, 77 | "phpdoc_scalar": true, 78 | "phpdoc_single_line_var_spacing": true, 79 | "phpdoc_separation": { 80 | "groups": [ 81 | [ 82 | "deprecated", 83 | "link", 84 | "see" 85 | ], 86 | [ 87 | "template", 88 | "template-extends", 89 | "template-implements" 90 | ] 91 | ] 92 | }, 93 | "phpdoc_summary": true, 94 | "phpdoc_tag_casing": true, 95 | "phpdoc_trim": true, 96 | "phpdoc_trim_consecutive_blank_line_separation": true, 97 | "phpdoc_var_without_name": true, 98 | "php_unit_construct": true, 99 | "php_unit_dedicate_assert": true, 100 | "php_unit_dedicate_assert_internal_type": true, 101 | "php_unit_internal_class": true, 102 | "php_unit_method_casing": true, 103 | "return_assignment": true, 104 | "return_type_declaration": true, 105 | "short_scalar_cast": true, 106 | "single_line_comment_spacing": true, 107 | "single_line_comment_style": true, 108 | "single_quote": true, 109 | "single_space_around_construct": true, 110 | "ternary_to_null_coalescing": true, 111 | "trailing_comma_in_multiline": { 112 | "elements": [ 113 | "arguments", 114 | "arrays", 115 | "match", 116 | "parameters" 117 | ] 118 | }, 119 | "trim_array_spaces": true, 120 | "type_declaration_spaces": true, 121 | "types_spaces": { 122 | "space": "single" 123 | }, 124 | "use_arrow_functions": true, 125 | "void_return": true, 126 | "whitespace_after_comma_in_array": { 127 | "ensure_single_space": true 128 | } 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /rector.php: -------------------------------------------------------------------------------- 1 | withPhpSets(php84: true) 9 | ->withAttributesSets(phpunit: true) 10 | ->withRules([ 11 | Rector\CodeQuality\Rector\Ternary\ArrayKeyExistsTernaryThenValueToCoalescingRector::class, 12 | Rector\CodeQuality\Rector\NullsafeMethodCall\CleanupUnneededNullsafeOperatorRector::class, 13 | Rector\CodeQuality\Rector\ClassMethod\InlineArrayReturnAssignRector::class, 14 | Rector\CodeQuality\Rector\Ternary\UnnecessaryTernaryExpressionRector::class, 15 | Rector\DeadCode\Rector\Foreach_\RemoveUnusedForeachKeyRector::class, 16 | Rector\TypeDeclaration\Rector\ClassMethod\ReturnTypeFromStrictFluentReturnRector::class, 17 | Rector\Php80\Rector\Class_\StringableForToStringRector::class, 18 | Rector\CodingStyle\Rector\ArrowFunction\StaticArrowFunctionRector::class, 19 | Rector\CodingStyle\Rector\Closure\StaticClosureRector::class, 20 | Rector\DeadCode\Rector\Node\RemoveNonExistingVarAnnotationRector::class, 21 | Rector\DeadCode\Rector\ClassMethod\RemoveUnusedPrivateMethodParameterRector::class, 22 | Rector\TypeDeclaration\Rector\ClassMethod\BoolReturnTypeFromBooleanStrictReturnsRector::class, 23 | Rector\TypeDeclaration\Rector\ClassMethod\ReturnTypeFromReturnNewRector::class, 24 | Rector\TypeDeclaration\Rector\ClassMethod\ParamTypeByMethodCallTypeRector::class, 25 | Rector\TypeDeclaration\Rector\ClassMethod\NumericReturnTypeFromStrictScalarReturnsRector::class, 26 | Rector\TypeDeclaration\Rector\ClassMethod\AddMethodCallBasedStrictParamTypeRector::class, 27 | Rector\CodeQuality\Rector\If_\ExplicitBoolCompareRector::class, 28 | Rector\CodeQuality\Rector\Foreach_\ForeachItemsAssignToEmptyArrayToAssignRector::class, 29 | Rector\CodeQuality\Rector\Foreach_\ForeachToInArrayRector::class, 30 | Rector\CodeQuality\Rector\BooleanAnd\RemoveUselessIsObjectCheckRector::class, 31 | ]) 32 | ->withPaths([ 33 | __DIR__ . '/src', 34 | __DIR__ . '/tests', 35 | ]) 36 | ->withTypeCoverageLevel(0); 37 | -------------------------------------------------------------------------------- /routes/web.php: -------------------------------------------------------------------------------- 1 | Webhook::handle($request)); 13 | 14 | Route::post(substr(md5(config('igdb.credentials.client_id')), 0, 8) . '/{model}/{method}', static fn (Request $request) => Webhook::handle($request))->name('handle-igdb-webhook'); 15 | -------------------------------------------------------------------------------- /src/ApiHelper.php: -------------------------------------------------------------------------------- 1 | config('igdb.credentials.client_id'), 34 | 'client_secret' => config('igdb.credentials.client_secret'), 35 | 'grant_type' => 'client_credentials', 36 | ]); 37 | $response = Http::post('https://id.twitch.tv/oauth2/token?' . $query) 38 | ->throw() 39 | ->json(); 40 | 41 | if (is_array($response) && isset($response['access_token']) && $response['expires_in']) { 42 | Cache::put($accessTokenCacheKey, (string) $response['access_token'], (int) $response['expires_in'] - 60); 43 | 44 | $accessToken = $response['access_token']; 45 | } 46 | } catch (Exception) { 47 | throw new AuthenticationException('Access Token could not be retrieved from Twitch.'); 48 | } 49 | 50 | return (string) $accessToken; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Client.php: -------------------------------------------------------------------------------- 1 | self::request($endpoint, $query)); 23 | } 24 | 25 | public static function count(string $endpoint, string $query, int $cacheLifetime): int 26 | { 27 | $endpoint = Str::finish($endpoint, '/count'); 28 | $cacheKey = self::handleCache($endpoint, $query, $cacheLifetime); 29 | 30 | return Cache::remember($cacheKey, $cacheLifetime, static function () use ($endpoint, $query): int { 31 | $response = self::request($endpoint, $query); 32 | if (is_array($response)) { 33 | return (int) $response['count']; 34 | } 35 | 36 | return 0; 37 | }); 38 | } 39 | 40 | private static function handleCache(string $endpoint, string $query, int $cacheLifetime): string 41 | { 42 | $key = config('igdb.cache_prefix', 'igdb_cache') . '.' . md5($endpoint . $query); 43 | 44 | if ($cacheLifetime === 0) { 45 | Cache::forget($key); 46 | } 47 | 48 | return $key; 49 | } 50 | 51 | /** 52 | * @throws AuthenticationException 53 | * @throws RequestException 54 | */ 55 | private static function request(string $endpoint, string $query): mixed 56 | { 57 | $client = Http::withOptions([ 58 | 'base_uri' => ApiHelper::IGDB_BASE_URI, 59 | ])->withHeaders([ 60 | 'Accept' => 'application/json', 61 | 'Client-ID' => config('igdb.credentials.client_id'), 62 | ]); 63 | 64 | return $client->withHeaders([ 65 | 'Authorization' => 'Bearer ' . ApiHelper::retrieveAccessToken(), 66 | ]) 67 | ->withBody($query, 'plain/text') 68 | ->retry(3, 100) 69 | ->post($endpoint) 70 | ->throw() 71 | ->json(); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/Console/CreateWebhook.php: -------------------------------------------------------------------------------- 1 | argument('model') ?? $this->choice($modelQuestionString, $this->getModels()); 31 | 32 | if (!is_string($model)) { 33 | throw new InvalidArgumentException( 34 | 'Argument model has to be of type string. ' . gettype($model) . ' given.', 35 | ); 36 | } 37 | 38 | $namespace = 'MarcReichel\IGDBLaravel\Models\\'; 39 | $fullQualifiedName = $namespace . $model; 40 | 41 | if (!class_exists($fullQualifiedName)) { 42 | $this->line(''); 43 | $this->error('Model "' . $model . '" does not exist.'); 44 | $closestModel = $this->getClosestModel($model); 45 | if (!$closestModel) { 46 | return self::FAILURE; 47 | } 48 | if (!$this->confirm('Did you mean ' . $closestModel . '?')) { 49 | return self::FAILURE; 50 | } 51 | $fullQualifiedName = $namespace . $closestModel; 52 | } 53 | 54 | /** @var class-string $class */ 55 | $class = $fullQualifiedName; 56 | 57 | $methods = ['create', 'update', 'delete']; 58 | 59 | $method = $this->option('method') ?? $this->choice( 60 | 'For which event do you want to create the webhook?', 61 | $methods, 62 | 'update', 63 | ); 64 | 65 | if (!in_array($method, $methods, true)) { 66 | $this->error((new InvalidWebhookMethodException())->getMessage()); 67 | 68 | return self::FAILURE; 69 | } 70 | 71 | $mappedMethod = match ($method) { 72 | 'create' => Method::CREATE, 73 | 'update' => Method::UPDATE, 74 | 'delete' => Method::DELETE, 75 | }; 76 | 77 | try { 78 | $class::createWebhook($mappedMethod); 79 | } catch (Exception $e) { 80 | $this->error($e->getMessage()); 81 | 82 | return self::FAILURE; 83 | } 84 | 85 | $this->info('Webhook created successfully!'); 86 | 87 | return self::SUCCESS; 88 | } 89 | 90 | private function getModels(): array 91 | { 92 | $glob = glob(__DIR__ . '/../Models/*.php') ?: []; 93 | 94 | $pattern = '/\/(?:Model|PopularityPrimitive|Search|Webhook|Image)\.php$/'; 95 | $grep = preg_grep($pattern, $glob, PREG_GREP_INVERT); 96 | 97 | return collect($grep ?: []) 98 | ->map(static fn (string $path): string => basename($path, '.php')) 99 | ->toArray(); 100 | } 101 | 102 | private function getClosestModel(string $model): ?string 103 | { 104 | return collect($this->getModels())->map(static fn (string $m): array => [ 105 | 'model' => $m, 106 | 'levenshtein' => levenshtein($m, $model), 107 | ]) 108 | ->filter(static fn (array $m) => $m['levenshtein'] <= 5) 109 | ->sortBy(static fn (array $m) => $m['levenshtein']) 110 | ->map(static fn (array $m) => $m['model']) 111 | ->first(); 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/Console/DeleteWebhook.php: -------------------------------------------------------------------------------- 1 | argument('id'); 25 | 26 | if ($id !== 0) { 27 | return $this->deleteOne($id); 28 | } 29 | 30 | if ($this->option('all')) { 31 | return $this->deleteAll(); 32 | } 33 | 34 | return self::FAILURE; 35 | } 36 | 37 | private function deleteOne(int $id): int 38 | { 39 | $webhook = Webhook::find($id); 40 | 41 | if (!$webhook instanceof Webhook) { 42 | $this->error('Webhook not found.'); 43 | 44 | return self::FAILURE; 45 | } 46 | 47 | if (!$webhook->delete()) { 48 | $this->error('Webhook could not be deleted.'); 49 | 50 | return self::FAILURE; 51 | } 52 | 53 | $this->info('Webhook deleted.'); 54 | 55 | return self::SUCCESS; 56 | } 57 | 58 | private function deleteAll(): int 59 | { 60 | $webhooks = Webhook::all(); 61 | 62 | if ($webhooks->count() === 0) { 63 | $this->info('You do not have any registered webhooks.'); 64 | 65 | return self::SUCCESS; 66 | } 67 | 68 | $this->comment('Deleting all your registered webhooks ...'); 69 | 70 | $this->withProgressBar($webhooks, static function (Webhook $webhook): void { 71 | $webhook->delete(); 72 | }); 73 | 74 | $this->info(''); 75 | 76 | $this->info('All Webhooks deleted.'); 77 | 78 | return self::SUCCESS; 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/Console/ListWebhooks.php: -------------------------------------------------------------------------------- 1 | count() === 0) { 21 | $this->warn('You do not have any registered webhooks.'); 22 | 23 | return self::FAILURE; 24 | } 25 | $this->table( 26 | ['ID', 'URL', 'Model', 'Method', 'Retries', 'Active'], 27 | $webhooks->map(static function (Webhook $webhook) { 28 | $data = $webhook->toArray(); 29 | 30 | $data['active'] = $data['active'] ? ' ✅ ' : ' ❌ '; 31 | 32 | return $data; 33 | })->sortBy('id')->toArray(), 34 | ); 35 | 36 | return self::SUCCESS; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Console/PublishCommand.php: -------------------------------------------------------------------------------- 1 | call('vendor:publish', ['--tag' => 'igdb:config', '--force' => true]); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Console/ReactivateWebhook.php: -------------------------------------------------------------------------------- 1 | argument('id')); 29 | 30 | if (!$webhook) { 31 | $this->error('Webhook not found.'); 32 | 33 | return self::FAILURE; 34 | } 35 | 36 | if ($webhook->active) { 37 | $this->info('Webhook does not need to be reactivated.'); 38 | 39 | return self::SUCCESS; 40 | } 41 | 42 | $model = $webhook->getModel(); 43 | $method = $webhook->getMethod(); 44 | 45 | $fullQualifiedName = 'MarcReichel\\IGDBLaravel\\Models\\' . $model; 46 | 47 | if (!class_exists($fullQualifiedName)) { 48 | $this->error('Model not found.'); 49 | 50 | return self::FAILURE; 51 | } 52 | 53 | /** @var class-string $class */ 54 | $class = $fullQualifiedName; 55 | 56 | try { 57 | $class::createWebhook($method); 58 | } catch (AuthenticationException | WebhookSecretMissingException $e) { 59 | $this->error($e->getMessage()); 60 | 61 | return self::FAILURE; 62 | } 63 | 64 | $this->info('Webhook reactivated.'); 65 | 66 | return self::SUCCESS; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/Enums/AgeRating/Category.php: -------------------------------------------------------------------------------- 1 | class = static::class; 27 | $this->url = $request->fullUrl(); 28 | /** @var string $method */ 29 | $method = $request->route('method'); 30 | $this->method = $method; 31 | $this->created_at = new Carbon(); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Events/EventCreated.php: -------------------------------------------------------------------------------- 1 | publishes([ 20 | __DIR__ . '/../config/config.php' => config_path('igdb.php'), 21 | ], 'igdb:config'); 22 | 23 | Route::group($this->routeConfiguration(), function (): void { 24 | $this->loadRoutesFrom(__DIR__ . '/../routes/web.php'); 25 | }); 26 | 27 | if ($this->app->runningInConsole()) { 28 | $this->commands([ 29 | PublishCommand::class, 30 | CreateWebhook::class, 31 | ListWebhooks::class, 32 | DeleteWebhook::class, 33 | ReactivateWebhook::class, 34 | ]); 35 | } 36 | } 37 | 38 | public function register(): void 39 | { 40 | $this->mergeConfigFrom( 41 | __DIR__ . '/../config/config.php', 42 | 'igdb', 43 | ); 44 | } 45 | 46 | protected function routeConfiguration(): array 47 | { 48 | return [ 49 | 'prefix' => config('igdb.webhook_path'), 50 | ]; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Models/AgeRating.php: -------------------------------------------------------------------------------- 1 | AgeRatingContentDescription::class, 11 | ]; 12 | } 13 | -------------------------------------------------------------------------------- /src/Models/AgeRatingContentDescription.php: -------------------------------------------------------------------------------- 1 | Game::class, 11 | ]; 12 | } 13 | -------------------------------------------------------------------------------- /src/Models/Artwork.php: -------------------------------------------------------------------------------- 1 | Game::class, 11 | ]; 12 | } 13 | -------------------------------------------------------------------------------- /src/Models/Character.php: -------------------------------------------------------------------------------- 1 | Game::class, 11 | 'mug_shot' => CharacterMugShot::class, 12 | ]; 13 | } 14 | -------------------------------------------------------------------------------- /src/Models/CharacterMugShot.php: -------------------------------------------------------------------------------- 1 | CollectionRelation::class, 11 | 'as_parent_relations' => CollectionRelation::class, 12 | 'type' => CollectionType::class, 13 | ]; 14 | } 15 | -------------------------------------------------------------------------------- /src/Models/CollectionMembership.php: -------------------------------------------------------------------------------- 1 | Collection::class, 11 | 'game' => Game::class, 12 | 'type' => CollectionMembershipType::class, 13 | ]; 14 | } 15 | -------------------------------------------------------------------------------- /src/Models/CollectionMembershipType.php: -------------------------------------------------------------------------------- 1 | CollectionType::class, 11 | ]; 12 | } 13 | -------------------------------------------------------------------------------- /src/Models/CollectionRelation.php: -------------------------------------------------------------------------------- 1 | Collection::class, 11 | 'parent_collection' => Collection::class, 12 | 'type' => CollectionRelationType::class, 13 | ]; 14 | } 15 | -------------------------------------------------------------------------------- /src/Models/CollectionRelationType.php: -------------------------------------------------------------------------------- 1 | CollectionType::class, 11 | 'allowed_parent_type' => CollectionType::class, 12 | ]; 13 | } 14 | -------------------------------------------------------------------------------- /src/Models/CollectionType.php: -------------------------------------------------------------------------------- 1 | self::class, 11 | 'developed' => Game::class, 12 | 'logo' => CompanyLogo::class, 13 | 'parent' => self::class, 14 | 'published' => Game::class, 15 | 'websites' => CompanyWebsite::class, 16 | ]; 17 | } 18 | -------------------------------------------------------------------------------- /src/Models/CompanyLogo.php: -------------------------------------------------------------------------------- 1 | Game::class, 11 | 'game_localization' => GameLocalization::class, 12 | ]; 13 | } 14 | -------------------------------------------------------------------------------- /src/Models/Event.php: -------------------------------------------------------------------------------- 1 | EventLogo::class, 11 | 'games' => Game::class, 12 | 'videos' => GameVideo::class, 13 | ]; 14 | } 15 | -------------------------------------------------------------------------------- /src/Models/EventLogo.php: -------------------------------------------------------------------------------- 1 | Event::class, 11 | ]; 12 | } 13 | -------------------------------------------------------------------------------- /src/Models/EventNetwork.php: -------------------------------------------------------------------------------- 1 | Event::class, 11 | 'network_type' => NetworkType::class, 12 | ]; 13 | } 14 | -------------------------------------------------------------------------------- /src/Models/ExternalGame.php: -------------------------------------------------------------------------------- 1 | Game::class, 11 | 'platform' => Platform::class, 12 | ]; 13 | } 14 | -------------------------------------------------------------------------------- /src/Models/Franchise.php: -------------------------------------------------------------------------------- 1 | Game::class, 11 | ]; 12 | } 13 | -------------------------------------------------------------------------------- /src/Models/Game.php: -------------------------------------------------------------------------------- 1 | AgeRating::class, 11 | 'alternative_names' => AlternativeName::class, 12 | 'artwork' => Artwork::class, 13 | 'bundles' => self::class, 14 | 'collection' => Collection::class, 15 | 'collections' => Collection::class, 16 | 'cover' => Cover::class, 17 | 'dlcs' => self::class, 18 | 'expanded_games' => self::class, 19 | 'expansions' => self::class, 20 | 'external_games' => ExternalGame::class, 21 | 'forks' => self::class, 22 | 'franchise' => Franchise::class, 23 | 'franchises' => Franchise::class, 24 | 'game_engines' => GameEngine::class, 25 | 'game_localizations' => GameLocalization::class, 26 | 'game_modes' => GameMode::class, 27 | 'genres' => Genre::class, 28 | 'involved_companies' => InvolvedCompany::class, 29 | 'keywords' => Keyword::class, 30 | 'language_supports' => LanguageSupport::class, 31 | 'multiplayer_modes' => MultiplayerMode::class, 32 | 'parent_game' => self::class, 33 | 'platforms' => Platform::class, 34 | 'player_perspectives' => PlayerPerspective::class, 35 | 'ports' => self::class, 36 | 'release_dates' => ReleaseDate::class, 37 | 'remakes' => self::class, 38 | 'remasters' => self::class, 39 | 'screenshots' => Screenshot::class, 40 | 'similar_games' => self::class, 41 | 'standalone_expansions' => self::class, 42 | 'tags' => null, 43 | 'themes' => Theme::class, 44 | 'version_parent' => self::class, 45 | 'videos' => GameVideo::class, 46 | 'websites' => Website::class, 47 | ]; 48 | } 49 | -------------------------------------------------------------------------------- /src/Models/GameEngine.php: -------------------------------------------------------------------------------- 1 | Company::class, 11 | 'logo' => GameEngineLogo::class, 12 | 'platforms' => Platform::class, 13 | ]; 14 | } 15 | -------------------------------------------------------------------------------- /src/Models/GameEngineLogo.php: -------------------------------------------------------------------------------- 1 | Cover::class, 11 | 'game' => Game::class, 12 | 'region' => Region::class, 13 | ]; 14 | } 15 | -------------------------------------------------------------------------------- /src/Models/GameMode.php: -------------------------------------------------------------------------------- 1 | Game::class, 11 | ]; 12 | } 13 | -------------------------------------------------------------------------------- /src/Models/GameVersion.php: -------------------------------------------------------------------------------- 1 | GameVersionFeature::class, 11 | ]; 12 | } 13 | -------------------------------------------------------------------------------- /src/Models/GameVersionFeature.php: -------------------------------------------------------------------------------- 1 | GameVersionFeatureValue::class, 11 | ]; 12 | } 13 | -------------------------------------------------------------------------------- /src/Models/GameVersionFeatureValue.php: -------------------------------------------------------------------------------- 1 | Game::class, 11 | 'game_feature' => GameVersionFeature::class, 12 | ]; 13 | } 14 | -------------------------------------------------------------------------------- /src/Models/GameVideo.php: -------------------------------------------------------------------------------- 1 | Game::class, 11 | ]; 12 | } 13 | -------------------------------------------------------------------------------- /src/Models/Genre.php: -------------------------------------------------------------------------------- 1 | getAttribute('image_id'); 22 | 23 | if ($size instanceof Size) { 24 | $parsedSize = $size->value; 25 | } else { 26 | $parsedSize = $size; 27 | } 28 | 29 | $cases = collect(Size::cases()) 30 | ->map(static fn (Size $s) => $s->value) 31 | ->values() 32 | ->toArray(); 33 | 34 | if (!in_array($parsedSize, $cases, true)) { 35 | throw new InvalidArgumentException('Size must be one of ' . implode(', ', $cases)); 36 | } 37 | 38 | if ($retina) { 39 | $parsedSize = Str::finish($parsedSize, '_2x'); 40 | } 41 | 42 | return "$basePath/t_$parsedSize/$id.jpg"; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Models/InvolvedCompany.php: -------------------------------------------------------------------------------- 1 | Company::class, 11 | 'game' => Game::class, 12 | ]; 13 | } 14 | -------------------------------------------------------------------------------- /src/Models/Keyword.php: -------------------------------------------------------------------------------- 1 | Game::class, 11 | 'language' => Language::class, 12 | 'language_support_type' => LanguageSupportType::class, 13 | ]; 14 | } 15 | -------------------------------------------------------------------------------- /src/Models/LanguageSupportType.php: -------------------------------------------------------------------------------- 1 | Game::class, 11 | 'platform' => Platform::class, 12 | ]; 13 | } 14 | -------------------------------------------------------------------------------- /src/Models/NetworkType.php: -------------------------------------------------------------------------------- 1 | EventNetwork::class, 11 | ]; 12 | } 13 | -------------------------------------------------------------------------------- /src/Models/Platform.php: -------------------------------------------------------------------------------- 1 | PlatformFamily::class, 11 | 'platform_logo' => PlatformLogo::class, 12 | 'versions' => PlatformVersion::class, 13 | 'websites' => PlatformWebsite::class, 14 | ]; 15 | } 16 | -------------------------------------------------------------------------------- /src/Models/PlatformFamily.php: -------------------------------------------------------------------------------- 1 | PlatformVersionCompany::class, 11 | 'main_manufacturer' => PlatformVersionCompany::class, 12 | 'platform_logo' => PlatformLogo::class, 13 | 'platform_version_release_dates' => PlatformVersionReleaseDate::class, 14 | ]; 15 | } 16 | -------------------------------------------------------------------------------- /src/Models/PlatformVersionCompany.php: -------------------------------------------------------------------------------- 1 | Company::class, 11 | ]; 12 | } 13 | -------------------------------------------------------------------------------- /src/Models/PlatformVersionReleaseDate.php: -------------------------------------------------------------------------------- 1 | PlatformVersion::class, 11 | ]; 12 | } 13 | -------------------------------------------------------------------------------- /src/Models/PlatformWebsite.php: -------------------------------------------------------------------------------- 1 | PopularityType::class, 11 | ]; 12 | } 13 | -------------------------------------------------------------------------------- /src/Models/PopularityType.php: -------------------------------------------------------------------------------- 1 | Game::class, 11 | 'platform' => Platform::class, 12 | 'status' => ReleaseDateStatus::class, 13 | ]; 14 | } 15 | -------------------------------------------------------------------------------- /src/Models/ReleaseDateStatus.php: -------------------------------------------------------------------------------- 1 | Game::class, 11 | ]; 12 | } 13 | -------------------------------------------------------------------------------- /src/Models/Search.php: -------------------------------------------------------------------------------- 1 | Character::class, 11 | 'collection' => Collection::class, 12 | 'company' => Company::class, 13 | 'game' => Game::class, 14 | 'platform' => Platform::class, 15 | 'theme' => Theme::class, 16 | ]; 17 | } 18 | -------------------------------------------------------------------------------- /src/Models/Theme.php: -------------------------------------------------------------------------------- 1 | Game::class, 11 | ]; 12 | } 13 | -------------------------------------------------------------------------------- /src/Traits/DateCasts.php: -------------------------------------------------------------------------------- 1 | 'date', 17 | 'updated_at' => 'date', 18 | 'change_date' => 'date', 19 | 'start_date' => 'date', 20 | 'published_at' => 'date', 21 | 'first_release_date' => 'date', 22 | ]; 23 | } 24 | -------------------------------------------------------------------------------- /src/Traits/HasLimits.php: -------------------------------------------------------------------------------- 1 | query->put('limit', $limit); 19 | 20 | return $this; 21 | } 22 | 23 | /** 24 | * Alias to set the "limit" value of the query. 25 | */ 26 | public function take(int $limit): self 27 | { 28 | return $this->limit($limit); 29 | } 30 | 31 | /** 32 | * Set the "offset" value of the query. 33 | */ 34 | public function offset(int $offset): self 35 | { 36 | $this->query->put('offset', $offset); 37 | 38 | return $this; 39 | } 40 | 41 | /** 42 | * Alias to set the "offset" value of the query. 43 | */ 44 | public function skip(int $offset): self 45 | { 46 | return $this->offset($offset); 47 | } 48 | 49 | /** 50 | * Set the limit and offset for a given page. 51 | */ 52 | public function forPage(int $page, int $perPage = 10): self 53 | { 54 | return $this->skip(($page - 1) * $perPage)->take($perPage); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/Traits/HasNestedWhere.php: -------------------------------------------------------------------------------- 1 | class) && $this->class) { 27 | $class = $this->class; 28 | $callback($query = new Builder(new $class())); 29 | } else { 30 | $callback($query = new Builder($this->endpoint)); 31 | } 32 | 33 | return $this->addNestedWhereQuery($query, $boolean); 34 | } 35 | 36 | /** 37 | * Add another query builder as a nested where to the query builder. 38 | */ 39 | protected function addNestedWhereQuery(Builder $query, string $boolean): self 40 | { 41 | $where = $this->query->get('where', new Collection()); 42 | 43 | $nested = '(' . $query->query->get('where', new Collection())->implode(' ') . ')'; 44 | 45 | $where->push(($where->count() ? $boolean . ' ' : '') . $nested); 46 | 47 | $this->query->put('where', $where); 48 | 49 | return $this; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Traits/HasSearch.php: -------------------------------------------------------------------------------- 1 | query->put('search', '"' . $query . '"'); 23 | 24 | return $this; 25 | } 26 | 27 | /** 28 | * Add a fuzzy search to the query. 29 | * 30 | * @throws ReflectionException 31 | * @throws InvalidParamsException 32 | * @throws JsonException 33 | */ 34 | public function fuzzySearch( 35 | mixed $key, 36 | string $query, 37 | bool $caseSensitive = false, 38 | string $boolean = '&', 39 | ): self { 40 | $tokenizedQuery = explode(' ', $query); 41 | $keys = collect($key)->crossJoin($tokenizedQuery)->toArray(); 42 | 43 | return $this->whereNested(static function (Builder $query) use ($keys, $caseSensitive): void { 44 | foreach ($keys as $v) { 45 | if (is_array($v)) { 46 | $query->whereLike($v[0], $v[1], $caseSensitive, '|'); 47 | } 48 | } 49 | }, $boolean); 50 | } 51 | 52 | /** 53 | * Add an "or fuzzy search" to the query. 54 | * 55 | * @throws ReflectionException|InvalidParamsException|JsonException 56 | */ 57 | public function orFuzzySearch( 58 | mixed $key, 59 | string $query, 60 | bool $caseSensitive = false, 61 | string $boolean = '|', 62 | ): self { 63 | return $this->fuzzySearch($key, $query, $caseSensitive, $boolean); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/Traits/HasSelect.php: -------------------------------------------------------------------------------- 1 | isEmpty()) { 21 | $collection->push('*'); 22 | } 23 | 24 | $collection = $collection->filter(static fn (string $field) => !strpos($field, '.'))->flatten(); 25 | 26 | if ($collection->isEmpty()) { 27 | $collection->push('*'); 28 | } 29 | 30 | $this->query->put('fields', $collection); 31 | 32 | return $this; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Traits/HasWhere.php: -------------------------------------------------------------------------------- 1 | whereNested($key, $boolean); 35 | } 36 | 37 | if (is_array($key)) { 38 | return $this->addArrayOfWheres($key, $boolean); 39 | } 40 | 41 | if (!is_string($key)) { 42 | throw new InvalidArgumentException('Parameter #1 $key needs to be string. ' . gettype($key) . ' given.'); 43 | } 44 | 45 | [$value, $operator] = $this->prepareValueAndOperator( 46 | $value, 47 | $operator, 48 | func_num_args() === 2, 49 | ); 50 | 51 | $select = $this->query->get('fields', new Collection()); 52 | if (!$select->contains($key) && !$select->contains('*')) { 53 | $this->query->put('fields', $select->push($key)); 54 | } 55 | 56 | $where = $this->query->get('where', new Collection()); 57 | 58 | if (collect($this->dates)->has($key) && $this->dates[$key] === 'date') { 59 | $value = $this->castDate($value); 60 | } 61 | 62 | if (is_string($value)) { 63 | if ($operator === 'like') { 64 | $this->whereLike($key, $value, true, $boolean); 65 | } elseif ($operator === 'ilike') { 66 | $this->whereLike($key, $value, false, $boolean); 67 | } elseif ($operator === 'not like') { 68 | $this->whereNotLike($key, $value, true, $boolean); 69 | } elseif ($operator === 'not ilike') { 70 | $this->whereNotLike($key, $value, false, $boolean); 71 | } else { 72 | $where->push(($where->count() ? $boolean . ' ' : '') . $key . ' ' . $operator . ' "' . $value . '"'); 73 | $this->query->put('where', $where); 74 | } 75 | } else { 76 | $value = !is_int($value) ? json_encode($value, JSON_THROW_ON_ERROR) : $value; 77 | $where->push(($where->count() ? $boolean . ' ' : '') . $key . ' ' . $operator . ' ' . $value); 78 | $this->query->put('where', $where); 79 | } 80 | 81 | return $this; 82 | } 83 | 84 | /** 85 | * Add an "or where" clause to the query. 86 | * 87 | * @throws ReflectionException 88 | * @throws JsonException 89 | * @throws InvalidParamsException 90 | */ 91 | public function orWhere( 92 | mixed $key, 93 | ?string $operator = null, 94 | mixed $value = null, 95 | string $boolean = '|', 96 | ): self { 97 | if ($key instanceof Closure) { 98 | return $this->whereNested($key, $boolean); 99 | } 100 | 101 | if (is_array($key)) { 102 | return $this->addArrayOfWheres($key, $boolean); 103 | } 104 | 105 | [$value, $operator] = $this->prepareValueAndOperator( 106 | $value, 107 | $operator, 108 | func_num_args() === 2, 109 | ); 110 | 111 | return $this->where($key, $operator, $value, $boolean); 112 | } 113 | 114 | /** 115 | * Add an array of where clauses to the query. 116 | * 117 | * @throws ReflectionException 118 | * @throws InvalidParamsException 119 | */ 120 | protected function addArrayOfWheres( 121 | array $arrayOfWheres, 122 | string $boolean, 123 | string $method = 'where', 124 | ): self { 125 | return $this->whereNested(static function (Builder $query) use ( 126 | $arrayOfWheres, 127 | $method, 128 | $boolean 129 | ): void { 130 | foreach ($arrayOfWheres as $key => $value) { 131 | if (is_numeric($key) && is_array($value)) { 132 | $query->$method(...array_values($value)); 133 | } else { 134 | $query->$method($key, '=', $value, $boolean); 135 | } 136 | } 137 | }, $boolean); 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /src/Traits/HasWhereBetween.php: -------------------------------------------------------------------------------- 1 | dates)->has($key) && $this->dates[$key] === 'date') { 29 | $first = $this->castDate($first); 30 | $second = $this->castDate($second); 31 | } 32 | 33 | $this->whereNested(static function (Builder $query) use ( 34 | $key, 35 | $first, 36 | $second, 37 | $withBoundaries, 38 | ): void { 39 | $operator = ($withBoundaries ? '=' : ''); 40 | $query->where($key, '>' . $operator, $first)->where($key, '<' . $operator, $second); 41 | }, $boolean); 42 | 43 | return $this; 44 | } 45 | 46 | /** 47 | * Add a or where between statement to the query. 48 | * 49 | * @throws ReflectionException 50 | * @throws InvalidParamsException 51 | * @throws JsonException 52 | */ 53 | public function orWhereBetween( 54 | string $key, 55 | mixed $first, 56 | mixed $second, 57 | bool $withBoundaries = true, 58 | string $boolean = '|', 59 | ): self { 60 | return $this->whereBetween($key, $first, $second, $withBoundaries, $boolean); 61 | } 62 | 63 | /** 64 | * Add a where not between statement to the query. 65 | * 66 | * @throws ReflectionException 67 | * @throws InvalidParamsException 68 | * @throws JsonException 69 | */ 70 | public function whereNotBetween( 71 | string $key, 72 | mixed $first, 73 | mixed $second, 74 | bool $withBoundaries = false, 75 | string $boolean = '&', 76 | ): self { 77 | if (collect($this->dates)->has($key) && $this->dates[$key] === 'date') { 78 | $first = $this->castDate($first); 79 | $second = $this->castDate($second); 80 | } 81 | 82 | $this->whereNested(static function (Builder $query) use ( 83 | $key, 84 | $first, 85 | $second, 86 | $withBoundaries, 87 | ): void { 88 | $operator = ($withBoundaries ? '=' : ''); 89 | $query->where($key, '<' . $operator, $first)->orWhere($key, '>' . $operator, $second); 90 | }, $boolean); 91 | 92 | return $this; 93 | } 94 | 95 | /** 96 | * Add a or where not between statement to the query. 97 | * 98 | * @throws ReflectionException 99 | * @throws InvalidParamsException 100 | * @throws JsonException 101 | */ 102 | public function orWhereNotBetween( 103 | string $key, 104 | mixed $first, 105 | mixed $second, 106 | bool $withBoundaries = false, 107 | string $boolean = '|', 108 | ): self { 109 | return $this->whereNotBetween($key, $first, $second, $withBoundaries, $boolean); 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/Traits/HasWhereDate.php: -------------------------------------------------------------------------------- 1 | prepareValueAndOperator( 28 | $value, 29 | $operator, 30 | func_num_args() === 2, 31 | ); 32 | 33 | $start = Carbon::parse($value)->startOfDay()->timestamp; 34 | $end = Carbon::parse($value)->endOfDay()->timestamp; 35 | 36 | return match ($operator) { 37 | '>' => $this->whereDateGreaterThan($key, $operator, $value, $boolean), 38 | '>=' => $this->whereDateGreaterThanOrEquals($key, $operator, $value, $boolean), 39 | '<' => $this->whereDateLowerThan($key, $operator, $value, $boolean), 40 | '<=' => $this->whereDateLowerThanOrEquals($key, $operator, $value, $boolean), 41 | '!=' => $this->whereNotBetween($key, $start, $end, false, $boolean), 42 | default => $this->whereBetween($key, $start, $end, true, $boolean), 43 | }; 44 | } 45 | 46 | /** 47 | * @throws JsonException 48 | * @throws ReflectionException 49 | * @throws InvalidParamsException 50 | */ 51 | private function whereDateGreaterThan(string $key, mixed $operator, mixed $value, string $boolean): self 52 | { 53 | if (is_string($value) || $value instanceof DateTimeInterface) { 54 | $value = Carbon::parse($value)->addDay()->startOfDay()->timestamp; 55 | } 56 | 57 | return $this->where($key, $operator, $value, $boolean); 58 | } 59 | 60 | /** 61 | * @throws JsonException 62 | * @throws ReflectionException 63 | * @throws InvalidParamsException 64 | */ 65 | private function whereDateGreaterThanOrEquals(string $key, mixed $operator, mixed $value, string $boolean): self 66 | { 67 | if (is_string($value) || $value instanceof DateTimeInterface) { 68 | $value = Carbon::parse($value)->startOfDay()->timestamp; 69 | } 70 | 71 | return $this->where($key, $operator, $value, $boolean); 72 | } 73 | 74 | /** 75 | * @throws JsonException 76 | * @throws ReflectionException 77 | * @throws InvalidParamsException 78 | */ 79 | private function whereDateLowerThan(string $key, mixed $operator, mixed $value, string $boolean): self 80 | { 81 | if (is_string($value) || $value instanceof DateTimeInterface) { 82 | $value = Carbon::parse($value)->subDay()->endOfDay()->timestamp; 83 | } 84 | 85 | return $this->where($key, $operator, $value, $boolean); 86 | } 87 | 88 | /** 89 | * @throws JsonException 90 | * @throws ReflectionException 91 | * @throws InvalidParamsException 92 | */ 93 | private function whereDateLowerThanOrEquals(string $key, mixed $operator, mixed $value, string $boolean): self 94 | { 95 | if (is_string($value) || $value instanceof DateTimeInterface) { 96 | $value = Carbon::parse($value)->endOfDay()->timestamp; 97 | } 98 | 99 | return $this->where($key, $operator, $value, $boolean); 100 | } 101 | 102 | /** 103 | * Add an "or where date" statement to the query. 104 | * 105 | * @throws ReflectionException 106 | * @throws JsonException 107 | * @throws InvalidParamsException 108 | */ 109 | public function orWhereDate(string $key, mixed $operator, mixed $value = null, string $boolean = '|'): self 110 | { 111 | return $this->whereDate($key, $operator, $value, $boolean); 112 | } 113 | 114 | /** 115 | * Add a "where year" statement to the query. 116 | * 117 | * @throws ReflectionException 118 | * @throws JsonException 119 | * @throws InvalidParamsException 120 | */ 121 | public function whereYear(string $key, mixed $operator, mixed $value = null, string $boolean = '&'): self 122 | { 123 | [$value, $operator] = $this->prepareValueAndOperator( 124 | $value, 125 | $operator, 126 | func_num_args() === 2, 127 | ); 128 | 129 | $value = Carbon::now()->setYear($value)->startOfYear(); 130 | 131 | if ($operator === '=') { 132 | $start = $value->clone()->startOfYear()->timestamp; 133 | $end = $value->clone()->endOfYear()->timestamp; 134 | 135 | return $this->whereBetween($key, $start, $end, true, $boolean); 136 | } 137 | 138 | if ($operator === '>' || $operator === '<=') { 139 | $value = $value->clone()->endOfYear()->timestamp; 140 | } elseif ($operator === '>=' || $operator === '<') { 141 | $value = $value->clone()->startOfYear()->timestamp; 142 | } 143 | 144 | return $this->where($key, $operator, $value, $boolean); 145 | } 146 | 147 | /** 148 | * Add an "or where year" statement to the query. 149 | * 150 | * @throws ReflectionException 151 | * @throws JsonException 152 | * @throws InvalidParamsException 153 | */ 154 | public function orWhereYear(string $key, mixed $operator, mixed $value = null, string $boolean = '|'): self 155 | { 156 | [$value, $operator] = $this->prepareValueAndOperator( 157 | $value, 158 | $operator, 159 | func_num_args() === 2, 160 | ); 161 | 162 | return $this->whereYear($key, $operator, $value, $boolean); 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /src/Traits/HasWhereHas.php: -------------------------------------------------------------------------------- 1 | query->get('where', new Collection()); 20 | 21 | $currentWhere = $where; 22 | 23 | $where->push(($currentWhere->count() ? $boolean . ' ' : '') . $relationship . ' != null'); 24 | 25 | $this->query->put('where', $where); 26 | 27 | return $this; 28 | } 29 | 30 | /** 31 | * Add an "or where has" statement to the query. 32 | */ 33 | public function orWhereHas(string $relationship, string $boolean = '|'): self 34 | { 35 | return $this->whereHas($relationship, $boolean); 36 | } 37 | 38 | /** 39 | * Add a "where has not" statement to the query. 40 | */ 41 | public function whereHasNot(string $relationship, string $boolean = '&'): self 42 | { 43 | $where = $this->query->get('where', new Collection()); 44 | 45 | $currentWhere = $where; 46 | 47 | $where->push(($currentWhere->count() ? $boolean . ' ' : '') . $relationship . ' = null'); 48 | 49 | $this->query->put('where', $where); 50 | 51 | return $this; 52 | } 53 | 54 | /** 55 | * Add a "where has not" statement to the query. 56 | */ 57 | public function orWhereHasNot(string $relationship, string $boolean = '|'): self 58 | { 59 | return $this->whereHasNot($relationship, $boolean); 60 | } 61 | 62 | /** 63 | * Add a "where null" clause to the query. 64 | */ 65 | public function whereNull(string $key, string $boolean = '&'): self 66 | { 67 | return $this->whereHasNot($key, $boolean); 68 | } 69 | 70 | /** 71 | * Add an "or where null" clause to the query. 72 | */ 73 | public function orWhereNull(string $key, string $boolean = '|'): self 74 | { 75 | return $this->whereNull($key, $boolean); 76 | } 77 | 78 | /** 79 | * Add a "where not null" clause to the query. 80 | */ 81 | public function whereNotNull(string $key, string $boolean = '&'): self 82 | { 83 | return $this->whereHas($key, $boolean); 84 | } 85 | 86 | /** 87 | * Add an "or where not null" clause to the query. 88 | */ 89 | public function orWhereNotNull(string $key, string $boolean = '|'): self 90 | { 91 | return $this->whereNotNull($key, $boolean); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/Traits/HasWhereLike.php: -------------------------------------------------------------------------------- 1 | query->get('where', new Collection()); 28 | 29 | $clause = $this->generateWhereLikeClause($key, $value, $caseSensitive, '=', '~'); 30 | 31 | $where->push(($where->count() ? $boolean . ' ' : '') . $clause); 32 | 33 | $this->query->put('where', $where); 34 | 35 | return $this; 36 | } 37 | 38 | /** 39 | * Add an "or where like" clause to the query. 40 | * 41 | * @throws JsonException 42 | */ 43 | public function orWhereLike( 44 | string $key, 45 | string $value, 46 | bool $caseSensitive = true, 47 | string $boolean = '|', 48 | ): self { 49 | return $this->whereLike($key, $value, $caseSensitive, $boolean); 50 | } 51 | 52 | /** 53 | * Add a "where not like" clause to the query. 54 | * 55 | * @throws JsonException 56 | */ 57 | public function whereNotLike( 58 | string $key, 59 | string $value, 60 | bool $caseSensitive = true, 61 | string $boolean = '&', 62 | ): self { 63 | $where = $this->query->get('where', new Collection()); 64 | 65 | $clause = $this->generateWhereLikeClause($key, $value, $caseSensitive, '!=', '!~'); 66 | 67 | $where->push(($where->count() ? $boolean . ' ' : '') . $clause); 68 | 69 | $this->query->put('where', $where); 70 | 71 | return $this; 72 | } 73 | 74 | /** 75 | * Add an "or where not like" clause to the query. 76 | * 77 | * @throws JsonException 78 | */ 79 | public function orWhereNotLike( 80 | string $key, 81 | string $value, 82 | bool $caseSensitive = true, 83 | string $boolean = '|', 84 | ): self { 85 | return $this->whereNotLike($key, $value, $caseSensitive, $boolean); 86 | } 87 | 88 | /** 89 | * @throws JsonException 90 | */ 91 | private function generateWhereLikeClause( 92 | string $key, 93 | string $value, 94 | bool $caseSensitive, 95 | string $operator, 96 | string $insensitiveOperator, 97 | ): string { 98 | $hasPrefix = Str::startsWith($value, ['%', '*']); 99 | $hasSuffix = Str::endsWith($value, ['%', '*']); 100 | 101 | if ($hasPrefix) { 102 | $value = substr($value, 1); 103 | } 104 | if ($hasSuffix) { 105 | $value = substr($value, 0, -1); 106 | } 107 | 108 | $operator = $caseSensitive ? $operator : $insensitiveOperator; 109 | $prefix = $hasPrefix || !$hasSuffix ? '*' : ''; 110 | $suffix = $hasSuffix || !$hasPrefix ? '*' : ''; 111 | $value = json_encode($value, JSON_THROW_ON_ERROR); 112 | $value = Str::start(Str::finish($value, $suffix), $prefix); 113 | 114 | return implode(' ', [$key, $operator, $value]); 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /src/Traits/Operators.php: -------------------------------------------------------------------------------- 1 | ', 19 | '<=', 20 | '>=', 21 | '!=', 22 | '!=', 23 | '~', 24 | 'like', 25 | 'ilike', 26 | 'not like', 27 | 'not ilike', 28 | ]; 29 | } 30 | -------------------------------------------------------------------------------- /src/Traits/ValuePreparer.php: -------------------------------------------------------------------------------- 1 | invalidOperatorAndValue($operator, $value)) { 31 | throw new InvalidArgumentException('Illegal operator and value combination.'); 32 | } 33 | 34 | return [$value, $operator]; 35 | } 36 | 37 | /** 38 | * Determine if the given operator and value combination is legal. 39 | * 40 | * Prevents using Null values with invalid operators. 41 | */ 42 | private function invalidOperatorAndValue(?string $operator, mixed $value): bool 43 | { 44 | return null === $value && in_array($operator, $this->operators, true) && !in_array($operator, ['=', '!=']); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /tests/ApiHelperTest.php: -------------------------------------------------------------------------------- 1 | assertEquals('some-token', $token); 28 | } 29 | 30 | /** 31 | * @throws AuthenticationException 32 | */ 33 | public function testItShouldRetrieveAccessTokenFromTwitch(): void 34 | { 35 | Cache::forget('igdb_cache.access_token'); 36 | 37 | Http::fake([ 38 | '*/oauth2/token*' => Http::response([ 39 | 'access_token' => 'test-suite-token', 40 | 'expires_in' => 3600, 41 | ]), 42 | ]); 43 | 44 | $token = ApiHelper::retrieveAccessToken(); 45 | 46 | $this->assertEquals('test-suite-token', $token); 47 | } 48 | 49 | /** 50 | * @throws AuthenticationException 51 | */ 52 | public function testItShouldThrowAuthenticationException(): void 53 | { 54 | $this->expectException(AuthenticationException::class); 55 | 56 | Cache::forget('igdb_cache.access_token'); 57 | 58 | Http::fake([ 59 | '*/oauth2/token*' => Http::response([], Response::HTTP_INTERNAL_SERVER_ERROR), 60 | ]); 61 | 62 | ApiHelper::retrieveAccessToken(); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /tests/EventsTest.php: -------------------------------------------------------------------------------- 1 | assertTrue(class_exists($eventClassString)); 19 | } 20 | 21 | #[DataProvider('modelsDataProvider')] 22 | public function testItShouldHaveUpdatedEventForEveryModel(string $className): void 23 | { 24 | $eventClassString = 'MarcReichel\IGDBLaravel\Events\\' . $className . 'Updated'; 25 | $this->assertTrue(class_exists($eventClassString)); 26 | } 27 | 28 | #[DataProvider('modelsDataProvider')] 29 | public function testItShouldHaveDeletedEventForEveryModel(string $className): void 30 | { 31 | $eventClassString = 'MarcReichel\IGDBLaravel\Events\\' . $className . 'Deleted'; 32 | $this->assertTrue(class_exists($eventClassString)); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /tests/ImageTest.php: -------------------------------------------------------------------------------- 1 | Http::response([ 34 | [ 35 | 'id' => 1, 36 | 'alpha_channel' => false, 37 | 'animated' => false, 38 | 'checksum' => 'abc', 39 | 'height' => 100, 40 | 'image_id' => 'abc', 41 | 'url' => '//images.igdb.com/igdb/image/upload/t_thumb/abc.jpg', 42 | 'width' => 100, 43 | ], 44 | ]), 45 | ]); 46 | } 47 | 48 | public static function imageSizeDataProvider(): array 49 | { 50 | $enumCases = collect(Size::cases()) 51 | ->map(static fn (Size $size) => [$size, $size->value]) 52 | ->toArray(); 53 | $stringCases = collect(Size::cases()) 54 | ->map(static fn (Size $size) => [$size->value, $size->value]) 55 | ->toArray(); 56 | 57 | return array_merge($enumCases, $stringCases); 58 | } 59 | 60 | public function testArtworkShouldBeMappedAsInstanceOfImage(): void 61 | { 62 | self::assertInstanceOf(Image::class, Artwork::first()); 63 | } 64 | 65 | public function testCharacterMugShotShouldBeMappedAsInstanceOfImage(): void 66 | { 67 | self::assertInstanceOf(Image::class, CharacterMugShot::first()); 68 | } 69 | 70 | public function testCompanyLogoShouldBeMappedAsInstanceOfImage(): void 71 | { 72 | self::assertInstanceOf(Image::class, CompanyLogo::first()); 73 | } 74 | 75 | public function testCoverShouldBeMappedAsInstanceOfImage(): void 76 | { 77 | self::assertInstanceOf(Image::class, Cover::first()); 78 | } 79 | 80 | public function testGameEngineLogoShouldBeMappedAsInstanceOfImage(): void 81 | { 82 | self::assertInstanceOf(Image::class, GameEngineLogo::first()); 83 | } 84 | 85 | public function testPlatformLogoShouldBeMappedAsInstanceOfImage(): void 86 | { 87 | self::assertInstanceOf(Image::class, PlatformLogo::first()); 88 | } 89 | 90 | public function testScreenshotShouldBeMappedAsInstanceOfImage(): void 91 | { 92 | self::assertInstanceOf(Image::class, Screenshot::first()); 93 | } 94 | 95 | public function testItShouldGenerateDefaultImageUrlWithoutAttributes(): void 96 | { 97 | $url = Artwork::first()?->getUrl(); 98 | 99 | self::assertEquals('//images.igdb.com/igdb/image/upload/t_thumb/abc.jpg', $url); 100 | } 101 | 102 | #[DataProvider('imageSizeDataProvider')] 103 | public function testItShouldGenerateDesiredImageUrlWithParameter(Size | string $size, string $value): void 104 | { 105 | $url = Artwork::first()?->getUrl($size); 106 | 107 | self::assertEquals('//images.igdb.com/igdb/image/upload/t_' . $value . '/abc.jpg', $url); 108 | } 109 | 110 | #[DataProvider('imageSizeDataProvider')] 111 | public function testItShouldGenerateRetinaImageUrl(Size | string $size, string $value): void 112 | { 113 | $url = Artwork::first()?->getUrl($size, true); 114 | 115 | self::assertEquals('//images.igdb.com/igdb/image/upload/t_' . $value . '_2x/abc.jpg', $url); 116 | } 117 | 118 | public function testItShouldThrowExceptionWithInvalidImageSize(): void 119 | { 120 | $this->expectException(InvalidArgumentException::class); 121 | 122 | Artwork::first()?->getUrl('foo'); 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /tests/Pest.php: -------------------------------------------------------------------------------- 1 | in('Feature'); 17 | 18 | /* 19 | |-------------------------------------------------------------------------- 20 | | Expectations 21 | |-------------------------------------------------------------------------- 22 | | 23 | | When you're writing tests, you often need to check that values meet certain conditions. The 24 | | "expect()" function gives you access to a set of "expectations" methods that you can use 25 | | to assert different things. Of course, you may extend the Expectation API at any time. 26 | | 27 | */ 28 | 29 | /* expect()->extend('toBeOne', function () { 30 | return $this->toBe(1); 31 | }); */ 32 | 33 | /* 34 | |-------------------------------------------------------------------------- 35 | | Functions 36 | |-------------------------------------------------------------------------- 37 | | 38 | | While Pest is very powerful out-of-the-box, you may have some testing code specific to your 39 | | project that you don't want to repeat in every file. Here you can also expose helpers as 40 | | global functions to help you to reduce the number of lines of code in your test files. 41 | | 42 | */ 43 | 44 | /* function something() 45 | { 46 | // .. 47 | } */ 48 | -------------------------------------------------------------------------------- /tests/TestCase.php: -------------------------------------------------------------------------------- 1 | Webhook::handle($request), 32 | )->name('handle-igdb-webhook'); 33 | } 34 | 35 | protected function getPackageProviders($app): array 36 | { 37 | return [ 38 | IGDBLaravelServiceProvider::class, 39 | ]; 40 | } 41 | 42 | protected function getEnvironmentSetUp($app): void 43 | { 44 | // perform environment setup 45 | } 46 | 47 | protected function isApiCall(Request $request, string $endpoint, string $requestBody): bool 48 | { 49 | return Str::startsWith($request->url(), 'https://api.igdb.com/v4/' . $endpoint) 50 | && Str::of($request->body())->contains($requestBody); 51 | } 52 | 53 | protected function isWebhookCall(Request $request, string $endpoint): bool 54 | { 55 | return $request->url() === 'https://api.igdb.com/v4/' . $endpoint . '/webhooks' && $request->isForm(); 56 | } 57 | 58 | protected function createWebhookResponse(Request $request): PromiseInterface 59 | { 60 | $data = $request->data(); 61 | $subCategory = null; 62 | switch ($data['method']) { 63 | case 'create': 64 | $subCategory = 0; 65 | 66 | break; 67 | case 'delete': 68 | $subCategory = 1; 69 | 70 | break; 71 | case 'update': 72 | $subCategory = 2; 73 | 74 | break; 75 | } 76 | 77 | return Http::response([ 78 | 'id' => 1337, 79 | 'url' => $data['url'], 80 | 'category' => 1, 81 | 'sub_category' => $subCategory, 82 | 'active' => true, 83 | 'secret' => $data['secret'], 84 | 'created_at' => now()->toIso8601String(), 85 | 'updated_at' => now()->toIso8601String(), 86 | ]); 87 | } 88 | 89 | public static function modelsDataProvider(): array 90 | { 91 | $files = glob(__DIR__ . '/../src/Models/*.php'); 92 | $classNames = []; 93 | $blackList = ['PopularityPrimitive', 'Search', 'Webhook']; 94 | 95 | if (!$files) { 96 | return $classNames; 97 | } 98 | 99 | foreach ($files as $file) { 100 | $classString = 'MarcReichel\IGDBLaravel\Models\\' . basename($file, '.php'); 101 | if (!class_exists($classString)) { 102 | continue; 103 | } 104 | $reflection = new ReflectionClass($classString); 105 | if ($reflection->isAbstract()) { 106 | continue; 107 | } 108 | if (in_array(class_basename($classString), $blackList)) { 109 | continue; 110 | } 111 | 112 | $classBasename = class_basename($classString); 113 | 114 | $classNames[$classBasename] = [$classBasename]; 115 | } 116 | 117 | return $classNames; 118 | } 119 | } 120 | --------------------------------------------------------------------------------