├── .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 |
5 |
6 |
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 |
--------------------------------------------------------------------------------