├── .editorconfig ├── .github ├── CONTRIBUTING.md ├── SECURITY.md └── workflows │ ├── analyse.yml │ ├── changelog.yml │ ├── coverage.yml │ ├── style.yml │ └── tests.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── composer.json ├── config └── magento-products.php ├── database └── migrations │ ├── 2022_06_27_161003_create_magento_products_table.php │ ├── 2023_03_07_125103_magento_products_store.php │ ├── 2023_11_21_102103_magento_products_retrieved_field.php │ └── 2025_02_21_090000_magento_products_checksum_field.php ├── phpstan.neon ├── phpunit.xml ├── src ├── Actions │ ├── CheckAllKnownProducts.php │ ├── CheckKnownProducts.php │ ├── CheckMagentoEnabled.php │ ├── CheckMagentoExistence.php │ ├── CheckRemovedProducts.php │ ├── DiscoverMagentoProducts.php │ ├── ProcessMagentoSkus.php │ ├── RetrieveMagentoSkus.php │ └── RetrieveProductData.php ├── Commands │ ├── CheckKnownProductsExistenceCommand.php │ ├── DiscoverMagentoProductsCommand.php │ └── RetrieveProductDataCommand.php ├── Contracts │ ├── ChecksAllKnownProducts.php │ ├── ChecksKnownProducts.php │ ├── ChecksMagentoEnabled.php │ ├── ChecksMagentoExistence.php │ ├── ChecksRemovedProducts.php │ ├── DiscoversMagentoProducts.php │ ├── ProcessesMagentoSkus.php │ ├── RetrievesMagentoSkus.php │ └── RetrievesProductData.php ├── Events │ ├── ProductCreatedInMagentoEvent.php │ ├── ProductDataModifiedEvent.php │ ├── ProductDeletedInMagentoEvent.php │ └── ProductDiscoveredEvent.php ├── Jobs │ ├── CheckAllKnownProductsExistenceJob.php │ ├── CheckKnownProductsExistenceJob.php │ ├── CheckRemovedProductsJob.php │ └── DiscoverMagentoProductsJob.php ├── Listeners │ └── RegisterProduct.php ├── Models │ └── MagentoProduct.php └── ServiceProvider.php └── tests ├── Actions ├── CheckAllKnownProductsTest.php ├── CheckKnownProductsTest.php ├── CheckMagentoEnabledTest.php ├── CheckMagentoExistenceTest.php ├── CheckRemovedProductsTest.php ├── DiscoverMagentoProductsTest.php ├── ProcessMagentoSkusTest.php ├── RetrieveMagentoSkusTest.php └── RetrieveProductDataTest.php ├── Commands ├── CheckKnownProductExistenceCommandTest.php ├── DiscoverMagentoProductsCommandTest.php └── RetrieveProductDataCommandTest.php ├── Jobs ├── CheckAllKnownProductsExistenceJobTest.php ├── CheckKnownProductsExistenceJobTest.php ├── CheckRemovedProductsJobTest.php └── DiscoverMagentoProductsJobTest.php ├── Listeners └── RegisterProductListenerTest.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-products/compare/1.7.0...main) 4 | ## [1.7.0](https://github.com/justbetter/laravel-magento-products/releases/tag/1.7.0) - 2025-02-24 5 | 6 | ### What's Changed 7 | * Magento product data modified event by @VincentBean in https://github.com/justbetter/laravel-magento-products/pull/15 8 | 9 | 10 | **Full Changelog**: https://github.com/justbetter/laravel-magento-products/compare/1.6.0...1.7.0 11 | 12 | ## [1.6.0](https://github.com/justbetter/laravel-magento-products/releases/tag/1.6.0) - 2025-02-13 13 | 14 | ### What's Changed 15 | * Laravel 12 support by @VincentBean in https://github.com/justbetter/laravel-magento-products/pull/14 16 | 17 | 18 | **Full Changelog**: https://github.com/justbetter/laravel-magento-products/compare/1.5.0...1.6.0 19 | 20 | ## [1.5.0](https://github.com/justbetter/laravel-magento-products/releases/tag/1.5.0) - 2024-09-10 21 | 22 | ### What's Changed 23 | * Add Magento availability check by @VincentBean in https://github.com/justbetter/laravel-magento-products/pull/13 24 | 25 | 26 | **Full Changelog**: https://github.com/justbetter/laravel-magento-products/compare/1.4.1...1.5.0 27 | 28 | ## [1.4.1](https://github.com/justbetter/laravel-magento-products/releases/tag/1.4.1) - 2024-07-24 29 | 30 | ### What's Changed 31 | * Adjust product creation by @VincentBean in https://github.com/justbetter/laravel-magento-products/pull/12 32 | 33 | 34 | **Full Changelog**: https://github.com/justbetter/laravel-magento-products/compare/1.4.0...1.4.1 35 | 36 | ## [1.4.0](https://github.com/justbetter/laravel-magento-products/releases/tag/1.4.0) - 2024-03-29 37 | 38 | ### What's Changed 39 | * Support Laravel 11 by @VincentBean in https://github.com/justbetter/laravel-magento-products/pull/11 40 | 41 | 42 | **Full Changelog**: https://github.com/justbetter/laravel-magento-products/compare/1.3.1...1.4.0 43 | 44 | ## [1.3.1](https://github.com/justbetter/laravel-magento-products/releases/tag/1.3.1) - 2023-12-08 45 | 46 | ### What's Changed 47 | * Add URL encodes to Magento calls by @VincentBean in https://github.com/justbetter/laravel-magento-products/pull/10 48 | 49 | 50 | **Full Changelog**: https://github.com/justbetter/laravel-magento-products/compare/1.3.0...1.3.1 51 | 52 | ## [1.3.0](https://github.com/justbetter/laravel-magento-products/releases/tag/1.3.0) - 2023-11-22 53 | 54 | ### What's Changed 55 | * Check for deleted products in Magento by @VincentBean in https://github.com/justbetter/laravel-magento-products/pull/9 56 | 57 | 58 | **Full Changelog**: https://github.com/justbetter/laravel-magento-products/compare/1.2.1...1.3.0 59 | 60 | ## [1.2.1](https://github.com/justbetter/laravel-magento-products/releases/tag/1.2.1) - 2023-05-25 61 | 62 | ### What's Changed 63 | * Split known products checks into multiple jobs by @VincentBean in https://github.com/justbetter/laravel-magento-products/pull/8 64 | 65 | 66 | **Full Changelog**: https://github.com/justbetter/laravel-magento-products/compare/1.2.0...1.2.1 67 | 68 | ## [1.2.0](https://github.com/justbetter/laravel-magento-products/releases/tag/1.2.0) - 2023-03-10 69 | 70 | ### What's Changed 71 | * Support Laravel 10 by @VincentBean in https://github.com/justbetter/laravel-magento-products/pull/7 72 | 73 | 74 | **Full Changelog**: https://github.com/justbetter/laravel-magento-products/compare/1.1.1...1.2.0 75 | 76 | ## [1.1.1](https://github.com/justbetter/laravel-magento-products/releases/tag/1.1.1) - 2023-03-07 77 | 78 | ### What's Changed 79 | * Retrieve data for specific store by @VincentBean in https://github.com/justbetter/laravel-magento-products/pull/6 80 | 81 | 82 | **Full Changelog**: https://github.com/justbetter/laravel-magento-products/compare/1.1.0...1.1.1 83 | 84 | ## [1.1.0](https://github.com/justbetter/laravel-magento-products/releases/tag/1.1.0) - 2023-01-02 85 | 86 | ### What's Changed 87 | * Badges by @VincentBean in https://github.com/justbetter/laravel-magento-products/pull/3 88 | * Add action to check if product is enabled by @VincentBean in https://github.com/justbetter/laravel-magento-products/pull/4 89 | 90 | 91 | **Full Changelog**: https://github.com/justbetter/laravel-magento-products/compare/1.0.3...1.1.0 92 | 93 | ## [1.0.3](https://github.com/justbetter/laravel-magento-products/releases/tag/1.0.3) - 2022-10-06 94 | 95 | ### What's Changed 96 | * Add Github Actions for tests, static analysis and style by @VincentBean in https://github.com/justbetter/laravel-magento-products/pull/1 97 | * Fixed migration by @ramonrietdijk in https://github.com/justbetter/laravel-magento-products/pull/2 98 | 99 | ### New Contributors 100 | * @VincentBean made their first contribution in https://github.com/justbetter/laravel-magento-products/pull/1 101 | * @ramonrietdijk made their first contribution in https://github.com/justbetter/laravel-magento-products/pull/2 102 | 103 | **Full Changelog**: https://github.com/justbetter/laravel-magento-products/compare/1.0.2...1.0.3 104 | 105 | ## [1.0.2](https://github.com/justbetter/laravel-magento-products/releases/tag/1.0.2) - 2022-09-20 106 | 107 | **Full Changelog**: https://github.com/justbetter/laravel-magento-products/compare/1.0.1...1.0.2 108 | 109 | ## [1.0.1](https://github.com/justbetter/laravel-magento-products/releases/tag/1.0.1) - 2022-09-20 110 | 111 | **Full Changelog**: https://github.com/justbetter/laravel-magento-products/compare/1.0.0...1.0.1 112 | 113 | ## [1.0.0](https://github.com/justbetter/laravel-magento-products/releases/tag/1.0.0) - 2022-09-20 114 | 115 | Initial release of justbetter/laravel-magento-products 116 | 117 | -------------------------------------------------------------------------------- /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 | # Magento Products 2 | 3 |

4 | Tests 5 | Analysis 6 | Total downloads 7 |

8 | 9 | This package tracks if products exist in Magento by storing the status locally in the DB. 10 | We developed this to prevent multiple calls when multiple packages need to check product existence in Magento. 11 | 12 | 13 | ## Installation 14 | 15 | Require this package: 16 | 17 | ```shell 18 | composer require justbetter/laravel-magento-products 19 | ``` 20 | 21 | Add the following to your schedule to automatically search for products in Magento. 22 | 23 | ```php 24 | $schedule->command(\JustBetter\MagentoProducts\Commands\CheckKnownProductsExistenceCommand::class)->twiceDaily(); 25 | $schedule->command(\JustBetter\MagentoProducts\Commands\DiscoverMagentoProductsCommand::class)->daily(); 26 | ``` 27 | 28 | > [!IMPORTANT] 29 | > This package requires Job Batching 30 | 31 | ## Usage 32 | 33 | ### Checking if a product exists in Magento 34 | 35 | You can use this package to determine if products exist in Magento. 36 | For example: 37 | 38 | 39 | ```php 40 | $exists = app(\JustBetter\MagentoProducts\Contracts\ChecksMagentoExistence::class)->exists('sku') 41 | ``` 42 | 43 | If it does not exist the sku will still be stored in the database. The `\JustBetter\MagentoProducts\Commands\CheckKnownProductsExistenceCommand` command will automatically check these known products for existence. 44 | 45 | ### Retrieving product data 46 | 47 | You can use this package to retrieve product data. This data will be saved in the database and automatically retrieved when it is older than X hours. 48 | You can configure the amount of hours in the config file 49 | For example: 50 | 51 | ```php 52 | $exists = app(\JustBetter\MagentoProducts\Contracts\RetrievesProductData::class)->retrieve('sku') 53 | ``` 54 | 55 | 56 | ## Events 57 | 58 | When your application discovers new products in Magento you should dispatch one of the following events: 59 | 60 | `\JustBetter\MagentoProducts\Events\ProductDiscoveredEvent` containing a single sku. 61 | 62 | When a single product or multiple products appear in Magento, an event is dispatched: 63 | 64 | `\JustBetter\MagentoProducts\Events\ProductCreatedInMagentoEvent` containing a single sku. 65 | 66 | 67 | ## Quality 68 | 69 | To ensure the quality of this package, run the following command: 70 | 71 | ```shell 72 | composer quality 73 | ``` 74 | 75 | This will execute three tasks: 76 | 77 | 1. Makes sure all tests are passed 78 | 2. Checks for any issues using static code analysis 79 | 3. Checks if the code is correctly formatted 80 | 81 | ## Contributing 82 | 83 | Please see [CONTRIBUTING](.github/CONTRIBUTING.md) for details. 84 | 85 | ## Security Vulnerabilities 86 | 87 | Please review [our security policy](../../security/policy) on how to report security vulnerabilities. 88 | 89 | ## Credits 90 | 91 | - [Vincent Boon](https://github.com/VincentBean) 92 | - [All Contributors](../../contributors) 93 | 94 | ## License 95 | 96 | The MIT License (MIT). Please see [License File](LICENSE) for more information. 97 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "justbetter/laravel-magento-products", 3 | "description": "Package to store product data in a local DB", 4 | "type": "package", 5 | "license": "MIT", 6 | "require": { 7 | "php": "^8.3", 8 | "justbetter/laravel-magento-client": "^2.6.1", 9 | "laravel/framework": "^11.0|^12.0" 10 | }, 11 | "require-dev": { 12 | "doctrine/dbal": "^3.7.1", 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 | "autoload": { 28 | "psr-4": { 29 | "JustBetter\\MagentoProducts\\": "src" 30 | } 31 | }, 32 | "autoload-dev": { 33 | "psr-4": { 34 | "JustBetter\\MagentoProducts\\Tests\\": "tests" 35 | } 36 | }, 37 | "scripts": { 38 | "test": "phpunit", 39 | "analyse": "phpstan --memory-limit=256M", 40 | "style": "pint --test", 41 | "quality": [ 42 | "@style", 43 | "@analyse", 44 | "@test", 45 | "@coverage" 46 | ], 47 | "fix-style": "pint", 48 | "coverage": "XDEBUG_MODE=coverage php vendor/bin/pest --coverage --min=100" 49 | }, 50 | "config": { 51 | "sort-packages": true, 52 | "allow-plugins": { 53 | "pestphp/pest-plugin": true 54 | } 55 | }, 56 | "extra": { 57 | "laravel": { 58 | "providers": [ 59 | "JustBetter\\MagentoProducts\\ServiceProvider" 60 | ] 61 | } 62 | }, 63 | "minimum-stability": "stable", 64 | "prefer-stable": true 65 | } 66 | -------------------------------------------------------------------------------- /config/magento-products.php: -------------------------------------------------------------------------------- 1 | 'default', 6 | 7 | /** Interval in hours which the product data should re-downloaded */ 8 | 'check_interval' => 24, 9 | 10 | /** Page size of products to retrieve from Magento */ 11 | 'page_size' => 100, 12 | ]; 13 | -------------------------------------------------------------------------------- /database/migrations/2022_06_27_161003_create_magento_products_table.php: -------------------------------------------------------------------------------- 1 | id(); 13 | 14 | $table->string('sku')->unique(); 15 | $table->boolean('exists_in_magento')->default(false); 16 | $table->dateTime('last_checked')->nullable(); 17 | $table->json('data')->nullable(); 18 | 19 | $table->timestamps(); 20 | }); 21 | } 22 | 23 | public function down(): void 24 | { 25 | Schema::dropIfExists('magento_products'); 26 | } 27 | }; 28 | -------------------------------------------------------------------------------- /database/migrations/2023_03_07_125103_magento_products_store.php: -------------------------------------------------------------------------------- 1 | dropUnique('magento_products_sku_unique'); 13 | $table->string('store')->nullable()->after('sku'); 14 | 15 | $table->unique(['sku', 'store']); 16 | }); 17 | } 18 | 19 | public function down(): void 20 | { 21 | Schema::dropColumns('magento_products', ['store']); 22 | } 23 | }; 24 | -------------------------------------------------------------------------------- /database/migrations/2023_11_21_102103_magento_products_retrieved_field.php: -------------------------------------------------------------------------------- 1 | boolean('retrieved')->default(false)->after('last_checked'); 13 | }); 14 | } 15 | 16 | public function down(): void 17 | { 18 | Schema::dropColumns('magento_products', ['retrieved']); 19 | } 20 | }; 21 | -------------------------------------------------------------------------------- /database/migrations/2025_02_21_090000_magento_products_checksum_field.php: -------------------------------------------------------------------------------- 1 | string('checksum')->nullable()->after('data'); 13 | }); 14 | } 15 | 16 | public function down(): void 17 | { 18 | Schema::dropColumns('magento_products', ['checksum']); 19 | } 20 | }; 21 | -------------------------------------------------------------------------------- /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 | - identifier: missingType.generics 13 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | ./tests/* 6 | 7 | 8 | 9 | 10 | 11 | ./src 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/Actions/CheckAllKnownProducts.php: -------------------------------------------------------------------------------- 1 | where('exists_in_magento', '=', false) 16 | ->select(['sku', 'exists_in_magento']) 17 | ->get() 18 | ->chunk(CheckKnownProducts::CHUNK_SIZE) 19 | ->each(fn (Collection $skus) => CheckKnownProductsExistenceJob::dispatch($skus->pluck('sku')->toArray())); 20 | } 21 | 22 | public static function bind(): void 23 | { 24 | app()->singleton(ChecksAllKnownProducts::class, static::class); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Actions/CheckKnownProducts.php: -------------------------------------------------------------------------------- 1 | where('exists_in_magento', false) 22 | ->when(count($skus), fn (Builder $query): Builder => $query->whereIn('sku', $skus)) 23 | ->get() 24 | ->chunk(static::CHUNK_SIZE); 25 | 26 | foreach ($productChunks as $chunk) { 27 | $search = SearchCriteria::make() 28 | ->select(['items[sku]']) 29 | ->whereIn('sku', $chunk->pluck('sku')->toArray()) 30 | ->paginate(1, static::CHUNK_SIZE) 31 | ->get(); 32 | 33 | $response = $this->magento->get('products', $search); 34 | 35 | $skusThatExist = $response->json('items.*.sku', []); 36 | 37 | MagentoProduct::query() 38 | ->whereIn('sku', $skusThatExist) 39 | ->update(['exists_in_magento' => true]); 40 | 41 | foreach ($skusThatExist as $sku) { 42 | event(new ProductCreatedInMagentoEvent($sku)); 43 | } 44 | } 45 | } 46 | 47 | public static function bind(): void 48 | { 49 | app()->singleton(ChecksKnownProducts::class, static::class); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Actions/CheckMagentoEnabled.php: -------------------------------------------------------------------------------- 1 | productData->retrieve($sku, $force, $store); 17 | 18 | if ($data === null) { 19 | return false; 20 | } 21 | 22 | return $data['status'] === 1; 23 | } 24 | 25 | public static function bind(): void 26 | { 27 | app()->singleton(ChecksMagentoEnabled::class, static::class); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Actions/CheckMagentoExistence.php: -------------------------------------------------------------------------------- 1 | magento->available(), 'Magento unavailable'); 23 | $response = $this->getMagentoProduct($sku); 24 | 25 | $response->throwIf(! in_array($response->status(), [200, 404])); 26 | 27 | $magentoProduct = MagentoProduct::query()->create([ 28 | 'sku' => $sku, 29 | 'exists_in_magento' => $response->ok(), 30 | 'last_checked' => now(), 31 | ]); 32 | } 33 | 34 | return $magentoProduct->exists_in_magento; 35 | } 36 | 37 | protected function getMagentoProduct(string $sku): Response 38 | { 39 | return $this->magento->get('products/'.urlencode($sku), ['fields' => 'sku']); 40 | } 41 | 42 | public static function bind(): void 43 | { 44 | app()->singleton(ChecksMagentoExistence::class, static::class); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/Actions/CheckRemovedProducts.php: -------------------------------------------------------------------------------- 1 | where('exists_in_magento', '=', true) 15 | ->where('retrieved', '=', false); 16 | 17 | $skus = $query->select(['sku'])->get(); 18 | 19 | $query->update([ 20 | 'exists_in_magento' => false, 21 | ]); 22 | 23 | $skus->each(fn (MagentoProduct $product) => ProductDeletedInMagentoEvent::dispatch($product->sku)); 24 | } 25 | 26 | public static function bind(): void 27 | { 28 | app()->singleton(ChecksRemovedProducts::class, static::class); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Actions/DiscoverMagentoProducts.php: -------------------------------------------------------------------------------- 1 | update(['retrieved' => false]); 26 | } 27 | 28 | $search = SearchCriteria::make() 29 | ->paginate($page, config('magento-products.page_size', 50)) 30 | ->get(); 31 | 32 | $products = $this->magento->get('products', $search)->throw()->collect('items'); 33 | 34 | $hasNextPage = $products->count() == config('magento-products.page_size'); 35 | 36 | if ($hasNextPage) { 37 | $batch->add(new DiscoverMagentoProductsJob($page + 1)); 38 | } 39 | 40 | $skus = $products->pluck('sku'); 41 | $this->skuProcessor->process($skus); 42 | 43 | foreach ($products as $productData) { 44 | $product = MagentoProduct::query()->firstOrNew(['sku' => $productData['sku']]); 45 | 46 | /** @var non-empty-string $encoded */ 47 | $encoded = json_encode($productData); 48 | 49 | $checksum = md5($encoded); 50 | 51 | if ($product->checksum !== $checksum) { 52 | event(new ProductDataModifiedEvent($product->sku, $product->data, $productData)); 53 | } 54 | 55 | $product->retrieved = true; 56 | $product->data = $productData; 57 | $product->checksum = $checksum; 58 | 59 | $product->save(); 60 | } 61 | } 62 | 63 | public static function bind(): void 64 | { 65 | app()->singleton(DiscoversMagentoProducts::class, static::class); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/Actions/ProcessMagentoSkus.php: -------------------------------------------------------------------------------- 1 | whereIn('sku', $skus) 16 | ->where('exists_in_magento', true) 17 | ->select(['sku']) 18 | ->distinct() 19 | ->get() 20 | ->pluck('sku'); 21 | 22 | // Check if all retrieved skus also exist in Magento 23 | if ($knownProductsThatExist->count() == config('magento-products.page_size', 0)) { 24 | return; 25 | } 26 | 27 | // Get the skus that do not exist in Magento 28 | $skus = $skus->diff($knownProductsThatExist); 29 | 30 | // Get the products that are discovered but have the exists_in_magento set to false 31 | $knownProductsThatDontExistQuery = MagentoProduct::query() 32 | ->whereIn('sku', $skus) 33 | ->where('exists_in_magento', false); 34 | 35 | $knownProductsThatDontExist = $knownProductsThatDontExistQuery->get(); 36 | 37 | $knownProductsThatDontExistQuery->update(['exists_in_magento' => true, 'updated_at' => now()]); 38 | 39 | $missingProducts = $skus->diff($knownProductsThatDontExist->pluck('sku')); 40 | 41 | $missingProducts 42 | ->each(fn (string $sku) => MagentoProduct::query()->updateOrCreate( 43 | ['sku' => $sku], 44 | ['exists_in_magento' => true] 45 | )); 46 | 47 | $knownProductsThatDontExist 48 | ->pluck('sku') 49 | ->merge($missingProducts) 50 | ->each(fn (string $sku) => event(new ProductCreatedInMagentoEvent($sku))); 51 | } 52 | 53 | public static function bind(): void 54 | { 55 | app()->singleton(ProcessesMagentoSkus::class, static::class); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/Actions/RetrieveMagentoSkus.php: -------------------------------------------------------------------------------- 1 | select(['items[sku]']) 23 | ->paginate($page, config('magento-products.page_size', 50)) 24 | ->get(); 25 | 26 | return $this->magento->get('products', $search)->throw()->collect('items')->pluck('sku'); 27 | } 28 | 29 | public static function bind(): void 30 | { 31 | app()->singleton(RetrievesMagentoSkus::class, static::class); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Actions/RetrieveProductData.php: -------------------------------------------------------------------------------- 1 | magento->available(), 'Magento unavailable'); 20 | 21 | $magentoProductResponse = $this->getMagentoProduct($sku, $store); 22 | 23 | $magentoProductResponse->throwIf($magentoProductResponse->serverError()); 24 | 25 | $product = MagentoProduct::query() 26 | ->create([ 27 | 'sku' => $sku, 28 | 'last_checked' => now(), 29 | 'exists_in_magento' => $magentoProductResponse->successful(), 30 | 'data' => $magentoProductResponse->successful() ? $magentoProductResponse->json() : null, 31 | 'store' => $store, 32 | ]); 33 | } 34 | 35 | $lastChecked = $product->last_checked; 36 | 37 | if ($force || 38 | $product->data === null || 39 | $lastChecked === null || 40 | $lastChecked->diffInHours(now()) > config('magento-products.check_interval', 24) 41 | ) { 42 | $response = $this->getMagentoProduct($sku, $store); 43 | 44 | if (! $response->successful()) { 45 | return null; 46 | } 47 | 48 | $product->data = $response->json(); 49 | $product->last_checked = now(); 50 | $product->exists_in_magento = true; 51 | $product->save(); 52 | } 53 | 54 | return $product->data; 55 | } 56 | 57 | protected function getMagentoProduct(string $sku, ?string $store = null): Response 58 | { 59 | return $this->magento 60 | ->store($store) 61 | ->get('products/'.urlencode($sku)); 62 | } 63 | 64 | public static function bind(): void 65 | { 66 | app()->singleton(RetrievesProductData::class, static::class); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/Commands/CheckKnownProductsExistenceCommand.php: -------------------------------------------------------------------------------- 1 | info('Dispatching...'); 17 | 18 | CheckAllKnownProductsExistenceJob::dispatch(); 19 | 20 | $this->info('Done!'); 21 | 22 | return static::SUCCESS; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Commands/DiscoverMagentoProductsCommand.php: -------------------------------------------------------------------------------- 1 | name('Discover Magento Products') 20 | ->then(fn () => CheckRemovedProductsJob::dispatch()) 21 | ->onQueue(config('magento-products.queue')) 22 | ->dispatch(); 23 | 24 | return static::SUCCESS; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Commands/RetrieveProductDataCommand.php: -------------------------------------------------------------------------------- 1 | argument('sku'); 18 | 19 | $data = $retrievesProductData->retrieve($sku); 20 | $this->info(json_encode($data)); /** @phpstan-ignore-line */ 21 | 22 | return static::SUCCESS; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Contracts/ChecksAllKnownProducts.php: -------------------------------------------------------------------------------- 1 | onQueue(config('magento-products.queue')); 23 | } 24 | 25 | public function handle(ChecksAllKnownProducts $products): void 26 | { 27 | $products->check(); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Jobs/CheckKnownProductsExistenceJob.php: -------------------------------------------------------------------------------- 1 | onQueue(config('magento-products.queue')); 25 | } 26 | 27 | public function handle(ChecksKnownProducts $checksKnownProducts): void 28 | { 29 | $checksKnownProducts->handle($this->skus); 30 | } 31 | 32 | public function middleware(): array 33 | { 34 | return [ 35 | new AvailableMiddleware, 36 | ]; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Jobs/CheckRemovedProductsJob.php: -------------------------------------------------------------------------------- 1 | onQueue(config('magento-products.queue')); 25 | } 26 | 27 | public function handle(ChecksRemovedProducts $contract): void 28 | { 29 | $contract->check(); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Jobs/DiscoverMagentoProductsJob.php: -------------------------------------------------------------------------------- 1 | onQueue(config('magento-products.queue')); 32 | } 33 | 34 | public function handle(DiscoversMagentoProducts $contract): void 35 | { 36 | if ($this->batch() === null || $this->batch()->cancelled()) { 37 | return; 38 | } 39 | 40 | $contract->discover($this->page, $this->batch()); 41 | } 42 | 43 | public function tags(): array 44 | { 45 | return [ 46 | $this->page, 47 | ]; 48 | } 49 | 50 | public function uniqueId(): int 51 | { 52 | return $this->page; 53 | } 54 | 55 | public function middleware(): array 56 | { 57 | return [ 58 | new AvailableMiddleware, 59 | ]; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/Listeners/RegisterProduct.php: -------------------------------------------------------------------------------- 1 | updateOrCreate([ 14 | 'sku' => $event->sku, 15 | 'exists_in_magento' => $event->exists, 16 | ]); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Models/MagentoProduct.php: -------------------------------------------------------------------------------- 1 | 'array', 28 | 'exists_in_magento' => 'boolean', 29 | 'last_checked' => 'datetime', 30 | 'retrieved' => 'boolean', 31 | ]; 32 | 33 | public static function findBySku(string $sku, ?string $store = null): ?static 34 | { 35 | /** @var ?static $item */ 36 | $item = static::query() 37 | ->where('sku', '=', $sku) 38 | ->when($store !== null, fn (Builder $builder) => $builder->where('store', '=', $store)) 39 | ->first(); 40 | 41 | return $item; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/ServiceProvider.php: -------------------------------------------------------------------------------- 1 | mergeConfigFrom(__DIR__.'/../config/magento-products.php', 'magento-products'); 27 | 28 | CheckKnownProducts::bind(); 29 | CheckAllKnownProducts::bind(); 30 | CheckMagentoExistence::bind(); 31 | CheckMagentoEnabled::bind(); 32 | ProcessMagentoSkus::bind(); 33 | RetrieveMagentoSkus::bind(); 34 | RetrieveProductData::bind(); 35 | DiscoverMagentoProducts::bind(); 36 | CheckRemovedProducts::bind(); 37 | } 38 | 39 | public function boot(): void 40 | { 41 | $this 42 | ->bootConfig() 43 | ->bootMigrations() 44 | ->bootCommands() 45 | ->bootEvents(); 46 | } 47 | 48 | protected function bootConfig(): static 49 | { 50 | if ($this->app->runningInConsole()) { 51 | $this->publishes([ 52 | __DIR__.'/../config/magento-products.php' => config_path('magento-products.php'), 53 | ], 'config'); 54 | } 55 | 56 | return $this; 57 | } 58 | 59 | protected function bootCommands(): static 60 | { 61 | if ($this->app->runningInConsole()) { 62 | $this->commands([ 63 | CheckKnownProductsExistenceCommand::class, 64 | DiscoverMagentoProductsCommand::class, 65 | RetrieveProductDataCommand::class, 66 | ]); 67 | } 68 | 69 | return $this; 70 | } 71 | 72 | protected function bootMigrations(): static 73 | { 74 | $this->loadMigrationsFrom(__DIR__.'/../database/migrations'); 75 | 76 | return $this; 77 | } 78 | 79 | protected function bootEvents(): static 80 | { 81 | Event::listen(ProductDiscoveredEvent::class, RegisterProduct::class); 82 | 83 | return $this; 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /tests/Actions/CheckAllKnownProductsTest.php: -------------------------------------------------------------------------------- 1 | create(['sku' => $number]); 22 | } 23 | 24 | MagentoProduct::query()->create(['sku' => '::sku::', 'exists_in_magento' => true]); 25 | 26 | /** @var CheckAllKnownProducts $action */ 27 | $action = app(CheckAllKnownProducts::class); 28 | 29 | $action->check(); 30 | 31 | Bus::assertDispatchedTimes(CheckKnownProductsExistenceJob::class, 2); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /tests/Actions/CheckKnownProductsTest.php: -------------------------------------------------------------------------------- 1 | Http::response([ 23 | 'items' => [ 24 | [ 25 | 'sku' => '123', 26 | ], 27 | ], 28 | ]), 29 | ]); 30 | 31 | MagentoProduct::query()->create(['sku' => '123']); 32 | MagentoProduct::query()->create(['sku' => '456']); 33 | MagentoProduct::query()->create(['sku' => '789', 'exists_in_magento' => true]); 34 | } 35 | 36 | #[Test] 37 | public function it_sets_in_magento(): void 38 | { 39 | /** @var CheckKnownProducts $action */ 40 | $action = app(CheckKnownProducts::class); 41 | 42 | $action->handle(); 43 | 44 | $this->assertTrue(MagentoProduct::query()->where('sku', '123')->first()->exists_in_magento); /** @phpstan-ignore-line */ 45 | $this->assertFalse(MagentoProduct::query()->where('sku', '456')->first()->exists_in_magento); /** @phpstan-ignore-line */ 46 | Http::assertSent(function (Request $request) { 47 | $expectedSearchCriteria = [ 48 | 'fields' => 'items[sku]', 49 | 'searchCriteria[filter_groups][0][filters][0][field]' => 'sku', 50 | 'searchCriteria[filter_groups][0][filters][0][condition_type]' => 'in', 51 | 'searchCriteria[filter_groups][0][filters][0][value]' => '123,456', 52 | 'searchCriteria[pageSize]' => 100, 53 | 'searchCriteria[currentPage]' => 1, 54 | ]; 55 | 56 | return $expectedSearchCriteria == $request->data(); 57 | }); 58 | } 59 | 60 | #[Test] 61 | public function it_checks_limited_skus(): void 62 | { 63 | /** @var CheckKnownProducts $action */ 64 | $action = app(CheckKnownProducts::class); 65 | 66 | $action->handle(['123']); 67 | 68 | Http::assertSent(function (Request $request) { 69 | $expectedSearchCriteria = [ 70 | 'fields' => 'items[sku]', 71 | 'searchCriteria[filter_groups][0][filters][0][field]' => 'sku', 72 | 'searchCriteria[filter_groups][0][filters][0][condition_type]' => 'in', 73 | 'searchCriteria[filter_groups][0][filters][0][value]' => '123', 74 | 'searchCriteria[pageSize]' => 100, 75 | 'searchCriteria[currentPage]' => 1, 76 | ]; 77 | 78 | return $expectedSearchCriteria == $request->data(); 79 | }); 80 | } 81 | 82 | #[Test] 83 | public function it_dispatches_events(): void 84 | { 85 | /** @var CheckKnownProducts $action */ 86 | $action = app(CheckKnownProducts::class); 87 | 88 | $action->handle(); 89 | 90 | Event::assertDispatched(ProductCreatedInMagentoEvent::class, function (ProductCreatedInMagentoEvent $event) { 91 | return $event->sku === '123'; 92 | }); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /tests/Actions/CheckMagentoEnabledTest.php: -------------------------------------------------------------------------------- 1 | action = app(CheckMagentoEnabled::class); 19 | } 20 | 21 | #[Test] 22 | public function it_checks_unknown_products(): void 23 | { 24 | Http::fake([ 25 | '*/products/1' => Http::response([ 26 | 'status' => 1, 27 | ]), 28 | '*/products/2' => Http::response([ 29 | 'status' => 0, 30 | ]), 31 | ]); 32 | 33 | $this->assertTrue($this->action->enabled('1')); 34 | $this->assertFalse($this->action->enabled('2')); 35 | } 36 | 37 | #[Test] 38 | public function it_checks_missing_product(): void 39 | { 40 | Http::fake([ 41 | '*/products/3' => Http::response([], 404), 42 | ]); 43 | 44 | $this->assertFalse($this->action->enabled('3')); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /tests/Actions/CheckMagentoExistenceTest.php: -------------------------------------------------------------------------------- 1 | action = app(CheckMagentoExistence::class); 24 | 25 | config()->set('magento-products.check_interval', 2); 26 | 27 | Http::fake([ 28 | '*products/123?fields=sku' => Http::response(['data']), 29 | '*products/456?fields=sku' => Http::response([], 404), 30 | '*products/123%2B456?fields=sku' => Http::response(['data']), 31 | ])->preventStrayRequests(); 32 | } 33 | 34 | #[Test] 35 | public function existing_product(): void 36 | { 37 | MagentoProduct::query()->create([ 38 | 'sku' => '123', 'exists_in_magento' => true, 'last_checked' => now()->subHour(), 39 | ]); 40 | 41 | $this->assertTrue($this->action->exists('123')); 42 | } 43 | 44 | #[Test] 45 | public function urlencode(): void 46 | { 47 | $this->assertTrue($this->action->exists('123+456')); 48 | } 49 | 50 | #[Test] 51 | public function new_existing_product(): void 52 | { 53 | $this->assertTrue($this->action->exists('123')); 54 | $this->assertTrue(MagentoProduct::query()->where('sku', '123')->first()->exists_in_magento); /** @phpstan-ignore-line */ 55 | Http::assertSent(function (Request $request) { 56 | return $request->url() === 'magento/rest/all/V1/products/123?fields=sku'; 57 | }); 58 | } 59 | 60 | #[Test] 61 | public function new_non_existing_product(): void 62 | { 63 | $this->assertFalse($this->action->exists('456')); 64 | $this->assertFalse(MagentoProduct::query()->where('sku', '456')->first()->exists_in_magento); /** @phpstan-ignore-line */ 65 | Http::assertSent(function (Request $request) { 66 | return $request->url() === 'magento/rest/all/V1/products/456?fields=sku'; 67 | }); 68 | } 69 | 70 | #[Test] 71 | public function existing_last_checked(): void 72 | { 73 | MagentoProduct::query()->create([ 74 | 'sku' => '123', 'exists_in_magento' => true, 'last_checked' => now()->subHours(3), 75 | ]); 76 | 77 | $this->assertTrue($this->action->exists('123')); 78 | 79 | Http::assertNothingSent(); 80 | } 81 | 82 | #[Test] 83 | public function it_throws_exception_when_magento_is_not_available(): void 84 | { 85 | $this->mock(ChecksMagento::class, function (MockInterface $mock): void { 86 | $mock->shouldReceive('available')->andReturnFalse(); 87 | }); 88 | 89 | /** @var CheckMagentoExistence $action */ 90 | $action = app(CheckMagentoExistence::class); 91 | 92 | $this->expectException(RuntimeException::class); 93 | $action->exists('456'); 94 | 95 | Http::assertNothingSent(); 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /tests/Actions/CheckRemovedProductsTest.php: -------------------------------------------------------------------------------- 1 | create([ 20 | 'sku' => '::sku_1::', 21 | 'exists_in_magento' => true, 22 | 'retrieved' => false, 23 | ]); 24 | 25 | MagentoProduct::query()->create([ 26 | 'sku' => '::sku_2::', 27 | 'exists_in_magento' => false, 28 | 'retrieved' => false, 29 | ]); 30 | 31 | /** @var CheckRemovedProducts $action */ 32 | $action = app(CheckRemovedProducts::class); 33 | $action->check(); 34 | 35 | /** @var ?MagentoProduct $removedProduct */ 36 | $removedProduct = MagentoProduct::query()->firstWhere('sku', '=', '::sku_1::'); 37 | 38 | $this->assertNotNull($removedProduct); 39 | $this->assertFalse($removedProduct->exists_in_magento); 40 | 41 | Event::assertDispatchedTimes(ProductDeletedInMagentoEvent::class, 1); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /tests/Actions/DiscoverMagentoProductsTest.php: -------------------------------------------------------------------------------- 1 | set('magento-products.page_size', 2); 26 | } 27 | 28 | #[Test] 29 | public function it_processes_single_page(): void 30 | { 31 | Bus::fake(); 32 | Event::fake([ProductDataModifiedEvent::class]); 33 | Http::fake([ 34 | 'magento/rest/all/V1/products?searchCriteria%5BpageSize%5D=2&searchCriteria%5BcurrentPage%5D=0' => Http::response([ 35 | 'items' => [ 36 | ['sku' => '123'], 37 | ], 38 | ]), 39 | ])->preventingStrayRequests(); 40 | 41 | $this->mock(ProcessesMagentoSkus::class, function (MockInterface $mock): void { 42 | $mock->shouldReceive('process')->withArgs(function (Enumerable $skus): bool { 43 | return $skus->toArray() === ['123']; 44 | })->once(); 45 | }); 46 | 47 | $job = new DiscoverMagentoProductsJob; 48 | $job->withFakeBatch(); 49 | 50 | /** @var Batch $batch */ 51 | $batch = $job->batch(); 52 | 53 | /** @var DiscoverMagentoProducts $action */ 54 | $action = app(DiscoverMagentoProducts::class); 55 | $action->discover(0, $batch); 56 | 57 | Event::assertDispatched(ProductDataModifiedEvent::class, function (ProductDataModifiedEvent $event): bool { 58 | return $event->oldData === null && $event->newData === ['sku' => '123']; 59 | }); 60 | Bus::assertNothingBatched(); 61 | } 62 | 63 | #[Test] 64 | public function it_dispatches_modified_event_with_old_data(): void 65 | { 66 | Bus::fake(); 67 | Event::fake([ProductDataModifiedEvent::class]); 68 | Http::fake([ 69 | 'magento/rest/all/V1/products?searchCriteria%5BpageSize%5D=2&searchCriteria%5BcurrentPage%5D=0' => Http::response([ 70 | 'items' => [ 71 | ['sku' => '123'], 72 | ], 73 | ]), 74 | ])->preventingStrayRequests(); 75 | 76 | $this->mock(ProcessesMagentoSkus::class, function (MockInterface $mock): void { 77 | $mock->shouldReceive('process')->withArgs(function (Enumerable $skus): bool { 78 | return $skus->toArray() === ['123']; 79 | })->once(); 80 | }); 81 | 82 | $job = new DiscoverMagentoProductsJob; 83 | $job->withFakeBatch(); 84 | 85 | /** @var Batch $batch */ 86 | $batch = $job->batch(); 87 | 88 | MagentoProduct::query()->create([ 89 | 'sku' => '123', 90 | 'checksum' => 'old', 91 | 'data' => ['sku' => '123', 'old' => 'data'], 92 | ]); 93 | 94 | /** @var DiscoverMagentoProducts $action */ 95 | $action = app(DiscoverMagentoProducts::class); 96 | $action->discover(0, $batch); 97 | 98 | Event::assertDispatched(ProductDataModifiedEvent::class, function (ProductDataModifiedEvent $event): bool { 99 | return $event->oldData === ['sku' => '123', 'old' => 'data'] && $event->newData === ['sku' => '123']; 100 | }); 101 | Bus::assertNothingBatched(); 102 | } 103 | 104 | #[Test] 105 | public function it_does_not_dispatch_event_when_checksum_has_not_changed(): void 106 | { 107 | Bus::fake(); 108 | Event::fake([ProductDataModifiedEvent::class]); 109 | Http::fake([ 110 | 'magento/rest/all/V1/products?searchCriteria%5BpageSize%5D=2&searchCriteria%5BcurrentPage%5D=0' => Http::response([ 111 | 'items' => [ 112 | ['sku' => '123'], 113 | ], 114 | ]), 115 | ])->preventingStrayRequests(); 116 | 117 | $this->mock(ProcessesMagentoSkus::class, function (MockInterface $mock): void { 118 | $mock->shouldReceive('process')->withArgs(function (Enumerable $skus): bool { 119 | return $skus->toArray() === ['123']; 120 | })->once(); 121 | }); 122 | 123 | $job = new DiscoverMagentoProductsJob; 124 | $job->withFakeBatch(); 125 | 126 | /** @var Batch $batch */ 127 | $batch = $job->batch(); 128 | 129 | MagentoProduct::query()->create([ 130 | 'sku' => '123', 131 | 'checksum' => 'ffd4c4101da9cd00e59cab0b0874f192', 132 | 'data' => ['sku' => '123', 'old' => 'data'], 133 | ]); 134 | 135 | /** @var DiscoverMagentoProducts $action */ 136 | $action = app(DiscoverMagentoProducts::class); 137 | $action->discover(0, $batch); 138 | 139 | Event::assertNotDispatched(ProductDataModifiedEvent::class); 140 | Bus::assertNothingBatched(); 141 | } 142 | 143 | #[Test] 144 | public function it_dispatches_next_job(): void 145 | { 146 | Http::fake([ 147 | 'magento/rest/all/V1/products?searchCriteria%5BpageSize%5D=1&searchCriteria%5BcurrentPage%5D=0' => Http::response([ 148 | 'items' => [ 149 | ['sku' => '123'], 150 | ], 151 | ]), 152 | 'magento/rest/all/V1/products?searchCriteria%5BpageSize%5D=1&searchCriteria%5BcurrentPage%5D=1' => Http::response([ 153 | 'items' => [], 154 | ]), 155 | ])->preventingStrayRequests(); 156 | config()->set('magento-products.page_size', 1); 157 | 158 | $this->mock(ProcessesMagentoSkus::class, function (MockInterface $mock) { 159 | $mock->shouldReceive('process')->once(); 160 | }); 161 | 162 | $job = new DiscoverMagentoProductsJob; 163 | $job->withFakeBatch(); 164 | 165 | /** @var Batch $batch */ 166 | $batch = $job->batch(); 167 | 168 | /** @var DiscoverMagentoProducts $action */ 169 | $action = app(DiscoverMagentoProducts::class); 170 | $action->discover(0, $batch); 171 | 172 | /** @var ?DiscoverMagentoProductsJob $addedJob */ 173 | $addedJob = $job->batch()->added[0] ?? null; 174 | 175 | $this->assertNotNull($addedJob); 176 | $this->assertEquals(1, $addedJob->page); 177 | } 178 | 179 | #[Test] 180 | public function it_sets_retrieved_false(): void 181 | { 182 | Http::fake([ 183 | 'magento/rest/all/V1/products?searchCriteria%5BpageSize%5D=2&searchCriteria%5BcurrentPage%5D=0' => Http::response([ 184 | 'items' => [], 185 | ]), 186 | ])->preventingStrayRequests(); 187 | 188 | MagentoProduct::query()->create([ 189 | 'sku' => '123', 190 | 'exists_in_magento' => true, 191 | 'retrieved' => true, 192 | ]); 193 | 194 | $this->mock(ProcessesMagentoSkus::class, function (MockInterface $mock) { 195 | $mock->shouldReceive('process')->once(); 196 | }); 197 | 198 | $job = new DiscoverMagentoProductsJob; 199 | $job->withFakeBatch(); 200 | 201 | /** @var Batch $batch */ 202 | $batch = $job->batch(); 203 | 204 | /** @var DiscoverMagentoProducts $action */ 205 | $action = app(DiscoverMagentoProducts::class); 206 | $action->discover(0, $batch); 207 | 208 | /** @var ?MagentoProduct $product */ 209 | $product = MagentoProduct::query()->first(); 210 | 211 | $this->assertNotNull($product); 212 | $this->assertFalse($product->retrieved); 213 | } 214 | 215 | #[Test] 216 | public function it_sets_retrieved_true(): void 217 | { 218 | Http::fake([ 219 | 'magento/rest/all/V1/products?searchCriteria%5BpageSize%5D=2&searchCriteria%5BcurrentPage%5D=0' => Http::response([ 220 | 'items' => [ 221 | ['sku' => '123'], 222 | ], 223 | ]), 224 | ])->preventingStrayRequests(); 225 | 226 | MagentoProduct::query()->create([ 227 | 'sku' => '123', 228 | 'exists_in_magento' => true, 229 | 'retrieved' => true, 230 | ]); 231 | 232 | $this->mock(ProcessesMagentoSkus::class, function (MockInterface $mock) { 233 | $mock->shouldReceive('process')->once(); 234 | }); 235 | 236 | $job = new DiscoverMagentoProductsJob; 237 | $job->withFakeBatch(); 238 | 239 | /** @var Batch $batch */ 240 | $batch = $job->batch(); 241 | 242 | /** @var DiscoverMagentoProducts $action */ 243 | $action = app(DiscoverMagentoProducts::class); 244 | $action->discover(0, $batch); 245 | 246 | /** @var ?MagentoProduct $product */ 247 | $product = MagentoProduct::query()->first(); 248 | 249 | $this->assertNotNull($product); 250 | $this->assertTrue($product->retrieved); 251 | } 252 | } 253 | -------------------------------------------------------------------------------- /tests/Actions/ProcessMagentoSkusTest.php: -------------------------------------------------------------------------------- 1 | action = app(ProcessMagentoSkus::class); 23 | } 24 | 25 | #[Test] 26 | public function it_returns_when_page_size_equals(): void 27 | { 28 | config()->set('magento-products.page_size', 1); 29 | 30 | MagentoProduct::query()->create(['sku' => '123', 'exists_in_magento' => true]); 31 | $skus = collect(['123']); 32 | 33 | $this->action->process($skus); 34 | 35 | Event::assertNotDispatched(ProductCreatedInMagentoEvent::class); 36 | } 37 | 38 | #[Test] 39 | public function it_sets_exists_to_true(): void 40 | { 41 | MagentoProduct::query()->create(['sku' => '456', 'exists_in_magento' => false]); 42 | $skus = collect(['456']); 43 | 44 | $this->action->process($skus); 45 | 46 | $this->assertTrue(MagentoProduct::query()->where('sku', '456')->first()->exists_in_magento); /** @phpstan-ignore-line */ 47 | Event::assertDispatched(ProductCreatedInMagentoEvent::class); 48 | } 49 | 50 | #[Test] 51 | public function it_adds_missing_products(): void 52 | { 53 | $skus = collect(['789']); 54 | 55 | $this->action->process($skus); 56 | 57 | $this->assertTrue(MagentoProduct::query()->where('sku', '789')->first()->exists_in_magento); /** @phpstan-ignore-line */ 58 | Event::assertDispatched(ProductCreatedInMagentoEvent::class); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /tests/Actions/RetrieveMagentoSkusTest.php: -------------------------------------------------------------------------------- 1 | Http::response([ 17 | 'items' => [ 18 | [ 19 | 'sku' => '123', 20 | ], 21 | [ 22 | 'sku' => '456', 23 | ], 24 | ], 25 | ]), 26 | ]); 27 | 28 | /** @var RetrieveMagentoSkus $action */ 29 | $action = app(RetrieveMagentoSkus::class); 30 | 31 | $this->assertEquals(collect(['123', '456']), $action->retrieve(1)); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /tests/Actions/RetrieveProductDataTest.php: -------------------------------------------------------------------------------- 1 | action = app(RetrieveProductData::class); 24 | 25 | config()->set('magento-products.check_interval', 2); 26 | 27 | Http::fake([ 28 | '*/products/123' => Http::response(['123']), 29 | '*/products/123%2B456' => Http::response(['456']), 30 | '*/some_store/V1/products/789' => Http::response(['789']), 31 | '*/products/404' => Http::response([], 404), 32 | ]); 33 | } 34 | 35 | #[Test] 36 | public function it_retrieves_existing_product(): void 37 | { 38 | MagentoProduct::query()->create(['sku' => '123', 'data' => ['test'], 'last_checked' => now()->subHour()]); 39 | 40 | $data = $this->action->retrieve('123'); 41 | 42 | $this->assertEquals(['test'], $data); 43 | } 44 | 45 | #[Test] 46 | public function it_retrieves_new_product(): void 47 | { 48 | $data = $this->action->retrieve('123+456'); 49 | 50 | $this->assertEquals(['456'], $data); 51 | 52 | Http::assertSent(function (Request $request) { 53 | return $request->url() == 'magento/rest/all/V1/products/123%2B456'; 54 | }); 55 | } 56 | 57 | #[Test] 58 | public function it_retrieves_product_for_store(): void 59 | { 60 | $data = $this->action->retrieve('789', false, 'some_store'); 61 | 62 | $this->assertEquals(['789'], $data); 63 | 64 | /** @var MagentoProduct $createdProduct */ 65 | $createdProduct = MagentoProduct::findBySku('789', 'some_store'); 66 | 67 | $this->assertEquals('some_store', $createdProduct->store); 68 | 69 | Http::assertSent(function (Request $request) { 70 | return $request->url() == 'magento/rest/some_store/V1/products/789'; 71 | }); 72 | } 73 | 74 | #[Test] 75 | public function it_retrieves_missing_product(): void 76 | { 77 | $data = $this->action->retrieve('404'); 78 | 79 | $this->assertNull($data); 80 | $this->assertFalse(MagentoProduct::query()->where('sku', '404')->first()->exists_in_magento); /** @phpstan-ignore-line */ 81 | Http::assertSent(function (Request $request) { 82 | return $request->url() == 'magento/rest/all/V1/products/404'; 83 | }); 84 | } 85 | 86 | #[Test] 87 | public function it_rechecks_on_interval(): void 88 | { 89 | MagentoProduct::query()->create(['sku' => '123', 'data' => ['test'], 'last_checked' => now()->subHours(3)]); 90 | 91 | $data = $this->action->retrieve('123'); 92 | 93 | $this->assertEquals(['123'], $data); 94 | 95 | Http::assertSent(function (Request $request) { 96 | return $request->url() == 'magento/rest/all/V1/products/123'; 97 | }); 98 | } 99 | 100 | #[Test] 101 | public function it_forces_recheck(): void 102 | { 103 | MagentoProduct::query()->create(['sku' => '123', 'data' => ['test'], 'last_checked' => now()->subHour()]); 104 | 105 | $data = $this->action->retrieve('123', true); 106 | 107 | $this->assertEquals(['123'], $data); 108 | 109 | Http::assertSent(function (Request $request) { 110 | return $request->url() == 'magento/rest/all/V1/products/123'; 111 | }); 112 | } 113 | 114 | #[Test] 115 | public function it_returns_null_when_check_fails(): void 116 | { 117 | MagentoProduct::query()->create(['sku' => '404', 'data' => ['test'], 'last_checked' => now()->subHour()]); 118 | 119 | $data = $this->action->retrieve('404', true); 120 | 121 | $this->assertNull($data); 122 | 123 | Http::assertSent(function (Request $request) { 124 | return $request->url() == 'magento/rest/all/V1/products/404'; 125 | }); 126 | } 127 | 128 | #[Test] 129 | public function it_throws_exception_when_magento_is_not_available(): void 130 | { 131 | $this->mock(ChecksMagento::class, function (MockInterface $mock): void { 132 | $mock->shouldReceive('available')->andReturnFalse(); 133 | }); 134 | 135 | /** @var RetrieveProductData $action */ 136 | $action = app(RetrieveProductData::class); 137 | 138 | $this->expectException(RuntimeException::class); 139 | $action->retrieve('456'); 140 | 141 | Http::assertNothingSent(); 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /tests/Commands/CheckKnownProductExistenceCommandTest.php: -------------------------------------------------------------------------------- 1 | artisan(CheckKnownProductsExistenceCommand::class); 19 | 20 | Bus::assertDispatched(CheckAllKnownProductsExistenceJob::class); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /tests/Commands/DiscoverMagentoProductsCommandTest.php: -------------------------------------------------------------------------------- 1 | artisan(DiscoverMagentoProductsCommand::class); 20 | 21 | Bus::assertBatched(function (PendingBatchFake $batch) { 22 | return $batch->jobs->count() === 1 && get_class($batch->jobs->first()) === DiscoverMagentoProductsJob::class; 23 | }); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /tests/Commands/RetrieveProductDataCommandTest.php: -------------------------------------------------------------------------------- 1 | Http::response(['productdata']), 18 | ]); 19 | 20 | $this->artisan(RetrieveProductDataCommand::class, ['sku' => '123']); 21 | 22 | /** @var MagentoProduct $product */ 23 | $product = MagentoProduct::first(); 24 | 25 | $this->assertTrue($product->exists_in_magento); 26 | $this->assertEquals(['productdata'], $product->data); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /tests/Jobs/CheckAllKnownProductsExistenceJobTest.php: -------------------------------------------------------------------------------- 1 | mock(ChecksAllKnownProducts::class, function (MockInterface $mock) { 17 | $mock->shouldReceive('check')->once(); 18 | }); 19 | 20 | CheckAllKnownProductsExistenceJob::dispatch(); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /tests/Jobs/CheckKnownProductsExistenceJobTest.php: -------------------------------------------------------------------------------- 1 | mock(ChecksKnownProducts::class, function (MockInterface $mock) { 17 | $mock->shouldReceive('handle')->with(['123'])->once(); 18 | }); 19 | 20 | CheckKnownProductsExistenceJob::dispatch(['123']); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /tests/Jobs/CheckRemovedProductsJobTest.php: -------------------------------------------------------------------------------- 1 | mock(ChecksRemovedProducts::class, function (MockInterface $mock): void { 17 | $mock->shouldReceive('check')->once(); 18 | }); 19 | 20 | CheckRemovedProductsJob::dispatch(); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /tests/Jobs/DiscoverMagentoProductsJobTest.php: -------------------------------------------------------------------------------- 1 | mock(DiscoversMagentoProducts::class, function (MockInterface $mock): void { 18 | $mock->shouldReceive('discover')->once(); 19 | }); 20 | 21 | $job = new DiscoverMagentoProductsJob(0); 22 | $job->withFakeBatch(); 23 | 24 | Bus::dispatch($job); 25 | } 26 | 27 | #[Test] 28 | public function it_stops_when_batch_is_cancelled(): void 29 | { 30 | $this->mock(DiscoversMagentoProducts::class, function (MockInterface $mock): void { 31 | $mock->shouldNotReceive('discover'); 32 | }); 33 | 34 | $job = new DiscoverMagentoProductsJob(0); 35 | $job->withFakeBatch(); 36 | $job->batch()?->cancel(); 37 | 38 | Bus::dispatch($job); 39 | } 40 | 41 | #[Test] 42 | public function it_has_tags(): void 43 | { 44 | $job = new DiscoverMagentoProductsJob(0); 45 | 46 | $this->assertEquals([0], $job->tags()); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /tests/Listeners/RegisterProductListenerTest.php: -------------------------------------------------------------------------------- 1 | assertNotNull(MagentoProduct::query()->where('sku', '123')->first()); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /tests/TestCase.php: -------------------------------------------------------------------------------- 1 | set('magento.base_url', ''); 17 | config()->set('magento.access_token', '::token::'); 18 | config()->set('magento.timeout', 30); 19 | config()->set('magento.connect_timeout', 30); 20 | 21 | config()->set('database.default', 'testbench'); 22 | config()->set('database.connections.testbench', [ 23 | 'driver' => 'sqlite', 24 | 'database' => ':memory:', 25 | 'prefix' => '', 26 | ]); 27 | 28 | Magento::fake(); 29 | } 30 | 31 | protected function getPackageProviders($app): array 32 | { 33 | return [ 34 | ServiceProvider::class, 35 | \JustBetter\MagentoClient\ServiceProvider::class, 36 | ]; 37 | } 38 | } 39 | --------------------------------------------------------------------------------