diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md new file mode 100644 index 0000000..e9f7814 --- /dev/null +++ b/.github/CONTRIBUTING.md @@ -0,0 +1,71 @@ +# Contributing + +Contributions are **welcome** and will be fully **credited**. + +Please read and understand the contribution guide before creating an issue or pull request. + +## Etiquette + +This project is open source, and as such, the maintainers give their free time to build and maintain the source code +held within. They make the code freely available in the hope that it will be of use to other developers. It would be +extremely unfair for them to suffer abuse or anger for their hard work. + +Please be considerate towards maintainers when raising issues or presenting pull requests. Let's show the +world that developers are civilized and selfless people. + +It's the duty of the maintainer to ensure that all submissions to the project are of sufficient +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. + +## Viability + +When requesting or submitting new features, first consider whether it might be useful to others. Open +source projects are used by many developers, who may have entirely different needs to your own. Think about +whether or not your feature is likely to be used by other users of the project. + +## Procedure + +Before filing an issue: + +- Attempt to replicate the problem, to ensure that it wasn't a coincidental incident. +- Check to make sure your feature suggestion isn't already present within the project. +- Check the pull requests tab to ensure that the bug doesn't have a fix in progress. +- Check the pull requests tab to ensure that the feature isn't already in progress. + +Before submitting a pull request: + +- Check the codebase to ensure that your feature doesn't already exist. +- Check the pull requests to ensure that another person hasn't already submitted the feature or fix. + +## Requirements + +If the project maintainer has any additional requirements, you will find them listed here. + +- **[Magento Coding Standard](https://github.com/magento/magento-coding-standard)** - Follow Magento 2 coding standards (based on PSR-12 with Magento-specific rules) + +- **Code Quality Tools** - Run these commands before committing: + ```bash + composer install + + # Run all checks with GrumPHP (code style + static analysis) + composer grumphp + + # Or run checks individually: + composer codestyle # Check code style + composer codestyle:fix # Auto-fix code style issues + composer analyse # Run PHPStan static analysis + ``` + + **Note:** GrumPHP runs automatically on `git commit` and will block commits with code quality issues (both code style and static analysis). + +- **Document any change in behaviour** - Make sure the `README.md`, `FEATURES.md` and any other relevant documentation are kept up-to-date. + +- **Consider our release cycle** - We try to follow [SemVer v2.0.0](https://semver.org/). + +- **One pull request per feature** - If you want to do more than one thing, send multiple pull requests. + +- **Test in a Magento environment** - Ensure your changes work correctly with: + - Magento 2.4.4+ + - PHP 8.2, 8.3, 8.4 + - Akeneo Connector Community Edition + +**Happy coding**! \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/bug.yml b/.github/ISSUE_TEMPLATE/bug.yml new file mode 100644 index 0000000..6410603 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug.yml @@ -0,0 +1,149 @@ +name: Bug Report +description: File a bug report. +title: "[Bug]: " +labels: ["bug"] +body: + - type: markdown + attributes: + value: | + Thanks for participating in this project! + Please fill out the following sections to help us understand the issue you're experiencing. + + **Before submitting:** + - Make sure you are using the latest version of the extension + - Search existing issues - your question might already be answered + - Include as many details as possible (screenshots, console errors, logs, etc.) + + - type: textarea + id: current-behavior + attributes: + label: Current Behavior + description: What is happening? + validations: + required: true + + - type: textarea + id: expected-behavior + attributes: + label: Expected Behavior + description: What should happen? + validations: + required: true + + - type: textarea + id: steps-to-reproduce + attributes: + label: Steps to Reproduce + description: Please provide detailed steps to reproduce the issue + placeholder: | + 1. Go to '...' + 2. Click on '...' + 3. Run command '...' + 4. See error + validations: + required: true + + - type: dropdown + id: affected-feature + attributes: + label: Which feature is affected? + description: Select the feature that is affected by this bug + options: + - Tier Prices + - Tax Class Mapping + - Important Attributes + - Metric Units Import + - Category Import + - Product Import + - Default Store Values + - Exclude Families + - Set Products Active + - Enable Manage Stock + - Set Stock Status + - Remove Redundant EAV + - Format Media Name + - Not Visible Families + - Required Attribute Mapping + - Slack Notifications + - Akeneo Manager + - Event System + - Other / Unknown + validations: + required: true + + - type: input + id: magento-version + attributes: + label: Magento Version + placeholder: "e.g., 2.4.7" + validations: + required: true + + - type: input + id: akeneo-connector-version + attributes: + label: Akeneo Connector Version + placeholder: "e.g., 105.1.0" + validations: + required: true + + - type: input + id: bundle-version + attributes: + label: JustBetter Akeneo Bundle Version + placeholder: "e.g., 2.0.0" + validations: + required: true + + - type: input + id: php-version + attributes: + label: PHP Version + placeholder: "e.g., 8.3" + validations: + required: true + + - type: input + id: akeneo-version + attributes: + label: Akeneo PIM Version + placeholder: "e.g., 7.0" + validations: + required: false + + - type: textarea + id: proposed-solution + attributes: + label: Proposed Solution + description: If you have an idea how to fix this, please share it + + - type: textarea + id: logs + attributes: + label: Relevant Log Output + description: Please copy and paste any relevant log output (var/log/akeneo.log, var/log/exception.log, etc.) + render: shell + + - type: textarea + id: additional-information + attributes: + label: Additional Information + description: Screenshots, configuration details, or any other relevant information + + - type: checkboxes + attributes: + label: Pre-submission Checklist + options: + - label: I have searched the existing issues + required: true + - label: I am using the latest version of the bundle + required: true + - label: I have included all required version information + required: true + + - type: checkboxes + attributes: + label: Are you willing to create a pull request for this? + description: Thank you so much for your willingness to help us out! We really appreciate it. + options: + - label: If I have a fix, I would be willing to create a pull request \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..264f8f0 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,14 @@ +blank_issues_enabled: true +contact_links: + - name: ๐Ÿ“š Documentation + url: https://github.com/justbetter/magento2-akeneo-bundle/blob/master/README.md + about: Read the full documentation and feature guide + - name: ๐Ÿ’ฌ Discussions + url: https://github.com/justbetter/magento2-akeneo-bundle/discussions + about: Ask questions and discuss ideas with the community + - name: ๐Ÿ”’ Security Issue + url: https://github.com/justbetter/magento2-akeneo-bundle/security/advisories/new + about: Report a security vulnerability privately + - name: ๐Ÿ“ง Contact JustBetter + url: https://justbetter.nl/contact + about: Get in touch with JustBetter for support or inquiries diff --git a/.github/ISSUE_TEMPLATE/feature.yml b/.github/ISSUE_TEMPLATE/feature.yml new file mode 100644 index 0000000..97a7bfc --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature.yml @@ -0,0 +1,114 @@ +name: Feature Request +description: Request a new feature or enhancement. +title: "[Feature]: " +labels: ["enhancement"] +body: + - type: markdown + attributes: + value: | + Thanks for participating in this project! + Please fill out the following sections to help us understand what you want. + + **Before submitting:** + - Make sure you are using the latest version of the extension + - Search existing issues and PRs - there might be a similar request already + - Include as many details as possible about your use case + + - type: textarea + id: requested-feature + attributes: + label: Feature Description + description: What feature would you like to see added? + placeholder: A clear and concise description of what you want to happen + validations: + required: true + + - type: textarea + id: use-case + attributes: + label: Use Case + description: Why would you need this feature? How are you currently limited? + placeholder: Describe your use case and why this feature would be valuable + validations: + required: true + + - type: dropdown + id: feature-category + attributes: + label: Feature Category + description: Which area does this feature relate to? + options: + - Product Import + - Category Import + - Attribute Management + - Tax & Pricing + - Visibility & Stock + - Website Association + - Notifications + - Event System + - Configuration + - CLI Commands + - Other + validations: + required: true + + - type: textarea + id: proposed-solution + attributes: + label: Proposed Solution + description: How do you envision this feature working? + placeholder: | + Describe the solution you'd like: + - What configuration options would be needed? + - How would it integrate with existing features? + - What would the user workflow look like? + + - type: textarea + id: alternatives + attributes: + label: Alternatives Considered + description: Have you considered any alternative solutions or workarounds? + + - type: textarea + id: current-workaround + attributes: + label: Current Workaround + description: How are you currently solving this problem (if at all)? + + - type: input + id: magento-version + attributes: + label: Magento Version + placeholder: "e.g., 2.4.7" + validations: + required: false + + - type: input + id: bundle-version + attributes: + label: JustBetter Akeneo Bundle Version + placeholder: "e.g., 2.0.0" + validations: + required: false + + - type: textarea + id: additional-information + attributes: + label: Additional Information + description: Any other context, screenshots, or examples that would help us understand the request + + - type: checkboxes + attributes: + label: Pre-submission Checklist + options: + - label: I have searched the existing issues and pull requests + required: true + - label: I have described my use case clearly + required: true + + - type: checkboxes + attributes: + label: Are you willing to create a pull request for this? + description: Thank you so much for your willingness to help us out! We really appreciate it. + options: + - label: I would be willing to create a pull request for this feature \ No newline at end of file diff --git a/.github/SECURITY.md b/.github/SECURITY.md new file mode 100644 index 0000000..69c1900 --- /dev/null +++ b/.github/SECURITY.md @@ -0,0 +1,34 @@ +# Security Policy + +## Supported Versions + +We release patches for security vulnerabilities in the following versions: + +| Version | Supported | +| ------- | --------- | +| 2.x | โœ… | +| < 2.0 | โŒ | + +## Reporting a Vulnerability + +We take security seriously. If you discover any security related issues, please report them responsibly. + +### Preferred reporting method: + +Use [GitHub's private security reporting](https://github.com/justbetter/magento2-akeneo-bundle/security/advisories/new) to report vulnerabilities privately. + +### Alternative method: + +Email **security@justbetter.nl** if you prefer not to use GitHub. + +All security vulnerabilities will be promptly addressed. + +### What to include in your report: + +- Description of the vulnerability +- Steps to reproduce the issue +- Affected versions +- Potential impact +- Suggested fix (if available) + +Thank you for helping keep JustBetter Akeneo Bundle and its users safe! \ No newline at end of file diff --git a/.github/assets/banner.svg b/.github/assets/banner.svg new file mode 100644 index 0000000..7a60804 --- /dev/null +++ b/.github/assets/banner.svg @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/.github/assets/footer.svg b/.github/assets/footer.svg new file mode 100644 index 0000000..4fb00c3 --- /dev/null +++ b/.github/assets/footer.svg @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.github/workflows/analyse.yml b/.github/workflows/analyse.yml index c687e18..2cae533 100644 --- a/.github/workflows/analyse.yml +++ b/.github/workflows/analyse.yml @@ -1,25 +1,81 @@ -name: PHPStan +name: Code Quality -on: ['push', 'pull_request'] +on: + push: + branches: [ master, develop, feature/* ] + pull_request: + branches: [ master, develop ] jobs: - test: + phpstan: + name: PHPStan (PHP ${{ matrix.php-version }}) runs-on: ubuntu-latest - name: analyse - + + strategy: + fail-fast: false + matrix: + php-version: ['8.2', '8.3', '8.4'] + steps: - name: Checkout code - uses: actions/checkout@v2 - + uses: actions/checkout@v4 + - name: Setup PHP uses: shivammathur/setup-php@v2 with: - php-version: 8.2 - extensions: dom, curl, libxml, mbstring, zip, pdo, sqlite, pdo_sqlite, bcmath, soap, intl, gd, exif, iconv, imagick, fileinfo + php-version: ${{ matrix.php-version }} + extensions: bcmath, ctype, curl, dom, gd, hash, iconv, intl, mbstring, openssl, pdo_mysql, simplexml, soap, xsl, zip coverage: none - + + - name: Validate composer.json + run: composer validate --strict + + - name: Get composer cache directory + id: composer-cache + run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT + + - name: Cache dependencies + uses: actions/cache@v4 + with: + path: ${{ steps.composer-cache.outputs.dir }} + key: ${{ runner.os }}-php${{ matrix.php-version }}-composer-${{ hashFiles('**/composer.json') }} + restore-keys: | + ${{ runner.os }}-php${{ matrix.php-version }}-composer- + - name: Install dependencies - run: composer install --no-interaction - - - name: Analyse - run: vendor/bin/phpstan analyse \ No newline at end of file + run: composer install --prefer-dist --no-progress --no-interaction --ignore-platform-req=php+ + + - name: Run PHPStan + run: composer analyse + + phpcs: + name: Code Style + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '8.2' + extensions: bcmath, ctype, curl, dom, gd, hash, iconv, intl, mbstring, openssl, pdo_mysql, simplexml, soap, xsl, zip + coverage: none + + - name: Get composer cache directory + id: composer-cache + run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT + + - name: Cache dependencies + uses: actions/cache@v4 + with: + path: ${{ steps.composer-cache.outputs.dir }} + key: ${{ runner.os }}-php-composer-${{ hashFiles('**/composer.json') }} + restore-keys: ${{ runner.os }}-php-composer- + + - name: Install dependencies + run: composer install --prefer-dist --no-progress --no-interaction + + - name: Run PHPCS + run: composer codestyle diff --git a/.gitignore b/.gitignore index 88e99d5..b015961 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,26 @@ -vendor -composer.lock \ No newline at end of file +# Composer +vendor/ +composer.lock + +# PHP CS Fixer +.php-cs-fixer.cache +.php-cs-fixer.php + +# PHPStan +.phpstan.cache +phpstan-baseline.neon + +# IDE +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# OS +.DS_Store +Thumbs.db + +# Build +build/ +dist/ diff --git a/Block/Adminhtml/Akeneo.php b/Block/Adminhtml/Akeneo.php old mode 100755 new mode 100644 index 67aba79..d61216f --- a/Block/Adminhtml/Akeneo.php +++ b/Block/Adminhtml/Akeneo.php @@ -1,29 +1,26 @@ $data */ - public function __construct(Context $context, array $data = []) - { + public function __construct( + Context $context, + array $data = [] + ) { parent::__construct($context, $data); } - /** - * Prepare button and grid - */ - protected function _prepareLayout() + protected function _prepareLayout(): Akeneo { $addButtonProps = [ 'id' => 'add_new', @@ -31,49 +28,40 @@ protected function _prepareLayout() 'class' => 'add', 'button_class' => '', 'class_name' => 'Magento\Backend\Block\Widget\Button\SplitButton', - 'options' => $this->_getAddButtonOptions(), + 'options' => $this->getAddButtonOptions(), ]; $this->buttonList->add('add_new', $addButtonProps); - $this->setChild( 'grid', - $this->getLayout()->createBlock('JustBetter\AkeneoBundle\Block\Adminhtml\Akeneo\Grid', 'justbetter.akeneo.grid') + $this->getLayout()->createBlock( // @phpstan-ignore-line + 'JustBetter\AkeneoBundle\Block\Adminhtml\Akeneo\Grid', + 'justbetter.akeneo.grid' + ) ); + return parent::_prepareLayout(); } /** - * - * - * @return array + * @return array> */ - protected function _getAddButtonOptions() + protected function getAddButtonOptions(): array { - $splitButtonOptions[] = [ - 'label' => __('Add New'), - 'onclick' => "setLocation('" . $this->_getCreateUrl() . "')" + return [ + [ + 'label' => __('Add New'), + 'onclick' => "setLocation('" . $this->getCreateUrl() . "')" + ] ]; - - return $splitButtonOptions; } - /** - * @return string - */ - protected function _getCreateUrl() + protected function getCreateUrl(): string { - return $this->getUrl( - 'akeneomanager/*/new' - ); + return $this->getUrl('akeneomanager/*/new'); } - /** - * Render grid - * - * @return string - */ - public function getGridHtml() + public function getGridHtml(): string { return $this->getChildHtml('grid'); } diff --git a/Block/Adminhtml/Akeneo/Edit.php b/Block/Adminhtml/Akeneo/Edit.php old mode 100755 new mode 100644 index 3124e42..63d7647 --- a/Block/Adminhtml/Akeneo/Edit.php +++ b/Block/Adminhtml/Akeneo/Edit.php @@ -1,39 +1,31 @@ $data */ public function __construct( Context $context, - Registry $registry, + protected AkeneoFactory $akeneoFactory, array $data = [] ) { - $this->_coreRegistry = $registry; + $this->escaper = $context->getEscaper(); parent::__construct($context, $data); } - /** - * Initialize akeneo edit block - * - * @return void - */ - protected function _construct() + protected function _construct(): void { $this->_objectId = 'id'; $this->_blockGroup = 'JustBetter_AkeneoBundle'; @@ -41,7 +33,7 @@ protected function _construct() parent::_construct(); - $this->buttonList->update('save', 'label', __('Save Akeneo')); + $this->buttonList->update('save', 'label', (string)__('Save Akeneo')); $this->buttonList->add( 'saveandcontinue', [ @@ -56,40 +48,35 @@ protected function _construct() -100 ); - $this->buttonList->update('delete', 'label', __('Delete Akeneo')); + $this->buttonList->update('delete', 'label', (string)__('Delete Akeneo')); } - /** - * Retrieve text for header element depending on loaded post - * - * @return \Magento\Framework\Phrase - */ - public function getHeaderText() + public function getHeaderText(): string { - if ($this->_coreRegistry->registry('akeneo')->getId()) { - return __("Edit Akeneo '%1'", $this->escapeHtml($this->_coreRegistry->registry('akeneo')->getTitle())); - } else { - return __('New Akeneo'); + $id = (int)$this->getRequest()->getParam('id'); + + if ($id) { + $model = $this->akeneoFactory->create(); + $model->load($id); // @phpstan-ignore-line + + if ($model->getId()) { + return (string)__("Edit Akeneo '%1'", $this->escaper->escapeHtml($model->getTitle())); + } } + + return (string)__('New Akeneo'); } - /** - * Getter of url for "Save and Continue" button - * tab_id will be replaced by desired by JS later - * - * @return string - */ - protected function _getSaveAndContinueUrl() + protected function _getSaveAndContinueUrl(): string { - return $this->getUrl('akeneomanager/*/save', ['_current' => true, 'back' => 'edit', 'active_tab' => '{{tab_id}}']); + return $this->getUrl('akeneomanager/*/save', [ + '_current' => true, + 'back' => 'edit', + 'active_tab' => '{{tab_id}}' + ]); } - /** - * Prepare layout - * - * @return \Magento\Framework\View\Element\AbstractBlock - */ - protected function _prepareLayout() + protected function _prepareLayout(): Edit { $this->_formScripts[] = " function toggleEditor() { @@ -100,6 +87,7 @@ function toggleEditor() { } }; "; + return parent::_prepareLayout(); } } diff --git a/Block/Adminhtml/Akeneo/Edit/Form.php b/Block/Adminhtml/Akeneo/Edit/Form.php old mode 100755 new mode 100644 index 455d5a4..0909640 --- a/Block/Adminhtml/Akeneo/Edit/Form.php +++ b/Block/Adminhtml/Akeneo/Edit/Form.php @@ -1,4 +1,5 @@ _formFactory->create( - ['data' => ['id' => 'edit_form', 'action' => $this->getData('action'), 'method' => 'post' , 'enctype' => 'multipart/form-data']] - ); + $form = $this->_formFactory->create([ + 'data' => [ + 'id' => 'edit_form', + 'action' => $this->getData('action'), + 'method' => 'post', + 'enctype' => 'multipart/form-data' + ] + ]); + $form->setUseContainer(true); $this->setForm($form); + return parent::_prepareForm(); } } diff --git a/Block/Adminhtml/Akeneo/Edit/Tab/Main.php b/Block/Adminhtml/Akeneo/Edit/Tab/Main.php old mode 100755 new mode 100644 index f40dfa0..2ebe20c --- a/Block/Adminhtml/Akeneo/Edit/Tab/Main.php +++ b/Block/Adminhtml/Akeneo/Edit/Tab/Main.php @@ -1,67 +1,54 @@ $data */ public function __construct( Context $context, Registry $registry, FormFactory $formFactory, - Store $systemStore, - Status $status, + protected AkeneoFactory $akeneoFactory, + protected Store $systemStore, + protected Status $status, array $data = [] ) { - $this->_systemStore = $systemStore; - $this->_status = $status; parent::__construct($context, $registry, $formFactory, $data); } - /** - * Prepare form - * - * @return $this - * @SuppressWarnings(PHPMD.ExcessiveMethodLength) - */ - protected function _prepareForm() + protected function _prepareForm(): Main { + // Get model from registry (set by controller) or load from request $model = $this->_coreRegistry->registry('akeneo'); + + if (!$model) { + $model = $this->akeneoFactory->create(); + $id = (int)$this->getRequest()->getParam('id'); + + if ($id) { + $model->load($id); // @phpstan-ignore-line + } + } $isElementDisabled = false; - /** @var \Magento\Framework\Data\Form $form */ $form = $this->_formFactory->create(); - $form->setHtmlIdPrefix('page_'); $fieldset = $form->addFieldset('base_fieldset', ['legend' => __('Item Information')]); @@ -69,7 +56,7 @@ protected function _prepareForm() if ($model->getId()) { $fieldset->addField('id', 'hidden', ['name' => 'id']); } - + $fieldset->addField( 'import', 'select', @@ -78,7 +65,7 @@ protected function _prepareForm() 'title' => __('Type'), 'name' => 'import', 'required' => true, - 'options' => \JustBetter\AkeneoBundle\Block\Adminhtml\Akeneo\Grid::getOptionArray0(), + 'options' => Grid::getOptionArray0(), 'disabled' => $isElementDisabled ] ); @@ -107,89 +94,63 @@ protected function _prepareForm() ] ); - $dateFormat = $this->_localeDate->getDateFormat( - IntlDateFormatter::MEDIUM - ); - $timeFormat = $this->_localeDate->getTimeFormat( - IntlDateFormatter::MEDIUM - ); + $dateFormat = $this->_localeDate->getDateFormat(IntlDateFormatter::MEDIUM); $fieldset->addField( 'created_at', 'date', [ - 'name' => 'created_at', - 'label' => __('Created'), - 'title' => __('Created'), + 'name' => 'created_at', + 'label' => __('Created'), + 'title' => __('Created'), 'date_format' => $dateFormat, - //'time_format' => $timeFormat, - 'disabled' => $isElementDisabled, ] ); if (!$model->getId()) { - $model->setData('is_active', $isElementDisabled ? '0' : '1'); + $model->setData('is_active', '1'); } $form->setValues($model->getData()); $this->setForm($form); - + return parent::_prepareForm(); } - /** - * Prepare label for tab - * - * @return \Magento\Framework\Phrase - */ - public function getTabLabel() + public function getTabLabel(): string { - return __('Item Information'); + return (string)__('Item Information'); } - /** - * Prepare title for tab - * - * @return \Magento\Framework\Phrase - */ - public function getTabTitle() + public function getTabTitle(): string { - return __('Item Information'); + return (string)__('Item Information'); } - /** - * {@inheritdoc} - */ - public function canShowTab() + public function canShowTab(): bool { return true; } - /** - * {@inheritdoc} - */ - public function isHidden() + public function isHidden(): bool { return false; } - /** - * Check permission for passed action - * - * @param string $resourceId - * @return bool - */ - protected function _isAllowedAction($resourceId) + protected function _isAllowedAction(string $resourceId): bool { return $this->_authorization->isAllowed($resourceId); } - public function getTargetOptionArray() + /** + * @return array + */ + public function getTargetOptionArray(): array { - return array( - '_self' => "Self", - '_blank' => "New Page", - ); + return [ + '_self' => 'Self', + '_blank' => 'New Page', + ]; } } diff --git a/Block/Adminhtml/Akeneo/Edit/Tabs.php b/Block/Adminhtml/Akeneo/Edit/Tabs.php old mode 100755 new mode 100644 index dd0104b..fef5a33 --- a/Block/Adminhtml/Akeneo/Edit/Tabs.php +++ b/Block/Adminhtml/Akeneo/Edit/Tabs.php @@ -1,16 +1,14 @@ setId('akeneo_tabs'); diff --git a/Block/Adminhtml/Akeneo/Grid.php b/Block/Adminhtml/Akeneo/Grid.php old mode 100755 new mode 100644 index 5304a9f..be362fe --- a/Block/Adminhtml/Akeneo/Grid.php +++ b/Block/Adminhtml/Akeneo/Grid.php @@ -1,59 +1,32 @@ $data */ public function __construct( Context $context, Data $backendHelper, - AkeneoFactory $AkeneoFactory, - Status $status, - Manager $moduleManager, + protected CollectionFactory $collectionFactory, // @phpstan-ignore-line + protected Status $status, + protected Manager $moduleManager, array $data = [] ) { - $this->_akeneoFactory = $AkeneoFactory; - $this->_status = $status; - $this->moduleManager = $moduleManager; parent::__construct($context, $backendHelper, $data); } - /** - * @return void - */ - protected function _construct() + protected function _construct(): void { parent::_construct(); $this->setId('postGrid'); @@ -64,13 +37,9 @@ protected function _construct() $this->setVarNameFilter('post_filter'); } - /** - * @return $this - */ - protected function _prepareCollection() + protected function _prepareCollection(): Grid { - /* @phpstan-ignore-next-line */ - $collection = $this->_akeneoFactory->create()->getCollection(); + $collection = $this->collectionFactory->create(); // @phpstan-ignore-line $this->setCollection($collection); parent::_prepareCollection(); @@ -78,18 +47,14 @@ protected function _prepareCollection() return $this; } - /** - * @return $this - * @SuppressWarnings(PHPMD.ExcessiveMethodLength) - */ - protected function _prepareColumns() + protected function _prepareColumns(): Grid { $this->addColumn( 'id', [ - 'header' => __('ID'), - 'type' => 'number', - 'index' => 'id', + 'header' => __('ID'), + 'type' => 'number', + 'index' => 'id', 'header_css_class' => 'col-id', 'column_css_class' => 'col-id', ] @@ -98,10 +63,10 @@ protected function _prepareColumns() $this->addColumn( 'import', [ - 'header' => __('Type'), - 'index' => 'import', - 'type' => 'options', - 'options' => GridOption::getOptionArray0(), + 'header' => __('Type'), + 'index' => 'import', + 'type' => 'options', + 'options' => self::getOptionArray0(), ] ); @@ -109,7 +74,7 @@ protected function _prepareColumns() 'code', [ 'header' => __('Code'), - 'index' => 'code', + 'index' => 'code', ] ); @@ -117,8 +82,8 @@ protected function _prepareColumns() 'entity_id', [ 'header' => __('Magento Entity ID'), - 'index' => 'entity_id', - 'type' => 'int', + 'index' => 'entity_id', + 'type' => 'int', ] ); @@ -126,32 +91,28 @@ protected function _prepareColumns() 'created_at', [ 'header' => __('Created'), - 'index' => 'created_at', - 'type' => 'datetime', + 'index' => 'created_at', + 'type' => 'datetime', ] ); - - $this->addExportType($this->getUrl('akeneomanager/*/exportCsv', ['_current' => true]), __('CSV')); - $this->addExportType($this->getUrl('akeneomanager/*/exportExcel', ['_current' => true]), __('Excel XML')); + + $this->addExportType($this->getUrl('akeneomanager/*/exportCsv', ['_current' => true]), (string)__('CSV')); + $this->addExportType($this->getUrl('akeneomanager/*/exportExcel', ['_current' => true]), (string)__('Excel XML')); $block = $this->getLayout()->getBlock('grid.bottom.links'); if ($block) { - $this->setChild('grid.bottom.links', $block); + $this->setChild('grid.bottom.links', $block); // @phpstan-ignore-line } return parent::_prepareColumns(); } - - /** - * @return $this - */ - protected function _prepareMassaction() + protected function _prepareMassaction(): Grid { $this->setMassactionIdField('id'); $this->getMassactionBlock()->setFormFieldName('akeneo'); - $this->getMassactionBlock()->addItem( + $this->getMassactionBlock()->addItem( // @phpstan-ignore-line 'delete', [ 'label' => __('Delete'), @@ -160,9 +121,9 @@ protected function _prepareMassaction() ] ); - $statuses = $this->_status->getOptionArray(); + $statuses = $this->status->getOptionArray(); - $this->getMassactionBlock()->addItem( + $this->getMassactionBlock()->addItem( // @phpstan-ignore-line 'status', [ 'label' => __('Change status'), @@ -179,48 +140,42 @@ protected function _prepareMassaction() ] ); - return $this; } - /** - * @return string - */ - public function getGridUrl() + public function getGridUrl(): string { return $this->getUrl('akeneomanager/*/index', ['_current' => true]); } - /** - * @param \JustBetter\AkeneoBundle\Model\Akeneo|\Magento\Framework\Object $row - * @return string - */ - public function getRowUrl($row) + public function getRowUrl($row): string { - return $this->getUrl( - 'akeneomanager/*/edit', - ['id' => $row->getId()] - ); + return $this->getUrl('akeneomanager/*/edit', ['id' => $row->getId()]); } - - public static function getOptionArray0() + + /** + * @return array + */ + public static function getOptionArray0(): array { - $data_array = array(); - $data_array['family'] = 'family'; - $data_array['attribute'] = 'attribute'; - $data_array['category'] = 'category'; - $data_array['product'] = 'product'; - $data_array['option'] = 'option'; - - return ($data_array); + return [ + 'family' => 'family', + 'attribute' => 'attribute', + 'category' => 'category', + 'product' => 'product', + 'option' => 'option', + ]; } - public static function getValueArray0() + /** + * @return array> + */ + public static function getValueArray0(): array { - $data_array=array(); - foreach (GridOption::getOptionArray0() as $k=> $v) { - $data_array[]=array('value'=>$k,'label'=>$v); - } - return($data_array); + return array_map( + fn($k, $v) => ['value' => $k, 'label' => $v], + array_keys(self::getOptionArray0()), + self::getOptionArray0() + ); } } diff --git a/Block/Adminhtml/System/Config/Form/Field/TaxIdMapping.php b/Block/Adminhtml/System/Config/Form/Field/TaxIdMapping.php index 93b2993..d663a27 100644 --- a/Block/Adminhtml/System/Config/Form/Field/TaxIdMapping.php +++ b/Block/Adminhtml/System/Config/Form/Field/TaxIdMapping.php @@ -1,89 +1,45 @@ $data */ public function __construct( Context $context, - Factory $elementFactory, - Product $productTaxClassSource, + protected Factory $elementFactory, + protected Product $productTaxClassSource, array $data = [] ) { parent::__construct($context, $data); - - $this->elementFactory = $elementFactory; - $this->productTaxClassSource = $productTaxClassSource; } - /** - * Initialise form fields - * - * @return void - */ - protected function _construct() + protected function _construct(): void { $this->addColumn('akeneo', ['label' => __('Akeneo Attribute Option Code')]); $this->addColumn('magento', ['label' => __('Magento')]); - $this->_addAfter = false; - $this->_addButtonLabel = __('Add'); + $this->_addAfter = false; + $this->_addButtonLabel = (string)__('Add'); parent::_construct(); } - /** - * Render array cell for prototypeJS template - * - * @param string $columnName - * - * @return string - * @throws \Exception - */ - public function renderCellTemplate($columnName) + public function renderCellTemplate($columnName): string { - if (!in_array($columnName, ['magento']) || !isset($this->_columns[$columnName])) { + if (!in_array($columnName, ['magento'], true) || !isset($this->_columns[$columnName])) { return parent::renderCellTemplate($columnName); } - /** @var array $options */ - $options = []; - - if (isset($this->_columns[$columnName])) { - $options = $this->productTaxClassSource->getAllOptions(); - } + $options = $this->productTaxClassSource->getAllOptions(); /** @var Select $element */ $element = $this->elementFactory->create('select'); diff --git a/Block/Adminhtml/System/Config/Form/Field/Type.php b/Block/Adminhtml/System/Config/Form/Field/Type.php deleted file mode 100644 index 6d31cb7..0000000 --- a/Block/Adminhtml/System/Config/Form/Field/Type.php +++ /dev/null @@ -1,83 +0,0 @@ -attributeHelper = $attributeHelper; - $this->elementFactory = $elementFactory; - $this->customerGroup = $customerGroup; - } - - /** - * construct function - * - * @return void - */ - protected function _construct() - { - $this->addColumn('pim_type', ['label' => __('Akeneo Price Attribute Code (-EUR)')]); - $this->addColumn('magento_type', ['label' => __('Magento Customer Group')]); - $this->_addAfter = false; - $this->_addButtonLabel = __('Add'); - parent::_construct(); - } - - /** - * renderCellTemplate function - * - * @param string $columnName - * @return string - */ - public function renderCellTemplate($columnName) - { - if ($columnName != 'magento_type' || !isset($this->_columns[$columnName])) { - return parent::renderCellTemplate($columnName); - } - - $options = $this->customerGroup->toOptionArray(); - $element = $this->elementFactory->create('select'); - $element->setForm( - $this->getForm() - )->setName( - $this->_getCellInputElementName($columnName) - )->setHtmlId( - $this->_getCellInputElementId('<%- _id %>', $columnName) - )->setValues( - $options - ); - - return str_replace("\n", '', $element->getElementHtml()); - } -} diff --git a/Console/Command/ImportMetricUnits.php b/Console/Command/ImportMetricUnits.php index c13770c..f0e72f6 100644 --- a/Console/Command/ImportMetricUnits.php +++ b/Console/Command/ImportMetricUnits.php @@ -1,4 +1,5 @@ job = $job; - + public function __construct( + protected ImportMetricUnitsJob $job, + ?string $name = null + ) { parent::__construct($name); } @@ -26,8 +25,10 @@ protected function configure(): void parent::configure(); } - protected function execute(InputInterface $input, OutputInterface $output): void + protected function execute(InputInterface $input, OutputInterface $output): int { $this->job->execute($output); + + return self::SUCCESS; } } diff --git a/Console/Command/SetNotVisible.php b/Console/Command/SetNotVisible.php index 72c9b5e..ce98a07 100644 --- a/Console/Command/SetNotVisible.php +++ b/Console/Command/SetNotVisible.php @@ -1,4 +1,5 @@ job = $job; - + public function __construct( + protected SetNotVisibleJob $job, + ?string $name = null + ) { parent::__construct($name); } @@ -26,12 +25,14 @@ protected function configure(): void parent::configure(); } - protected function execute(InputInterface $input, OutputInterface $output): void + protected function execute(InputInterface $input, OutputInterface $output): int { $output->writeln('Starting'); $this->job->execute($output); $output->writeln('Finished!'); + + return self::SUCCESS; } } diff --git a/Console/Command/SlackNotificationCommand.php b/Console/Command/SlackNotificationCommand.php index 28864e9..4644f17 100644 --- a/Console/Command/SlackNotificationCommand.php +++ b/Console/Command/SlackNotificationCommand.php @@ -1,27 +1,23 @@ runSlackMessage = $runSlackMessage; + public function __construct( + protected RunSlackMessage $runSlackMessage, + ?string $name = null + ) { parent::__construct($name); } - protected function configure() + protected function configure(): void { $this->setName('slack:imports'); $this->setDescription( @@ -30,7 +26,7 @@ protected function configure() parent::configure(); } - protected function execute(InputInterface $input, OutputInterface $output) + protected function execute(InputInterface $input, OutputInterface $output): int { $this->runSlackMessage->execute($input, $output); diff --git a/Controller/Adminhtml/Akeneo/Delete.php b/Controller/Adminhtml/Akeneo/Delete.php new file mode 100644 index 0000000..2e650d6 --- /dev/null +++ b/Controller/Adminhtml/Akeneo/Delete.php @@ -0,0 +1,44 @@ +getRequest()->getParam('id'); + /** @var Redirect $resultRedirect */ + $resultRedirect = $this->resultRedirectFactory->create(); + + if ($id) { + try { + $model = $this->akeneoFactory->create(); + $model->load($id); // @phpstan-ignore-line + $model->delete(); // @phpstan-ignore-line + $this->messageManager->addSuccessMessage((string)__('The item has been deleted.')); + + return $resultRedirect->setPath('*/*/'); + } catch (\Exception $e) { + $this->messageManager->addErrorMessage($e->getMessage()); + + return $resultRedirect->setPath('*/*/edit', ['id' => $id]); + } + } + + $this->messageManager->addErrorMessage((string)__('We can\'t find an item to delete.')); + + return $resultRedirect->setPath('*/*/'); + } +} diff --git a/Controller/Adminhtml/Akeneo/Edit.php b/Controller/Adminhtml/Akeneo/Edit.php new file mode 100644 index 0000000..17b886f --- /dev/null +++ b/Controller/Adminhtml/Akeneo/Edit.php @@ -0,0 +1,77 @@ +resultPageFactory->create(); + $resultPage->setActiveMenu('JustBetter_AkeneoBundle::Akeneo') + ->addBreadcrumb((string)__('JustBetter AkeneoBundle'), (string)__('JustBetter AkeneoBundle')) + ->addBreadcrumb((string)__('Manage Item'), (string)__('Manage Item')); + + return $resultPage; + } + + public function execute(): Page|Redirect + { + $id = (int)$this->getRequest()->getParam('id'); + $model = $this->akeneoFactory->create(); + + if ($id) { + $model->load($id); // @phpstan-ignore-line + if (!$model->getId()) { + $this->messageManager->addErrorMessage((string)__('This item no longer exists.')); + /** @var Redirect $resultRedirect */ + $resultRedirect = $this->resultRedirectFactory->create(); + + return $resultRedirect->setPath('*/*/'); + } + } + + $data = $this->session->getFormData(true); + if (!empty($data)) { + $model->setData($data); + } + + $this->coreRegistry->register('akeneo', $model); + + /** @var Page $resultPage */ + $resultPage = $this->_initAction(); + $resultPage->setActiveMenu('JustBetter_AkeneoBundle::Akeneo'); + $resultPage->addBreadcrumb((string)__('JustBetter'), (string)__('JustBetter')); + $resultPage->addBreadcrumb( + (string)($id ? __('Edit Item') : __('New Item')), + (string)($id ? __('Edit Item') : __('New Item')) + ); + $resultPage->getConfig()->getTitle()->prepend((string)($id ? __('Edit Item') : __('New Item'))); + + return $resultPage; + } +} diff --git a/Controller/Adminhtml/Akeneo/ExportCsv.php b/Controller/Adminhtml/Akeneo/ExportCsv.php new file mode 100644 index 0000000..946ad66 --- /dev/null +++ b/Controller/Adminhtml/Akeneo/ExportCsv.php @@ -0,0 +1,37 @@ +layoutFactory->create(); + /** @var Grid $exportBlock */ + $exportBlock = $layout->createBlock(Grid::class); + + return $this->fileFactory->create( + $fileName, + $exportBlock->getCsvFile(), + DirectoryList::VAR_DIR + ); + } +} diff --git a/Controller/Adminhtml/Akeneo/ExportExcel.php b/Controller/Adminhtml/Akeneo/ExportExcel.php new file mode 100644 index 0000000..47180f6 --- /dev/null +++ b/Controller/Adminhtml/Akeneo/ExportExcel.php @@ -0,0 +1,37 @@ +layoutFactory->create(); + /** @var Grid $exportBlock */ + $exportBlock = $layout->createBlock(Grid::class); + + return $this->fileFactory->create( + $fileName, + $exportBlock->getExcelFile(), + DirectoryList::VAR_DIR + ); + } +} diff --git a/Controller/Adminhtml/Akeneo/Index.php b/Controller/Adminhtml/Akeneo/Index.php new file mode 100644 index 0000000..3594331 --- /dev/null +++ b/Controller/Adminhtml/Akeneo/Index.php @@ -0,0 +1,30 @@ +resultPageFactory->create(); + $resultPage->setActiveMenu('JustBetter_AkeneoBundle::akeneo'); + $resultPage->addBreadcrumb((string)__('JustBetter'), (string)__('JustBetter')); + $resultPage->addBreadcrumb((string)__('Manage item'), (string)__('Manage Akeneo')); + $resultPage->getConfig()->getTitle()->prepend((string)__('Manage Akeneo')); + + return $resultPage; + } +} diff --git a/Controller/Adminhtml/Akeneo/MassDelete.php b/Controller/Adminhtml/Akeneo/MassDelete.php new file mode 100644 index 0000000..4595a15 --- /dev/null +++ b/Controller/Adminhtml/Akeneo/MassDelete.php @@ -0,0 +1,44 @@ +getRequest()->getParam('akeneo'); + + if (!is_array($itemIds) || empty($itemIds)) { + $this->messageManager->addErrorMessage((string)__('Please select item(s).')); + } else { + try { + foreach ($itemIds as $itemId) { + $model = $this->akeneoFactory->create(); + $model->load($itemId); // @phpstan-ignore-line + $model->delete(); // @phpstan-ignore-line + } + $this->messageManager->addSuccessMessage( + (string)__('A total of %1 record(s) have been deleted.', count($itemIds)) + ); + } catch (\Exception $e) { + $this->messageManager->addErrorMessage($e->getMessage()); + } + } + + /** @var Redirect $resultRedirect */ + $resultRedirect = $this->resultRedirectFactory->create(); + return $resultRedirect->setPath('akeneomanager/*/index'); + } +} diff --git a/Controller/Adminhtml/Akeneo/MassStatus.php b/Controller/Adminhtml/Akeneo/MassStatus.php new file mode 100644 index 0000000..1766ac8 --- /dev/null +++ b/Controller/Adminhtml/Akeneo/MassStatus.php @@ -0,0 +1,45 @@ +getRequest()->getParam('akeneo'); + + if (!is_array($itemIds) || empty($itemIds)) { + $this->messageManager->addErrorMessage((string)__('Please select item(s).')); + } else { + try { + $status = (int)$this->getRequest()->getParam('status'); + foreach ($itemIds as $itemId) { + $model = $this->akeneoFactory->create(); + $model->load($itemId); // @phpstan-ignore-line + $model->setIsActive($status)->save(); // @phpstan-ignore-line + } + $this->messageManager->addSuccessMessage( + (string)__('A total of %1 record(s) have been updated.', count($itemIds)) + ); + } catch (\Exception $e) { + $this->messageManager->addErrorMessage($e->getMessage()); + } + } + + /** @var Redirect $resultRedirect */ + $resultRedirect = $this->resultRedirectFactory->create(); + return $resultRedirect->setPath('akeneomanager/*/index'); + } +} diff --git a/Controller/Adminhtml/Akeneo/NewAction.php b/Controller/Adminhtml/Akeneo/NewAction.php new file mode 100644 index 0000000..06bef2f --- /dev/null +++ b/Controller/Adminhtml/Akeneo/NewAction.php @@ -0,0 +1,31 @@ +resultForwardFactory->create(); // @phpstan-ignore-line + + return $resultForward->forward('edit'); + } + + protected function _isAllowed(): bool + { + return true; + } +} diff --git a/Controller/Adminhtml/Akeneo/Save.php b/Controller/Adminhtml/Akeneo/Save.php new file mode 100644 index 0000000..24a2f90 --- /dev/null +++ b/Controller/Adminhtml/Akeneo/Save.php @@ -0,0 +1,66 @@ +getRequest(); + $data = $request->getPostValue(); + + /** @var Redirect $resultRedirect */ + $resultRedirect = $this->resultRedirectFactory->create(); + + if ($data) { + $model = $this->akeneoFactory->create(); + + $id = (int)$this->getRequest()->getParam('id'); + if ($id) { + $model->load($id); // @phpstan-ignore-line + $model->setCreatedAt(date('Y-m-d H:i:s')); + } + + $model->setData($data); + + try { + $model->save(); // @phpstan-ignore-line + $this->messageManager->addSuccessMessage((string)__('The Akeneo has been saved.')); + $this->session->setFormData(false); + + if ($this->getRequest()->getParam('back')) { + return $resultRedirect->setPath('*/*/edit', ['id' => $model->getId(), '_current' => true]); + } + + return $resultRedirect->setPath('*/*/'); + } catch (\Magento\Framework\Exception\LocalizedException $e) { + $this->messageManager->addErrorMessage($e->getMessage()); + } catch (\RuntimeException $e) { + $this->messageManager->addErrorMessage($e->getMessage()); + } catch (\Exception $e) { + $this->messageManager->addExceptionMessage($e, (string)__('Something went wrong while saving the Akeneo.')); + } + + $this->session->setFormData($data); + + return $resultRedirect->setPath('*/*/edit', ['id' => $id]); + } + + return $resultRedirect->setPath('*/*/'); + } +} diff --git a/Controller/Adminhtml/akeneo/Delete.php b/Controller/Adminhtml/akeneo/Delete.php deleted file mode 100755 index c78c5e2..0000000 --- a/Controller/Adminhtml/akeneo/Delete.php +++ /dev/null @@ -1,39 +0,0 @@ -getRequest()->getParam('id'); - /** @var \Magento\Backend\Model\View\Result\Redirect $resultRedirect */ - $resultRedirect = $this->resultRedirectFactory->create(); - if ($id) { - try { - // init model and delete - $model = $this->_objectManager->create('JustBetter\AkeneoBundle\Model\Akeneo'); - $model->load($id); - $model->delete(); - // display success message - $this->messageManager->addSuccess(__('The item has been deleted.')); - return $resultRedirect->setPath('*/*/'); - } catch (\Exception $e) { - // display error message - $this->messageManager->addError($e->getMessage()); - // go back to edit form - return $resultRedirect->setPath('*/*/edit', ['id' => $id]); - } - } - // display error message - $this->messageManager->addError(__('We can\'t find a item to delete.')); - // go to grid - return $resultRedirect->setPath('*/*/'); - } -} diff --git a/Controller/Adminhtml/akeneo/Edit.php b/Controller/Adminhtml/akeneo/Edit.php deleted file mode 100755 index 7c2af9f..0000000 --- a/Controller/Adminhtml/akeneo/Edit.php +++ /dev/null @@ -1,109 +0,0 @@ -resultPageFactory = $resultPageFactory; - $this->_coreRegistry = $registry; - parent::__construct($context); - } - - /** - * {@inheritdoc} - */ - protected function _isAllowed() - { - return true; - } - - /** - * Init actions - * - * @return \Magento\Backend\Model\View\Result\Page - */ - protected function _initAction() - { - // load layout, set active menu and breadcrumbs - /** @var \Magento\Backend\Model\View\Result\Page $resultPage */ - $resultPage = $this->resultPageFactory->create(); - $resultPage->setActiveMenu('JustBetter_AkeneoBundle::Akeneo') - ->addBreadcrumb(__('JustBetter AkeneoBundle'), __('JustBetter AkeneoBundle')) - ->addBreadcrumb(__('Manage Item'), __('Manage Item')); - return $resultPage; - } - - /** - * Edit Item - * - * @return \Magento\Backend\Model\View\Result\Page|\Magento\Backend\Model\View\Result\Redirect - * @SuppressWarnings(PHPMD.NPathComplexity) - */ - public function execute() - { - // 1. Get ID and create model - $id = $this->getRequest()->getParam('id'); - $model = $this->_objectManager->create('JustBetter\AkeneoBundle\Model\Akeneo'); - - // 2. Initial checking - if ($id) { - $model->load($id); - if (!$model->getId()) { - $this->messageManager->addError(__('This item no longer exists.')); - /** \Magento\Backend\Model\View\Result\Redirect $resultRedirect */ - $resultRedirect = $this->resultRedirectFactory->create(); - - return $resultRedirect->setPath('*/*/'); - } - } - - // 3. Set entered data if was error when we do save - $data = $this->_objectManager->get('Magento\Backend\Model\Session')->getFormData(true); - if (!empty($data)) { - $model->setData($data); - } - - // 4. Register model to use later in blocks - $this->_coreRegistry->register('akeneo', $model); - - // 5. Build edit form - /** @var \Magento\Backend\Model\View\Result\Page $resultPage */ - $resultPage = $this->_initAction(); - $resultPage->setActiveMenu('JustBetter_AkeneoBundle::Akeneo'); - $resultPage->addBreadcrumb(__('JustBetter'), __('JustBetter')); - $resultPage->addBreadcrumb( - $id ? __('Edit Item') : __('New Item'), - $id ? __('Edit Item') : __('New Item') - ); - $resultPage->getConfig()->getTitle()->prepend($id ? __('Edit Item') : __('New Item')); - //$resultPage->getConfig()->getTitle()->prepend($model->getId() ? $model->getTitle() : __('New Item')); - - return $resultPage; - } -} diff --git a/Controller/Adminhtml/akeneo/ExportCsv.php b/Controller/Adminhtml/akeneo/ExportCsv.php deleted file mode 100755 index b35e3d8..0000000 --- a/Controller/Adminhtml/akeneo/ExportCsv.php +++ /dev/null @@ -1,33 +0,0 @@ -_view->loadLayout(false); - - $fileName = 'akeneo.csv'; - - $exportBlock = $this->_view->getLayout()->createBlock('JustBetter\AkeneoBundle\Block\Adminhtml\Akeneo\Grid'); - - $objectManager = ObjectManager::getInstance(); - - $this->_fileFactory = $objectManager->create('Magento\Framework\App\Response\Http\FileFactory'); - - return $this->_fileFactory->create( - $fileName, - $exportBlock->getCsvFile(), - DirectoryList::VAR_DIR - ); - } -} diff --git a/Controller/Adminhtml/akeneo/ExportExcel.php b/Controller/Adminhtml/akeneo/ExportExcel.php deleted file mode 100755 index 4b2630b..0000000 --- a/Controller/Adminhtml/akeneo/ExportExcel.php +++ /dev/null @@ -1,34 +0,0 @@ -_view->loadLayout(false); - - $fileName = 'akeneo.xml'; - - $exportBlock = $this->_view->getLayout()->createBlock('JustBetter\AkeneoBundle\Block\Adminhtml\Akeneo\Grid'); - - $objectManager = ObjectManager::getInstance(); - - $this->_fileFactory = $objectManager->create('Magento\Framework\App\Response\Http\FileFactory'); - - - return $this->_fileFactory->create( - $fileName, - $exportBlock->getExcelFile(), - DirectoryList::VAR_DIR - ); - } -} diff --git a/Controller/Adminhtml/akeneo/Index.php b/Controller/Adminhtml/akeneo/Index.php deleted file mode 100755 index 1fb6af0..0000000 --- a/Controller/Adminhtml/akeneo/Index.php +++ /dev/null @@ -1,48 +0,0 @@ -resultPageFactory = $resultPageFactory; - } - - /** - * Index action - * - * @return void - */ - public function execute() - { - /** @var \Magento\Backend\Model\View\Result\Page $resultPage */ - $resultPage = $this->resultPageFactory->create(); - $resultPage->setActiveMenu('JustBetter_AkeneoBundle::akeneo'); - $resultPage->addBreadcrumb(__('JustBetter'), __('JustBetter')); - $resultPage->addBreadcrumb(__('Manage item'), __('Manage Akeneo')); - $resultPage->getConfig()->getTitle()->prepend(__('Manage Akeneo')); - - return $resultPage; - } -} \ No newline at end of file diff --git a/Controller/Adminhtml/akeneo/MassDelete.php b/Controller/Adminhtml/akeneo/MassDelete.php deleted file mode 100755 index 1d94c4b..0000000 --- a/Controller/Adminhtml/akeneo/MassDelete.php +++ /dev/null @@ -1,35 +0,0 @@ -getRequest()->getParam('akeneo'); - if (!is_array($itemIds) || empty($itemIds)) { - $this->messageManager->addError(__('Please select item(s).')); - } else { - try { - foreach ($itemIds as $itemId) { - $post = $this->_objectManager->get('JustBetter\AkeneoBundle\Model\Akeneo')->load($itemId); - $post->delete(); - } - $this->messageManager->addSuccess( - __('A total of %1 record(s) have been deleted.', count($itemIds)) - ); - } catch (\Exception $e) { - $this->messageManager->addError($e->getMessage()); - } - } - return $this->resultRedirectFactory->create()->setPath('akeneomanager/*/index'); - } -} diff --git a/Controller/Adminhtml/akeneo/MassStatus.php b/Controller/Adminhtml/akeneo/MassStatus.php deleted file mode 100755 index 2fe0e1f..0000000 --- a/Controller/Adminhtml/akeneo/MassStatus.php +++ /dev/null @@ -1,37 +0,0 @@ -getRequest()->getParam('akeneo'); - if (!is_array($itemIds) || empty($itemIds)) { - $this->messageManager->addError(__('Please select item(s).')); - } else { - try { - $status = (int) $this->getRequest()->getParam('status'); - foreach ($itemIds as $postId) { - $post = $this->_objectManager->get('JustBetter\AkeneoBundle\Model\Akeneo')->load($postId); - $post->setIsActive($status)->save(); - } - $this->messageManager->addSuccess( - __('A total of %1 record(s) have been updated.', count($itemIds)) - ); - } catch (\Exception $e) { - $this->messageManager->addError($e->getMessage()); - } - } - return $this->resultRedirectFactory->create()->setPath('akeneomanager/*/index'); - } - -} diff --git a/Controller/Adminhtml/akeneo/NewAction.php b/Controller/Adminhtml/akeneo/NewAction.php deleted file mode 100755 index b131353..0000000 --- a/Controller/Adminhtml/akeneo/NewAction.php +++ /dev/null @@ -1,47 +0,0 @@ -resultForwardFactory = $resultForwardFactory; - } - - /** - * Forward to edit - * - * @return \Magento\Backend\Model\View\Result\Forward - */ - public function execute() - { - /** @var \Magento\Backend\Model\View\Result\Forward $resultForward */ - $resultForward = $this->resultForwardFactory->create(); - return $resultForward->forward('edit'); - } - - /** - * {@inheritdoc} - */ - protected function _isAllowed() - { - return true; - } -} diff --git a/Controller/Adminhtml/akeneo/Save.php b/Controller/Adminhtml/akeneo/Save.php deleted file mode 100755 index 153bc3c..0000000 --- a/Controller/Adminhtml/akeneo/Save.php +++ /dev/null @@ -1,62 +0,0 @@ -getRequest()->getPostValue(); - - /** @var \Magento\Backend\Model\View\Result\Redirect $resultRedirect */ - $resultRedirect = $this->resultRedirectFactory->create(); - if ($data) { - $model = $this->_objectManager->create('JustBetter\AkeneoBundle\Model\Akeneo'); - - $id = $this->getRequest()->getParam('id'); - if ($id) { - $model->load($id); - $model->setCreatedAt(date('Y-m-d H:i:s')); - } - - $model->setData($data); - - try { - $model->save(); - $this->messageManager->addSuccess(__('The Akeneo has been saved.')); - $this->_objectManager->get('Magento\Backend\Model\Session')->setFormData(false); - if ($this->getRequest()->getParam('back')) { - return $resultRedirect->setPath('*/*/edit', ['id' => $model->getId(), '_current' => true]); - } - return $resultRedirect->setPath('*/*/'); - } catch (\Magento\Framework\Exception\LocalizedException $e) { - $this->messageManager->addError($e->getMessage()); - } catch (\RuntimeException $e) { - $this->messageManager->addError($e->getMessage()); - } catch (\Exception $e) { - $this->messageManager->addException($e, __('Something went wrong while saving the Akeneo.')); - } - - $this->_getSession()->setFormData($data); - return $resultRedirect->setPath('*/*/edit', ['id' => $this->getRequest()->getParam('id')]); - } - return $resultRedirect->setPath('*/*/'); - } -} diff --git a/Cron/SlackNotificationCron.php b/Cron/SlackNotificationCron.php index 9db2f17..d01e7fe 100644 --- a/Cron/SlackNotificationCron.php +++ b/Cron/SlackNotificationCron.php @@ -1,4 +1,5 @@ runSlackMessage = $runSlackMessage; + public function __construct( + protected RunSlackMessage $runSlackMessage + ) { } - /** - * Write to system.log - * - * @return void - */ - public function execute() + public function execute(): void { $this->runSlackMessage->execute(); } diff --git a/Data/FamilyOptions.php b/Data/FamilyOptions.php index 23d1506..5ef601e 100644 --- a/Data/FamilyOptions.php +++ b/Data/FamilyOptions.php @@ -1,4 +1,5 @@ collectionFactory = $collectionFactory; } + /** + * @return array + */ public function toOptionArray(): array { - return array_map(function ($set) { - return [ - 'value' => $set->getData('attribute_set_id'), - 'label' => $set->getData('attribute_set_name') - ]; - }, $this->collectionFactory->create()->getItems()); + return array_map(fn ($set) => [ + 'value' => $set->getData('attribute_set_id'), + 'label' => $set->getData('attribute_set_name'), + ], $this->collectionFactory->create()->getItems()); // @phpstan-ignore-line } -} \ No newline at end of file +} diff --git a/FEATURES.md b/FEATURES.md new file mode 100644 index 0000000..eebb51f --- /dev/null +++ b/FEATURES.md @@ -0,0 +1,257 @@ +# Features Documentation + +This document provides detailed information about all features available in the JustBetter Akeneo Bundle. + +**[โ† Back to README](README.md)** + +## Table of Contents + +- [Configuration Guide](#configuration-guide) +- [Product Import Features](#product-import-features) + - [Important Attributes](#important-attributes) + - [Default Store Values for Required Attributes](#default-store-values) + - [Exclude Families from Import](#exclude-families) + - [Remove Redundant EAV Attributes](#remove-redundant-eav) +- [Category Features](#category-features) + - [Category Exist - Skip URL Path Regeneration](#category-exist) +- [Tax & Pricing Features](#tax--pricing-features) + - [Set Tax Class](#set-tax-class) +- [Attribute Features](#attribute-features) + - [Metric Units Import](#metric-units) + - [Format Media Name (SEO Friendly)](#format-media-name) +- [Visibility Features](#visibility-features) + - [Set Families to Not Visible Individually](#not-visible-individually) +- [Website Association Features](#website-association-features) + - [Required Attribute Mapping - Website Validation](#required-attribute-mapping) +- [Management & Administration](#management--administration) + - [Akeneo Manager](#akeneo-manager) +- [Notification Features](#notification-features) + - [Slack Notifications](#slack-notifications) +- [Event System](#event-system) + - [Import Finished Events](#import-finished-events) + +--- + +## Configuration Guide + + +All features are configured via the Magento Admin Panel under: + +**`Stores > Configuration > Catalog > Akeneo Connector > JustBetter Akeneo`** + +### Configuration Sections + +#### Main Configuration +**Path:** `Stores > Configuration > Catalog > Akeneo Connector > JustBetter Akeneo` + +#### Products Configuration +**Path:** `Stores > Configuration > Catalog > Akeneo Connector > Products` + +- **Customer Group Pricing:** Grid mapping Akeneo attribute codes to Magento customer groups +- **Tax Class Mapping:** Grid mapping Akeneo tax codes to Magento tax class IDs +- **Required Attribute Mapping:** Grid defining required attributes per website + +#### Products Filters +**Path:** `Stores > Configuration > Catalog > Akeneo Connector > Products Filters` + +- **Excluded Families:** Multiselect of families to exclude from import + +--- + +## Product Import Features + +### Important Attributes + + +Select attributes that should always be imported and added to product temporary tables even when all values are empty. This ensures attribute columns are always present during import, fixing cases where bulk emptying attributes in Akeneo isn't reflected in Magento. + +**Configuration:** `Stores > Configuration > Catalog > Akeneo Connector > JustBetter Akeneo > Important Attributes` + +--- + +### Default Store Values for Required Attributes + + +Automatically sets default language values for required product attributes when the admin channel value is missing. Ensures all required attributes are populated using a fallback language. + +**Configuration:** +- Enable: `Stores > Configuration > Catalog > Akeneo Connector > JustBetter Akeneo > Default Store Values` +- Fallback Language: `Stores > Configuration > Catalog > Akeneo Connector > JustBetter Akeneo > Default Language` (e.g., `nl_NL`) + +--- + +### Exclude Families from Import + + +Prevents specific product families from being imported. Products belonging to excluded families will be completely skipped during the Akeneo import process. + +**Configuration:** `Stores > Configuration > Catalog > Akeneo Connector > Products Filters > Excluded Families` + +**Note:** This feature works in combination with the "Family Attribute as Filter" configuration. + +--- + +### Remove Redundant EAV Attributes + + + +Automatically cleans up orphaned EAV values when a product's family changes in Akeneo. Removes attribute values that no longer belong to the product's new attribute set. + +**Configuration:** `Stores > Configuration > Catalog > Akeneo Connector > JustBetter Akeneo > Remove Redundant EAV Attributes` + +**Example:** Product changes from Family A (attributes: name, price, color) to Family B (attributes: name, price, weight). The "color" EAV values are automatically deleted. + +--- + +## Category Features + +### Category Exist - Skip URL Path Regeneration + + +When enabled, preserves existing category URL keys instead of regenerating them during import. Improves performance by skipping unnecessary URL path updates for categories that already exist. + +**Configuration:** `Stores > Configuration > Catalog > Akeneo Connector > JustBetter Akeneo > Category Exist` + +--- + +## Tax & Pricing Features + +### Set Tax Class + + +Maps Akeneo tax class attribute values to Magento tax class IDs. Supports both non-localizable and localizable tax attributes across multiple channels and locales. + +**Configuration:** +- Enable: `Stores > Configuration > Catalog > Akeneo Connector > JustBetter Akeneo > Set Tax Class` +- Mapping: `Stores > Configuration > Catalog > Akeneo Connector > Products > Tax Class Mapping` (Grid: Akeneo Code โ†’ Magento Tax Class) + +**Requirements:** Map the Akeneo tax attribute in the Attribute Types configuration with type "tax". + +--- + +## Attribute Features + +### Metric Units Import + + +Imports Akeneo metric attribute units into Magento's `eav_attribute.unit` field. Supports channel-specific unit conversions. + +**Configuration:** +- Enable: `Stores > Configuration > Catalog > Akeneo Connector > JustBetter Akeneo > Enable Metric Units` +- Channel: `Stores > Configuration > Catalog > Akeneo Connector > JustBetter Akeneo > Metric Conversion Channel` + +**Usage:** +- Automatically runs after attribute import +- Manual execution: `bin/magento metric:import` + +--- + +### Format Media Name (SEO Friendly) + + +Replaces underscores with hyphens in media file names for SEO-friendly URLs. + +**Configuration:** `Stores > Configuration > Catalog > Akeneo Connector > JustBetter Akeneo > Format Media Name` + +**Example:** `product_image_2024.jpg` โ†’ `product-image-2024.jpg` + +--- + +## Visibility Features + +### Set Families to Not Visible Individually + + +Automatically sets visibility to "Not Visible Individually" for all products belonging to selected families. Useful for components, configurable product children, or internal-use products. + +**Configuration:** `Stores > Configuration > Catalog > Akeneo Connector > JustBetter Akeneo > Not Visible Families` + +**Usage:** +- Automatically runs after product import +- Manual execution: `bin/magento akeneo:setfamilynotvisible` + +--- + +## Website Association Features + +### Required Attribute Mapping - Website Validation + + +Validates that required product attributes contain values before assigning website associations. Automatically removes websites from products when required attributes are empty for that website's channel/locale. + +**Configuration:** `Stores > Configuration > Catalog > Akeneo Connector > Products > Required Attribute Mapping` (Grid: Akeneo Attribute) + +**How it works:** +1. Gets mapped channel from website configuration +2. Checks if required attribute values exist for channel locales +3. Removes website from association if any required attribute is empty + +**Example:** Product mapped to US website requires "description". If "description-en_US-ecommerce" is empty, US website association is removed. + +--- + +## Management & Administration + +### Akeneo Manager + + +Manual mapping interface for adjusting Akeneo codes versus Magento entity IDs in the connector mapping tables. + +**Configuration:** `Stores > Configuration > Catalog > Akeneo Connector > JustBetter Akeneo > Akeneo Manager` +**Access:** `JUSTBETTER > Akeneo Manager` (when enabled) + +**Features:** Create, edit, and delete mappings for families, categories, attributes, and other entities synced between Akeneo and Magento. + +--- + +## Notification Features + +### Slack Notifications + + +Sends daily import status notifications to Slack at 8:00 AM. Reports successful imports, failures, or imports still in processing state. + +**Configuration:** `Stores > Configuration > Catalog > Akeneo Connector > JustBetter Akeneo > Slack` +- Enable Slack: Yes/No +- Token: Slack bot token +- Username: Bot display name +- Channel: Slack channel ID or name +- API: Slack API URL + +**Schedule:** Daily at 08:00 (cron: `0 8 * * *`) + +**Message Formats:** +- โœ… Success: "All of today's imports in *Store Name* have been successfully completed." +- โš ๏ธ Warning: Lists failed imports with timestamp and name +- โš ๏ธ Processing: Lists imports still running with timestamp and name +- โš ๏ธ No Imports: "No imports have been made today." + +**Usage:** Manual execution: `bin/magento slack:notification` + +--- + +## Event System + +### Import Finished Events + + +Custom event system that dispatches entity-specific events when imports are fully completed, enabling custom post-import logic. + +**Available Events:** +```php +akeneo_connector_import_finish_category +akeneo_connector_import_finish_family +akeneo_connector_import_finish_attribute +akeneo_connector_import_finish_option +akeneo_connector_import_finish_product +``` + +**Event Data:** `JobExecutor` instance available via `$observer->getData('import')` + +**Important:** Product import runs per family (since Akeneo Connector 102.1.1), so `akeneo_connector_import_finish_product` fires multiple times if importing multiple families. + +**Note:** The `akeneo_connector_import_finish_product` event fires multiple times (once per family) since Akeneo Connector 102.1.1. Use `$import->getFamily()` to process specific families only. + +--- + +**[โ† Back to README](README.md)** diff --git a/Helper/Import/Product.php b/Helper/Import/Product.php index 9ee8813..4d20fda 100644 --- a/Helper/Import/Product.php +++ b/Helper/Import/Product.php @@ -1,15 +1,22 @@ $result + * @param array $keys + * @return array + */ protected function getColumnsFromResult(array $result, array $keys = []): array { - // This returns the result for the temp table DB columns. ex: 'name-nl_NL-ecommerce' => 'value' $mappedResult = parent::getColumnsFromResult($result, $keys); if (!$this->scopeConfig->getValue('akeneo_connector/justbetter/defaultstorevalues') || !array_key_exists('values', $result)) { @@ -17,23 +24,19 @@ protected function getColumnsFromResult(array $result, array $keys = []): array } $adminChannel = $this->scopeConfig->getValue('akeneo_connector/akeneo_api/admin_channel'); - $defaultLanguage = $this->scopeConfig->getValue('akeneo_connector/justbetter/defaultlanguage'); - $requiredAttributes = $this->getRequiredAttributes(); foreach ($requiredAttributes as $requiredAttribute) { - - if ( - !array_key_exists($requiredAttribute, $result['values']) || - count($result['values'][$requiredAttribute]) == 0 || + if (!array_key_exists($requiredAttribute, $result['values']) || + count($result['values'][$requiredAttribute]) === 0 || !$this->isScopableOrLocalizable($requiredAttribute, $mappedResult) ) { continue; } - if (!array_key_exists($requiredAttribute.'-'.$defaultLanguage.'-'.$adminChannel, $mappedResult) && $defaultLanguage) { - $mappedResult[$requiredAttribute.'-'.$defaultLanguage.'-'.$adminChannel] = $this->getFirstValue($result['values'][$requiredAttribute]); + if (!array_key_exists($requiredAttribute . '-' . $defaultLanguage . '-' . $adminChannel, $mappedResult) && $defaultLanguage) { + $mappedResult[$requiredAttribute . '-' . $defaultLanguage . '-' . $adminChannel] = $this->getFirstValue($result['values'][$requiredAttribute]); } $mappedResult[$requiredAttribute] = $this->getFirstValue($result['values'][$requiredAttribute]); @@ -42,24 +45,29 @@ protected function getColumnsFromResult(array $result, array $keys = []): array return $mappedResult; } + /** + * @param array> $values + */ protected function getFirstValue(array $values): mixed { $array = array_reverse($values); + return array_pop($array)['data'] ?? ''; } - /** Check if an attribute is scopeable or localizable based on the column result name, ex. name-nl_NL-ecommerce */ + /** + * @param array $columnResult + */ protected function isScopableOrLocalizable(string $attributeCode, array $columnResult): bool { $columns = array_keys($columnResult); foreach ($columns as $column) { - if ($column === $attributeCode) { return false; } - if (substr($column, 0, strlen($attributeCode)) === $attributeCode) { + if (str_starts_with($column, $attributeCode)) { return true; } } @@ -67,20 +75,24 @@ protected function isScopableOrLocalizable(string $attributeCode, array $columnR return false; } - /** Get a list of Magento's required product attributes */ + /** + * @return array + */ protected function getRequiredAttributes(): array { $eavAttributeTable = $this->connection->getTableName('eav_attribute'); $eavEntityTypeTable = $this->connection->getTableName('eav_entity_type'); - $select = $this->connection->select() + $select = $this->connection->select() // @phpstan-ignore-line ->from("$eavAttributeTable AS attr") - ->join("$eavEntityTypeTable AS type", - "attr.entity_type_id = type.entity_type_id AND type.entity_type_code = 'catalog_product'") + ->join( + "$eavEntityTypeTable AS type", + "attr.entity_type_id = type.entity_type_id AND type.entity_type_code = 'catalog_product'" + ) ->where('is_required = 1'); - $requiredAttributes = $this->connection->fetchAll($select); + $requiredAttributes = $this->connection->fetchAll($select); // @phpstan-ignore-line return array_map(fn (array $attribute) => $attribute['attribute_code'], $requiredAttributes); } -} \ No newline at end of file +} diff --git a/Helper/SlackHelper.php b/Helper/SlackHelper.php index fc11065..f55c558 100644 --- a/Helper/SlackHelper.php +++ b/Helper/SlackHelper.php @@ -1,20 +1,16 @@ scopeConfig->getValue( $field, @@ -23,22 +19,13 @@ public function getConfigValue($field, int|string|null $storeId = null) ); } - /** - * getGeneralConfig function - * @param String $code - * @param null $storeId - */ - public function getGeneralConfig($code, int|string|null $storeId = null) + public function getGeneralConfig(string $code, ?int $storeId = null): mixed { return $this->getConfigValue(self::XML_PATH . $code, $storeId); } - /** - * isEnable function - * @return boolean - */ - public function isEnable() + public function isEnable(): bool { - return $this->getGeneralConfig('enable'); + return (bool)$this->getGeneralConfig('enable'); } } diff --git a/Job/ImportMetricUnits.php b/Job/ImportMetricUnits.php index f34d6aa..f5e0986 100644 --- a/Job/ImportMetricUnits.php +++ b/Job/ImportMetricUnits.php @@ -1,10 +1,10 @@ authenticator = $authenticator; - $this->attributeRepository = $attributeRepository; - $this->config = $config; } public function execute(?OutputInterface $output = null): void { - if (! $this->authenticator->getAkeneoApiClient()) { + if (!$this->authenticator->getAkeneoApiClient()) { if ($output) { $output->writeln('Akeneo client not configured!'); } + return; } - + if (!$this->config->getValue(static::CONFIG_PREFIX . static::ENABLED_CONFIG_KEY)) { if ($output) { $output->writeln('Metrics not enabled!'); } + return; } @@ -63,15 +58,16 @@ public function execute(?OutputInterface $output = null): void if ($output) { $output->writeln("Skipping $code because it does not exist in Magento"); } + continue; } - if ($magentoAttribute->getData(self::EAV_ATTRIBUTE_UNIT_FIELD) == $unit) { + if ($magentoAttribute->getData(self::EAV_ATTRIBUTE_UNIT_FIELD) == $unit) { // @phpstan-ignore-line continue; } - $magentoAttribute->setData(self::EAV_ATTRIBUTE_UNIT_FIELD, $unit); - $magentoAttribute->save(); + $magentoAttribute->setData(self::EAV_ATTRIBUTE_UNIT_FIELD, $unit); // @phpstan-ignore-line + $this->attributeRepository->save($magentoAttribute); if ($output) { $output->writeln("Set unit for $code to $unit"); @@ -83,13 +79,16 @@ public function execute(?OutputInterface $output = null): void } } - protected function getMetricAttributes(): ResourceCursor + protected function getMetricAttributes(): ResourceCursorInterface { $search = (new SearchBuilder())->addFilter('type', 'IN', ['pim_catalog_metric']); return $this->authenticator->getAkeneoApiClient()->getAttributeApi()->all(100, ['search' => $search->getFilters()]); } + /** + * @return array + */ protected function getChannelConversions(): array { $channel = $this->config->getValue(static::CONFIG_PREFIX . static::CHANNEL_CONFIG_KEY); diff --git a/Job/RunSlackMessage.php b/Job/RunSlackMessage.php index dbdc47b..770aa37 100644 --- a/Job/RunSlackMessage.php +++ b/Job/RunSlackMessage.php @@ -1,24 +1,30 @@ logs = $this->getLogs(); + } + + public function execute(?InputInterface $input = null, ?OutputInterface $output = null): void { $message = $this->getMessage(); if ($this->helperData->isEnable()) { @@ -31,77 +37,51 @@ public function execute(?InputInterface $input = null, ?OutputInterface $output } } - public function __construct(Client $client, SlackHelper $helperData, Log\Collection $logCollection, SlackMessage $slackMessage) - { - $this->client = $client; - $this->helperData = $helperData; - $this->logCollection = $logCollection; - $this->logs = $this->getLogs(); - $this->slackMessage = $slackMessage; - } - - /** - * Gets a collection of all Import logs of today - * @return Log\Collection - */ - protected function getLogs() + protected function getLogs(): Log\Collection { + $tomorrow = strtotime(date('Y-m-d') . ' +1 day'); + return $this->logCollection ->addFieldToFilter('created_at', ['gteq' => date('Y-m-d')]) - ->addFieldToFilter('created_at', ['lt' => date('Y-m-d', strtotime(date('Y-m-d') . ' +1 day'))]); + ->addFieldToFilter('created_at', ['lt' => date('Y-m-d', $tomorrow !== false ? $tomorrow : strtotime('tomorrow'))]); } - /** - * Gets a collection of all Import logs of today with a specific status - * @param int $status - * @return Log\Collection - */ - protected function getLogsByStatus(int $status) + protected function getLogsByStatus(int $status): Log\Collection { $logs = clone $this->logs; - return $logs->addFieldToFilter('status', $status); + + return $logs->addFieldToFilter('status', (string)$status); } - /** - * Checks if a log with a specific status exists in the collection - * @param int $status - * @return bool - */ - protected function checkLogStatus(int $status) + protected function checkLogStatus(int $status): bool { foreach ($this->logs as $log) { - if ($log->getStatus() == $status) { + if ($log->getStatus() === $status) { return true; } } + return false; } - /** - * Get the message to be sent - * @return string - */ - protected function getMessage() + protected function getMessage(): string { if (!$this->logs->getData()) { return $this->slackMessage->noImports(); - } elseif ($this->checkLogStatus(ImportInterface::IMPORT_ERROR) || + } + + if ($this->checkLogStatus(ImportInterface::IMPORT_ERROR) || $this->checkLogStatus(ImportInterface::IMPORT_PROCESSING)) { return $this->slackMessage->warning( $this->getLogsByStatus(ImportInterface::IMPORT_ERROR), $this->getLogsByStatus(ImportInterface::IMPORT_PROCESSING) ); } + return $this->slackMessage->success(); } - /** - * Sends the message to Slack - * @param string $message - * @return string - * @throws \GuzzleHttp\Exception\GuzzleException - */ - protected function send(string $message) + protected function send(string $message): string { try { $slackApi = $this->helperData->getGeneralConfig('api'); @@ -110,17 +90,19 @@ protected function send(string $message) 'token' => $this->helperData->getGeneralConfig('token'), 'channel' => $this->helperData->getGeneralConfig('channel'), 'text' => $message, - 'username' => $this->helperData->getGeneralConfig('username') + 'username' => $this->helperData->getGeneralConfig('username'), ]]); + return 'โœ… Message has been send to Slack channel: ' . $this->helperData->getGeneralConfig('channel') . ''; } catch (RequestException $e) { - $response = - 'โš ๏ธ There\'s a problem with sending the message to Slack channel: ' + $response = $e->getResponse(); + $responseBody = $response ? (string)$response->getBody() : 'No response body'; + + return 'โš ๏ธ There\'s a problem with sending the message to Slack channel: ' . $this->helperData->getGeneralConfig('channel') . " \n\n" . 'The following exception appeared:' - . '' . "\n\n" . $e->getResponse() . ''; - return $response; + . '' . "\n\n" . $responseBody . ''; } } } diff --git a/Job/SetNotVisible.php b/Job/SetNotVisible.php index 5a1663c..8f4cba7 100644 --- a/Job/SetNotVisible.php +++ b/Job/SetNotVisible.php @@ -1,16 +1,11 @@ collectionFactory = $collectionFactory; - $this->config = $config; - $this->action = $action; + public function __construct( + protected CollectionFactory $collectionFactory, // @phpstan-ignore-line + protected ScopeConfigInterface $config, + protected Action $action + ) { } public function execute(?OutputInterface $output = null): void { - $products = $this->collectionFactory->create() + $products = $this->collectionFactory->create() // @phpstan-ignore-line ->addFieldToFilter('attribute_set_id', ['in' => $this->getNotVisibleFamilies()]) ->addFieldToFilter('visibility', ['neq' => '1']) ->getItems(); - if (count($products) == 0) { + if (count($products) === 0) { if ($output) { $output->writeln('No updates necessary'); } + return; } @@ -47,13 +39,14 @@ public function execute(?OutputInterface $output = null): void $output->writeln('Found ' . count($products) . ' products that should have visibility set to not visible individually'); } - $entityIds = array_map(function ($p) { - return $p->getEntityId(); - }, $products); + $entityIds = array_map(fn ($p) => $p->getEntityId(), $products); $this->action->updateAttributes($entityIds, ['visibility' => '1'], 0); } + /** + * @return array + */ protected function getNotVisibleFamilies(): array { return explode( diff --git a/Job/SlackMessage.php b/Job/SlackMessage.php index 8cea5c0..632786d 100644 --- a/Job/SlackMessage.php +++ b/Job/SlackMessage.php @@ -1,35 +1,29 @@ store = $store->getStore(); + $this->store = $storeManager->getStore(); } - /** - * @return string - */ - public function success() + public function success(): string { return ':white_check_mark: All of today\'s imports in *' . $this->store->getName() . '* have been successfully completed.'; } - /** - * @param Collection $errorLogs - * @param Collection $processingLogs - * @return string - */ - public function warning(?Collection $errorLogs = null, ?Collection $processingLogs = null) + public function warning(?Collection $errorLogs = null, ?Collection $processingLogs = null): string { $message = ':warning: *Warning!* There\'s a problem with todayโ€™s imports in *' . $this->store->getName() . "*.\n\n"; @@ -39,45 +33,31 @@ public function warning(?Collection $errorLogs = null, ?Collection $processingLo $message .= (isset($processingLogs) && $processingLogs->getData()) ? $this->logList($processingLogs, ImportInterface::IMPORT_PROCESSING) : ''; + return $message; } - /** - * @param Collection $logs - * @param int $status - * @return string - */ - protected function logList(Collection $logs, int $status) + protected function logList(Collection $logs, int $status): string { - $message = ''; - switch ($status) { - case $status == ImportInterface::IMPORT_ERROR: - $message = "The following imports have failed:\n\n"; - break; - case ImportInterface::IMPORT_PROCESSING: - $message = "The following imports are still in process:\n\n"; - break; - } + $message = match ($status) { + ImportInterface::IMPORT_ERROR => "The following imports have failed:\n\n", + ImportInterface::IMPORT_PROCESSING => "The following imports are still in process:\n\n", + default => '', + }; + foreach ($logs->getData() as $log) { $message .= $this->formatList(date('H:i:s', strtotime($log['created_at'])), $log['name']); } + return $message . "\n"; } - /** - * @return string - */ - public function noImports() + public function noImports(): string { return $this->warning() . 'No imports have been made today.'; } - /** - * @param string $dateTime - * @param string $name - * @return string - */ - protected function formatList(string $dateTime, string $name) + protected function formatList(string $dateTime, string $name): string { return '> โ€ข _' . $dateTime . '_ *' . $name . "*\n"; } diff --git a/Model/Akeneo.php b/Model/Akeneo.php old mode 100755 new mode 100644 index 8dd2186..70f3415 --- a/Model/Akeneo.php +++ b/Model/Akeneo.php @@ -1,16 +1,14 @@ _init('JustBetter\AkeneoBundle\Model\ResourceModel\Akeneo'); + $this->_init(ResourceModel\Akeneo::class); } } diff --git a/Model/AkeneoFactory.php b/Model/AkeneoFactory.php old mode 100755 new mode 100644 index 27d7214..78e59b2 --- a/Model/AkeneoFactory.php +++ b/Model/AkeneoFactory.php @@ -1,30 +1,22 @@ _objectManager = $objectManager; + public function __construct( + protected ObjectManagerInterface $objectManager + ) { } /** - * Create new country model - * - * @param array $arguments - * @return \Magento\Directory\Model\Country + * @param array $arguments */ - public function create(array $arguments = []) + public function create(array $arguments = []): Akeneo { - return $this->_objectManager->create('JustBetter\AkeneoBundle\Model\Akeneo', $arguments, false); + return $this->objectManager->create(Akeneo::class, $arguments); } -} \ No newline at end of file +} diff --git a/Model/ResourceModel/Akeneo.php b/Model/ResourceModel/Akeneo.php old mode 100755 new mode 100644 index 6e6bbcf..b70fa6d --- a/Model/ResourceModel/Akeneo.php +++ b/Model/ResourceModel/Akeneo.php @@ -1,4 +1,5 @@ _init('akeneo_connector_entities', 'id'); } diff --git a/Model/ResourceModel/Akeneo/Collection.php b/Model/ResourceModel/Akeneo/Collection.php old mode 100755 new mode 100644 index d7a9787..29485b6 --- a/Model/ResourceModel/Akeneo/Collection.php +++ b/Model/ResourceModel/Akeneo/Collection.php @@ -1,20 +1,17 @@ _init('JustBetter\AkeneoBundle\Model\Akeneo', 'JustBetter\AkeneoBundle\Model\ResourceModel\Akeneo'); + $this->_init(Akeneo::class, AkeneoResourceModel::class); $this->_map['fields']['page_id'] = 'main_table.page_id'; } - } diff --git a/Model/Status.php b/Model/Status.php old mode 100755 new mode 100644 index 324b7d8..3d0fd73 --- a/Model/Status.php +++ b/Model/Status.php @@ -1,32 +1,27 @@ */ - public static function getOptionArray() + public static function getOptionArray(): array { - return [self::STATUS_ENABLED => __('Enabled'), self::STATUS_DISABLED => __('Disabled')]; + return [ + self::STATUS_ENABLED => (string)__('Enabled'), + self::STATUS_DISABLED => (string)__('Disabled'), + ]; } /** - * Retrieve option array with empty value - * - * @return string[] + * @return array */ - public function getAllOptions() + public function getAllOptions(): array { $result = []; @@ -37,16 +32,10 @@ public function getAllOptions() return $result; } - /** - * Retrieve option text by option value - * - * @param string $optionId - * @return string - */ - public function getOptionText($optionId) + public function getOptionText(string $optionId): ?string { $options = self::getOptionArray(); - return isset($options[$optionId]) ? $options[$optionId] : null; + return $options[$optionId] ?? null; } -} \ No newline at end of file +} diff --git a/Observer/CategoryImport.php b/Observer/CategoryImport.php new file mode 100644 index 0000000..a823b69 --- /dev/null +++ b/Observer/CategoryImport.php @@ -0,0 +1,30 @@ +getData('import'); + + $method = $executor->getMethod(); + + // Run before setValues step + if ($method === 'setValues') { + $this->categoryExist->execute(); + } + } +} diff --git a/Observer/ImportFinished.php b/Observer/ImportFinished.php new file mode 100644 index 0000000..cba52ec --- /dev/null +++ b/Observer/ImportFinished.php @@ -0,0 +1,33 @@ +getData('import'); + + $method = $executor->getMethod(); + + // Only dispatch for cleanCache method (end of import) + if ($method === 'cleanCache') { + $code = $executor->getCurrentJob()->getCode(); + $event = 'akeneo_connector_import_finish_' . $code; + + $this->eventManager->dispatch($event, ['import' => $executor]); + } + } +} diff --git a/Observer/ImportMetricUnits.php b/Observer/ImportMetricUnits.php index 750f0b2..f7431f2 100644 --- a/Observer/ImportMetricUnits.php +++ b/Observer/ImportMetricUnits.php @@ -1,18 +1,17 @@ job = $job; + public function __construct( + protected ImportMetricUnitsJob $job + ) { } public function execute(Observer $observer): void diff --git a/Observer/ProductImport.php b/Observer/ProductImport.php new file mode 100644 index 0000000..f11dfe5 --- /dev/null +++ b/Observer/ProductImport.php @@ -0,0 +1,38 @@ +getData('import'); + + $method = $executor->getMethod(); + $code = $executor->getCurrentJob()->getCode(); + + // Run before setWebsites step + if ($method === 'setWebsites') { + $this->checkWebsiteAssociation->execute($code); + } + + // Run before updateOption step + if ($method === 'updateOption') { + $this->setTaxClassId->execute($code); + } + } +} diff --git a/Observer/RemoveRedundantEav.php b/Observer/RemoveRedundantEav.php index 673f9c2..00e9cac 100644 --- a/Observer/RemoveRedundantEav.php +++ b/Observer/RemoveRedundantEav.php @@ -1,14 +1,15 @@ job = $job; + public function __construct( + protected SetNotVisibleJob $job + ) { } public function execute(Observer $observer): void diff --git a/Plugin/CheckWebsiteAssociation.php b/Plugin/CheckWebsiteAssociation.php deleted file mode 100644 index cab2ad3..0000000 --- a/Plugin/CheckWebsiteAssociation.php +++ /dev/null @@ -1,134 +0,0 @@ -entitiesHelper = $entitiesHelper; - $this->storeHelper = $storeHelper; - $this->config = $config; - $this->configHelper = $configHelper; - $this->authenticator = $authenticator; - $this->serializer = $serializer; - } - - public function beforeSetWebsites(product $subject) - { - $connection = $this->entitiesHelper->getConnection(); - /** @var string $tmpTable */ - $tmpTable = $this->entitiesHelper->getTableName($subject->getCode()); - $websiteAttribute = $this->configHelper->getWebsiteAttribute(); - $websites = $this->storeHelper->getStores('website_code'); - $websiteAssociation = $this->config->getValue('akeneo_connector/product/website_attribute'); - - $requiredAttributes = $this->getRequiredAttributes(); - - if ($connection->tableColumnExists($tmpTable, $websiteAttribute)) { - /** @var Select $select */ - $select = $connection->select()->from( - $tmpTable - ); - /** @var Mysql $query */ - $query = $connection->query($select); - /** @var array $row */ - while (($row = $query->fetch())) { - - if(!isset($row[$websiteAssociation])) { - continue; - } - - $websites = explode(',', $row[$websiteAssociation]); - $mapping = $this->getMappedWebsiteChannels(); - - foreach ($websites as $key => $website) { - $channel = $mapping[$website] ?? ''; - if (empty($channel)) { - continue; - } - - $locales = $this->storeHelper->getChannelStoreLangs($channel); - foreach ($requiredAttributes as $attribute) { - if (isset($attribute['localizable']) && $attribute['localizable'] === true) { - foreach ($locales as $locale) { - if (empty($row[$attribute['akeneo_attribute'] . '-' . $locale . '-' . $channel])) { - unset($websites[$key]); - break(2); - } - } - } else { - if (empty($row[$attribute])) { - unset($websites[$key]); - break(2); - } - } - } - } - - $connection->update( - $tmpTable, - [ - $websiteAssociation => implode(',', $websites), - ], - ['identifier = ?' => $row['identifier']] - ); - } - } - return [$subject]; - } - - public function getMappedWebsiteChannels() - { - /** @var mixed[] $mapping */ - $mapping = $this->configHelper->getWebsiteMapping(); - /** @var string[] $channels */ - $channels = array_column($mapping, 'channel', 'website'); - - return $channels; - } - - public function getRequiredAttributes() - { - if (!($requiredAttributes = $this->config->getValue('akeneo_connector/product/required_attribute_mapping'))) { - return []; - } - $requiredAttributes = $this->serializer->unserialize($requiredAttributes); - - foreach ($requiredAttributes as $key => &$requiredAttribute) { - $akeneoAttribute = $this->authenticator->getAkeneoApiClient()->getAttributeApi()->get($requiredAttribute['akeneo_attribute']); - $requiredAttribute['localizable'] = $akeneoAttribute['localizable']; - } - - return $requiredAttributes; - } -} diff --git a/Plugin/EnableManageStock.php b/Plugin/EnableManageStock.php deleted file mode 100644 index 8e3c6da..0000000 --- a/Plugin/EnableManageStock.php +++ /dev/null @@ -1,59 +0,0 @@ -entitiesHelper = $entitiesHelper; - $this->config = $config; - - $this->connection = $this->entitiesHelper->getConnection(); - } - - public function afterInitStock(Subject $subject): bool - { - $extensionEnabled = $this->config->getValue('akeneo_connector/justbetter/enablemanagestock', Scope::SCOPE_WEBSITE); - - if (! $extensionEnabled) { - return true; - } - - $products = $this->getProducts($subject); - if(!empty($products)) { - $connection = $this->entitiesHelper->getConnection(); - $where = ['product_id' . ' IN(?)' => [$products]]; - $connection->update($this->entitiesHelper->getTable('cataloginventory_stock_item'), ['manage_stock' => '1','use_config_manage_stock' => '1'], $where); - } - return true; - } - - protected function getProducts(Subject $subject): array - { - $tmpTableName = $this->entitiesHelper->getTableName($subject->getCode()); - $query = $this->connection->select()->from(['t' => $tmpTableName],['c.entity_id'])->joinInner( - ['c' => 'catalog_product_entity'], - 't.identifier = c.sku' - ); - - return $this->connection->fetchPairs($query); - } -} diff --git a/Plugin/Helper/Import/Entities.php b/Plugin/Helper/Import/Entities.php index a902246..b6314a6 100644 --- a/Plugin/Helper/Import/Entities.php +++ b/Plugin/Helper/Import/Entities.php @@ -1,16 +1,17 @@ config->getValue('akeneo_connector/justbetter/formatmedianame', scope::SCOPE_WEBSITE); + $extensionEnabled = $this->config->getValue('akeneo_connector/justbetter/formatmedianame', ScopeInterface::SCOPE_WEBSITE); if (!$extensionEnabled) { return $result; } - return str_replace("_", "-", $result); + return str_replace('_', '-', $result); } /** * Before setting the values we use Tax Type value from Akeneo when available * - * @param $subject - * @param $jobCode - * @param $entityTable - * @param $data - * @param $entityTypeId - * @param $storeId - * @param $mode - * @return array + * @param array $data + * @return array{0: string, 1: string, 2: array, 3: int, 4: int, 5: int} */ - public function beforesetValues($subject, $jobCode, $entityTable, $data, $entityTypeId, $storeId, $mode = AdapterInterface::INSERT_ON_DUPLICATE) - { - $additonalTypes = $this->attributeHelper->getAdditionalTypes(); + public function beforeSetValues( + EntitiesHelper $subject, + string $jobCode, + string $entityTable, + array $data, + int $entityTypeId, + int $storeId, + int $mode = AdapterInterface::INSERT_ON_DUPLICATE + ): array { + $additionalTypes = $this->attributeHelper->getAdditionalTypes(); - foreach ($additonalTypes as $key => $additonalType) { - if ($additonalType === 'tax' && isset($data[$key])) { + foreach ($additionalTypes as $key => $additionalType) { + if ($additionalType === 'tax' && isset($data[$key])) { if (isset($data['tax_class_id']) && $data['tax_class_id'] instanceof \Zend_Db_Expr) { $defaultTaxClassId = $data['tax_class_id']->__toString(); $data['tax_class_id'] = new \Zend_Db_Expr( - "IF(`".$data[$key]."` IS NULL OR `".$data[$key]."` = '', '".$defaultTaxClassId."', `".$data[$key]."`)" + "IF(`{$data[$key]}` IS NULL OR `{$data[$key]}` = '', '{$defaultTaxClassId}', `{$data[$key]}`)" ); } else { $data['tax_class_id'] = new \Zend_Db_Expr( - "IF(`".$data[$key]."` IS NULL OR `".$data[$key]."` = '', `_tax_class_id`, `".$data[$key]."`)" + "IF(`{$data[$key]}` IS NULL OR `{$data[$key]}` = '', `_tax_class_id`, `{$data[$key]}`)" ); } } } + return [$jobCode, $entityTable, $data, $entityTypeId, $storeId, $mode]; } } diff --git a/Plugin/Helper/Import/Product.php b/Plugin/Helper/Import/Product.php index c9f5f12..1a6daa1 100644 --- a/Plugin/Helper/Import/Product.php +++ b/Plugin/Helper/Import/Product.php @@ -1,21 +1,30 @@ |null + */ + protected ?array $codes = null; public function __construct( protected ScopeConfigInterface $config, - protected StoreHelper $storeHelper, + protected StoreHelper $storeHelper ) { } - public function beforeCreateTmpTableFromApi($subject, $result, $tableSuffix, mixed $family = null) + /** + * @param array $result + * @return array{0: array, 1: string, 2: mixed} + */ + public function beforeCreateTmpTableFromApi(ProductHelper $subject, array $result, string $tableSuffix, mixed $family = null): array { if (is_null($this->codes)) { $this->codes = explode(',', (string)$this->config->getValue('akeneo_connector/justbetter/important_attributes')); @@ -31,7 +40,7 @@ public function beforeCreateTmpTableFromApi($subject, $result, $tableSuffix, mix foreach ($affectedStores as $affectedStore) { $storeCodes[$affectedStore['lang'] . '-' . $affectedStore['channel_code']] = [ $affectedStore['lang'], - $affectedStore['channel_code'] + $affectedStore['channel_code'], ]; } } @@ -41,9 +50,9 @@ public function beforeCreateTmpTableFromApi($subject, $result, $tableSuffix, mix continue; } $result['values'][$code] = [[ - 'locale' => null, - 'scope' => null, - 'data' => null, + 'locale' => null, + 'scope' => null, + 'data' => null, ]]; foreach ($storeCodes as $store) { diff --git a/Plugin/ImportFinished.php b/Plugin/ImportFinished.php deleted file mode 100644 index d13213f..0000000 --- a/Plugin/ImportFinished.php +++ /dev/null @@ -1,22 +0,0 @@ -eventManager = $eventManager; - } - - public function beforeCleanCache($subject) - { - $this->eventManager->dispatch('akeneo_connector_import_finish_' . $subject->getCode()); - - return null; - } -} diff --git a/Plugin/InsertNewProducts.php b/Plugin/InsertNewProducts.php deleted file mode 100644 index e871ee7..0000000 --- a/Plugin/InsertNewProducts.php +++ /dev/null @@ -1,28 +0,0 @@ -config->getValue('akeneo_connector/justbetter/insertnewproducts', Scope::SCOPE_WEBSITE); - if ($extensionEnabled) { - $connection = $this->entitiesHelper->getConnection(); - $tmpTableName = $this->entitiesHelper->getTableName($subject->getCode()); - - $connection->delete($tmpTableName, ['_is_new = ?' => 1]); - } - } -} diff --git a/Plugin/Job/Product.php b/Plugin/Job/Product.php index 6d3de09..9f02cd2 100644 --- a/Plugin/Job/Product.php +++ b/Plugin/Job/Product.php @@ -1,4 +1,5 @@ scopeConfig->getValue(self::PRODUCTS_FILTERS_EXCLUDED_FAMILIES); } - public function afterGetFamiliesToImport( - AkeneoProduct $subject, - array $families - ): array { - $familiesToExclude = explode(',', $this->getFamiliesToExclude() ?? ''); - + /** + * @param array|null $families + * @return array + */ + public function afterGetFamiliesToImport(AkeneoProduct $subject, ?array $families = null): array + { + $familiesToExclude = explode(',', (string)$this->getFamiliesToExclude()); + if (!$families || $families[0] === '') { - $families = array_values($this->familyFilter->getFamilies() ?? []); + $allFamilies = $this->familyFilter->getFamilies(); + $families = is_array($allFamilies) ? array_values($allFamilies) : []; } return array_diff($families, $familiesToExclude); } -} \ No newline at end of file +} diff --git a/Plugin/SetProductsActive.php b/Plugin/SetProductsActive.php deleted file mode 100644 index 1b47ce1..0000000 --- a/Plugin/SetProductsActive.php +++ /dev/null @@ -1,85 +0,0 @@ -config = $config; - $this->entitiesHelper = $entitiesHelper; - $this->attribute = $attribute; - $this->connection = $this->entitiesHelper->getConnection(); - } - - public function afterInitStock(Product $subject, $result) - { - if (!$this->config->getValue(static::CONFIG_KEY, Scope::SCOPE_WEBSITE)) { - return $result; - } - - $products = $this->getProducts($subject); - - $this->update($products); - - return $result; - } - - protected function getProducts(Product $subject): array - { - $tmpTableName = $this->entitiesHelper->getTableName($subject->getCode()); - - $query = $this->connection - ->select() - ->from(['t' => $tmpTableName]) - ->joinInner( - ['c' => 'catalog_product_entity'], - 't.identifier = c.sku' - ); - - return $this->connection->fetchAll($query); - } - - protected function update(array $products): void - { - $ids = array_map(function ($product) { - return $product['_entity_id']; - }, $products); - - $table = $this->connection->getTableName('catalog_product_entity_int'); - $attributeId = $this->attribute->getIdByCode('catalog_product', 'status'); - - foreach (array_chunk($ids, 100) as $chunk) { - $this->connection->update( - $table, - [ - 'value' => Status::STATUS_ENABLED - ], - [ - 'attribute_id = ' . $attributeId - . ' AND entity_id IN (' . implode(',', $chunk) . ')' - . ' AND value <> ' . Status::STATUS_ENABLED - . ' AND store_id = 0' - ] - ); - } - } -} diff --git a/Plugin/SetStockStatus.php b/Plugin/SetStockStatus.php deleted file mode 100644 index 6d59c79..0000000 --- a/Plugin/SetStockStatus.php +++ /dev/null @@ -1,60 +0,0 @@ -entitiesHelper = $entitiesHelper; - $this->config = $config; - - $this->connection = $this->entitiesHelper->getConnection(); - } - - public function afterInitStock(Subject $subject): bool - { - $extensionEnabled = $this->config->getValue('akeneo_connector/justbetter/setstockstatus', Scope::SCOPE_WEBSITE); - - if (! $extensionEnabled) { - return true; - } - - $products = $this->getProducts($subject); - if(!empty($products)) { - $connection = $this->entitiesHelper->getConnection(); - $where = ['product_id' . ' IN(?)' => [$products], 'backorders' . ' IN(?)' => [Stock::BACKORDERS_YES_NONOTIFY, Stock::BACKORDERS_YES_NOTIFY]]; - $connection->update($this->entitiesHelper->getTable('cataloginventory_stock_item'), ['is_in_stock' => Stock::STOCK_IN_STOCK], $where); - } - return true; - } - - protected function getProducts(Subject $subject): array - { - $tmpTableName = $this->entitiesHelper->getTableName($subject->getCode()); - $query = $this->connection->select()->from(['t' => $tmpTableName],['c.entity_id'])->joinInner( - ['c' => 'catalog_product_entity'], - 't.identifier = c.sku' - ); - - return $this->connection->fetchPairs($query); - } -} diff --git a/Plugin/SetTaxClassId.php b/Plugin/SetTaxClassId.php deleted file mode 100644 index 027f3f1..0000000 --- a/Plugin/SetTaxClassId.php +++ /dev/null @@ -1,210 +0,0 @@ -entitiesHelper = $entitiesHelper; - $this->storeHelper = $storeHelper; - $this->serializer = $serializer; - $this->configHelper = $configHelper; - $this->authenticator = $authenticator; - $this->scopeConfig = $scopeConfig; - } - - /** - * Overwrite Magento Tax Class with the one from Akeneo. - * - * @param Product $context - */ - public function afterAddRequiredData(Product $context) - { - $extensionEnabled = $this->scopeConfig->getValue('akeneo_connector/justbetter/settaxclass', scope::SCOPE_WEBSITE); - if (!$extensionEnabled) { - return ; - } - - if ( - !($attributes = $this->scopeConfig->getValue(ConfigHelper::ATTRIBUTE_TYPES)) || - !($mappings = $this->scopeConfig->getValue('akeneo_connector/product/tax_id_mapping'))) { - return ; - } - - $attributes = $this->serializer->unserialize($attributes); - - $mappings = $this->serializer->unserialize($mappings); - - $this->tax_id_columns = []; - foreach ($attributes as $attribute) { - if ($attribute['magento_type'] === "tax") { - $this->tax_id_columns[] = $attribute['pim_type']; - } - } - - if (!$this->tax_id_columns || !count($mappings)) { - return; - } - - $tmpTable = $this->entitiesHelper->getTableName($context->getCode()); - - $taxColumns = $this->checkTaxColumnsExist($this->tax_id_columns, $tmpTable); - - if (empty($taxColumns)) { - return; - } - } - - /** - * Before UpdateOption - Map Akeneo Tax Class option to Magento counterpart - * - * @param $subject - * @return array - */ - public function beforeUpdateOption($subject) - { - if (!$this->tax_id_columns) { - return [$subject]; - } - - $connection = $this->entitiesHelper->getConnection(); - $tmpTable = $this->entitiesHelper->getTableName($subject->getCode()); - - if ($taxColumns = $this->checkTaxColumnsExist($this->tax_id_columns, $tmpTable)) { - foreach ($taxColumns as $tax_id_column) { - try { - $taxQuery = $this->createQuery($tax_id_column, $tmpTable); - $connection->query($taxQuery); - } catch (Exception $e) { - throw $e; - } - } - } - - return [$subject]; - } - - /** - * Create the query to update the rows. - * - * @param string $tax_id_column - * @param string $tableName - * - * @return string - */ - public function createQuery($tax_id_column, $tableName) - { - $query = " - UPDATE `" . $tableName . "` - SET `".$tax_id_column."` = - "; - - $query = $this->addCase($query, $tax_id_column); - - return $query; - } - - /** - * Add the switch case to the query. - * - * @param string $query - * @param string $tax_id_column - * - * @return string - */ - public function addCase($query, $tax_id_column) - { - if (!($mappings = $this->scopeConfig->getValue('akeneo_connector/product/tax_id_mapping'))) { - return ; - } - $mappings = $this->serializer->unserialize($mappings); - - if (!count($mappings)) { - return $query; - } - - $query .= "CASE - "; - - foreach ($mappings as $mapping) { - $query .= "WHEN `" . $tax_id_column . "` = '" . $mapping['akeneo'] . "' then '" . $mapping['magento'] . "' - "; - } - - $query .= 'END'; - - return $query; - } - - /** - * Check If the Tax Class is localizable and exist - * - * @param $mappings - * @param $tmpTable - * @return array - */ - public function checkTaxColumnsExist($mappings, $tmpTable) - { - $newMappings = []; - - /** @var AdapterInterface $connection */ - $connection = $this->entitiesHelper->getConnection(); - - foreach ($mappings as $mapping) { - - $akeneoAttribute = $this->authenticator->getAkeneoApiClient()->getAttributeApi()->get($mapping); - - if($akeneoAttribute['localizable'] === false) { - if ($connection->tableColumnExists($tmpTable, $mapping)) { - $newMappings[] = $mapping; - } - } - - if (isset($akeneoAttribute['localizable'])) { - $mappedChannels = $this->configHelper->getMappedChannels(); - foreach ($mappedChannels as $channel) { - foreach ($this->storeHelper->getChannelStoreLangs($channel) as $locale) { - if ($connection->tableColumnExists($tmpTable, $mapping . '-' . $locale . '-' . $channel)) { - $newMappings[] = $mapping . '-' . $locale . '-' . $channel; - } - } - } - } - } - - return $newMappings; - } -} diff --git a/Plugin/SetTierPrices.php b/Plugin/SetTierPrices.php deleted file mode 100644 index cb32cac..0000000 --- a/Plugin/SetTierPrices.php +++ /dev/null @@ -1,139 +0,0 @@ -serializer = $serializer; - $this->config = $config; - $this->entitiesHelper = $entitiesHelper; - } - - /** - * AfterImportMedia function - * - * @param product $subject - * @param bool $result - * @return bool $result - */ - public function afterImportMedia(product $subject, $result) - { - $extensionEnabled = $this->config->getValue('akeneo_connector/justbetter/tierprices', scope::SCOPE_WEBSITE); - if (!$extensionEnabled) { - return $result; - } - - $this->customerGroups = $this->config->getValue('akeneo_connector/product/groups', scope::SCOPE_WEBSITE); - $this->customerGroupsUnserialized = $this->serializer->unserialize($this->customerGroups ?? '{}'); - - $tmpTableName = $this->entitiesHelper->getTableName($subject->getCode()); - $connection = $this->entitiesHelper->getConnection(); - $this->removeTierPrices(); - $connection->beginTransaction(); - $this->setTierPrices($tmpTableName); - - try { - $connection->commit(); - } catch (Exception $e) { - $connection->rollBack(); - throw $e; - } - - return $result; - } - - /** - * Remove tierprices function - * Remove all tier prices that match with the tmp table - */ - public function removeTierPrices() - { - try { - $connection = $this->entitiesHelper->getConnection(); - $connection->query( - " - DELETE cpetp FROM `catalog_product_entity_tier_price` AS cpetp - INNER JOIN `tmp_akeneo_connector_entities_product` AS tacep - ON cpetp.`entity_id` = tacep.`_entity_id` - " - ); - } catch (Exception $e) { - throw $e; - } - } - - /** - * set tierprices function - * - * @param string $tmpTableName - * @return void - */ - public function setTierPrices($tmpTableName) - { - foreach ($this->customerGroupsUnserialized as $option) { - $connection = $this->entitiesHelper->getConnection(); - $exist = $connection->tableColumnExists($tmpTableName, $option['pim_type']); - if ($exist) { - $select = $connection->select() - ->from( - $tmpTableName, - [ - 'value_id' => new Expr("'" . null . "'"), - 'entity_id' => '_entity_id', - 'all_groups' => new Expr("'" . 0 . "'"), - 'customer_group_id' => new Expr("'" . $option['magento_type'] . "'"), - 'qty' => new Expr("'" . 1 . "'"), - 'value' => $option['pim_type'], - 'website_id' => new Expr("'" . 0 . "'"), - 'percentage_value' => new Expr("NULL"), - ] - )->where("`" . $option['pim_type'] . "`" . ' != ?', ['notnull' => true]); - - $connection->query( - $connection->insertFromSelect( - $select, - 'catalog_product_entity_tier_price', - [ - 'value_id', - 'entity_id', - 'all_groups', - 'customer_group_id', - 'qty', - 'value', - 'website_id', - 'percentage_value' - ], - 2 - ) - ); - } - } - } -} diff --git a/README.md b/README.md index 5519505..6dc6d01 100644 --- a/README.md +++ b/README.md @@ -1,82 +1,214 @@ -# Magento2 Akeneo Bundle - -This Magento2 extension made by [JustBetter](https://justbetter.nl) extends the official [Akeneo Connector](https://github.com/akeneo/magento2-connector-community) with several features and optimizations. + + JustBetter Magento 2 Akeneo Bundle - Essential features the Akeneo Connector is missing + +

JustBetter - Magento 2 Akeneo Bundle

+ + +[![Latest Version on Packagist][packagist-version-shield]][packagist-version-url] +[![GPL-3.0 License][license-shield]][license-url] +[![Total Downloads][packagist-downloads-shield]][packagist-downloads-url] +[![Stargazers][stars-shield]][stars-url] +[![Issues][issues-shield]][issues-url] +[![Contributors][contributors-shield]][contributors-url] +[![Forks][forks-shield]][forks-url] + +
+
+ + JustBetter Logo + + +

JustBetter - Magento 2 Akeneo Bundle

+ +

+ Extends the official Akeneo Connector with several features and optimizations. +
+ Report Bug + ยท + Request Feature +

+
+ +
+ Table of Contents +
    +
  1. + About The Package + +
  2. +
  3. + Getting Started + +
  4. +
  5. Features
  6. +
  7. Configuration
  8. +
  9. Usage
  10. +
  11. Events
  12. +
  13. Contributing
  14. +
  15. License
  16. +
  17. Contact
  18. +
+
+ +## About The Package + +This Magento 2 extension made by [JustBetter](https://justbetter.nl) extends the official [Akeneo Connector](https://github.com/akeneo/magento2-connector-community) with several features and optimizations. These features can be enabled / disabled via an extra configuration section called `JustBetter Akeneo` that is added to the default Akeneo Connector Configuration in Magento. +### Built With + +* [![PHP][PHP-badge]][PHP-url] +* [![Magento][Magento-badge]][Magento-url] +* [![Akeneo][Akeneo-badge]][Akeneo-url] + +### Contributors + +Thanks to all the people who have contributed to this project: + + + Contributors + + +

(back to top)

+ +## Getting Started + +### Installation + +1. Install via Composer + ```sh + composer require justbetter/magento2-akeneo-bundle + ``` + +2. Enable the module + ```sh + bin/magento module:enable JustBetter_AkeneoBundle + ``` + +3. Run setup upgrade and flush cache + ```sh + bin/magento setup:upgrade && bin/magento cache:flush + ``` + +

(back to top)

+ ## Features -| Feature | Description | -|--------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| Important Attributes | Select attributes that should always be added to the product tables even if all are empty, this fixes cases where you bulk empty attributes and it is not reflected in Magento. | -| Tier Prices | Maps specific Akeneo attribute code with a Magento Customer group. This ensures that the tier prices from Akeneo are imported into Magento customer tier prices | -| Set default value for required attributes | Set a default value for required attributes if the value is missing | -| Category exist | Skip inserting url paths when the category already exist | -| Akeneo Manager | Manual adjustment of Akeneo codes vs magento entity idโ€™s connector mapping. When enabled you can make adjustments of the values via the Menu option `JUSTBETTER > Akeneo Manager` | -| Insert New Products | Disable the insertion of new products | -| Set Tax Class | When you have multiple tax classes in Akeneo and you want to use them in Magento. Map Akeneo tax class codes to Magento tax class - See configuration | -| Set Required admin attribute | When having multiple stores and channels, the main attribute for de admin channel isn't always set. This means adding an attribute with the default language to do this for you. | -| Set products active | Enable all products from Akeneo | -| Enable Manage stock by default | This sets the manage stock to value `Yes` for imported products by default | -| Set stock status | Automatically sets the stock status of imported products to "In Stock" when backorder-able | -| Apply SEO friendly media name formatting | Formats the Media name from "_" to "-" | -| Enable retrieving metric units | Sets Akeneo's metric unit in the eav_attribute - See configuration | -| Channel for metric conversions | What channel to use for metric conversions | -| Set families to not visible individually after importing | Sets products in selected families to `Not Visible Individually` | -| Unset Website when empty Product Attribute Mapping | When enabled this will unset the website from the product when a required attribute has no specific value. For example when the Name attribute in Akeneo is empty for the associated website | -| Slack Akeneo import notifications | Setup Slack notifications of Akeneo imports | -| Import finished events | Fires an event for every job that is fully finished | | -| Exclude Families from Import | Allows you to exclude specific families from being imported from Akeneo. _(Stores > Configuration > Catalog > Akeneo Connector > JustBetter Akeneo)_ | | - -## Installation -``` -composer require justbetter/magento2-akeneo-bundle -bin/magento module:enable JustBetter_AkeneoBundle -bin/magento setup:upgrade && bin/magento cache:flush -``` +### Feature Overview + +For detailed documentation of each feature, see **[FEATURES.md](FEATURES.md)**. +For configuration instructions and best practices, see **[Configuration Guide](FEATURES.md#configuration-guide)**. + +| Category | Feature | Configuration Path | +|----------|---------|-------------------| +| **Product Import** | Important Attributes | JustBetter Akeneo > Important Attributes | +| | Default Store Values | JustBetter Akeneo > Default Store Values | +| | Exclude Families from Import | Products Filters > Excluded Families | +| | Remove Redundant EAV | JustBetter Akeneo > Remove Redundant EAV | +| **Category** | Category Exist - Skip URL Regeneration | JustBetter Akeneo > Category Exist | +| **Tax & Pricing** | Set Tax Class | JustBetter Akeneo > Set Tax Class | +| **Attributes** | Metric Units Import | JustBetter Akeneo > Enable Metric Units | +| | Format Media Name (SEO) | JustBetter Akeneo > Format Media Name | +| **Visibility** | Set Families Not Visible | JustBetter Akeneo > Not Visible Families | +| **Website Association** | Required Attribute Mapping | Products > Required Attribute Mapping | +| **Management** | Akeneo Manager | JustBetter Akeneo > Akeneo Manager | +| **Notifications** | Slack Notifications | JustBetter Akeneo > Slack | +| **Events** | Import Finished Events | - | + +

(back to top)

+ ## Configuration -- Enable and disable different Akeneo features. Go to `Stores > Configuration > Catalog > Akeneo Connector > JustBetter Akeneo`. -- To map specific Akeneo attribute code with a Magento Customer group. Simply go to `Stores > Configuration > Catalog > Akeneo Connector > Products > Customer Group Pricing` -- When you would like to use the Tax Class Mapping: map the Akeneo Attribute Option Codes to the Magento Tax Classes. Don't forget to define the Tax attribute within the Attribute configuration for this feature to work. -## Import finished events -We added a total of 5 events: -``` -akeneo_connector_import_finish_category -akeneo_connector_import_finish_family -akeneo_connector_import_finish_attribute -akeneo_connector_import_finish_option -akeneo_connector_import_finish_product -``` +All features are configured in the Magento Admin Panel: + +**`Stores > Configuration > Catalog > Akeneo Connector > JustBetter Akeneo`** -These events are fired before the `cleanCache` function which only runs at the end of the job execution. -That way the cache will still be flushed after your hook. +For detailed configuration instructions, grid mappings, and best practices, see **[FEATURES.md - Configuration Guide](FEATURES.md#configuration-guide)**. -*Please keep in mind that the Akeneo Products Import is executed per family ([since 102.1.1](https://github.com/akeneo/magento2-connector-community/blob/master/CHANGELOG.md#version-10211-)). So if you import products from multiple families the `akeneo_connector_import_finish_product` event will be called multiple times.* +

(back to top)

-## Metric Units -When enabled the default metric unit for metric attributes will be added to the `unit` field in the `eav_attribute` table. -This can be overridden at a channel, currently we only support one channel for this which is configurable in the backend. +## Usage -You can run this from the command line using `bin/magento metric:import` +The bundle extends the Akeneo Connector with additional features that can be configured in the Magento Admin Panel. Some features also provide CLI commands for manual execution. -It is also automatically run after the attribute import +For detailed usage instructions and CLI commands, see **[FEATURES.md](FEATURES.md)**. -## Family - Not Visible Individually -If you need to set the visibility of all products that belong to certain families to `Not Visible Individually` you can select those families. -After each import this will run and set products to not visible. +

(back to top)

-You can also run this from the command line using `bin/magento akeneo:setfamilynotvisible` +## Events -## Ideas, bugs or suggestions? -It would be awesome if you can submit an [issue](https://github.com/justbetter/magento2-akeneo-bundle/issues) if you encounter any problems or for kudos create a [pull request](https://github.com/justbetter/magento2-akeneo-bundle/pulls). +The bundle dispatches custom events after import completion to enable custom post-processing. -## About us -We are an innovative development agency from The Netherlands building awesome websites, webshops and web applications with Laravel and Magento2. Check out our website [justbetter.nl](https://justbetter.nl) and our [open source projects](https://github.com/justbetter). +For available events, implementation examples and use cases, see **[FEATURES.md - Event System](FEATURES.md#event-system)**. + +

(back to top)

+ +## Contributing +Please read our [Contributing Guide](.github/CONTRIBUTING.md) for details on our code of conduct and the process for submitting pull requests. + +**Development:** + +```bash +# Install dependencies +composer install + +# Run quality checks (PHPStan) +composer analyse + +# Run GrumPHP tasks manually +composer grumphp +``` + +**Note:** GrumPHP runs automatically on git commit. + +For security vulnerabilities, please see our [Security Policy](.github/SECURITY.md). + +

(back to top)

## License -[GNU GENERAL PUBLIC LICENSE](LICENSE) ---- +Distributed under the GPL-3.0 License. See `LICENSE` for more information. Please see [License File](LICENSE) for more information. + +

(back to top)

+ +## Contact + +[JustBetter B.V.](https://justbetter.nl/contact) + +

(back to top)

+ + + + JustBetter - We're an innovative development agency from The Netherlands building better Magento solutions + + +

(back to top)

+ + +[packagist-version-shield]: https://img.shields.io/packagist/v/justbetter/magento2-akeneo-bundle.svg?style=for-the-badge +[packagist-version-url]: https://packagist.org/packages/justbetter/magento2-akeneo-bundle +[packagist-downloads-shield]: https://img.shields.io/packagist/dt/justbetter/magento2-akeneo-bundle.svg?style=for-the-badge +[packagist-downloads-url]: https://packagist.org/packages/justbetter/magento2-akeneo-bundle +[contributors-shield]: https://img.shields.io/github/contributors/justbetter/magento2-akeneo-bundle.svg?style=for-the-badge +[contributors-url]: https://github.com/justbetter/magento2-akeneo-bundle/graphs/contributors +[forks-shield]: https://img.shields.io/github/forks/justbetter/magento2-akeneo-bundle.svg?style=for-the-badge +[forks-url]: https://github.com/justbetter/magento2-akeneo-bundle/network/members +[stars-shield]: https://img.shields.io/github/stars/justbetter/magento2-akeneo-bundle.svg?style=for-the-badge +[stars-url]: https://github.com/justbetter/magento2-akeneo-bundle/stargazers +[issues-shield]: https://img.shields.io/github/issues/justbetter/magento2-akeneo-bundle.svg?style=for-the-badge +[issues-url]: https://github.com/justbetter/magento2-akeneo-bundle/issues +[license-shield]: https://img.shields.io/github/license/justbetter/magento2-akeneo-bundle.svg?style=for-the-badge +[license-url]: https://github.com/justbetter/magento2-akeneo-bundle/blob/master/LICENSE -JustBetter logo +[PHP-badge]: https://img.shields.io/badge/PHP-777BB4?style=for-the-badge&logo=php&logoColor=white +[PHP-url]: https://www.php.net/ +[Magento-badge]: https://img.shields.io/badge/Magento-EE672F?style=for-the-badge&logo=magento&logoColor=white +[Magento-url]: https://business.adobe.com/products/magento/magento-commerce.html +[Akeneo-badge]: https://img.shields.io/badge/Akeneo-7C1B8A?style=for-the-badge&logo=akeneo&logoColor=white +[Akeneo-url]: https://www.akeneo.com/ diff --git a/Plugin/CategoryExist.php b/Service/CategoryExist.php similarity index 92% rename from Plugin/CategoryExist.php rename to Service/CategoryExist.php index 86f6b9b..3e91de4 100644 --- a/Plugin/CategoryExist.php +++ b/Service/CategoryExist.php @@ -1,6 +1,7 @@ config->getValue('akeneo_connector/justbetter/categoryexist', ScopeInterface::SCOPE_WEBSITE); if (!$extensionEnabled) { @@ -24,12 +25,12 @@ public function beforeSetValues() } $connection = $this->entitiesHelper->getConnection(); - $stores = $this->storeHelper->getStores('lang'); + foreach ($stores as $local => $affected) { foreach ($affected as $store) { $columnName = 'url_key-' . $store['lang']; - + $query = " UPDATE tmp_akeneo_connector_entities_category temp LEFT JOIN catalog_category_entity_varchar eav ON ( @@ -43,14 +44,14 @@ public function beforeSetValues() FROM eav_entity_type WHERE entity_type_code = 'catalog_category' ) - AND eav.`value` != '' + AND eav.value != '' ) AND eav.store_id = {$store['store_id']} ) SET temp.`{$columnName}` = eav.value WHERE temp._is_new = 0 AND eav.value IS NOT NULL "; - + $connection->query($query); } } diff --git a/Service/CheckWebsiteAssociation.php b/Service/CheckWebsiteAssociation.php new file mode 100644 index 0000000..7dae384 --- /dev/null +++ b/Service/CheckWebsiteAssociation.php @@ -0,0 +1,124 @@ +entitiesHelper->getConnection(); + $tmpTable = $this->entitiesHelper->getTableName($code); + $websiteAttribute = $this->configHelper->getWebsiteAttribute(); + $websites = $this->storeHelper->getStores('website_code'); + $websiteAssociation = $this->config->getValue('akeneo_connector/product/website_attribute'); + + $requiredAttributes = $this->getRequiredAttributes(); + + if (!$connection->tableColumnExists($tmpTable, $websiteAttribute)) { + return; + } + + $select = $connection->select()->from($tmpTable); + $query = $connection->query($select); + + while (($row = $query->fetch()) && is_array($row)) { + if (!isset($row[$websiteAssociation])) { + continue; + } + + $websites = explode(',', $row[$websiteAssociation]); + $mapping = $this->getMappedWebsiteChannels(); + + foreach ($websites as $key => $website) { + $channel = $mapping[$website] ?? ''; + if (empty($channel)) { + continue; + } + + $locales = $this->storeHelper->getChannelStoreLangs($channel); + foreach ($requiredAttributes as $attribute) { + if (!is_array($attribute)) { + continue; + } + if (isset($attribute['localizable']) && $attribute['localizable'] === true) { + foreach ($locales as $locale) { + if (empty($row[$attribute['akeneo_attribute'] . '-' . $locale . '-' . $channel])) { + unset($websites[$key]); + + break 2; + } + } + } else { + $attrKey = $attribute['akeneo_attribute'] ?? ''; + if (empty($row[$attrKey])) { + unset($websites[$key]); + + break 2; + } + } + } + } + + $connection->update( + $tmpTable, + [ + $websiteAssociation => implode(',', $websites), + ], + ['identifier = ?' => $row['identifier']] + ); + } + } + + /** + * @return array + */ + protected function getMappedWebsiteChannels(): array + { + $mapping = $this->configHelper->getWebsiteMapping(); + + return array_column($mapping, 'channel', 'website'); + } + + /** + * @return array> + */ + protected function getRequiredAttributes(): array + { + if (!($requiredAttributes = $this->config->getValue('akeneo_connector/product/required_attribute_mapping'))) { + return []; + } + $unserialized = $this->serializer->unserialize($requiredAttributes); + + if (!is_array($unserialized)) { + return []; + } + + foreach ($unserialized as $key => &$requiredAttribute) { + if (!is_array($requiredAttribute)) { + continue; + } + $akeneoAttribute = $this->authenticator->getAkeneoApiClient()->getAttributeApi()->get($requiredAttribute['akeneo_attribute']); + $requiredAttribute['localizable'] = $akeneoAttribute['localizable']; + } + + return $unserialized; + } +} diff --git a/Service/SetTaxClassId.php b/Service/SetTaxClassId.php new file mode 100644 index 0000000..b87111a --- /dev/null +++ b/Service/SetTaxClassId.php @@ -0,0 +1,140 @@ + + */ + protected array $taxIdColumns = []; + + public function __construct( + protected ProductImportHelper $entitiesHelper, + protected StoreHelper $storeHelper, + protected Json $serializer, + protected ConfigHelper $configHelper, + protected Authenticator $authenticator, + protected ScopeConfigInterface $scopeConfig + ) { + } + + public function execute(string $code): void + { + if (!$this->scopeConfig->getValue('akeneo_connector/justbetter/settaxclass', ScopeInterface::SCOPE_WEBSITE) || + !($attributes = $this->scopeConfig->getValue(ConfigHelper::ATTRIBUTE_TYPES)) || + !($mappings = $this->scopeConfig->getValue('akeneo_connector/product/tax_id_mapping')) + ) { + return; + } + + $unserializedAttributes = $this->serializer->unserialize($attributes); + $unserializedMappings = $this->serializer->unserialize($mappings); + + if (!is_array($unserializedAttributes) || !is_array($unserializedMappings)) { + return; + } + + $this->taxIdColumns = []; + foreach ($unserializedAttributes as $attribute) { + if (is_array($attribute) && isset($attribute['magento_type']) && $attribute['magento_type'] === "tax") { + $this->taxIdColumns[] = $attribute['pim_type']; + } + } + + if (empty($this->taxIdColumns) || empty($unserializedMappings)) { + return; + } + + $tmpTable = $this->entitiesHelper->getTableName($code); + $taxColumns = $this->checkTaxColumnsExist($this->taxIdColumns, $tmpTable); + + if (empty($taxColumns)) { + return; + } + + $connection = $this->entitiesHelper->getConnection(); + + foreach ($taxColumns as $taxIdColumn) { + $taxQuery = $this->createQuery($taxIdColumn, $tmpTable); + $connection->query($taxQuery); + } + } + + protected function createQuery(string $taxIdColumn, string $tableName): string + { + $connection = $this->entitiesHelper->getConnection(); + $query = "UPDATE " . $connection->quoteIdentifier($tableName) . " SET " . $connection->quoteIdentifier($taxIdColumn) . " = "; + + return $this->addCase($query, $taxIdColumn); + } + + protected function addCase(string $query, string $taxIdColumn): string + { + if (!($mappings = $this->scopeConfig->getValue('akeneo_connector/product/tax_id_mapping'))) { + return $query; + } + $unserializedMappings = $this->serializer->unserialize($mappings); + + if (!is_array($unserializedMappings) || !count($unserializedMappings)) { + return $query; + } + + $connection = $this->entitiesHelper->getConnection(); + $query .= "CASE "; + + foreach ($unserializedMappings as $mapping) { + if (!is_array($mapping)) { + continue; + } + $query .= "WHEN " . $connection->quoteIdentifier($taxIdColumn) . " = " . $connection->quote($mapping['akeneo']) . " THEN " . $connection->quote($mapping['magento']) . " "; + } + + $query .= 'END'; + + return $query; + } + + /** + * @param array $mappings + * @return array + */ + protected function checkTaxColumnsExist(array $mappings, string $tmpTable): array + { + $newMappings = []; + $connection = $this->entitiesHelper->getConnection(); + + foreach ($mappings as $mapping) { + $akeneoAttribute = $this->authenticator->getAkeneoApiClient()->getAttributeApi()->get($mapping); + + if ($akeneoAttribute['localizable'] === false) { + if ($connection->tableColumnExists($tmpTable, $mapping)) { + $newMappings[] = $mapping; + } + } + + if (isset($akeneoAttribute['localizable'])) { + $mappedChannels = $this->configHelper->getMappedChannels(); + foreach ($mappedChannels as $channel) { + foreach ($this->storeHelper->getChannelStoreLangs($channel) as $locale) { + if ($connection->tableColumnExists($tmpTable, $mapping . '-' . $locale . '-' . $channel)) { + $newMappings[] = $mapping . '-' . $locale . '-' . $channel; + } + } + } + } + } + + return $newMappings; + } +} diff --git a/composer.json b/composer.json index b860627..73d1a41 100644 --- a/composer.json +++ b/composer.json @@ -1,23 +1,23 @@ { "name": "justbetter/magento2-akeneo-bundle", "description": "Magento2 bundle for extending the Akeneo connector with awesome features.", + "license": "GPL-3.0-or-later", "require": { - "php": ">=8.0", - "akeneo/module-magento2-connector-community": "*" + "php": "^8.2", + "akeneo/module-magento2-connector-community": "^105.1.0" }, "require-dev": { "bitexpert/phpstan-magento": "^0.11.0", - "phpstan/phpstan": "^1.10" + "phpstan/phpstan": "^1.10", + "phpro/grumphp": "^2.0", + "magento/magento-coding-standard": "^33", + "squizlabs/php_codesniffer": "^3.0" }, "type": "magento2-module", "authors": [ { - "name": "Pim Ruiter", - "email": "pim@justbetter.nl" - }, - { - "name": "Robin Mulder", - "email": "robin@justbetter.nl" + "name": "JustBetter B.V.", + "email": "hello@justbetter.nl" } ], "repositories": { @@ -37,7 +37,21 @@ "config": { "allow-plugins": { "php-http/discovery": true, - "magento/composer-dependency-version-audit-plugin": true + "magento/composer-dependency-version-audit-plugin": true, + "dealerdirect/phpcodesniffer-composer-installer": true, + "phpro/grumphp": true } + }, + "scripts": { + "analyse": "phpstan analyse --memory-limit=1G", + "codestyle": "vendor/bin/phpcs", + "codestyle:fix": "vendor/bin/phpcbf", + "grumphp": "grumphp run" + }, + "scripts-descriptions": { + "analyse": "Run PHPStan static analysis", + "codestyle": "Check code style with PHP_CodeSniffer", + "codestyle:fix": "Automatically fix code style issues", + "grumphp": "Run all GrumPHP tasks (phpcs + phpstan)" } } diff --git a/etc/adminhtml/system.xml b/etc/adminhtml/system.xml index 32c239b..2c4989d 100644 --- a/etc/adminhtml/system.xml +++ b/etc/adminhtml/system.xml @@ -13,18 +13,6 @@ - - - JustBetter\AkeneoBundle\Block\Adminhtml\System\Config\Form\Field\Type - Magento\Config\Model\Config\Backend\Serialized\ArraySerialized - Map specific Akeneo attribute code with a Magento Customer group. This ensures that the - tier prices from Akeneo are imported into Magento customer tier prices - - - 1 - - @@ -52,12 +40,6 @@ Akeneo\Connector\Model\Source\Filters\Attribute 1 - - - Magento\Config\Model\Config\Source\Yesno - Map specific Akeneo attribute code with a Magento Customer group. (Default no) - @@ -70,36 +52,12 @@ Magento\Config\Model\Config\Source\Yesno Manages akeneo codes and magento entity id's. (Default: no) - - - Magento\Config\Model\Config\Source\Yesno - Imports new products from akeneo. (Default: yes) - Magento\Config\Model\Config\Source\Yesno Map Akeneo tax class codes to Magento tax class (Default: no) - - - Magento\Config\Model\Config\Source\Yesno - Set all products from import to active. (Default: no) - - - - Magento\Config\Model\Config\Source\Yesno - Set manage stock on product always on (Default: no) - - - - Magento\Config\Model\Config\Source\Yesno - Set stock status to "In Stock" when backorders are allowed - diff --git a/etc/config.xml b/etc/config.xml index 90467f4..6196ec4 100644 --- a/etc/config.xml +++ b/etc/config.xml @@ -3,12 +3,8 @@ - 0 0 0 - 1 - 0 - 0 0 0 ecommerce diff --git a/etc/crontab.xml b/etc/crontab.xml index 014122b..8ab2923 100644 --- a/etc/crontab.xml +++ b/etc/crontab.xml @@ -1,8 +1,8 @@ - - - 0 8 * * * - - + + + 0 8 * * * + + diff --git a/etc/di.xml b/etc/di.xml index 1030544..ba3b607 100644 --- a/etc/di.xml +++ b/etc/di.xml @@ -1,37 +1,9 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/etc/events.xml b/etc/events.xml index 8beaeef..e5b020f 100644 --- a/etc/events.xml +++ b/etc/events.xml @@ -6,4 +6,13 @@ + + + + + + + + + \ No newline at end of file diff --git a/grumphp.yml b/grumphp.yml new file mode 100644 index 0000000..0204a44 --- /dev/null +++ b/grumphp.yml @@ -0,0 +1,20 @@ +grumphp: + tasks: + phpcs: + standard: phpcs.xml + triggered_by: ['php'] + ignore_patterns: + - vendor/ + - Block/ + + phpstan: + configuration: phpstan.neon + memory_limit: "1G" + use_grumphp_paths: false + triggered_by: ['php'] + + testsuites: + git_commit: + tasks: + - phpcs + - phpstan diff --git a/phpcs.xml b/phpcs.xml new file mode 100644 index 0000000..5241684 --- /dev/null +++ b/phpcs.xml @@ -0,0 +1,35 @@ + + + Magento 2 Coding Standard for Akeneo Bundle + + + + + + + + + + + + + + + + . + + + */vendor/* + */Block/* + + + + + + + + + + + + diff --git a/phpstan.neon b/phpstan.neon index d5ab6f2..ca7a3a9 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -1,12 +1,8 @@ includes: - - vendor/bitexpert/phpstan-magento/extension.neon + - %currentWorkingDirectory%/vendor/bitexpert/phpstan-magento/extension.neon parameters: paths: - . excludePaths: - vendor - level: 1 - ignoreErrors: - - '#Magento\\Backend\\Model\\View\\Result\\ForwardFactory#' - - '#Magento\\Eav\\Model\\ResourceModel\\Entity\\Attribute\\Set\\CollectionFactory#' - - '#Magento\\Catalog\\Model\\ResourceModel\\Product\\CollectionFactory#' \ No newline at end of file + level: 7 \ No newline at end of file diff --git a/registration.php b/registration.php index bbbcb7d..63ae34f 100644 --- a/registration.php +++ b/registration.php @@ -1,6 +1,6 @@ getGridHtml() ?> \ No newline at end of file +. + */ +?> + +getGridHtml() ?> diff --git a/view/adminhtml/templates/akeneo/grid/massaction_extended.phtml b/view/adminhtml/templates/akeneo/grid/massaction_extended.phtml old mode 100755 new mode 100644 index d4e71b2..8af936e --- a/view/adminhtml/templates/akeneo/grid/massaction_extended.phtml +++ b/view/adminhtml/templates/akeneo/grid/massaction_extended.phtml @@ -1,31 +1,57 @@ -
+. + */ +?> +getEscaper(); +?> +
- getHideFormElement() !== true):?> -
- - getBlockHtml('formkey')?> - - - - - - - - getApplyButtonHtml() ?> - - getHideFormElement() !== true):?> -
- + getHideFormElement() !== true): ?> +
+ + getBlockHtml('formkey') ?> + + + + + + + + getApplyButtonHtml() ?> + + getHideFormElement() !== true): ?> +
+
getItems() as $_item): ?> -
- getAdditionalActionBlockHtml() ?> +
+ getAdditionalActionBlockHtml() ?>
@@ -33,55 +59,53 @@
- - 0 - + + 0 + escapeHtml(__('items selected')) ?> +