├── .editorconfig ├── .github ├── CONTRIBUTING.md ├── SECURITY.md └── workflows │ ├── analyse.yml │ ├── changelog.yml │ ├── coverage.yml │ ├── style.yml │ └── tests.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── UPGRADE.md ├── art ├── banner.svg └── footer.svg ├── composer.json ├── config └── magento.php ├── database └── migrations │ └── 2023_12_11_150000_create_magento_oauth_keys_table.php ├── docs └── migrations │ └── GRAYLOON.md ├── phpstan.neon ├── phpunit.xml ├── routes └── web.php ├── src ├── Actions │ ├── AuthenticateRequest.php │ ├── BuildRequest.php │ ├── CheckMagento.php │ └── OAuth │ │ └── RequestAccessToken.php ├── Client │ └── Magento.php ├── Contracts │ ├── AuthenticatesRequest.php │ ├── BuildsRequest.php │ ├── ChecksMagento.php │ └── OAuth │ │ └── RequestsAccessToken.php ├── Enums │ └── AuthenticationMethod.php ├── Events │ └── MagentoResponseEvent.php ├── Http │ ├── Controllers │ │ └── OAuthController.php │ ├── Middleware │ │ └── OAuthMiddleware.php │ └── Requests │ │ ├── CallbackRequest.php │ │ └── IdentityRequest.php ├── Jobs │ └── Middleware │ │ └── AvailableMiddleware.php ├── Listeners │ └── StoreAvailabilityListener.php ├── Models │ └── OAuthKey.php ├── OAuth │ ├── HmacSha256Signature.php │ ├── KeyStore │ │ ├── DatabaseKeyStore.php │ │ ├── FileKeyStore.php │ │ └── KeyStore.php │ └── MagentoServer.php ├── Providers │ ├── BaseProvider.php │ ├── BearerTokenProvider.php │ └── OAuthProvider.php ├── Query │ ├── Grammar.php │ └── SearchCriteria.php └── ServiceProvider.php └── tests ├── Actions ├── CheckMagentoTest.php └── OAuth │ └── RequestAccessTokenTest.php ├── Client ├── ClientTest.php └── MultiConnectionTest.php ├── Enums └── AuthenticationMethodTest.php ├── Fakes └── TestJob.php ├── Http ├── Controllers │ └── OAuthControllerTest.php └── Middleware │ └── OAuthMiddlewareTest.php ├── Jobs └── Middleware │ └── AvailableMiddlewareTest.php ├── Listeners └── StoreAvailabilityListenerTest.php ├── OAuth ├── HmacSha256SignatureTest.php └── KeyStore │ ├── DatabaseKeyStoreTest.php │ └── FileKeyStoreTest.php ├── Providers ├── BearerTokenProviderTest.php └── OAuthProviderTest.php ├── Query └── SearchCriteriaTest.php └── TestCase.php /.editorconfig: -------------------------------------------------------------------------------- 1 | [*] 2 | charset = utf-8 3 | end_of_line = lf 4 | insert_final_newline = true 5 | indent_style = space 6 | indent_size = 4 7 | trim_trailing_whitespace = true 8 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Contributions are **welcome** and will be fully **credited**. 4 | 5 | Please read and understand the contribution guide before creating an issue or pull request. 6 | 7 | ## Etiquette 8 | 9 | This project is open source, and as such, the maintainers give their free time to build and maintain the source code 10 | held within. They make the code freely available in the hope that it will be of use to other developers. It would be 11 | extremely unfair for them to suffer abuse or anger for their hard work. 12 | 13 | Please be considerate towards maintainers when raising issues or presenting pull requests. Let's show the 14 | world that developers are civilized and selfless people. 15 | 16 | It's the duty of the maintainer to ensure that all submissions to the project are of sufficient 17 | quality to benefit the project. Many developers have different skillsets, strengths, and weaknesses. Respect the maintainer's decision, and do not be upset or abusive if your submission is not used. 18 | 19 | ## Viability 20 | 21 | When requesting or submitting new features, first consider whether it might be useful to others. Open 22 | source projects are used by many developers, who may have entirely different needs to your own. Think about 23 | whether or not your feature is likely to be used by other users of the project. 24 | 25 | ## Procedure 26 | 27 | Before filing an issue: 28 | 29 | - Attempt to replicate the problem, to ensure that it wasn't a coincidental incident. 30 | - Check to make sure your feature suggestion isn't already present within the project. 31 | - Check the pull requests tab to ensure that the bug doesn't have a fix in progress. 32 | - Check the pull requests tab to ensure that the feature isn't already in progress. 33 | 34 | Before submitting a pull request: 35 | 36 | - Check the codebase to ensure that your feature doesn't already exist. 37 | - Check the pull requests to ensure that another person hasn't already submitted the feature or fix. 38 | 39 | ## Requirements 40 | 41 | If the project maintainer has any additional requirements, you will find them listed here. 42 | 43 | - **[PSR-2 Coding Standard](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-2-coding-style-guide.md)** - The easiest way to apply the conventions is to install [PHP Code Sniffer](https://pear.php.net/package/PHP_CodeSniffer). 44 | 45 | - **Add tests!** - Your patch won't be accepted if it doesn't have tests. 46 | 47 | - **Document any change in behaviour** - Make sure the `README.md` and any other relevant documentation are kept up-to-date. 48 | 49 | - **Consider our release cycle** - We try to follow [SemVer v2.0.0](https://semver.org/). Randomly breaking public APIs is not an option. 50 | 51 | - **One pull request per feature** - If you want to do more than one thing, send multiple pull requests. 52 | 53 | - **Send coherent history** - Make sure each individual commit in your pull request is meaningful. If you had to make multiple intermediate commits while developing, please [squash them](https://www.git-scm.com/book/en/v2/Git-Tools-Rewriting-History#Changing-Multiple-Commit-Messages) before submitting. 54 | 55 | **Happy coding**! 56 | -------------------------------------------------------------------------------- /.github/SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | If you discover any security related issues, please email security@justbetter.nl instead of using the issue tracker. 4 | -------------------------------------------------------------------------------- /.github/workflows/analyse.yml: -------------------------------------------------------------------------------- 1 | name: analyse 2 | 3 | on: ['push', 'pull_request'] 4 | 5 | jobs: 6 | test: 7 | runs-on: ${{ matrix.os }} 8 | strategy: 9 | fail-fast: true 10 | matrix: 11 | os: [ubuntu-latest] 12 | php: [8.3, 8.4] 13 | laravel: [11.*] 14 | stability: [prefer-stable] 15 | include: 16 | - laravel: 11.* 17 | testbench: 9.* 18 | 19 | name: P${{ matrix.php }} - L${{ matrix.laravel }} - ${{ matrix.stability }} - ${{ matrix.os }} 20 | 21 | steps: 22 | - name: Checkout code 23 | uses: actions/checkout@v4 24 | 25 | - name: Setup PHP 26 | uses: shivammathur/setup-php@v2 27 | with: 28 | php-version: ${{ matrix.php }} 29 | extensions: dom, curl, libxml, mbstring, zip, pdo, sqlite, pdo_sqlite, bcmath, soap, intl, gd, exif, iconv, imagick, fileinfo 30 | coverage: none 31 | 32 | - name: Install dependencies 33 | run: | 34 | composer require "laravel/framework:${{ matrix.laravel }}" "orchestra/testbench:${{ matrix.testbench }}" --no-interaction --no-update 35 | composer update --${{ matrix.stability }} --prefer-dist --no-interaction 36 | - name: Analyse 37 | run: composer analyse 38 | -------------------------------------------------------------------------------- /.github/workflows/changelog.yml: -------------------------------------------------------------------------------- 1 | name: "Update Changelog" 2 | 3 | on: 4 | release: 5 | types: [ published, edited, deleted ] 6 | 7 | jobs: 8 | generate: 9 | runs-on: ubuntu-latest 10 | 11 | steps: 12 | - name: Checkout code 13 | uses: actions/checkout@v3 14 | with: 15 | ref: ${{ github.event.release.target_commitish }} 16 | 17 | - name: Generate changelog 18 | uses: justbetter/generate-changelogs-action@main 19 | env: 20 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 21 | with: 22 | repository: ${{ github.repository }} 23 | 24 | - name: Commit CHANGELOG 25 | uses: stefanzweifel/git-auto-commit-action@v4 26 | with: 27 | branch: ${{ github.event.release.target_commitish }} 28 | commit_message: Update CHANGELOG 29 | file_pattern: CHANGELOG.md -------------------------------------------------------------------------------- /.github/workflows/coverage.yml: -------------------------------------------------------------------------------- 1 | name: coverage 2 | 3 | on: ['push', 'pull_request'] 4 | 5 | jobs: 6 | test: 7 | runs-on: ${{ matrix.os }} 8 | strategy: 9 | fail-fast: true 10 | matrix: 11 | os: [ubuntu-latest] 12 | php: [8.4] 13 | laravel: [11.*] 14 | stability: [prefer-stable] 15 | include: 16 | - laravel: 11.* 17 | testbench: 9.* 18 | 19 | name: P${{ matrix.php }} - L${{ matrix.laravel }} - ${{ matrix.stability }} - ${{ matrix.os }} 20 | 21 | steps: 22 | - name: Checkout code 23 | uses: actions/checkout@v4 24 | 25 | - name: Setup PHP 26 | uses: shivammathur/setup-php@v2 27 | with: 28 | php-version: ${{ matrix.php }} 29 | extensions: dom, curl, libxml, mbstring, zip, pdo, sqlite, pdo_sqlite, bcmath, soap, intl, gd, exif, iconv, imagick, fileinfo, xdebug 30 | coverage: xdebug 31 | 32 | - name: Install dependencies 33 | run: | 34 | composer require "laravel/framework:${{ matrix.laravel }}" "orchestra/testbench:${{ matrix.testbench }}" --no-interaction --no-update 35 | composer update --${{ matrix.stability }} --prefer-dist --no-interaction 36 | - name: Execute tests 37 | run: composer coverage 38 | -------------------------------------------------------------------------------- /.github/workflows/style.yml: -------------------------------------------------------------------------------- 1 | name: style 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | jobs: 8 | style: 9 | name: Style 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - name: Checkout code 14 | uses: actions/checkout@v4 15 | 16 | - name: Setup PHP 17 | uses: shivammathur/setup-php@v2 18 | with: 19 | php-version: 8.4 20 | extensions: dom, curl, libxml, mbstring, zip, pdo, sqlite, pdo_sqlite, bcmath, soap, intl, gd, exif, iconv, imagick, fileinfo 21 | coverage: none 22 | 23 | - name: Install dependencies 24 | run: composer install 25 | 26 | - name: Style 27 | run: composer fix-style 28 | 29 | - name: Commit Changes 30 | uses: stefanzweifel/git-auto-commit-action@v4 31 | with: 32 | commit_message: Fix styling changes 33 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: tests 2 | 3 | on: ['push', 'pull_request'] 4 | 5 | jobs: 6 | test: 7 | runs-on: ${{ matrix.os }} 8 | strategy: 9 | fail-fast: true 10 | matrix: 11 | os: [ubuntu-latest] 12 | php: [8.3, 8.4] 13 | laravel: [11.*] 14 | stability: [prefer-lowest, prefer-stable] 15 | include: 16 | - laravel: 11.* 17 | testbench: 9.* 18 | 19 | name: P${{ matrix.php }} - L${{ matrix.laravel }} - ${{ matrix.stability }} - ${{ matrix.os }} 20 | 21 | steps: 22 | - name: Checkout code 23 | uses: actions/checkout@v4 24 | 25 | - name: Setup PHP 26 | uses: shivammathur/setup-php@v2 27 | with: 28 | php-version: ${{ matrix.php }} 29 | extensions: dom, curl, libxml, mbstring, zip, pdo, sqlite, pdo_sqlite, bcmath, soap, intl, gd, exif, iconv, imagick, fileinfo 30 | coverage: none 31 | 32 | - name: Install dependencies 33 | run: | 34 | composer require "laravel/framework:${{ matrix.laravel }}" "orchestra/testbench:${{ matrix.testbench }}" --no-interaction --no-update 35 | composer update --${{ matrix.stability }} --prefer-dist --no-interaction 36 | - name: Execute tests 37 | run: composer test 38 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | vendor 2 | composer.lock 3 | .phpunit.result.cache 4 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | [Unreleased changes](https://github.com/justbetter/laravel-magento-client/compare/2.7.0...main) 4 | ## [2.7.0](https://github.com/justbetter/laravel-magento-client/releases/tag/2.7.0) - 2025-02-13 5 | 6 | ### What's Changed 7 | * fix typo for docs by @fritsjustbetter in https://github.com/justbetter/laravel-magento-client/pull/44 8 | * Laravel 12 support by @VincentBean in https://github.com/justbetter/laravel-magento-client/pull/48 9 | * Drop PHP 8.1 support 10 | 11 | ### New Contributors 12 | * @fritsjustbetter made their first contribution in https://github.com/justbetter/laravel-magento-client/pull/44 13 | 14 | **Full Changelog**: https://github.com/justbetter/laravel-magento-client/compare/2.6.1...2.7.0 15 | 16 | ## [2.6.1](https://github.com/justbetter/laravel-magento-client/releases/tag/2.6.1) - 2024-09-09 17 | 18 | ### What's Changed 19 | * Increase release time to 30 seconds by @VincentBean in https://github.com/justbetter/laravel-magento-client/pull/43 20 | 21 | 22 | **Full Changelog**: https://github.com/justbetter/laravel-magento-client/compare/2.6.0...2.6.1 23 | 24 | ## [2.6.0](https://github.com/justbetter/laravel-magento-client/releases/tag/2.6.0) - 2024-08-23 25 | 26 | ### What's Changed 27 | * Check Magento availability by @VincentBean in https://github.com/justbetter/laravel-magento-client/pull/42 28 | 29 | 30 | **Full Changelog**: https://github.com/justbetter/laravel-magento-client/compare/2.5.0...2.6.0 31 | 32 | ## [2.5.0](https://github.com/justbetter/laravel-magento-client/releases/tag/2.5.0) - 2024-05-21 33 | 34 | ### What's Changed 35 | * Dispatch event on responses by @VincentBean in https://github.com/justbetter/laravel-magento-client/pull/41 36 | 37 | 38 | **Full Changelog**: https://github.com/justbetter/laravel-magento-client/compare/2.4.0...2.5.0 39 | 40 | ## [2.4.0](https://github.com/justbetter/laravel-magento-client/releases/tag/2.4.0) - 2024-03-14 41 | 42 | ### What's Changed 43 | * Added support for Laravel 11 by @ramonrietdijk in https://github.com/justbetter/laravel-magento-client/pull/37 44 | 45 | 46 | **Full Changelog**: https://github.com/justbetter/laravel-magento-client/compare/2.3.0...2.4.0 47 | 48 | ## [2.3.0](https://github.com/justbetter/laravel-magento-client/releases/tag/2.3.0) - 2024-02-05 49 | 50 | ### What's Changed 51 | * Added ordering to the search criteria by @ramonrietdijk in https://github.com/justbetter/laravel-magento-client/pull/35 52 | 53 | 54 | **Full Changelog**: https://github.com/justbetter/laravel-magento-client/compare/2.2.0...2.3.0 55 | 56 | ## [2.2.0](https://github.com/justbetter/laravel-magento-client/releases/tag/2.2.0) - 2024-01-26 57 | 58 | ### What's Changed 59 | * PendingRequest changes per cycle. by @mennotempelaar in https://github.com/justbetter/laravel-magento-client/pull/34 60 | 61 | ### New Contributors 62 | * @mennotempelaar made their first contribution in https://github.com/justbetter/laravel-magento-client/pull/34 63 | 64 | **Full Changelog**: https://github.com/justbetter/laravel-magento-client/compare/2.1.0...2.2.0 65 | 66 | ## [2.1.0](https://github.com/justbetter/laravel-magento-client/releases/tag/2.1.0) - 2024-01-19 67 | 68 | ### What's Changed 69 | * Add Graphql support by @indykoning in https://github.com/justbetter/laravel-magento-client/pull/32 70 | 71 | ### New Contributors 72 | * @indykoning made their first contribution in https://github.com/justbetter/laravel-magento-client/pull/32 73 | 74 | **Full Changelog**: https://github.com/justbetter/laravel-magento-client/compare/2.0.0...2.1.0 75 | 76 | ## [2.0.0](https://github.com/justbetter/laravel-magento-client/releases/tag/2.0.0) - 2023-12-13 77 | 78 | ### What's Changed 79 | * Support multiple connections by @VincentBean in https://github.com/justbetter/laravel-magento-client/pull/31 80 | 81 | See UPGRADE.md on how to upgrade from 1.x 82 | 83 | **Full Changelog**: https://github.com/justbetter/laravel-magento-client/compare/1.5.0...2.0.0 84 | 85 | ## [1.5.0](https://github.com/justbetter/laravel-magento-client/releases/tag/1.5.0) - 2023-09-16 86 | 87 | ### What's Changed 88 | * Upgrade Magento Class to accept bulk option by @lucassmacedo in https://github.com/justbetter/laravel-magento-client/pull/27 89 | 90 | ### New Contributors 91 | * @lucassmacedo made their first contribution in https://github.com/justbetter/laravel-magento-client/pull/27 92 | 93 | **Full Changelog**: https://github.com/justbetter/laravel-magento-client/compare/1.4.0...1.5.0 94 | 95 | ## [1.4.0](https://github.com/justbetter/laravel-magento-client/releases/tag/1.4.0) - 2023-06-07 96 | 97 | ### What's Changed 98 | * Add async methods by @VincentBean in https://github.com/justbetter/laravel-magento-client/pull/21 99 | 100 | 101 | **Full Changelog**: https://github.com/justbetter/laravel-magento-client/compare/1.3.1...1.4.0 102 | 103 | ## [1.3.1](https://github.com/justbetter/laravel-magento-client/releases/tag/1.3.1) - 2023-05-10 104 | 105 | ### What's Changed 106 | * Allow selection as a string by @ramonrietdijk in https://github.com/justbetter/laravel-magento-client/pull/20 107 | 108 | 109 | **Full Changelog**: https://github.com/justbetter/laravel-magento-client/compare/1.3.0...1.3.1 110 | 111 | ## [1.3.0](https://github.com/justbetter/laravel-magento-client/releases/tag/1.3.0) - 2023-05-05 112 | 113 | ### What's Changed 114 | * Magento OAuth 1.0 implementation by @ramonrietdijk in https://github.com/justbetter/laravel-magento-client/pull/19 115 | 116 | 117 | **Full Changelog**: https://github.com/justbetter/laravel-magento-client/compare/1.2.0...1.3.0 118 | 119 | ## [1.2.0](https://github.com/justbetter/laravel-magento-client/releases/tag/1.2.0) - 2023-03-22 120 | 121 | ### What's Changed 122 | * Add whereNull and ORs by @VincentBean in https://github.com/justbetter/laravel-magento-client/pull/17 123 | 124 | 125 | **Full Changelog**: https://github.com/justbetter/laravel-magento-client/compare/1.1.0...1.2.0 126 | 127 | ## [1.1.0](https://github.com/justbetter/laravel-magento-client/releases/tag/1.1.0) - 2023-02-24 128 | 129 | ### What's Changed 130 | * Support for Laravel 10 by @ramonrietdijk in https://github.com/justbetter/laravel-magento-client/pull/14 131 | 132 | 133 | **Full Changelog**: https://github.com/justbetter/laravel-magento-client/compare/1.0.9...1.1.0 134 | 135 | ## [1.0.9](https://github.com/justbetter/laravel-magento-client/releases/tag/1.0.9) - 2023-02-22 136 | 137 | ### What's Changed 138 | * Lazily load results from Magento by @ramonrietdijk in https://github.com/justbetter/laravel-magento-client/pull/13 139 | 140 | 141 | **Full Changelog**: https://github.com/justbetter/laravel-magento-client/compare/1.0.8...1.0.9 142 | 143 | ## [1.0.8](https://github.com/justbetter/laravel-magento-client/releases/tag/1.0.8) - 2023-01-31 144 | 145 | ### What's Changed 146 | * Updated grammar by @ramonrietdijk in https://github.com/justbetter/laravel-magento-client/pull/9 147 | * Grayloon migration guide by @ramonrietdijk in https://github.com/justbetter/laravel-magento-client/pull/10 148 | 149 | 150 | **Full Changelog**: https://github.com/justbetter/laravel-magento-client/compare/1.0.7...1.0.8 151 | 152 | ## [1.0.7](https://github.com/justbetter/laravel-magento-client/releases/tag/1.0.7) - 2023-01-25 153 | 154 | Fix typo in composer.json 155 | 156 | **Full Changelog**: https://github.com/justbetter/laravel-magento-client/compare/1.0.6...1.0.7 157 | 158 | ## [1.0.6](https://github.com/justbetter/laravel-magento-client/releases/tag/1.0.6) - 2023-01-12 159 | 160 | **Full Changelog**: https://github.com/justbetter/laravel-magento-client/compare/1.0.5...1.0.6 161 | 162 | ## [1.0.5](https://github.com/justbetter/laravel-magento-client/releases/tag/1.0.5) - 2023-01-10 163 | 164 | ### What's Changed 165 | * Add delete method by @VincentBean in https://github.com/justbetter/laravel-magento-client/pull/8 166 | 167 | 168 | **Full Changelog**: https://github.com/justbetter/laravel-magento-client/compare/1.0.4...1.0.5 169 | 170 | ## [1.0.4](https://github.com/justbetter/laravel-magento-client/releases/tag/1.0.4) - 2023-01-02 171 | 172 | ### What's Changed 173 | * Make store nullable by @ramonrietdijk in https://github.com/justbetter/laravel-magento-client/pull/7 174 | 175 | 176 | **Full Changelog**: https://github.com/justbetter/laravel-magento-client/compare/1.0.3...1.0.4 177 | 178 | ## [1.0.3](https://github.com/justbetter/laravel-magento-client/releases/tag/1.0.3) - 2022-11-25 179 | 180 | ### What's Changed 181 | * Coverage action by @VincentBean in https://github.com/justbetter/laravel-magento-client/pull/3 182 | * Badges by @VincentBean in https://github.com/justbetter/laravel-magento-client/pull/4 183 | * Add references to other packages by @VincentBean in https://github.com/justbetter/laravel-magento-client/pull/5 184 | * Added a fake method to ease testing in third party packages by @ramonrietdijk in https://github.com/justbetter/laravel-magento-client/pull/6 185 | 186 | ### New Contributors 187 | * @ramonrietdijk made their first contribution in https://github.com/justbetter/laravel-magento-client/pull/6 188 | 189 | **Full Changelog**: https://github.com/justbetter/laravel-magento-client/compare/1.0.2...1.0.3 190 | 191 | ## [1.0.2](https://github.com/justbetter/laravel-magento-client/releases/tag/1.0.2) - 2022-10-10 192 | 193 | ### What's Changed 194 | * Add GitHub Actions for tests, static analysis and style check by @VincentBean in https://github.com/justbetter/laravel-magento-client/pull/1 195 | * Default response items by @VincentBean in https://github.com/justbetter/laravel-magento-client/pull/2 196 | 197 | ### New Contributors 198 | * @VincentBean made their first contribution in https://github.com/justbetter/laravel-magento-client/pull/1 199 | 200 | **Full Changelog**: https://github.com/justbetter/laravel-magento-client/compare/1.0.1...1.0.2 201 | 202 | ## [1.0.1](https://github.com/justbetter/laravel-magento-client/releases/tag/1.0.1) - 2022-09-20 203 | 204 | **Full Changelog**: https://github.com/justbetter/laravel-magento-client/compare/1.0.0...1.0.1 205 | 206 | ## [1.0.0](https://github.com/justbetter/laravel-magento-client/releases/tag/1.0.0) - 2022-09-20 207 | 208 | Initial release of justbetter/laravel-magento-client 209 | 210 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) JustBetter 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated 6 | documentation files (the "Software"), to deal in the Software without restriction, including without limitation the 7 | rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit 8 | persons to whom the Software is furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the 11 | Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE 14 | WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 15 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 16 | OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 17 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | Banner 3 | 4 | 5 | # Laravel Magento Client 6 | 7 |

8 | Tests 9 | Coverage 10 | Analysis 11 | Total downloads 12 |

13 | 14 | A client to communicate with Magento from your Laravel application. 15 | 16 | ```php 17 | magento->get('products/1'); 29 | } 30 | 31 | public function retrieveOrdersLazily() 32 | { 33 | $retrievedOrders = []; 34 | 35 | $searchCriteria = \JustBetter\MagentoClient\Query\SearchCriteria::make() 36 | ->where('state', 'processing'); 37 | 38 | foreach ($this->magento->lazy('orders', $searchCriteria->get()) as $order) { 39 | $retrievedOrders[] = $order['increment_id']; 40 | } 41 | } 42 | 43 | public function multipleConnections() 44 | { 45 | $this->magento->connection('connection_one')->get('products/1'); 46 | $this->magento->connection('connection_two')->get('products/1'); 47 | } 48 | } 49 | 50 | ``` 51 | 52 | > Looking to synchronize [prices](https://github.com/justbetter/laravel-magento-prices) 53 | > or [stock](https://github.com/justbetter/laravel-magento-stock) to Magento? 54 | 55 | ## Installation 56 | 57 | > Are you coming from `grayloon/laravel-magento-api`? We have written 58 | > a [migration guide](./docs/migrations/GRAYLOON.md)! 59 | 60 | Require this package: 61 | 62 | ```shell 63 | composer require justbetter/laravel-magento-client 64 | ``` 65 | 66 | ## Configuration 67 | 68 | Add the following to your `.env`: 69 | 70 | ```.dotenv 71 | MAGENTO_BASE_URL= 72 | MAGENTO_ACCESS_TOKEN= 73 | ``` 74 | 75 | Optionally, publish the configuration file of this package. 76 | 77 | ```bash 78 | php artisan vendor:publish --provider="JustBetter\MagentoClient\ServiceProvider" --tag=config 79 | ``` 80 | 81 | ## Multiple connections 82 | 83 | This package supports connecting to multiple Magento instances. 84 | In the configuration file you will find an array with the connections where you can configure each connection. 85 | 86 | ```php 87 | 'connection' => 'default', 88 | 'connections' => [ 89 | 'default' => [ 90 | /* Base URL of Magento, for example: https://magento.test */ 91 | 'base_url' => env('MAGENTO_BASE_URL'), 92 | 93 | // Other settings 94 | ], 95 | 'another_connection' => [ 96 | 'base_url' => env('ANOTHER_MAGENTO_BASE_URL'), 97 | 98 | // Other settings 99 | ], 100 | ], 101 | ``` 102 | 103 | The `connection` setting sets which connection is used by default. 104 | You can switch connections by using the `connection` method on the client. 105 | 106 | ```php 107 | /** @var \JustBetter\MagentoClient\Client\Magento $client */ 108 | $client = app(\JustBetter\MagentoClient\Client\Magento::class); 109 | 110 | $client->connection('connection_one')->get('products'); 111 | ``` 112 | 113 | ## Authentication 114 | 115 | By default, this packages uses Bearer tokens to authenticate to Magento. Since Magento 2.4.4, this method of authentication requires additional configuration in Magento as [described here](https://developer.adobe.com/commerce/webapi/get-started/authentication/gs-authentication-token). 116 | 117 | It is recommended to authenticate to Magento using OAuth 1.0 which will also prevent the additional configuration requirements, besides setting up the integration itself. 118 | 119 | ### Setting up OAuth 1.0 120 | 121 | Start by adding the following variable to your `.env`-file. 122 | 123 | ```.dotenv 124 | MAGENTO_AUTH_METHOD=oauth 125 | ``` 126 | 127 | Note that you can also remove `MAGENTO_ACCESS_TOKEN` at this point, as it will be saved in a file instead. 128 | 129 | Next, open Magento and create a new integration. When configuring, supply a `Callback URL` and `Identity link URL`. If you have not made any changes to your configuration, these are the URLs: 130 | 131 | ``` 132 | Callback URL: https://example.com/magento/oauth/callback/{connection} 133 | Identity link URL: https://example.com/magento/oauth/identity/{connection} 134 | ``` 135 | 136 | `connection` is the key in your connections array in the configuration file. 137 | 138 | When creating the integration, Magento will send multiple tokens and secrets to your application via the `callback`-endpoint. This information will be saved in the database, as configured in `magento.php`. You may adjust this to save the key in a JSON file or create your own implementation 139 | Magento will redirect you to the `identity`-endpoint in order to activate the integration. 140 | 141 | For more information about OAuth 1.0 in Magento, please consult the [documentation](https://developer.adobe.com/commerce/webapi/get-started/authentication/gs-authentication-oauth). 142 | 143 | #### Identity endpoint 144 | 145 | > [!CAUTION] 146 | > Be sure to add your authentication middleware 147 | 148 | Note that the `identity`-endpoint **does not** have any authentication or authorization middleware by default - you should add this in the configuration yourself. If you do not have any form of protection, anyone could change the tokens in your secret file. 149 | 150 | It is recommended that only administrators of your application are allowed to access the identity endpoint. 151 | 152 | ## Usage 153 | 154 | You can get an instance of the client by injecting it or by resolving it: 155 | 156 | ```php 157 | select('items[sku,name]') 183 | ->where('sku', '!=', '123') 184 | ->orWhere('price', '>', 10), 185 | ->whereIn('sku', ['123', '456']) 186 | ->orWhereNull('name') 187 | ->orderBy('sku') 188 | ->paginate(1, 50) 189 | ->get(); 190 | ``` 191 | 192 | You can view more examples in [Magento's documentation](https://developer.adobe.com/commerce/webapi/rest/use-rest/performing-searches/). 193 | 194 | ### Connections 195 | 196 | You can connect to other connections using the `connection` method on the client. 197 | 198 | ```php 199 | /** @var \JustBetter\MagentoClient\Client\Magento $client */ 200 | $client = app(\JustBetter\MagentoClient\Client\Magento::class); 201 | 202 | $client->connection('connection_one')->get('products'); 203 | $client->connection('connection_two')->get('products'); 204 | ``` 205 | 206 | ### GraphQL 207 | 208 | You can run authenticated GraphQL queries using the `graphql` method on the client. 209 | 210 | ```php 211 | /** @var \JustBetter\MagentoClient\Client\Magento $client */ 212 | $client = app(\JustBetter\MagentoClient\Client\Magento::class); 213 | 214 | $client->graphql( 215 | 'query searchProducts($search: String) { 216 | products( 217 | search: $search 218 | ) { 219 | items { 220 | sku 221 | } 222 | } 223 | }', 224 | [ 225 | 'search' => 'test' 226 | ] 227 | ); 228 | ``` 229 | 230 | ### More Examples 231 | 232 | You can view the tests for more examples. 233 | 234 | ### Checking if Magento is available 235 | 236 | This client keeps track whether Magento is available by storing the count of any >=502 and <=504 response code in cache. 237 | You may use the `available()` method on the client in order to check if Magento is available. 238 | 239 | The threshold, timespan and status codes can be configured in the configuration file per connection. 240 | 241 | #### Jobs 242 | 243 | This package provides a job middleware that releases jobs back onto the queue when Magento is not available. 244 | The middleware is located at `\JustBetter\MagentoClient\Jobs\Middleware\AvailableMiddleware`. 245 | 246 | ## Testing 247 | 248 | This package uses Laravel's HTTP client so that you can fake the requests. 249 | 250 | ```php 251 | Http::response([ 255 | 'items' => [['sku' => '::some_sku::']], 256 | 'total_count' => 1, 257 | ]), 258 | ]); 259 | ``` 260 | 261 | ## Quality 262 | 263 | To ensure the quality of this package, run the following command: 264 | 265 | ```shell 266 | composer quality 267 | ``` 268 | 269 | This will execute three tasks: 270 | 271 | 1. Makes sure all tests are passed 272 | 2. Checks for any issues using static code analysis 273 | 3. Checks if the code is correctly formatted 274 | 275 | ## Contributing 276 | 277 | Please see [CONTRIBUTING](.github/CONTRIBUTING.md) for details. 278 | 279 | ## Security Vulnerabilities 280 | 281 | Please review [our security policy](../../security/policy) on how to report security vulnerabilities. 282 | 283 | ## Credits 284 | 285 | - [Vincent Boon](https://github.com/VincentBean) 286 | - [Ramon Rietdijk](https://github.com/ramonrietdijk) 287 | - [All Contributors](../../contributors) 288 | 289 | ## License 290 | 291 | The MIT License (MIT). Please see [License File](LICENSE) for more information. 292 | 293 | 294 | JustBetter logo 295 | 296 | -------------------------------------------------------------------------------- /UPGRADE.md: -------------------------------------------------------------------------------- 1 | # Upgrade guide 2 | 3 | # 1.x to 2.x 4 | 5 | 2.x introduces support for multiple connections. To upgrade you must adjust your configuration file and OAuth urls. 6 | 7 | ## Configuration file 8 | 9 | The configuration file has been changed to support multiple connections. 10 | It is recommended to overwrite the version from the package so that you are up to date. 11 | 12 | The environment variables did not change. 13 | This means that if you have a single Magento connection you code will not break. 14 | 15 | ## OAuth 16 | 17 | > [!NOTE] 18 | > This step is only required when using OAuth authentication 19 | 20 | Reauthorize your Magento integration to create the record in your DB. 21 | 22 | ### URL 23 | 24 | The OAuth URL's now require a connection parameter, if you have a single Magento instance the connection value is `default` by default. 25 | 26 | ``` 27 | Callback URL: https://example.com/magento/oauth/callback/{connection} 28 | Identity link URL: https://example.com/magento/oauth/identity/{connection} 29 | ``` 30 | 31 | ### Key Location 32 | 33 | OAuth keys are now stored in the database by default. 34 | If you prefer to store them on disk you can use the `\JustBetter\MagentoClient\OAuth\KeyStore\FileKeyStore` or implement your own keystore. 35 | 36 | 37 | ## Testing 38 | 39 | The `Magento::fake()` method has been altered to use the URL `magento` instead of `http://magento.test`. You should replace this in your tests. 40 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "justbetter/laravel-magento-client", 3 | "description": "A client to interact with Magento", 4 | "type": "package", 5 | "license": "MIT", 6 | "require": { 7 | "php": "^8.3", 8 | "guzzlehttp/guzzle": "^7.5", 9 | "laravel/framework": "^11.0|^12.0", 10 | "league/oauth1-client": "^1.10" 11 | }, 12 | "require-dev": { 13 | "larastan/larastan": "^3.0", 14 | "laravel/pint": "^1.20", 15 | "orchestra/testbench": "^9.0", 16 | "pestphp/pest": "^3.7", 17 | "phpstan/phpstan-mockery": "^2.0", 18 | "phpunit/phpunit": "^11.5" 19 | }, 20 | "authors": [ 21 | { 22 | "name": "Vincent Boon", 23 | "email": "vincent@justbetter.nl", 24 | "role": "Developer" 25 | }, 26 | { 27 | "name": "Ramon Rietdijk", 28 | "email": "ramon@justbetter.nl", 29 | "role": "Developer" 30 | } 31 | ], 32 | "autoload": { 33 | "psr-4": { 34 | "JustBetter\\MagentoClient\\": "src" 35 | } 36 | }, 37 | "autoload-dev": { 38 | "psr-4": { 39 | "JustBetter\\MagentoClient\\Tests\\": "tests" 40 | } 41 | }, 42 | "scripts": { 43 | "test": "phpunit", 44 | "analyse": "phpstan --memory-limit=256M", 45 | "style": "pint --test", 46 | "quality": [ 47 | "@style", 48 | "@analyse", 49 | "@test", 50 | "@coverage" 51 | ], 52 | "fix-style": "pint", 53 | "coverage": "XDEBUG_MODE=coverage php vendor/bin/pest --coverage --min=100" 54 | }, 55 | "config": { 56 | "sort-packages": true, 57 | "allow-plugins": { 58 | "pestphp/pest-plugin": true 59 | } 60 | }, 61 | "extra": { 62 | "laravel": { 63 | "providers": [ 64 | "JustBetter\\MagentoClient\\ServiceProvider" 65 | ] 66 | } 67 | }, 68 | "minimum-stability": "dev", 69 | "prefer-stable": true 70 | } 71 | -------------------------------------------------------------------------------- /config/magento.php: -------------------------------------------------------------------------------- 1 | 'default', 6 | 7 | 'connections' => [ 8 | 'default' => [ 9 | /* Base URL of Magento, for example: https://magento.test */ 10 | 'base_url' => env('MAGENTO_BASE_URL'), 11 | 12 | /* Base path, only modify if your API is not at /rest */ 13 | 'base_path' => env('MAGENTO_BASE_PATH', 'rest'), 14 | 15 | /* Graphql path, only modify if your API is not at /graphql */ 16 | 'graphql_path' => env('MAGENTO_GRAPHQL_PATH', 'graphql'), 17 | 18 | /* Store code, modify if you want to set a store by default. */ 19 | 'store_code' => env('MAGENTO_STORE_CODE', 'all'), 20 | 21 | /* Modify if Magento has a new API version */ 22 | 'version' => env('MAGENTO_API_VERSION', 'V1'), 23 | 24 | /* Magento access token of an integration */ 25 | 'access_token' => env('MAGENTO_ACCESS_TOKEN'), 26 | 27 | /* Specify the timeout (in seconds) for the request. */ 28 | 'timeout' => 30, 29 | 30 | /* Specify the connection timeout (in seconds) for the request. */ 31 | 'connect_timeout' => 10, 32 | 33 | /* Authentication method, choose either "oauth" or "token". */ 34 | 'authentication_method' => env('MAGENTO_AUTH_METHOD', 'token'), 35 | 36 | /* Availability configuration. */ 37 | 'availability' => [ 38 | /* The response codes that should trigger the availability check. */ 39 | 'codes' => [502, 503, 504], 40 | 41 | /* The amount of failed requests before the service is marked as unavailable. */ 42 | 'threshold' => 10, 43 | 44 | /* The timespan in minutes in which the failed requests should occur. */ 45 | 'timespan' => 10, 46 | 47 | /* The cooldown in minutes after the threshold is reached. */ 48 | 'cooldown' => 2, 49 | ], 50 | ], 51 | ], 52 | 53 | /* OAuth configuration */ 54 | 'oauth' => [ 55 | 56 | /* Add your middleware that authenticates users here, this is used for the identity callback. */ 57 | 'middleware' => [ 58 | // 59 | ], 60 | 61 | /* Prefix for the oauth routes. */ 62 | 'prefix' => 'magento/oauth', 63 | 64 | /* Class that manages how the keys are stored */ 65 | 'keystore' => \JustBetter\MagentoClient\OAuth\KeyStore\DatabaseKeyStore::class, 66 | ], 67 | ]; 68 | -------------------------------------------------------------------------------- /database/migrations/2023_12_11_150000_create_magento_oauth_keys_table.php: -------------------------------------------------------------------------------- 1 | id(); 13 | $table->string('magento_connection')->index(); 14 | 15 | $table->json('keys'); 16 | 17 | $table->timestamps(); 18 | }); 19 | } 20 | 21 | public function down(): void 22 | { 23 | Schema::dropIfExists('magento_oauth_keys'); 24 | } 25 | }; 26 | -------------------------------------------------------------------------------- /docs/migrations/GRAYLOON.md: -------------------------------------------------------------------------------- 1 | # Migration guide from grayloon/laravel-magento-api 2 | 3 | This is a migration guide to migrate from `grayloon/laravel-magento-api` to `justbetter/laravel-magento-client`. 4 | 5 | ## Installation 6 | 7 | > It is possible to use both clients in a project without issues. 8 | 9 | First of all, remove the `grayloon/laravel-magento-api` package. 10 | 11 | ```shell 12 | composer remove grayloon/laravel-magento-api 13 | ``` 14 | 15 | Next, install the `justbetter/laravel-magento-client` package. 16 | 17 | ```shell 18 | composer require justbetter/laravel-magento-client 19 | ``` 20 | 21 | ## Configuration 22 | 23 | If you have not modified the configuration file there are no changes necessary. The variables defined in the `.env`-file 24 | are equal, thus backwards compatible. 25 | 26 | ## Logging Failed Requests 27 | 28 | The `grayloon/laravel-magento-api` package contains a feature that logs failed API requests to the Laravel log file when 29 | the option `log_failed_requests` is enabled in the configuration file. 30 | 31 | This package does not have this functionality. We aim to keep this package simplistic and expect you to handle errors 32 | yourself. 33 | 34 | ## Update Your Code 35 | 36 | All references to Grayloon have to be updated to use the new client. 37 | 38 | ```php 39 | show($sku); 72 | 73 | // After 74 | $this->magento->get("products/$sku"); 75 | } 76 | 77 | public function updateProduct(string $sku, array $data): void 78 | { 79 | // Before 80 | Magento::api('products')->edit($sku, $data); 81 | 82 | // After 83 | $this->magento->put("products/$sku", $data); 84 | } 85 | 86 | public function getSpecificCustomers(): void 87 | { 88 | // Before 89 | Magento::api('customers')->all(50, 1, [ 90 | 'searchCriteria[filter_groups][0][filters][0][field]' => 'email', 91 | 'searchCriteria[filter_groups][0][filters][0][condition_type]' => 'like', 92 | 'searchCriteria[filter_groups][0][filters][0][value]' => '%@example.com', 93 | ]); 94 | 95 | // After 96 | $searchCriteria = SearchCriteria::make() 97 | ->paginate(1, 50) 98 | ->where('email', 'like', '%@example.com') 99 | ->get(); 100 | 101 | $this->magento->get('customers/search', $searchCriteria); 102 | } 103 | 104 | public function getStoreConfigs(): void 105 | { 106 | // Before 107 | Magento::api('store')->storeConfigs(); 108 | 109 | // After 110 | $this->magento->get('store/storeConfigs'); 111 | } 112 | 113 | public function getWebsites(): void 114 | { 115 | // Before 116 | Magento::api('store')->websites(); 117 | 118 | // After 119 | $this->magento->get('store/websites'); 120 | } 121 | } 122 | ``` 123 | -------------------------------------------------------------------------------- /phpstan.neon: -------------------------------------------------------------------------------- 1 | includes: 2 | - ./vendor/larastan/larastan/extension.neon 3 | - ./vendor/phpstan/phpstan-mockery/extension.neon 4 | 5 | parameters: 6 | paths: 7 | - src 8 | - tests 9 | level: 8 10 | ignoreErrors: 11 | - identifier: missingType.iterableValue 12 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | ./tests/* 6 | 7 | 8 | 9 | 10 | 11 | ./src 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /routes/web.php: -------------------------------------------------------------------------------- 1 | name('magento.oauth.callback'); 8 | 9 | Route::get('identity/{connection}', [OAuthController::class, 'identity']) 10 | ->middleware(config('magento.oauth.middleware')) 11 | ->name('magento.oauth.identity'); 12 | -------------------------------------------------------------------------------- /src/Actions/AuthenticateRequest.php: -------------------------------------------------------------------------------- 1 | provider()->authenticate($request, $connection); 19 | } 20 | 21 | public static function bind(): void 22 | { 23 | app()->singleton(AuthenticatesRequest::class, static::class); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Actions/BuildRequest.php: -------------------------------------------------------------------------------- 1 | timeout($options['timeout']) 23 | ->connectTimeout($options['connect_timeout']) 24 | ->acceptJson() 25 | ->asJson(); 26 | 27 | return $this->request->authenticate($pendingRequest, $connection); 28 | } 29 | 30 | public static function bind(): void 31 | { 32 | app()->singleton(BuildsRequest::class, static::class); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Actions/CheckMagento.php: -------------------------------------------------------------------------------- 1 | get(static::AVAILABLE_KEY.$connection, true); 14 | } 15 | 16 | public static function bind(): void 17 | { 18 | app()->singleton(ChecksMagento::class, static::class); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/Actions/OAuth/RequestAccessToken.php: -------------------------------------------------------------------------------- 1 | get($connection); 17 | 18 | $callback = $keys['callback'] ?? []; 19 | 20 | Validator::validate($callback, [ 21 | 'oauth_consumer_key' => 'required|string', 22 | 'oauth_consumer_secret' => 'required|string', 23 | 'oauth_verifier' => 'required|string', 24 | ]); 25 | 26 | if ($callback['oauth_consumer_key'] !== $key) { 27 | abort(403); 28 | } 29 | 30 | $credentials = new ClientCredentials; 31 | $credentials->setIdentifier($callback['oauth_consumer_key']); 32 | $credentials->setSecret($callback['oauth_consumer_secret']); 33 | $credentials->setCallbackUri(route('magento.oauth.callback', ['connection' => $connection])); 34 | 35 | /** @var MagentoServer $server */ 36 | $server = app()->makeWith(MagentoServer::class, [ 37 | 'clientCredentials' => $credentials, 38 | 'signature' => new HmacSha256Signature($credentials), 39 | ]); 40 | 41 | $server->connection = $connection; 42 | 43 | $temporaryCredentials = $server->getTemporaryCredentials(); 44 | 45 | $tokenCredentials = $server->getTokenCredentials( 46 | $temporaryCredentials, 47 | $temporaryCredentials->getIdentifier(), 48 | $callback['oauth_verifier'] 49 | ); 50 | 51 | $auth['access_token'] = $tokenCredentials->getIdentifier(); 52 | $auth['access_token_secret'] = $tokenCredentials->getSecret(); 53 | 54 | KeyStore::instance()->set( 55 | $connection, 56 | array_merge($callback, $auth), 57 | ); 58 | } 59 | 60 | public static function bind(): void 61 | { 62 | app()->singleton(RequestsAccessToken::class, static::class); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/Client/Magento.php: -------------------------------------------------------------------------------- 1 | connection = config('magento.connection'); 29 | } 30 | 31 | public function connection(string $connection): static 32 | { 33 | $this->connection = $connection; 34 | 35 | return $this; 36 | } 37 | 38 | public function store(?string $store = null): static 39 | { 40 | $this->storeCode = $store; 41 | 42 | return $this; 43 | } 44 | 45 | public function graphql(string $query, array $variables = []): Response 46 | { 47 | /** @var string $endpoint */ 48 | $endpoint = config("magento.connections.{$this->connection}.graphql_path"); 49 | 50 | /** @var Response $response */ 51 | $response = $this->request() 52 | ->when($this->storeCode !== null, fn (PendingRequest $request): PendingRequest => $request->withHeaders(['Store' => $this->storeCode])) 53 | ->post($endpoint, [ 54 | 'query' => $query, 55 | 'variables' => $variables, 56 | ]); 57 | 58 | return $this->handleResponse($response); 59 | } 60 | 61 | public function get(string $path, array $data = []): Response 62 | { 63 | $response = $this->request()->get($this->getUrl($path), $data); 64 | 65 | return $this->handleResponse($response); 66 | } 67 | 68 | public function post(string $path, array $data = []): Response 69 | { 70 | /** @var Response $response */ 71 | $response = $this->request()->post($this->getUrl($path), $data); 72 | 73 | return $this->handleResponse($response); 74 | } 75 | 76 | public function postAsync(string $path, array $data = []): Response 77 | { 78 | /** @var Response $response */ 79 | $response = $this->request()->post($this->getUrl($path, true), $data); 80 | 81 | return $this->handleResponse($response); 82 | } 83 | 84 | public function postBulk(string $path, array $data = []): Response 85 | { 86 | /** @var Response $response */ 87 | $response = $this->request()->post($this->getUrl($path, true, true), $data); 88 | 89 | return $this->handleResponse($response); 90 | } 91 | 92 | public function patch(string $path, array $data = []): Response 93 | { 94 | /** @var Response $response */ 95 | $response = $this->request()->patch($this->getUrl($path), $data); 96 | 97 | return $this->handleResponse($response); 98 | } 99 | 100 | public function patchAsync(string $path, array $data = []): Response 101 | { 102 | /** @var Response $response */ 103 | $response = $this->request()->patch($this->getUrl($path, true), $data); 104 | 105 | return $this->handleResponse($response); 106 | } 107 | 108 | public function patchBulk(string $path, array $data = []): Response 109 | { 110 | /** @var Response $response */ 111 | $response = $this->request()->patch($this->getUrl($path, true, true), $data); 112 | 113 | return $this->handleResponse($response); 114 | } 115 | 116 | public function put(string $path, array $data = []): Response 117 | { 118 | /** @var Response $response */ 119 | $response = $this->request()->put($this->getUrl($path), $data); 120 | 121 | return $this->handleResponse($response); 122 | } 123 | 124 | public function putAsync(string $path, array $data = []): Response 125 | { 126 | /** @var Response $response */ 127 | $response = $this->request()->put($this->getUrl($path, true), $data); 128 | 129 | return $this->handleResponse($response); 130 | } 131 | 132 | public function putBulk(string $path, array $data = []): Response 133 | { 134 | /** @var Response $response */ 135 | $response = $this->request()->put($this->getUrl($path, true, true), $data); 136 | 137 | return $this->handleResponse($response); 138 | } 139 | 140 | public function delete(string $path, array $data = []): Response 141 | { 142 | /** @var Response $response */ 143 | $response = $this->request()->delete($this->getUrl($path), $data); 144 | 145 | return $this->handleResponse($response); 146 | } 147 | 148 | public function deleteAsync(string $path, array $data = []): Response 149 | { 150 | /** @var Response $response */ 151 | $response = $this->request()->delete($this->getUrl($path, true), $data); 152 | 153 | return $this->handleResponse($response); 154 | } 155 | 156 | public function deleteBulk(string $path, array $data = []): Response 157 | { 158 | /** @var Response $response */ 159 | $response = $this->request()->delete($this->getUrl($path, true, true), $data); 160 | 161 | return $this->handleResponse($response); 162 | } 163 | 164 | /** @return LazyCollection */ 165 | public function lazy(string $path, array $data = [], int $pageSize = 100): LazyCollection 166 | { 167 | return LazyCollection::make(function () use ($path, $data, $pageSize): Generator { 168 | $currentPage = 1; 169 | $hasNextPage = true; 170 | 171 | while ($hasNextPage) { 172 | $data['searchCriteria[pageSize]'] = $pageSize; 173 | $data['searchCriteria[currentPage]'] = $currentPage; 174 | 175 | $response = $this->get($path, $data)->throw(); 176 | 177 | /** @var Enumerable> $items */ 178 | $items = $response->collect('items'); 179 | 180 | foreach ($items as $item) { 181 | yield $item; 182 | } 183 | 184 | $hasNextPage = $items->count() >= $pageSize; 185 | $currentPage++; 186 | } 187 | }); 188 | } 189 | 190 | public function available(): bool 191 | { 192 | return $this->checksMagento->available($this->connection); 193 | } 194 | 195 | public function getUrl(string $path, bool $async = false, bool $bulk = false): string 196 | { 197 | /** @var array $config */ 198 | $config = config('magento.connections.'.$this->connection); 199 | 200 | $options = [ 201 | $config['base_path'] ?? 'rest', 202 | $this->storeCode ?? $config['store_code'] ?? 'all', 203 | ]; 204 | 205 | if ($async || $bulk) { 206 | $options[] = 'async'; 207 | 208 | if ($bulk) { 209 | $options[] = 'bulk'; 210 | } 211 | } 212 | 213 | $options[] = $config['version'] ?? 'V1'; 214 | $options[] = $path; 215 | 216 | return implode('/', $options); 217 | } 218 | 219 | public function intercept(Closure $callable): static 220 | { 221 | $this->interceptor = $callable; 222 | 223 | return $this; 224 | } 225 | 226 | protected function request(): PendingRequest 227 | { 228 | $request = $this->request->build($this->connection); 229 | 230 | if (isset($this->interceptor)) { 231 | call_user_func($this->interceptor, $request); 232 | $this->interceptor = null; 233 | } 234 | 235 | return $request; 236 | } 237 | 238 | protected function handleResponse(Response $response): Response 239 | { 240 | MagentoResponseEvent::dispatch($response, $this->connection); 241 | 242 | return $response; 243 | } 244 | 245 | public static function fake(): void 246 | { 247 | config()->set('magento.connection', 'default'); 248 | config()->set('magento.connections.default', [ 249 | 'base_url' => 'magento', 250 | 'base_path' => 'rest', 251 | 'graphql_path' => 'graphql', 252 | 'store_code' => 'all', 253 | 'version' => 'V1', 254 | 'access_token' => '::token::', 255 | 'timeout' => 30, 256 | 'connect_timeout' => 10, 257 | 'authentication_method' => 'token', 258 | ]); 259 | 260 | config()->set('magento.oauth', [ 261 | 'middleware' => [], 262 | 'prefix' => 'magento/oauth', 263 | 'keystore' => FileKeyStore::class, 264 | ]); 265 | } 266 | } 267 | -------------------------------------------------------------------------------- /src/Contracts/AuthenticatesRequest.php: -------------------------------------------------------------------------------- 1 | BearerTokenProvider::class, 18 | AuthenticationMethod::OAuth => OAuthProvider::class, 19 | }; 20 | 21 | /** @var BaseProvider $instance */ 22 | $instance = app($class); 23 | 24 | return $instance; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Events/MagentoResponseEvent.php: -------------------------------------------------------------------------------- 1 | merge($connection, [ 18 | 'callback' => $request->validated(), 19 | ]); 20 | 21 | return response()->json(); 22 | } 23 | 24 | public function identity( 25 | IdentityRequest $request, 26 | RequestsAccessToken $contract, 27 | string $connection 28 | ): RedirectResponse { 29 | $contract->request( 30 | $connection, 31 | $request->oauth_consumer_key, 32 | ); 33 | 34 | return redirect()->to($request->success_call_back); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Http/Middleware/OAuthMiddleware.php: -------------------------------------------------------------------------------- 1 | 21 | */ 22 | public function rules(): array 23 | { 24 | return [ 25 | 'oauth_consumer_key' => 'required|string|max:32', 26 | 'oauth_consumer_secret' => 'required|string|max:32', 27 | 'oauth_verifier' => 'required|string|max:32', 28 | ]; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Http/Requests/IdentityRequest.php: -------------------------------------------------------------------------------- 1 | 20 | */ 21 | public function rules(): array 22 | { 23 | return [ 24 | 'oauth_consumer_key' => 'required|string|max:32', 25 | 'success_call_back' => 'required|string|max:4096', 26 | ]; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Jobs/Middleware/AvailableMiddleware.php: -------------------------------------------------------------------------------- 1 | connection = $connection ?? config('magento.connection'); 17 | $this->seconds = $seconds; 18 | } 19 | 20 | public function handle(object $job, Closure $next): void 21 | { 22 | /** @var Magento $magento */ 23 | $magento = app(Magento::class); 24 | $magento->connection($this->connection); 25 | 26 | if ($magento->available()) { 27 | $next($job); 28 | } elseif (method_exists($job, 'release')) { 29 | $job->release($this->seconds); 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Listeners/StoreAvailabilityListener.php: -------------------------------------------------------------------------------- 1 | $codes */ 15 | $codes = config('magento.connections.'.$event->connection.'.availability.codes', [502, 503, 504]); 16 | 17 | if (! in_array($event->response->status(), $codes)) { 18 | return; 19 | } 20 | 21 | $countKey = static::COUNT_KEY.$event->connection; 22 | 23 | /** @var int $count */ 24 | $count = cache()->get($countKey, 0); 25 | $count++; 26 | 27 | /** @var int $threshold */ 28 | $threshold = config('magento.connections.'.$event->connection.'.availability.threshold', 10); 29 | 30 | /** @var int $timespan */ 31 | $timespan = config('magento.connections.'.$event->connection.'.availability.timespan', 10); 32 | 33 | /** @var int $cooldown */ 34 | $cooldown = config('magento.connections.'.$event->connection.'.availability.cooldown', 2); 35 | 36 | cache()->put($countKey, $count, now()->addMinutes($timespan)); 37 | 38 | if ($count >= $threshold) { 39 | cache()->put(CheckMagento::AVAILABLE_KEY.$event->connection, false, now()->addMinutes($cooldown)); 40 | 41 | cache()->forget($countKey); 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Models/OAuthKey.php: -------------------------------------------------------------------------------- 1 | 'array', 23 | ]; 24 | } 25 | -------------------------------------------------------------------------------- /src/OAuth/HmacSha256Signature.php: -------------------------------------------------------------------------------- 1 | key(), true); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/OAuth/KeyStore/DatabaseKeyStore.php: -------------------------------------------------------------------------------- 1 | firstWhere('magento_connection', '=', $connection); 13 | 14 | return $key->keys ?? []; 15 | } 16 | 17 | public function set(string $connection, array $data): void 18 | { 19 | OAuthKey::query()->updateOrCreate( 20 | [ 21 | 'magento_connection' => $connection, 22 | ], 23 | [ 24 | 'keys' => $data, 25 | ] 26 | ); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/OAuth/KeyStore/FileKeyStore.php: -------------------------------------------------------------------------------- 1 | disk)->get($this->path.'/'.$connection.'.json') ?? ''; 17 | 18 | return json_decode($content, true) ?? []; 19 | } 20 | 21 | public function set(string $connection, array $data): void 22 | { 23 | $storage = Storage::disk($this->disk); 24 | 25 | File::ensureDirectoryExists($storage->path($this->path)); 26 | 27 | if ($encoded = json_encode($data)) { 28 | Storage::disk($this->disk)->put($this->path.'/'.$connection.'.json', $encoded, 'private'); 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/OAuth/KeyStore/KeyStore.php: -------------------------------------------------------------------------------- 1 | $class */ 10 | $class = config('magento.oauth.keystore'); 11 | 12 | /** @var KeyStore $instance */ 13 | $instance = app($class); 14 | 15 | return $instance; 16 | } 17 | 18 | abstract public function get(string $connection): array; 19 | 20 | abstract public function set(string $connection, array $data): void; 21 | 22 | public function merge(string $connection, array $data): void 23 | { 24 | $merged = array_merge($this->get($connection), $data); 25 | 26 | $this->set($connection, $merged); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/OAuth/MagentoServer.php: -------------------------------------------------------------------------------- 1 | connection.'.base_url').'/oauth/token/request'; 21 | } 22 | 23 | public function urlAuthorization(): string 24 | { 25 | return ''; 26 | } 27 | 28 | public function urlTokenCredentials(): string 29 | { 30 | return config('magento.connections.'.$this->connection.'.base_url').'/oauth/token/access'; 31 | } 32 | 33 | public function getTokenCredentials(TemporaryCredentials $temporaryCredentials, $temporaryIdentifier, $verifier) 34 | { 35 | $this->verifier = $verifier; 36 | 37 | return parent::getTokenCredentials($temporaryCredentials, $temporaryIdentifier, $verifier); 38 | } 39 | 40 | protected function additionalProtocolParameters(): array 41 | { 42 | return [ 43 | 'oauth_verifier' => $this->verifier, 44 | ]; 45 | } 46 | 47 | protected function createTemporaryCredentials($body): TemporaryCredentials 48 | { 49 | parse_str($body, $data); 50 | 51 | if (! $data) { 52 | throw new CredentialsException('Unable to parse temporary credentials response.'); 53 | } 54 | 55 | /** @var string $token */ 56 | $token = $data['oauth_token']; 57 | 58 | /** @var string $secret */ 59 | $secret = $data['oauth_token_secret']; 60 | 61 | $temporaryCredentials = new TemporaryCredentials; 62 | $temporaryCredentials->setIdentifier($token); 63 | $temporaryCredentials->setSecret($secret); 64 | 65 | return $temporaryCredentials; 66 | } 67 | 68 | public function urlUserDetails(): string 69 | { 70 | return ''; 71 | } 72 | 73 | public function userDetails($data, TokenCredentials $tokenCredentials): User 74 | { 75 | return new User; 76 | } 77 | 78 | public function userUid($data, TokenCredentials $tokenCredentials): int 79 | { 80 | return 0; 81 | } 82 | 83 | public function userEmail($data, TokenCredentials $tokenCredentials): ?string 84 | { 85 | return null; 86 | } 87 | 88 | public function userScreenName($data, TokenCredentials $tokenCredentials): ?string 89 | { 90 | return null; 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/Providers/BaseProvider.php: -------------------------------------------------------------------------------- 1 | withToken($token); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/Providers/OAuthProvider.php: -------------------------------------------------------------------------------- 1 | get($connection); 23 | 24 | Validator::validate($keys, [ 25 | 'oauth_consumer_key' => 'required|string', 26 | 'oauth_consumer_secret' => 'required|string', 27 | 'oauth_verifier' => 'required|string', 28 | 'access_token' => 'required|string', 29 | 'access_token_secret' => 'required|string', 30 | ]); 31 | 32 | return $request->withMiddleware( 33 | Middleware::mapRequest(function (RequestInterface $request) use ($keys) { 34 | $credentials = new ClientCredentials; 35 | $credentials->setIdentifier($keys['oauth_consumer_key']); 36 | $credentials->setSecret($keys['oauth_consumer_secret']); 37 | 38 | $server = new MagentoServer($credentials, new HmacSha256Signature($credentials)); 39 | $server->verifier = $keys['oauth_verifier']; 40 | 41 | $tokenCredentials = new TokenCredentials; 42 | $tokenCredentials->setIdentifier($keys['access_token']); 43 | $tokenCredentials->setSecret($keys['access_token_secret']); 44 | 45 | $query = $request->getUri()->getQuery(); 46 | 47 | $data = Str::of($query) 48 | ->explode('&') 49 | ->filter(fn (string $parameter): bool => strlen($parameter) > 0) 50 | ->mapWithKeys(function (string $parameter): array { 51 | $pair = explode('=', $parameter); 52 | 53 | return count($pair) === 2 54 | ? [$pair[0] => $pair[1]] 55 | : [$pair[0] => '']; 56 | })->toArray(); 57 | 58 | $headers = $server->getHeaders( 59 | $tokenCredentials, 60 | $request->getMethod(), 61 | (string) $request->getUri()->withFragment('')->withQuery(''), 62 | $data 63 | ); 64 | 65 | return $request->withHeader('Authorization', $headers['Authorization']); 66 | }) 67 | ); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/Query/Grammar.php: -------------------------------------------------------------------------------- 1 | 'eq', 9 | '<' => 'lt', 10 | '>' => 'gt', 11 | '<=' => 'lteq', 12 | '>=' => 'gteq', 13 | '<>' => 'neq', 14 | '!=' => 'neq', 15 | ]; 16 | 17 | public function getOperator(string $operator): string 18 | { 19 | return $this->mapping[$operator] ?? $operator; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Query/SearchCriteria.php: -------------------------------------------------------------------------------- 1 | pagination = [ 26 | 'searchCriteria[pageSize]' => $pageSize, 27 | 'searchCriteria[currentPage]' => $page, 28 | ]; 29 | 30 | return $this; 31 | } 32 | 33 | public function select(array|string $fields): static 34 | { 35 | $this->select['fields'] = is_array($fields) 36 | ? implode(',', $fields) 37 | : $fields; 38 | 39 | return $this; 40 | } 41 | 42 | public function where( 43 | string $field, 44 | string $operator, 45 | mixed $value = null, 46 | ?int $filterGroup = null, 47 | int $filterIndex = 0 48 | ): static { 49 | // Assume the operator is equals 50 | if ($value === null) { 51 | $value = $operator; 52 | $operator = '='; 53 | } 54 | 55 | return $this->addWhere($field, $operator, $value, $filterGroup, $filterIndex); 56 | } 57 | 58 | public function orWhere(string $field, string $operator, mixed $value = null): static 59 | { 60 | $this->currentFilterIndex++; 61 | 62 | $this->where( 63 | $field, 64 | $operator, 65 | $value, 66 | $this->currentFilterGroup > 0 ? $this->currentFilterGroup - 1 : 0, 67 | $this->currentFilterIndex 68 | ); 69 | 70 | return $this; 71 | } 72 | 73 | public function whereIn(string $field, array $values): static 74 | { 75 | return $this->addWhere($field, 'in', implode(',', $values)); 76 | } 77 | 78 | public function orWhereIn(string $field, array $values): static 79 | { 80 | $this->currentFilterIndex++; 81 | 82 | return $this->addWhere( 83 | $field, 84 | 'in', 85 | implode(',', $values), 86 | $this->currentFilterGroup > 0 ? $this->currentFilterGroup - 1 : 0, 87 | $this->currentFilterIndex 88 | ); 89 | } 90 | 91 | public function whereNotIn(string $field, array $values): static 92 | { 93 | return $this->addWhere($field, 'nin', implode(',', $values)); 94 | } 95 | 96 | public function orWhereNotIn(string $field, array $values): static 97 | { 98 | $this->currentFilterIndex++; 99 | 100 | return $this->addWhere( 101 | $field, 102 | 'nin', 103 | implode(',', $values), 104 | $this->currentFilterGroup > 0 ? $this->currentFilterGroup - 1 : 0, 105 | $this->currentFilterIndex 106 | ); 107 | } 108 | 109 | public function whereNull(string $field): static 110 | { 111 | return $this->addWhere($field, 'null'); 112 | } 113 | 114 | public function orWhereNull(string $field): static 115 | { 116 | $this->currentFilterIndex++; 117 | 118 | return $this->addWhere( 119 | $field, 120 | 'null', 121 | null, 122 | $this->currentFilterGroup > 0 ? $this->currentFilterGroup - 1 : 0, 123 | $this->currentFilterIndex 124 | ); 125 | } 126 | 127 | public function whereNotNull(string $field): static 128 | { 129 | return $this->addWhere($field, 'notnull'); 130 | } 131 | 132 | public function orWhereNotNull(string $field): static 133 | { 134 | $this->currentFilterIndex++; 135 | 136 | return $this->addWhere( 137 | $field, 138 | 'notnull', 139 | null, 140 | $this->currentFilterGroup > 0 ? $this->currentFilterGroup - 1 : 0, 141 | $this->currentFilterIndex 142 | ); 143 | } 144 | 145 | protected function addWhere( 146 | string $field, 147 | string $operator, 148 | mixed $value = null, 149 | ?int $filterGroup = null, 150 | int $filterIndex = 0 151 | ): static { 152 | $currentFilterGroup = $filterGroup ?? $this->currentFilterGroup; 153 | 154 | $prefix = "searchCriteria[filter_groups][$currentFilterGroup][filters][$filterIndex]"; 155 | 156 | $this->wheres[$prefix.'[field]'] = $field; 157 | $this->wheres[$prefix.'[condition_type]'] = $this->grammar->getOperator($operator); 158 | 159 | if ($value !== null) { 160 | $this->wheres[$prefix.'[value]'] = $value; 161 | } 162 | 163 | if ($filterGroup === null) { 164 | $this->currentFilterGroup++; 165 | } 166 | 167 | if ($filterIndex === 0) { 168 | $this->currentFilterIndex = 0; 169 | } 170 | 171 | return $this; 172 | } 173 | 174 | public function orderBy(string $field, string $direction = 'ASC'): static 175 | { 176 | $index = count($this->orders) / 2; 177 | 178 | $this->orders["searchCriteria[sortOrders][$index][field]"] = $field; 179 | $this->orders["searchCriteria[sortOrders][$index][direction]"] = $direction; 180 | 181 | return $this; 182 | } 183 | 184 | public function orderByDesc(string $field): static 185 | { 186 | return $this->orderBy($field, 'DESC'); 187 | } 188 | 189 | public function get(): array 190 | { 191 | return array_merge( 192 | $this->select, 193 | $this->wheres, 194 | $this->orders, 195 | $this->pagination, 196 | ); 197 | } 198 | 199 | public function dd(): void 200 | { 201 | dd($this->get()); 202 | } 203 | 204 | public static function make(): static 205 | { 206 | return app(static::class); 207 | } 208 | } 209 | -------------------------------------------------------------------------------- /src/ServiceProvider.php: -------------------------------------------------------------------------------- 1 | registerConfig() 22 | ->registerActions(); 23 | } 24 | 25 | protected function registerConfig(): static 26 | { 27 | $this->mergeConfigFrom(__DIR__.'/../config/magento.php', 'magento'); 28 | 29 | return $this; 30 | } 31 | 32 | protected function registerActions(): static 33 | { 34 | RequestAccessToken::bind(); 35 | AuthenticateRequest::bind(); 36 | BuildRequest::bind(); 37 | CheckMagento::bind(); 38 | 39 | return $this; 40 | } 41 | 42 | public function boot(): void 43 | { 44 | $this 45 | ->bootConfig() 46 | ->bootEvents() 47 | ->bootRoutes() 48 | ->bootMigrations(); 49 | } 50 | 51 | protected function bootConfig(): static 52 | { 53 | $this->publishes([ 54 | __DIR__.'/../config/magento.php' => config_path('magento.php'), 55 | ], 'config'); 56 | 57 | return $this; 58 | } 59 | 60 | protected function bootEvents(): static 61 | { 62 | Event::listen(MagentoResponseEvent::class, StoreAvailabilityListener::class); 63 | 64 | return $this; 65 | } 66 | 67 | protected function bootRoutes(): static 68 | { 69 | if (! app()->routesAreCached()) { 70 | Route::prefix(config('magento.oauth.prefix')) 71 | ->middleware([OAuthMiddleware::class]) 72 | ->group(__DIR__.'/../routes/web.php'); 73 | } 74 | 75 | return $this; 76 | } 77 | 78 | protected function bootMigrations(): static 79 | { 80 | $this->loadMigrationsFrom(__DIR__.'/../database/migrations'); 81 | 82 | return $this; 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /tests/Actions/CheckMagentoTest.php: -------------------------------------------------------------------------------- 1 | assertTrue($action->available('default')); 18 | } 19 | 20 | #[Test] 21 | public function it_can_be_unavailable(): void 22 | { 23 | /** @var CheckMagento $action */ 24 | $action = app(CheckMagento::class); 25 | 26 | cache()->put(CheckMagento::AVAILABLE_KEY.config('magento.connection'), false); 27 | 28 | $this->assertFalse($action->available('default')); 29 | } 30 | 31 | #[Test] 32 | public function it_can_handle_multiple_connections(): void 33 | { 34 | /** @var CheckMagento $action */ 35 | $action = app(CheckMagento::class); 36 | 37 | cache()->put(CheckMagento::AVAILABLE_KEY.'connection', false); 38 | cache()->put(CheckMagento::AVAILABLE_KEY.'another-connection', true); 39 | 40 | $this->assertFalse($action->available('connection')); 41 | $this->assertTrue($action->available('another-connection')); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /tests/Actions/OAuth/RequestAccessTokenTest.php: -------------------------------------------------------------------------------- 1 | setIdentifier('::request-token::'); 21 | $temporaryCredentials->setSecret('::request-token-secret::'); 22 | 23 | $tokenCredentials = new TokenCredentials; 24 | $tokenCredentials->setIdentifier('::access-token::'); 25 | $tokenCredentials->setSecret('::access-token-secret::'); 26 | 27 | /** @var MagentoServer $server */ 28 | $server = $this->mock(MagentoServer::class, 29 | function (MockInterface $mock) use ($temporaryCredentials, $tokenCredentials): void { 30 | $mock 31 | ->shouldReceive('getTemporaryCredentials') 32 | ->once() 33 | ->andReturn($temporaryCredentials); 34 | 35 | $mock 36 | ->shouldReceive('getTokenCredentials') 37 | ->once() 38 | ->andReturn($tokenCredentials); 39 | }); 40 | 41 | app()->bind(MagentoServer::class, fn () => $server); 42 | 43 | $this->mock(FileKeyStore::class, function (MockInterface $mock): void { 44 | $mock 45 | ->shouldReceive('get') 46 | ->once() 47 | ->andReturn([ 48 | 'callback' => [ 49 | 'oauth_consumer_key' => '::oauth-consumer-key::', 50 | 'oauth_consumer_secret' => '::oauth-consumer-secret::', 51 | 'oauth_verifier' => '::oauth-verifier::', 52 | ], 53 | ]); 54 | 55 | $mock 56 | ->shouldReceive('set') 57 | ->once() 58 | ->with('default', [ 59 | 'oauth_consumer_key' => '::oauth-consumer-key::', 60 | 'oauth_consumer_secret' => '::oauth-consumer-secret::', 61 | 'oauth_verifier' => '::oauth-verifier::', 62 | 'access_token' => '::access-token::', 63 | 'access_token_secret' => '::access-token-secret::', 64 | ]); 65 | }); 66 | 67 | $key = '::oauth-consumer-key::'; 68 | 69 | /** @var RequestAccessToken $action */ 70 | $action = app(RequestAccessToken::class); 71 | $action->request('default', $key); 72 | } 73 | 74 | /** @test */ 75 | public function it_can_throw_http_exceptions(): void 76 | { 77 | $this->expectException(HttpException::class); 78 | 79 | $this->mock(FileKeyStore::class, function (MockInterface $mock): void { 80 | $mock 81 | ->shouldReceive('get') 82 | ->once() 83 | ->andReturn([ 84 | 'callback' => [ 85 | 'oauth_consumer_key' => '::oauth-consumer-key::', 86 | 'oauth_consumer_secret' => '::oauth-consumer-secret::', 87 | 'oauth_verifier' => '::oauth-verifier::', 88 | ], 89 | ]); 90 | }); 91 | 92 | $key = '::different-key::'; 93 | 94 | /** @var RequestAccessToken $action */ 95 | $action = app(RequestAccessToken::class); 96 | $action->request('default', $key); 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /tests/Client/ClientTest.php: -------------------------------------------------------------------------------- 1 | Http::response([ 22 | 'data' => [ 23 | 'currency' => [ 24 | 'base_currency_code' => 'EUR', 25 | ], 26 | ], 27 | ]), 28 | ]); 29 | 30 | /** @var Magento $magento */ 31 | $magento = app(Magento::class); 32 | 33 | $response = $magento->store('default')->graphql('query { currency { base_currency_code } }', ['testing']); 34 | 35 | $this->assertTrue($response->ok()); 36 | $this->assertEquals('EUR', $response->json('data.currency.base_currency_code')); 37 | 38 | Http::assertSent(function (Request $request): bool { 39 | 40 | return $request->method() === 'POST' 41 | && $request->url() === 'magento/graphql' 42 | && $request->body() === '{"query":"query { currency { base_currency_code } }","variables":["testing"]}' 43 | && $request->header('Store') === ['default']; 44 | }); 45 | } 46 | 47 | public function test_it_can_make_a_get_call(): void 48 | { 49 | Http::fake([ 50 | 'magento/rest/all/V1/products*' => Http::response(['items' => []]), 51 | ]); 52 | 53 | /** @var Magento $magento */ 54 | $magento = app(Magento::class); 55 | 56 | $response = $magento->get('products', [ 57 | 'searchCriteria[pageSize]' => 10, 58 | 'searchCriteria[currentPage]' => 0, 59 | ]); 60 | $this->assertEquals(true, $response->ok()); 61 | $this->assertCount(0, $response->json('items')); 62 | 63 | Http::assertSent(function (Request $request) { 64 | return $request->method() === 'GET' && 65 | $request->url() == 'magento/rest/all/V1/products?searchCriteria%5BpageSize%5D=10&searchCriteria%5BcurrentPage%5D=0'; 66 | }); 67 | } 68 | 69 | public function test_it_can_make_a_post_call(): void 70 | { 71 | Http::fake([ 72 | 'magento/rest/all/V1/products' => Http::response([ 73 | 'product' => [ 74 | 'entity_id' => 1, 75 | 'sku' => '::some-sku::', 76 | ], 77 | ]), 78 | ]); 79 | 80 | /** @var Magento $magento */ 81 | $magento = app(Magento::class); 82 | 83 | $response = $magento->post('products', [ 84 | 'product' => [ 85 | 'sku' => '::some-sku::', 86 | ], 87 | ]); 88 | $this->assertEquals(true, $response->ok()); 89 | $this->assertCount(2, $response->json('product')); 90 | 91 | Http::assertSent(function (Request $request) { 92 | return $request->method() === 'POST' && 93 | $request->url() == 'magento/rest/all/V1/products' && 94 | $request->body() === '{"product":{"sku":"::some-sku::"}}'; 95 | }); 96 | } 97 | 98 | public function test_it_can_make_an_async_post_call(): void 99 | { 100 | Http::fake([ 101 | 'magento/rest/all/async/V1/products' => Http::response([ 102 | 'product' => [ 103 | 'entity_id' => 1, 104 | 'sku' => '::some-sku::', 105 | ], 106 | ]), 107 | ]); 108 | 109 | /** @var Magento $magento */ 110 | $magento = app(Magento::class); 111 | 112 | $response = $magento->postAsync('products', [ 113 | 'product' => [ 114 | 'sku' => '::some-sku::', 115 | ], 116 | ]); 117 | $this->assertEquals(true, $response->ok()); 118 | $this->assertCount(2, $response->json('product')); 119 | 120 | Http::assertSent(function (Request $request) { 121 | return $request->method() === 'POST' && 122 | $request->url() == 'magento/rest/all/async/V1/products' && 123 | $request->body() === '{"product":{"sku":"::some-sku::"}}'; 124 | }); 125 | } 126 | 127 | public function test_it_can_make_a_bulk_post_call(): void 128 | { 129 | Http::fake([ 130 | 'magento/rest/all/async/bulk/V1/products' => Http::response([ 131 | 'bulk_uuid' => Str::uuid()->toString(), 132 | 'request_items' => [ 133 | [ 134 | 'id' => 0, 135 | 'data_hash' => '0000000000000000000000000000000000000000000000000000000000000000', 136 | 'status' => 'accepted', 137 | ], 138 | [ 139 | 'id' => 1, 140 | 'data_hash' => '0000000000000000000000000000000000000000000000000000000000000000', 141 | 'status' => 'accepted', 142 | ], 143 | ], 144 | 'errors' => false, 145 | ]), 146 | ]); 147 | 148 | /** @var Magento $magento */ 149 | $magento = app(Magento::class); 150 | 151 | $response = $magento->postBulk('products', [ 152 | [ 153 | 'product' => [ 154 | 'sku' => '::sku-1::', 155 | ], 156 | ], 157 | [ 158 | 'product' => [ 159 | 'sku' => '::sku-2::', 160 | ], 161 | ], 162 | ]); 163 | 164 | $this->assertEquals(true, $response->ok()); 165 | $this->assertCount(2, $response->json('request_items')); 166 | 167 | Http::assertSent(function (Request $request): bool { 168 | return $request->method() === 'POST' && 169 | $request->url() == 'magento/rest/all/async/bulk/V1/products' && 170 | $request->body() === '[{"product":{"sku":"::sku-1::"}},{"product":{"sku":"::sku-2::"}}]'; 171 | }); 172 | } 173 | 174 | public function test_it_can_make_a_put_call(): void 175 | { 176 | Http::fake([ 177 | 'magento/rest/all/V1/products' => Http::response([ 178 | 'product' => [ 179 | 'entity_id' => 1, 180 | 'sku' => '::some-sku::', 181 | ], 182 | ]), 183 | ]); 184 | 185 | /** @var Magento $magento */ 186 | $magento = app(Magento::class); 187 | 188 | $response = $magento->put('products', [ 189 | 'product' => [ 190 | 'sku' => '::some-sku::', 191 | ], 192 | ]); 193 | $this->assertEquals(true, $response->ok()); 194 | $this->assertCount(2, $response->json('product')); 195 | 196 | Http::assertSent(function (Request $request) { 197 | return $request->method() === 'PUT' && 198 | $request->url() == 'magento/rest/all/V1/products' && 199 | $request->body() === '{"product":{"sku":"::some-sku::"}}'; 200 | }); 201 | } 202 | 203 | public function test_it_can_make_an_async_put_call(): void 204 | { 205 | Http::fake([ 206 | 'magento/rest/all/async/V1/products' => Http::response([ 207 | 'product' => [ 208 | 'entity_id' => 1, 209 | 'sku' => '::some-sku::', 210 | ], 211 | ]), 212 | ]); 213 | 214 | /** @var Magento $magento */ 215 | $magento = app(Magento::class); 216 | 217 | $response = $magento->putAsync('products', [ 218 | 'product' => [ 219 | 'sku' => '::some-sku::', 220 | ], 221 | ]); 222 | $this->assertEquals(true, $response->ok()); 223 | $this->assertCount(2, $response->json('product')); 224 | 225 | Http::assertSent(function (Request $request) { 226 | return $request->method() === 'PUT' && 227 | $request->url() == 'magento/rest/all/async/V1/products' && 228 | $request->body() === '{"product":{"sku":"::some-sku::"}}'; 229 | }); 230 | } 231 | 232 | public function test_it_can_make_a_bulk_put_call(): void 233 | { 234 | Http::fake([ 235 | 'magento/rest/all/async/bulk/V1/products' => Http::response([ 236 | 'bulk_uuid' => Str::uuid()->toString(), 237 | 'request_items' => [ 238 | [ 239 | 'id' => 0, 240 | 'data_hash' => '0000000000000000000000000000000000000000000000000000000000000000', 241 | 'status' => 'accepted', 242 | ], 243 | [ 244 | 'id' => 1, 245 | 'data_hash' => '0000000000000000000000000000000000000000000000000000000000000000', 246 | 'status' => 'accepted', 247 | ], 248 | ], 249 | 'errors' => false, 250 | ]), 251 | ]); 252 | 253 | /** @var Magento $magento */ 254 | $magento = app(Magento::class); 255 | 256 | $response = $magento->putBulk('products', [ 257 | [ 258 | 'product' => [ 259 | 'sku' => '::sku-1::', 260 | ], 261 | ], 262 | [ 263 | 'product' => [ 264 | 'sku' => '::sku-2::', 265 | ], 266 | ], 267 | ]); 268 | 269 | $this->assertEquals(true, $response->ok()); 270 | $this->assertCount(2, $response->json('request_items')); 271 | 272 | Http::assertSent(function (Request $request): bool { 273 | return $request->method() === 'PUT' && 274 | $request->url() == 'magento/rest/all/async/bulk/V1/products' && 275 | $request->body() === '[{"product":{"sku":"::sku-1::"}},{"product":{"sku":"::sku-2::"}}]'; 276 | }); 277 | } 278 | 279 | public function test_it_can_make_a_patch_call(): void 280 | { 281 | Http::fake([ 282 | 'magento/rest/all/V1/products' => Http::response([ 283 | 'product' => [ 284 | 'entity_id' => 1, 285 | 'sku' => '::some-sku::', 286 | ], 287 | ]), 288 | ]); 289 | 290 | /** @var Magento $magento */ 291 | $magento = app(Magento::class); 292 | 293 | $response = $magento->patch('products', [ 294 | 'product' => [ 295 | 'sku' => '::some-sku::', 296 | ], 297 | ]); 298 | $this->assertEquals(true, $response->ok()); 299 | $this->assertCount(2, $response->json('product')); 300 | 301 | Http::assertSent(function (Request $request) { 302 | return $request->method() === 'PATCH' && 303 | $request->url() == 'magento/rest/all/V1/products' && 304 | $request->body() === '{"product":{"sku":"::some-sku::"}}'; 305 | }); 306 | } 307 | 308 | public function test_it_can_make_an_async_patch_call(): void 309 | { 310 | Http::fake([ 311 | 'magento/rest/all/async/V1/products' => Http::response([ 312 | 'product' => [ 313 | 'entity_id' => 1, 314 | 'sku' => '::some-sku::', 315 | ], 316 | ]), 317 | ]); 318 | 319 | /** @var Magento $magento */ 320 | $magento = app(Magento::class); 321 | 322 | $response = $magento->patchAsync('products', [ 323 | 'product' => [ 324 | 'sku' => '::some-sku::', 325 | ], 326 | ]); 327 | $this->assertEquals(true, $response->ok()); 328 | $this->assertCount(2, $response->json('product')); 329 | 330 | Http::assertSent(function (Request $request) { 331 | return $request->method() === 'PATCH' && 332 | $request->url() == 'magento/rest/all/async/V1/products' && 333 | $request->body() === '{"product":{"sku":"::some-sku::"}}'; 334 | }); 335 | } 336 | 337 | public function test_it_can_make_a_bulk_patch_call(): void 338 | { 339 | Http::fake([ 340 | 'magento/rest/all/async/bulk/V1/products' => Http::response([ 341 | 'bulk_uuid' => Str::uuid()->toString(), 342 | 'request_items' => [ 343 | [ 344 | 'id' => 0, 345 | 'data_hash' => '0000000000000000000000000000000000000000000000000000000000000000', 346 | 'status' => 'accepted', 347 | ], 348 | [ 349 | 'id' => 1, 350 | 'data_hash' => '0000000000000000000000000000000000000000000000000000000000000000', 351 | 'status' => 'accepted', 352 | ], 353 | ], 354 | 'errors' => false, 355 | ]), 356 | ]); 357 | 358 | /** @var Magento $magento */ 359 | $magento = app(Magento::class); 360 | 361 | $response = $magento->patchBulk('products', [ 362 | [ 363 | 'product' => [ 364 | 'sku' => '::sku-1::', 365 | ], 366 | ], 367 | [ 368 | 'product' => [ 369 | 'sku' => '::sku-2::', 370 | ], 371 | ], 372 | ]); 373 | 374 | $this->assertEquals(true, $response->ok()); 375 | $this->assertCount(2, $response->json('request_items')); 376 | 377 | Http::assertSent(function (Request $request): bool { 378 | return $request->method() === 'PATCH' && 379 | $request->url() == 'magento/rest/all/async/bulk/V1/products' && 380 | $request->body() === '[{"product":{"sku":"::sku-1::"}},{"product":{"sku":"::sku-2::"}}]'; 381 | }); 382 | } 383 | 384 | public function test_it_can_make_a_delete_call(): void 385 | { 386 | Http::fake([ 387 | 'magento/rest/all/V1/products/1' => Http::response([], 204), 388 | ]); 389 | 390 | /** @var Magento $magento */ 391 | $magento = app(Magento::class); 392 | 393 | $response = $magento->delete('products/1'); 394 | $this->assertEquals(204, $response->status()); 395 | 396 | Http::assertSent(function (Request $request) { 397 | return $request->method() === 'DELETE' && 398 | $request->url() == 'magento/rest/all/V1/products/1'; 399 | }); 400 | } 401 | 402 | public function test_it_can_make_an_async_delete_call(): void 403 | { 404 | Http::fake([ 405 | 'magento/rest/all/async/V1/products/1' => Http::response([], 204), 406 | ]); 407 | 408 | /** @var Magento $magento */ 409 | $magento = app(Magento::class); 410 | 411 | $response = $magento->deleteAsync('products/1'); 412 | $this->assertEquals(204, $response->status()); 413 | 414 | Http::assertSent(function (Request $request) { 415 | return $request->method() === 'DELETE' && 416 | $request->url() == 'magento/rest/all/async/V1/products/1'; 417 | }); 418 | } 419 | 420 | public function test_it_can_make_a_bulk_delete_call(): void 421 | { 422 | Http::fake([ 423 | 'magento/rest/all/async/bulk/V1/products/bySku' => Http::response([ 424 | 'bulk_uuid' => Str::uuid()->toString(), 425 | 'request_items' => [ 426 | [ 427 | 'id' => 0, 428 | 'data_hash' => '0000000000000000000000000000000000000000000000000000000000000000', 429 | 'status' => 'accepted', 430 | ], 431 | [ 432 | 'id' => 1, 433 | 'data_hash' => '0000000000000000000000000000000000000000000000000000000000000000', 434 | 'status' => 'accepted', 435 | ], 436 | ], 437 | 'errors' => false, 438 | ]), 439 | ]); 440 | 441 | /** @var Magento $magento */ 442 | $magento = app(Magento::class); 443 | 444 | $response = $magento->deleteBulk('products/bySku', [ 445 | [ 446 | 'sku' => '::sku-1::', 447 | ], 448 | [ 449 | 'sku' => '::sku-2::', 450 | ], 451 | ]); 452 | 453 | $this->assertEquals(true, $response->ok()); 454 | $this->assertCount(2, $response->json('request_items')); 455 | 456 | Http::assertSent(function (Request $request): bool { 457 | return $request->method() === 'DELETE' && 458 | $request->url() == 'magento/rest/all/async/bulk/V1/products/bySku' && 459 | $request->body() === '[{"sku":"::sku-1::"},{"sku":"::sku-2::"}]'; 460 | }); 461 | } 462 | 463 | public function test_it_can_get_results_lazily(): void 464 | { 465 | Http::fake([ 466 | 'magento/rest/all/V1/products?searchCriteria%5Bfilter_groups%5D%5B0%5D%5Bfilters%5D%5B0%5D%5Bfield%5D=sku&searchCriteria%5Bfilter_groups%5D%5B0%5D%5Bfilters%5D%5B0%5D%5Bcondition_type%5D=neq&searchCriteria%5Bfilter_groups%5D%5B0%5D%5Bfilters%5D%5B0%5D%5Bvalue%5D=something&searchCriteria%5BpageSize%5D=5&searchCriteria%5BcurrentPage%5D=1' => Http::response([ 467 | 'items' => [ 468 | ['sku' => '1000'], 469 | ['sku' => '2000'], 470 | ['sku' => '3000'], 471 | ['sku' => '4000'], 472 | ['sku' => '5000'], 473 | ], 474 | ]), 475 | 'magento/rest/all/V1/products?searchCriteria%5Bfilter_groups%5D%5B0%5D%5Bfilters%5D%5B0%5D%5Bfield%5D=sku&searchCriteria%5Bfilter_groups%5D%5B0%5D%5Bfilters%5D%5B0%5D%5Bcondition_type%5D=neq&searchCriteria%5Bfilter_groups%5D%5B0%5D%5Bfilters%5D%5B0%5D%5Bvalue%5D=something&searchCriteria%5BpageSize%5D=5&searchCriteria%5BcurrentPage%5D=2' => Http::response([ 476 | 'items' => [ 477 | ['sku' => '6000'], 478 | ['sku' => '7000'], 479 | ], 480 | ]), 481 | ]); 482 | 483 | /** @var Magento $magento */ 484 | $magento = app(Magento::class); 485 | 486 | $products = $magento->lazy('products', [ 487 | 'searchCriteria[filter_groups][0][filters][0][field]' => 'sku', 488 | 'searchCriteria[filter_groups][0][filters][0][condition_type]' => 'neq', 489 | 'searchCriteria[filter_groups][0][filters][0][value]' => 'something', 490 | ], 5); 491 | 492 | $collection = $products->collect(); 493 | 494 | $this->assertEquals(7, $collection->count()); 495 | } 496 | 497 | public function test_it_can_set_store_code(): void 498 | { 499 | Http::fake([ 500 | 'magento/rest/store/V1/products*' => Http::response(['items' => []]), 501 | ]); 502 | 503 | /** @var Magento $magento */ 504 | $magento = app(Magento::class); 505 | 506 | $magento->store('store')->get('products'); 507 | 508 | Http::assertSent(function (Request $request) { 509 | return $request->url() == 'magento/rest/store/V1/products'; 510 | }); 511 | } 512 | 513 | public function test_it_can_intersect_the_client_and_adjust_its_body_format(): void 514 | { 515 | Http::fake([ 516 | 'magento/rest/all/V1/products*' => Http::response(['items' => []]), 517 | ]); 518 | 519 | /** @var Magento $magento */ 520 | $magento = app(Magento::class); 521 | 522 | $result = $magento->intercept(function (PendingRequest $request) { 523 | $request->asForm(); 524 | }); 525 | 526 | $this->assertInstanceOf(Magento::class, $result); 527 | 528 | $response = $magento->get('products', [ 529 | 'searchCriteria[pageSize]' => 10, 530 | 'searchCriteria[currentPage]' => 0, 531 | ]); 532 | 533 | $this->assertTrue($response->ok()); 534 | $this->assertCount(0, $response->json('items')); 535 | 536 | Http::assertSent(function (Request $request) { 537 | return $request->method() === 'GET' && 538 | $request->header('Content-Type')[0] === 'application/x-www-form-urlencoded' && 539 | $request->url() == 'magento/rest/all/V1/products?searchCriteria%5BpageSize%5D=10&searchCriteria%5BcurrentPage%5D=0'; 540 | }); 541 | } 542 | 543 | public function test_it_resets_the_interceptor(): void 544 | { 545 | Http::fake([ 546 | 'magento/rest/all/V1/products' => Http::response([ 547 | 'product' => [ 548 | 'entity_id' => 1, 549 | 'sku' => '::some-sku::', 550 | ], 551 | ]), 552 | ]); 553 | 554 | /** @var Magento $magento */ 555 | $magento = app(Magento::class); 556 | 557 | $magento->intercept(function (PendingRequest $request) { 558 | $request->withHeaders(['some-header' => '::test::']); 559 | })->post('products', [ 560 | 'product' => [ 561 | 'sku' => '::some-sku::', 562 | ], 563 | ]); 564 | 565 | $magento->post('products', [ 566 | 'product' => [ 567 | 'sku' => '::some-sku::', 568 | ], 569 | ]); 570 | 571 | Http::assertSentInOrder([ 572 | fn (Request $request) => $request->hasHeader('some-header'), 573 | fn (Request $request) => ! $request->hasHeader('some-header'), 574 | ]); 575 | } 576 | 577 | public function test_it_dispatches_event(): void 578 | { 579 | Event::fake(); 580 | 581 | Http::fake([ 582 | 'magento/rest/all/V1/products*' => Http::response(['items' => ['item']]), 583 | ]); 584 | 585 | /** @var Magento $magento */ 586 | $magento = app(Magento::class); 587 | 588 | $magento->get('products', [ 589 | 'searchCriteria[pageSize]' => 10, 590 | 'searchCriteria[currentPage]' => 0, 591 | ]); 592 | 593 | Event::assertDispatched(MagentoResponseEvent::class, function (MagentoResponseEvent $event): bool { 594 | return $event->response->ok() && $event->response->json('items') === ['item']; 595 | }); 596 | } 597 | 598 | public function test_it_checks_available(): void 599 | { 600 | $this->mock(ChecksMagento::class, function (MockInterface $mock): void { 601 | $mock->shouldReceive('available')->with('default')->once()->andReturnTrue(); 602 | $mock->shouldReceive('available')->with('unavailable')->once()->andReturnFalse(); 603 | }); 604 | 605 | /** @var Magento $magento */ 606 | $magento = app(Magento::class); 607 | 608 | $this->assertTrue($magento->available()); 609 | $this->assertFalse($magento->connection('unavailable')->available()); 610 | } 611 | } 612 | -------------------------------------------------------------------------------- /tests/Client/MultiConnectionTest.php: -------------------------------------------------------------------------------- 1 | set('magento.connections.otherconnection', [ 19 | 'base_url' => 'otherconnection', 20 | 'base_path' => 'rest', 21 | 'store_code' => 'all', 22 | 'version' => 'V1', 23 | 'access_token' => '::token::', 24 | 'timeout' => 30, 25 | 'connect_timeout' => 10, 26 | 'authentication_method' => 'token', 27 | ]); 28 | } 29 | 30 | /** @test */ 31 | public function it_can_do_requests_to_multiple_connections(): void 32 | { 33 | Http::fake([ 34 | 'magento*' => Http::response(), 35 | 'otherconnection*' => Http::response(), 36 | ])->preventStrayRequests(); 37 | 38 | /** @var Magento $magento */ 39 | $magento = app(Magento::class); 40 | 41 | $magento->connection('default')->get('products'); 42 | $magento->connection('otherconnection')->get('products'); 43 | 44 | Http::assertSentInOrder([ 45 | fn (Request $request): bool => $request->url() === 'magento/rest/all/V1/products', 46 | fn (Request $request): bool => $request->url() === 'otherconnection/rest/all/V1/products', 47 | ]); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /tests/Enums/AuthenticationMethodTest.php: -------------------------------------------------------------------------------- 1 | assertInstanceOf($expectedProvider, $authenticationMethod->provider()); 20 | } 21 | 22 | public static function providers(): array 23 | { 24 | return [ 25 | [ 26 | AuthenticationMethod::Token, 27 | BearerTokenProvider::class, 28 | ], 29 | [ 30 | AuthenticationMethod::OAuth, 31 | OAuthProvider::class, 32 | ], 33 | ]; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /tests/Fakes/TestJob.php: -------------------------------------------------------------------------------- 1 | '::oauth_consumer_key::', 17 | 'oauth_consumer_secret' => '::oauth_consumer_secret::', 18 | 'oauth_verifier' => '::oauth_verifier::', 19 | ]; 20 | 21 | $this->mock(FileKeyStore::class, function (MockInterface $mock) use ($payload): void { 22 | $mock 23 | ->shouldReceive('merge') 24 | ->with('default', ['callback' => $payload]) 25 | ->once(); 26 | }); 27 | 28 | $this 29 | ->withoutMiddleware() 30 | ->post(route('magento.oauth.callback', ['connection' => 'default']), $payload, [ 31 | 'Accept' => 'application/json', 32 | ]) 33 | ->assertSuccessful(); 34 | } 35 | 36 | /** @test */ 37 | public function it_can_validate_the_callback_endpoint(): void 38 | { 39 | $this 40 | ->withoutMiddleware() 41 | ->post(route('magento.oauth.callback', ['connection' => 'default']), [], [ 42 | 'Accept' => 'application/json', 43 | ]) 44 | ->assertStatus(422); 45 | } 46 | 47 | /** @test */ 48 | public function it_can_block_the_callback_endpoint_without_oauth_authentication(): void 49 | { 50 | $this->post(route('magento.oauth.callback', ['connection' => 'default']), [], [ 51 | 'Accept' => 'application/json', 52 | ])->assertStatus(403); 53 | } 54 | 55 | /** @test */ 56 | public function it_can_call_the_identity_endpoint(): void 57 | { 58 | $this->mock(RequestsAccessToken::class, function (MockInterface $mock): void { 59 | $mock 60 | ->shouldReceive('request') 61 | ->with('default', '::oauth_consumer_key::') 62 | ->once(); 63 | }); 64 | 65 | $route = route('magento.oauth.identity', [ 66 | 'connection' => 'default', 67 | 'oauth_consumer_key' => '::oauth_consumer_key::', 68 | 'success_call_back' => '::success_call_back::', 69 | ]); 70 | 71 | $this->withoutMiddleware()->get($route, [ 72 | 'Accept' => 'application/json', 73 | ])->assertRedirect('::success_call_back::'); 74 | } 75 | 76 | /** @test */ 77 | public function it_can_validate_the_identity_endpoint(): void 78 | { 79 | $this 80 | ->withoutMiddleware() 81 | ->get(route('magento.oauth.identity', ['connection' => 'default']), [ 82 | 'Accept' => 'application/json', 83 | ]) 84 | ->assertStatus(422); 85 | } 86 | 87 | /** @test */ 88 | public function it_can_block_the_identity_endpoint_without_oauth_authentication(): void 89 | { 90 | $this->get(route('magento.oauth.identity', ['connection' => 'default']), [ 91 | 'Accept' => 'application/json', 92 | ])->assertStatus(403); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /tests/Http/Middleware/OAuthMiddlewareTest.php: -------------------------------------------------------------------------------- 1 | set('magento.connections.default.authentication_method', 'oauth'); 16 | 17 | /** @var OAuthMiddleware $middleware */ 18 | $middleware = app(OAuthMiddleware::class); 19 | 20 | /** @var Response $response */ 21 | $response = $middleware->handle(request(), function (): Response { 22 | return response('passed'); 23 | }); 24 | 25 | $this->assertEquals('passed', $response->getContent()); 26 | } 27 | 28 | /** @test */ 29 | public function it_can_abort(): void 30 | { 31 | $this->expectException(HttpException::class); 32 | 33 | /** @var OAuthMiddleware $middleware */ 34 | $middleware = app(OAuthMiddleware::class); 35 | $middleware->handle(request(), function (): Response { 36 | return response('passed'); 37 | }); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /tests/Jobs/Middleware/AvailableMiddlewareTest.php: -------------------------------------------------------------------------------- 1 | handle($job, function () use (&$ran): void { 23 | $ran = true; 24 | }); 25 | 26 | $this->assertTrue($ran); 27 | } 28 | 29 | #[Test] 30 | public function it_can_release_jobs(): void 31 | { 32 | $this->mock(Magento::class, function (MockInterface $mock): void { 33 | $mock->shouldReceive('connection')->with('default')->once()->andReturnSelf(); 34 | $mock->shouldReceive('available')->once()->andReturnFalse(); 35 | }); 36 | 37 | $job = $this->mock(TestJob::class, function (MockInterface $mock): void { 38 | $mock->shouldReceive('release')->once(); 39 | }); 40 | 41 | $middleware = new AvailableMiddleware('default'); 42 | $middleware->handle($job, function (): void { 43 | $this->fail('Job should not have run.'); 44 | }); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /tests/Listeners/StoreAvailabilityListenerTest.php: -------------------------------------------------------------------------------- 1 | Http::response(null, 200), 19 | ])->preventStrayRequests(); 20 | 21 | $magento = app(Magento::class); 22 | $magento->get('/'); 23 | 24 | $this->assertNull( 25 | cache()->get(StoreAvailabilityListener::COUNT_KEY.'default') 26 | ); 27 | } 28 | 29 | #[Test] 30 | public function it_can_keep_track_of_the_count(): void 31 | { 32 | Http::fake([ 33 | '*' => Http::response(null, 503), 34 | ])->preventStrayRequests(); 35 | 36 | /** @var Magento $magento */ 37 | $magento = app(Magento::class); 38 | $magento->get('/'); 39 | 40 | $this->assertEquals(1, cache()->get(StoreAvailabilityListener::COUNT_KEY.'default')); 41 | $this->assertNull(cache()->get(CheckMagento::AVAILABLE_KEY.'default')); 42 | } 43 | 44 | #[Test] 45 | public function it_can_be_unavailable(): void 46 | { 47 | Http::fake([ 48 | '*' => Http::response(null, 503), 49 | ])->preventStrayRequests(); 50 | 51 | config()->set('magento.connections.default.availability.threshold', 1); 52 | 53 | /** @var Magento $magento */ 54 | $magento = app(Magento::class); 55 | $magento->get('/'); 56 | 57 | $this->assertNull(cache()->get(StoreAvailabilityListener::COUNT_KEY.'default')); 58 | $this->assertFalse(cache()->get(CheckMagento::AVAILABLE_KEY.'default')); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /tests/OAuth/HmacSha256SignatureTest.php: -------------------------------------------------------------------------------- 1 | setIdentifier('::oauth-consumer-key::'); 16 | $clientCredentials->setSecret('::oauth-consumer-secret::'); 17 | 18 | /** @var HmacSha256Signature $signature */ 19 | $signature = app()->makeWith(HmacSha256Signature::class, [ 20 | 'clientCredentials' => $clientCredentials, 21 | ]); 22 | 23 | $method = $signature->method(); 24 | 25 | $this->assertEquals('HMAC-SHA256', $method); 26 | 27 | $signed = $signature->sign('test'); 28 | 29 | $this->assertEquals('mpy8OxeIYvroN5WQW9f/aBMtC+qbmus1oaa0KAMoMiQ=', $signed); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /tests/OAuth/KeyStore/DatabaseKeyStoreTest.php: -------------------------------------------------------------------------------- 1 | set('magento.oauth.keystore', DatabaseKeyStore::class); 20 | } 21 | 22 | /** @test */ 23 | public function it_can_get_data(): void 24 | { 25 | $content = [ 26 | 'key' => 'value', 27 | ]; 28 | 29 | $store = KeyStore::instance(); 30 | 31 | OAuthKey::query()->create([ 32 | 'magento_connection' => 'default', 33 | 'keys' => $content, 34 | ]); 35 | 36 | $data = $store->get('default'); 37 | 38 | $this->assertEquals($content, $data); 39 | } 40 | 41 | /** @test */ 42 | public function it_can_set_data(): void 43 | { 44 | $content = [ 45 | 'key' => 'value', 46 | ]; 47 | 48 | $store = KeyStore::instance(); 49 | $store->set('default', $content); 50 | 51 | $data = $store->get('default'); 52 | 53 | $this->assertEquals($content, $data); 54 | } 55 | 56 | /** @test */ 57 | public function it_can_merge_data(): void 58 | { 59 | $content = [ 60 | 'key' => 'value', 61 | ]; 62 | 63 | OAuthKey::query()->create([ 64 | 'magento_connection' => 'default', 65 | 'keys' => $content, 66 | ]); 67 | 68 | $store = KeyStore::instance(); 69 | 70 | $new = [ 71 | 'something' => 'else', 72 | ]; 73 | 74 | $store->merge('default', $new); 75 | 76 | $data = $store->get('default'); 77 | 78 | $this->assertEquals(array_merge($content, $new), $data); 79 | } 80 | 81 | /** @test */ 82 | public function it_can_handle_multiple_connections(): void 83 | { 84 | $store = KeyStore::instance(); 85 | 86 | $store->set('connection_one', ['one']); 87 | $store->set('connection_two', ['two']); 88 | 89 | $this->assertEquals(['one'], $store->get('connection_one')); 90 | $this->assertEquals(['two'], $store->get('connection_two')); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /tests/OAuth/KeyStore/FileKeyStoreTest.php: -------------------------------------------------------------------------------- 1 | set('magento.oauth.keystore', FileKeyStore::class); 19 | } 20 | 21 | /** @test */ 22 | public function it_can_get_data(): void 23 | { 24 | $content = [ 25 | 'key' => 'value', 26 | ]; 27 | 28 | /** @var FileKeyStore $store */ 29 | $store = KeyStore::instance(); 30 | 31 | /** @var non-empty-string $encoded */ 32 | $encoded = json_encode($content); 33 | 34 | Storage::disk($store->disk)->put($store->path.'/default.json', $encoded); 35 | 36 | $data = $store->get('default'); 37 | 38 | $this->assertEquals($content, $data); 39 | } 40 | 41 | /** @test */ 42 | public function it_can_set_data(): void 43 | { 44 | $content = [ 45 | 'key' => 'value', 46 | ]; 47 | 48 | /** @var FileKeyStore $store */ 49 | $store = KeyStore::instance(); 50 | $store->set('default', $content); 51 | 52 | $data = $store->get('default'); 53 | 54 | $this->assertEquals($content, $data); 55 | } 56 | 57 | /** @test */ 58 | public function it_can_merge_data(): void 59 | { 60 | $content = [ 61 | 'key' => 'value', 62 | ]; 63 | 64 | /** @var non-empty-string $encoded */ 65 | $encoded = json_encode($content); 66 | 67 | /** @var FileKeyStore $store */ 68 | $store = KeyStore::instance(); 69 | 70 | Storage::disk($store->disk)->put($store->path.'/default.json', $encoded); 71 | 72 | $new = [ 73 | 'something' => 'else', 74 | ]; 75 | 76 | $store->merge('default', $new); 77 | 78 | $data = $store->get('default'); 79 | 80 | $this->assertEquals(array_merge($content, $new), $data); 81 | } 82 | 83 | /** @test */ 84 | public function it_can_handle_multiple_connections(): void 85 | { 86 | $store = KeyStore::instance(); 87 | 88 | $store->set('connection_one', ['one']); 89 | $store->set('connection_two', ['two']); 90 | 91 | $this->assertEquals(['one'], $store->get('connection_one')); 92 | $this->assertEquals(['two'], $store->get('connection_two')); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /tests/Providers/BearerTokenProviderTest.php: -------------------------------------------------------------------------------- 1 | authenticate($pendingRequest, 'default'); 21 | 22 | $options = $pendingRequest->getOptions(); 23 | 24 | $authorization = data_get($options, 'headers.Authorization'); 25 | 26 | $this->assertEquals($authorization, 'Bearer ::token::'); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /tests/Providers/OAuthProviderTest.php: -------------------------------------------------------------------------------- 1 | mock(FileKeyStore::class, function (MockInterface $mock): void { 20 | $mock 21 | ->shouldReceive('get') 22 | ->once() 23 | ->andReturn([ 24 | 'oauth_consumer_key' => '::oauth-consumer-key::', 25 | 'oauth_consumer_secret' => '::oauth-consumer-secret::', 26 | 'oauth_verifier' => '::oauth-verifier::', 27 | 'access_token' => '::access-token::', 28 | 'access_token_secret' => '::access-token-secret::', 29 | ]); 30 | }); 31 | 32 | $pendingRequest = Http::baseUrl('localhost'); 33 | 34 | /** @var OAuthProvider $provider */ 35 | $provider = app(OAuthProvider::class); 36 | $provider->authenticate($pendingRequest, 'default'); 37 | 38 | $authorization = null; 39 | 40 | try { 41 | $pendingRequest->withMiddleware( 42 | Middleware::mapRequest(function (RequestInterface $request) use (&$authorization) { 43 | $authorization = $request->getHeader('Authorization'); 44 | 45 | throw new Exception('Cancel request execution'); 46 | }) 47 | )->get('/', [ 48 | 'key' => 'value', 49 | ]); 50 | } catch (Exception) { 51 | // 52 | } 53 | 54 | if ($authorization === null) { 55 | $this->fail('No authorization header has been set'); 56 | } 57 | 58 | $authorization = $authorization[0]; 59 | 60 | if (! preg_match_all('/(?[a-z_]+)(?>=)/', $authorization, $matches)) { 61 | $this->fail('Could not match any keys'); 62 | } 63 | 64 | $this->assertEquals([ 65 | 'oauth_consumer_key', 66 | 'oauth_nonce', 67 | 'oauth_signature_method', 68 | 'oauth_timestamp', 69 | 'oauth_version', 70 | 'oauth_verifier', 71 | 'oauth_token', 72 | 'oauth_signature', 73 | ], $matches['keys']); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /tests/Query/SearchCriteriaTest.php: -------------------------------------------------------------------------------- 1 | paginate(1, 10) 16 | ->get(); 17 | 18 | $this->assertEquals([ 19 | 'searchCriteria[pageSize]' => 10, 20 | 'searchCriteria[currentPage]' => 1, 21 | ], $searchCriteria); 22 | } 23 | 24 | public function test_it_can_add_a_simple_where(): void 25 | { 26 | $searchCriteria = SearchCriteria::make() 27 | ->where('sku', '::some-sku::') 28 | ->get(); 29 | 30 | $this->assertEquals([ 31 | 'searchCriteria[filter_groups][0][filters][0][field]' => 'sku', 32 | 'searchCriteria[filter_groups][0][filters][0][condition_type]' => 'eq', 33 | 'searchCriteria[filter_groups][0][filters][0][value]' => '::some-sku::', 34 | ], $searchCriteria); 35 | } 36 | 37 | public function test_it_can_add_multiple_wheres(): void 38 | { 39 | $searchCriteria = SearchCriteria::make() 40 | ->where('sku', '=', '::some-sku::') 41 | ->where('name', '::some-name::') 42 | ->get(); 43 | 44 | $this->assertEquals([ 45 | 'searchCriteria[filter_groups][0][filters][0][field]' => 'sku', 46 | 'searchCriteria[filter_groups][0][filters][0][condition_type]' => 'eq', 47 | 'searchCriteria[filter_groups][0][filters][0][value]' => '::some-sku::', 48 | 'searchCriteria[filter_groups][1][filters][0][field]' => 'name', 49 | 'searchCriteria[filter_groups][1][filters][0][condition_type]' => 'eq', 50 | 'searchCriteria[filter_groups][1][filters][0][value]' => '::some-name::', 51 | ], $searchCriteria); 52 | } 53 | 54 | public function test_it_can_add_or_where(): void 55 | { 56 | $searchCriteria = SearchCriteria::make() 57 | ->where('sku', '=', '::some-sku::') 58 | ->orWhere('name', '=', '::some-name::') 59 | ->get(); 60 | 61 | $this->assertEquals([ 62 | 'searchCriteria[filter_groups][0][filters][0][field]' => 'sku', 63 | 'searchCriteria[filter_groups][0][filters][0][condition_type]' => 'eq', 64 | 'searchCriteria[filter_groups][0][filters][0][value]' => '::some-sku::', 65 | 'searchCriteria[filter_groups][0][filters][1][field]' => 'name', 66 | 'searchCriteria[filter_groups][0][filters][1][condition_type]' => 'eq', 67 | 'searchCriteria[filter_groups][0][filters][1][value]' => '::some-name::', 68 | ], $searchCriteria); 69 | } 70 | 71 | public function test_it_can_add_multiple_or_where(): void 72 | { 73 | $searchCriteria = SearchCriteria::make() 74 | ->where('sku', '=', '::some-sku::') 75 | ->orWhere('name', '=', '::some-name::') 76 | ->where('some_attribute', '>', 10) 77 | ->orWhere('another_attribute', '<=', 100) 78 | ->orWhere('test_attribute', '<>', '::some_value::') 79 | ->get(); 80 | 81 | $this->assertEquals([ 82 | 'searchCriteria[filter_groups][0][filters][0][field]' => 'sku', 83 | 'searchCriteria[filter_groups][0][filters][0][condition_type]' => 'eq', 84 | 'searchCriteria[filter_groups][0][filters][0][value]' => '::some-sku::', 85 | 'searchCriteria[filter_groups][0][filters][1][field]' => 'name', 86 | 'searchCriteria[filter_groups][0][filters][1][condition_type]' => 'eq', 87 | 'searchCriteria[filter_groups][0][filters][1][value]' => '::some-name::', 88 | 'searchCriteria[filter_groups][1][filters][0][field]' => 'some_attribute', 89 | 'searchCriteria[filter_groups][1][filters][0][condition_type]' => 'gt', 90 | 'searchCriteria[filter_groups][1][filters][0][value]' => '10', 91 | 'searchCriteria[filter_groups][1][filters][1][field]' => 'another_attribute', 92 | 'searchCriteria[filter_groups][1][filters][1][condition_type]' => 'lteq', 93 | 'searchCriteria[filter_groups][1][filters][1][value]' => '100', 94 | 'searchCriteria[filter_groups][1][filters][2][field]' => 'test_attribute', 95 | 'searchCriteria[filter_groups][1][filters][2][condition_type]' => 'neq', 96 | 'searchCriteria[filter_groups][1][filters][2][value]' => '::some_value::', 97 | ], $searchCriteria); 98 | } 99 | 100 | public function test_it_can_add_where_in(): void 101 | { 102 | $searchCriteria = SearchCriteria::make() 103 | ->whereIn('sku', ['::sku_1::', '::sku_2::', '::sku_3::']) 104 | ->get(); 105 | 106 | $this->assertEquals([ 107 | 'searchCriteria[filter_groups][0][filters][0][field]' => 'sku', 108 | 'searchCriteria[filter_groups][0][filters][0][condition_type]' => 'in', 109 | 'searchCriteria[filter_groups][0][filters][0][value]' => '::sku_1::,::sku_2::,::sku_3::', 110 | ], $searchCriteria); 111 | } 112 | 113 | public function test_it_can_add_or_where_in(): void 114 | { 115 | $searchCriteria = SearchCriteria::make() 116 | ->whereIn('sku', ['::sku_1::', '::sku_2::', '::sku_3::']) 117 | ->orWhereIn('ean', ['::sku_1::', '::sku_2::', '::sku_3::']) 118 | ->get(); 119 | 120 | $this->assertEquals([ 121 | 'searchCriteria[filter_groups][0][filters][0][field]' => 'sku', 122 | 'searchCriteria[filter_groups][0][filters][0][condition_type]' => 'in', 123 | 'searchCriteria[filter_groups][0][filters][0][value]' => '::sku_1::,::sku_2::,::sku_3::', 124 | 125 | 'searchCriteria[filter_groups][0][filters][1][field]' => 'ean', 126 | 'searchCriteria[filter_groups][0][filters][1][condition_type]' => 'in', 127 | 'searchCriteria[filter_groups][0][filters][1][value]' => '::sku_1::,::sku_2::,::sku_3::', 128 | ], $searchCriteria); 129 | } 130 | 131 | public function test_it_can_add_where_not_in(): void 132 | { 133 | $searchCriteria = SearchCriteria::make() 134 | ->whereNotIn('sku', ['::sku_1::', '::sku_2::', '::sku_3::']) 135 | ->get(); 136 | 137 | $this->assertEquals([ 138 | 'searchCriteria[filter_groups][0][filters][0][field]' => 'sku', 139 | 'searchCriteria[filter_groups][0][filters][0][condition_type]' => 'nin', 140 | 'searchCriteria[filter_groups][0][filters][0][value]' => '::sku_1::,::sku_2::,::sku_3::', 141 | ], $searchCriteria); 142 | } 143 | 144 | public function test_it_can_add_or_where_not_in(): void 145 | { 146 | $searchCriteria = SearchCriteria::make() 147 | ->whereNotIn('sku', ['::sku_1::', '::sku_2::', '::sku_3::']) 148 | ->orWhereNotIn('ean', ['::sku_1::', '::sku_2::', '::sku_3::']) 149 | ->get(); 150 | 151 | $this->assertEquals([ 152 | 'searchCriteria[filter_groups][0][filters][0][field]' => 'sku', 153 | 'searchCriteria[filter_groups][0][filters][0][condition_type]' => 'nin', 154 | 'searchCriteria[filter_groups][0][filters][0][value]' => '::sku_1::,::sku_2::,::sku_3::', 155 | 156 | 'searchCriteria[filter_groups][0][filters][1][field]' => 'ean', 157 | 'searchCriteria[filter_groups][0][filters][1][condition_type]' => 'nin', 158 | 'searchCriteria[filter_groups][0][filters][1][value]' => '::sku_1::,::sku_2::,::sku_3::', 159 | ], $searchCriteria); 160 | } 161 | 162 | public function test_it_can_add_a_field_selection_as_string(): void 163 | { 164 | $searchCriteria = SearchCriteria::make() 165 | ->select('items[sku,price]') 166 | ->get(); 167 | 168 | $this->assertEquals([ 169 | 'fields' => 'items[sku,price]', 170 | ], $searchCriteria); 171 | } 172 | 173 | public function test_it_can_add_a_field_selection_as_array(): void 174 | { 175 | $searchCriteria = SearchCriteria::make() 176 | ->select(['items[sku,price]', 'total_count']) 177 | ->get(); 178 | 179 | $this->assertEquals([ 180 | 'fields' => 'items[sku,price],total_count', 181 | ], $searchCriteria); 182 | } 183 | 184 | public function test_it_can_dd(): void 185 | { 186 | $searchCriteria = SearchCriteria::make() 187 | ->select(['sku', 'price']); 188 | 189 | $this->expectException(Exception::class); 190 | 191 | VarDumper::setHandler(function (array $data) { 192 | $this->assertEquals([ 193 | 'fields' => 'sku,price', 194 | ], $data); 195 | 196 | throw new Exception; 197 | }); 198 | 199 | $searchCriteria->dd(); 200 | } 201 | 202 | public function test_it_can_add_where_null(): void 203 | { 204 | $searchCriteria = SearchCriteria::make() 205 | ->where('sku', '=', '::sku::') 206 | ->whereNull('::some_field::') 207 | ->get(); 208 | 209 | $this->assertEquals([ 210 | 'searchCriteria[filter_groups][0][filters][0][field]' => 'sku', 211 | 'searchCriteria[filter_groups][0][filters][0][condition_type]' => 'eq', 212 | 'searchCriteria[filter_groups][0][filters][0][value]' => '::sku::', 213 | 'searchCriteria[filter_groups][1][filters][0][field]' => '::some_field::', 214 | 'searchCriteria[filter_groups][1][filters][0][condition_type]' => 'null', 215 | ], $searchCriteria); 216 | } 217 | 218 | public function test_it_can_add_or_where_null(): void 219 | { 220 | $searchCriteria = SearchCriteria::make() 221 | ->whereNull('::some_field::') 222 | ->orWhereNull('::some_other_field::') 223 | ->get(); 224 | 225 | $this->assertEquals([ 226 | 'searchCriteria[filter_groups][0][filters][0][field]' => '::some_field::', 227 | 'searchCriteria[filter_groups][0][filters][0][condition_type]' => 'null', 228 | 'searchCriteria[filter_groups][0][filters][1][field]' => '::some_other_field::', 229 | 'searchCriteria[filter_groups][0][filters][1][condition_type]' => 'null', 230 | ], $searchCriteria); 231 | } 232 | 233 | public function test_it_can_add_where_not_null(): void 234 | { 235 | $searchCriteria = SearchCriteria::make() 236 | ->where('sku', '=', '::sku::') 237 | ->whereNotNull('::some_field::') 238 | ->get(); 239 | 240 | $this->assertEquals([ 241 | 'searchCriteria[filter_groups][0][filters][0][field]' => 'sku', 242 | 'searchCriteria[filter_groups][0][filters][0][condition_type]' => 'eq', 243 | 'searchCriteria[filter_groups][0][filters][0][value]' => '::sku::', 244 | 'searchCriteria[filter_groups][1][filters][0][field]' => '::some_field::', 245 | 'searchCriteria[filter_groups][1][filters][0][condition_type]' => 'notnull', 246 | ], $searchCriteria); 247 | } 248 | 249 | public function test_it_can_add_or_where_not_null(): void 250 | { 251 | $searchCriteria = SearchCriteria::make() 252 | ->whereNotNull('::some_field::') 253 | ->orWhereNotNull('::some_other_field::') 254 | ->get(); 255 | 256 | $this->assertEquals([ 257 | 'searchCriteria[filter_groups][0][filters][0][field]' => '::some_field::', 258 | 'searchCriteria[filter_groups][0][filters][0][condition_type]' => 'notnull', 259 | 'searchCriteria[filter_groups][0][filters][1][field]' => '::some_other_field::', 260 | 'searchCriteria[filter_groups][0][filters][1][condition_type]' => 'notnull', 261 | ], $searchCriteria); 262 | } 263 | 264 | public function test_it_can_order_by(): void 265 | { 266 | $searchCriteria = SearchCriteria::make() 267 | ->orderBy('sku') 268 | ->get(); 269 | 270 | $this->assertEquals([ 271 | 'searchCriteria[sortOrders][0][field]' => 'sku', 272 | 'searchCriteria[sortOrders][0][direction]' => 'ASC', 273 | ], $searchCriteria); 274 | } 275 | 276 | public function test_it_can_order_by_desc(): void 277 | { 278 | $searchCriteria = SearchCriteria::make() 279 | ->orderByDesc('sku') 280 | ->get(); 281 | 282 | $this->assertEquals([ 283 | 'searchCriteria[sortOrders][0][field]' => 'sku', 284 | 'searchCriteria[sortOrders][0][direction]' => 'DESC', 285 | ], $searchCriteria); 286 | } 287 | 288 | public function test_it_can_add_multiple_orders(): void 289 | { 290 | $searchCriteria = SearchCriteria::make() 291 | ->orderBy('sku') 292 | ->orderByDesc('entity_id') 293 | ->get(); 294 | 295 | $this->assertEquals([ 296 | 'searchCriteria[sortOrders][0][field]' => 'sku', 297 | 'searchCriteria[sortOrders][0][direction]' => 'ASC', 298 | 'searchCriteria[sortOrders][1][field]' => 'entity_id', 299 | 'searchCriteria[sortOrders][1][direction]' => 'DESC', 300 | ], $searchCriteria); 301 | } 302 | } 303 | -------------------------------------------------------------------------------- /tests/TestCase.php: -------------------------------------------------------------------------------- 1 | set('database.default', 'testbench'); 26 | $app['config']->set('database.connections.testbench', [ 27 | 'driver' => 'sqlite', 28 | 'database' => ':memory:', 29 | 'prefix' => '', 30 | ]); 31 | } 32 | } 33 | --------------------------------------------------------------------------------