diff --git a/Dockerfile b/Dockerfile index 17c10b4..3104d84 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,6 @@ ARG ALPINE_VERSION=3.16 -FROM php:8.0-cli-alpine$ALPINE_VERSION AS php-cli +FROM php:8.1-cli-alpine$ALPINE_VERSION AS php-cli RUN apk add php git make perl icu-dev --no-cache RUN docker-php-ext-install intl @@ -10,7 +10,7 @@ WORKDIR /app COPY --from=composer:2 /usr/bin/composer /usr/bin/composer COPY composer.json ./ ENV COMPOSER_ALLOW_SUPERUSER 1 -RUN composer install --prefer-dist --no-progress --no-suggest --no-interaction --no-plugins --ignore-platform-reqs +RUN composer install --prefer-dist --no-progress --no-interaction --no-plugins --ignore-platform-reqs COPY dev/docker/docker-entrypoint.sh /usr/local/bin/docker-entrypoint RUN chmod +x /usr/local/bin/docker-entrypoint diff --git a/README.md b/README.md index a769881..bb94d7e 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ This module adds import Akeneo data to Silverstripe enviroment # Requirements * SilverStripe ^4.11 -* PHP ^8.0 +* PHP ^8.1 # Installation - `composer require "wedevelopnl/silverstripe-akeneo"` diff --git a/composer.json b/composer.json index 066a2fe..9cc5979 100644 --- a/composer.json +++ b/composer.json @@ -12,14 +12,20 @@ }], "type": "silverstripe-vendormodule", "require": { - "php": "^8.0", + "php": "^8.1", "silverstripe/framework": "^4.11", "silverstripe/cms": "^4.11", "symbiote/silverstripe-gridfieldextensions": "^3.4", "undefinedoffset/sortablegridfield": "^2.1" }, "require-dev": { - "friendsofphp/php-cs-fixer": "^3.4" + "friendsofphp/php-cs-fixer": "^3.4", + "rector/rector": "^0.18.2", + "wernerkrauss/silverstripe-rector": "dev-main", + "phpstan/phpstan": "1.10.54", + "phpstan/extension-installer": "^1.3", + "phpstan/phpstan-deprecation-rules": "^1.0", + "syntro/silverstripe-phpstan": "1.0" }, "extra": { "installer-name": "silverstripe-akeneo", @@ -35,7 +41,8 @@ "config": { "allow-plugins": { "composer/installers": false, - "silverstripe/vendor-plugin": false + "silverstripe/vendor-plugin": false, + "phpstan/extension-installer": true } } } diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon new file mode 100644 index 0000000..ee94cf2 --- /dev/null +++ b/phpstan-baseline.neon @@ -0,0 +1,26 @@ +parameters: + ignoreErrors: + - + message: "#^Call to method delete\\(\\) on an unknown class WeDevelop\\\\Akeneo\\\\Models\\\\ProductAssociation\\.ProductModel\\.$#" + count: 1 + path: src/Imports/AkeneoImport.php + + - + message: "#^Access to property \\$Type on an unknown class WeDevelop\\\\Akeneo\\\\Models\\\\ProductAssociation\\.Product\\.$#" + count: 2 + path: src/Models/Product.php + + - + message: "#^Call to method RelatedProduct\\(\\) on an unknown class WeDevelop\\\\Akeneo\\\\Models\\\\ProductAssociation\\.Product\\.$#" + count: 3 + path: src/Models/Product.php + + - + message: "#^Using nullsafe method call on non\\-nullable type WeDevelop\\\\Akeneo\\\\Models\\\\ProductMediaFile\\. Use \\-\\> instead\\.$#" + count: 1 + path: src/Models/ProductAttributeValue.php + + - + message: "#^Method WeDevelop\\\\Akeneo\\\\Service\\\\AkeneoApi\\:\\:refreshToken\\(\\) is unused\\.$#" + count: 1 + path: src/Service/AkeneoApi.php diff --git a/phpstan.neon b/phpstan.neon new file mode 100644 index 0000000..c93e0d0 --- /dev/null +++ b/phpstan.neon @@ -0,0 +1,12 @@ +parameters: + level: 5 + bootstrapFiles: + - vendor/syntro/silverstripe-phpstan/bootstrap.php + scanDirectories: + - src + paths: + - src + +includes: + - vendor/syntro/silverstripe-phpstan/phpstan.neon + - phpstan-baseline.neon \ No newline at end of file diff --git a/rector.php b/rector.php new file mode 100644 index 0000000..b145d62 --- /dev/null +++ b/rector.php @@ -0,0 +1,24 @@ +paths([ + __DIR__ . '/src' + ]); + + // define sets of rules + $rectorConfig->sets([ + LevelSetList::UP_TO_PHP_81, + SetList::CODE_QUALITY, + SetList:: CODING_STYLE, + SilverstripeSetList::CODE_STYLE, + ]); + + //$rectorConfig->phpstanConfig(__DIR__ . '/phpstan.neon'); +}; \ No newline at end of file diff --git a/src/Admins/AkeneoAdmin.php b/src/Admins/AkeneoAdmin.php index 15b9d3d..a3be778 100644 --- a/src/Admins/AkeneoAdmin.php +++ b/src/Admins/AkeneoAdmin.php @@ -4,14 +4,14 @@ use SilverStripe\Admin\ModelAdmin; use SilverStripe\Control\Controller; -use SilverStripe\Core\Injector\Injector; +use SilverStripe\Forms\Form; use SilverStripe\Forms\FormAction; use SilverStripe\Forms\GridField\GridField; use SilverStripe\Forms\HeaderField; use SilverStripe\Forms\TabSet; +use SilverStripe\ORM\DataList; use SilverStripe\ORM\DataObject; use Symbiote\GridFieldExtensions\GridFieldOrderableRows; -use WeDevelop\Akeneo\Imports\AkeneoImport; use WeDevelop\Akeneo\Models\Display\DisplayGroup; use WeDevelop\Akeneo\Models\Family; use WeDevelop\Akeneo\Models\Product; @@ -22,7 +22,10 @@ class AkeneoAdmin extends ModelAdmin { - /** @config */ + /** + * @config + * @var array + */ private static array $managed_models = [ Product::class, ProductModel::class, @@ -40,31 +43,34 @@ class AkeneoAdmin extends ModelAdmin /** @config */ private static string $menu_icon = 'wedevelopnl/silverstripe-akeneo:images/akeneo.png'; - public function getEditForm($id = null, $fields = null) + public function getEditForm($id = null, $fields = null): Form { $form = parent::getEditForm($id, $fields); - if ($this->modelClass === ProductCategory::class && $gridField = $form->Fields()->dataFieldByName($this->sanitiseClassName($this->modelClass))) { - if ($gridField instanceof GridField) { - $gridField->getConfig()->addComponent(new GridFieldOrderableRows('Sort')); - } + if ($this->modelClass === ProductCategory::class && ($gridField = $form->Fields()->dataFieldByName($this->sanitiseClassName($this->modelClass))) && $gridField instanceof GridField) { + $gridField->getConfig()->addComponent(GridFieldOrderableRows::create('Sort')); } if ($this->modelClass === DisplayGroup::class && $gridField = $form->Fields()->dataFieldByName($this->sanitiseClassName($this->modelClass))) { if ($gridField instanceof GridField) { $originalField = clone $gridField; + /** @var DataList $gridFieldList */ + $gridFieldList = $gridField->getList(); + /** @var DataList $originalFieldList */ + $originalFieldList = $originalField->getList(); + $originalField->setList( - $originalField->getList()->filter([ + $originalFieldList->filter([ 'IsRootGroup' => 0, - ]) + ]), ); $gridField->setName('RootDisplayGroups'); $gridField->setList( - $gridField->getList()->filter([ + $gridFieldList->filter([ 'IsRootGroup' => 1, - ]) + ]), ); } @@ -76,10 +82,12 @@ public function getEditForm($id = null, $fields = null) $gridField, ]); - $fields->addFieldsToTab('Root.Sub Groups', [ - HeaderField::create('SubHeader', 'Sub groups are part of a group chain, and therefore have a parent group defined somewhere.'), - $originalField, - ]); + if (!empty($originalField)) { + $fields->addFieldsToTab('Root.Sub Groups', [ + HeaderField::create('SubHeader', 'Sub groups are part of a group chain, and therefore have a parent group defined somewhere.'), + $originalField, + ]); + } } $importRunning = self::isImportRunning(); @@ -96,9 +104,9 @@ public function getEditForm($id = null, $fields = null) public function doSync(): void { try { - $importMessage = self::asyncImport(); - } catch (\Exception $e) { - $importMessage = $e->getMessage(); + $importMessage = self::asyncImport(); + } catch (\Exception $exception) { + $importMessage = $exception->getMessage(); } Controller::curr()->getResponse()->addHeader('X-Status', $importMessage); @@ -111,7 +119,7 @@ private static function isImportRunning(): bool return (!empty($psOutput) && count($psOutput) > 2); } - public static function asyncImport(): string + public static function asyncImport(): string { if (self::isImportRunning()) { throw new \Exception('An import is still running.'); @@ -129,7 +137,7 @@ public function getCMSEditLink(DataObject $object, string $subTab = ''): string $editFormField = 'EditForm/field/'; - if ($subTab) { + if ($subTab !== '' && $subTab !== '0') { $editFormField .= $subTab . '/'; } diff --git a/src/Controllers/ProductPageController.php b/src/Controllers/ProductPageController.php index 42945ae..5d50d1e 100644 --- a/src/Controllers/ProductPageController.php +++ b/src/Controllers/ProductPageController.php @@ -1,5 +1,7 @@ owner->AkeneoURL = rtrim($this->owner->AkeneoURL ?? '', '/ '); + $this->getOwner()->AkeneoURL = rtrim($this->getOwner()->AkeneoURL ?? '', '/ '); parent::onBeforeWrite(); } private function credentialsExist(): bool { - return $this->owner->AkeneoURL && - $this->owner->AkeneoClientID && - $this->owner->AkeneoSecret && - $this->owner->AkeneoUsername && - $this->owner->AkeneoPassword; + return $this->getOwner()->AkeneoURL && + $this->getOwner()->AkeneoClientID && + $this->getOwner()->AkeneoSecret && + $this->getOwner()->AkeneoUsername && + $this->getOwner()->AkeneoPassword; } private function canConnect(): bool @@ -71,7 +71,7 @@ private function canConnect(): bool try { $this->akeneoApi->authorize(); - } catch (ClientException $e) { + } catch (ClientException) { return false; } diff --git a/src/Extensions/AkeneoSiteTreeExtension.php b/src/Extensions/AkeneoSiteTreeExtension.php index 0ec3ba5..81a2ace 100644 --- a/src/Extensions/AkeneoSiteTreeExtension.php +++ b/src/Extensions/AkeneoSiteTreeExtension.php @@ -10,10 +10,8 @@ class AkeneoSiteTreeExtension extends DataExtension { public function augmentStageChildren(DataList &$staged): void { - if ($this->owner->ClassName === SiteTree::class && $this->owner->ID === 0) { - if ($excludedPages = $this->owner->config()->get('excluded_root_pages')) { - $staged = $staged->Filter('ClassName:not', $excludedPages); - } + if ($this->getOwner()->ClassName === SiteTree::class && $this->getOwner()->ID === 0 && ($excludedPages = $this->getOwner()->config()->get('excluded_root_pages'))) { + $staged = $staged->Filter('ClassName:not', $excludedPages); } } } diff --git a/src/Filters/TranslationLabelFilter.php b/src/Filters/TranslationLabelFilter.php index d695ab5..3c1df1f 100644 --- a/src/Filters/TranslationLabelFilter.php +++ b/src/Filters/TranslationLabelFilter.php @@ -1,5 +1,7 @@ getValue(); - $query = call_user_func([ProductAttribute::class, 'filterByLabel'], $query, $value); + $query = call_user_func(ProductAttribute::filterByLabel(...), $query, $value); return $query; } protected function excludeOne(DataQuery $query) { - // TODO: Implement excludeOne() method. + throw new \RuntimeException('Not implemented yet.'); } } diff --git a/src/Imports/AkeneoImport.php b/src/Imports/AkeneoImport.php index 53a45b8..252d926 100644 --- a/src/Imports/AkeneoImport.php +++ b/src/Imports/AkeneoImport.php @@ -26,40 +26,44 @@ class AkeneoImport use Injectable; use Configurable; - /** @var array */ + /** @var array */ private array $categories = []; - /** @var array */ + /** @var array */ private array $attributes = []; - /** @var array */ + /** @var array */ private array $attributeGroups = []; - /** @var array */ + /** @var array */ private array $attributesOptions = []; - /** @var array */ + /** @var array */ private array $families = []; - /** @var array */ + /** @var array */ private array $variants = []; - /** @var array */ + /** @var array */ private array $productModels = []; - /** @var array */ + /** @var array */ private array $products = []; - /** @var array */ + /** @var array */ private array $productMediaFiles = []; private array $productModelsAssociations = []; + private array $productsAssociations = []; private bool $verbose = true; - private AkeneoApi $akeneoApi; + private readonly AkeneoApi $akeneoApi; + /** + * @var array + */ private array $associationMapping = [ 'products' => [ 'class' => Product::class, @@ -73,6 +77,9 @@ class AkeneoImport ], ]; + /** + * @var array + */ private array $imports = [ 'categories' => ProductCategory::class, 'attributeGroups' => ProductAttributeGroup::class, @@ -84,6 +91,9 @@ class AkeneoImport 'products' => Product::class, ]; + /** + * @var array>}> + */ private array $importParents = [ 'attributeOptions' => [ 'class' => ProductAttribute::class, @@ -98,6 +108,9 @@ class AkeneoImport ], ]; + /** + * @var array + */ private array $requiredParentImport = [ 'attributes' => 'attributeOptions', 'families' => 'variants', @@ -118,7 +131,7 @@ public function __construct() public function run(array $imports): void { foreach (array_keys($this->imports) as $type) { - if (!empty($imports) && !in_array($type, $imports, true) && !$this->isRequiredParentImport($type, $imports)) { + if ($imports !== [] && !in_array($type, $imports, true) && !$this->isRequiredParentImport($type, $imports)) { continue; } @@ -155,7 +168,7 @@ protected function isRequiredParentImport(string $type, array $import): bool protected function import(string $type, ?string $parentImport = null, ?string $parentImportKey = null): void { - $this->output("Import " . $type . ($parentImportKey ? " of {$parentImportKey}" : '')); + $this->output("Import " . $type . ($parentImportKey ? ' of ' . $parentImportKey : '')); $class = $this->imports[$type]; if ($parentImport && $parentImportKey) { @@ -168,7 +181,7 @@ protected function import(string $type, ?string $parentImport = null, ?string $p $limit = 50; do { - $page++; + ++$page; $apiMethod = 'get' . ucfirst($type); $akeneoData = $this->akeneoApi->$apiMethod($page, $limit, $parentImportKey); @@ -176,7 +189,7 @@ protected function import(string $type, ?string $parentImport = null, ?string $p foreach ($akeneoData['_embedded']['items'] as $akeneoItem) { if ($this->shouldSkip($type, $akeneoItem)) { - $this->output("Skipping {$akeneoItem['code']}"); + $this->output('Skipping ' . $akeneoItem['code']); continue; } @@ -221,13 +234,13 @@ protected function import(string $type, ?string $parentImport = null, ?string $p protected function setAssociations(): void { - if (!empty($this->productModelsAssociations)) { + if ($this->productModelsAssociations !== []) { foreach (ProductModel::get() as $productModel) { $this->setProductAssociations($productModel, $this->productModelsAssociations[$productModel->Code]); } } - if (!empty($this->productsAssociations)) { + if ($this->productsAssociations !== []) { foreach (Product::get() as $product) { $this->setProductAssociations($product, $this->productsAssociations[$product->SKU]); } @@ -261,6 +274,7 @@ protected function findRelatedObjectIds(string $type, array $akeneoItem, ?string if ($type === 'productModels') { $variantCode = $akeneoItem['family_variant']; + /** @var FamilyVariant|null $variant */ $variant = FamilyVariant::get()->filter([ 'Code' => $variantCode, 'Family.Code' => $familyCode, @@ -269,6 +283,7 @@ protected function findRelatedObjectIds(string $type, array $akeneoItem, ?string 'FamilyVariantID' => $variant?->ID, ]; } else { + /** @var Family|null $family */ $family = Family::get()->find('Code', $familyCode); $ids = [ 'FamilyID' => $family?->ID, @@ -276,6 +291,7 @@ protected function findRelatedObjectIds(string $type, array $akeneoItem, ?string } if ($parentCode) { + /** @var ProductModel|null $parent */ $parent = ProductModel::get()->find('Code', $parentCode); $ids['ProductModelID'] = $parent?->ID; } @@ -288,11 +304,7 @@ protected function findRelatedObjectIds(string $type, array $akeneoItem, ?string protected function shouldSkip(string $type, $akeneoItem): bool { - if ($type === 'categories' && $akeneoItem['parent'] && !array_key_exists($akeneoItem['parent'], $this->categories)) { - return true; - } - - return false; + return $type === 'categories' && $akeneoItem['parent'] && !array_key_exists($akeneoItem['parent'], $this->categories); } protected function prepareImport(string $type, string $class): void @@ -320,32 +332,38 @@ protected function prepareImportWithParent(string $type, string $class, string $ protected function setProductAttributes(AkeneoImportInterface $productInstance, array $attributeValues): void { + if (!$productInstance instanceof ProductModel && !$productInstance instanceof Product) { + return; + } + foreach ($attributeValues as $attributeCode => $values) { + /** @var ProductAttribute|null $attribute */ $attribute = ProductAttribute::get()->find('Code', $attributeCode); - if (!$attribute) { + if ($attribute === null) { continue; } - /** @var ProductModel|Product $productInstance */ foreach ($values as $value) { $akeneoLocale = $value['locale'] ?? null; + /** @var Locale|null $locale */ $locale = Locale::get()->find('Code', $akeneoLocale); + /** @var ProductAttributeValue|null $attributeValue */ $attributeValue = $productInstance->AttributeValues()->filter([ 'AttributeID' => $attribute->ID, 'LocaleID' => $locale->ID ?? 0, ])->first(); - - $attributeValue = $attributeValue ?: new ProductAttributeValue(); + $attributeValue ??= ProductAttributeValue::create(); $attributeValue->AttributeID = $attribute->ID; $attributeValue->LocaleID = $locale?->ID; - if (in_array(ProductAttributeType::tryFrom($attribute->Type), [ProductAttributeType::TEXT, ProductAttributeType::TEXTAREA])) { + if (in_array(ProductAttributeType::tryFrom($attribute->Type), [ProductAttributeType::TEXT, ProductAttributeType::TEXTAREA], true)) { $attributeValue->TextValue = $value['data']; } else { $attributeValue->Value = is_array($value['data']) ? json_encode($value['data']) : $value['data']; } + $productInstance->AttributeValues()->add($attributeValue); } } @@ -353,7 +371,10 @@ protected function setProductAttributes(AkeneoImportInterface $productInstance, protected function setProductAssociations(AkeneoImportInterface $productInstance, array $associations): void { - /** @var Product|ProductModel $productInstance */ + if (!$productInstance instanceof ProductModel && !$productInstance instanceof Product) { + return; + } + foreach ($productInstance->Associations() as $association) { $association->delete(); } @@ -369,7 +390,7 @@ protected function setProductAssociations(AkeneoImportInterface $productInstance $relatedField = $this->associationMapping[$relatedType]['relatedField']; foreach ($relatedObjectKeys as $relatedObjectKey) { - $association = new ProductAssociation(); + $association = ProductAssociation::create(); $association->Type = $associationType; if ($productInstance instanceof ProductModel) { $association->ProductModelID = $productInstance->ID; @@ -399,7 +420,7 @@ protected function importMediaFiles(): void $limit = 50; do { - $page++; + ++$page; $mediaFiles = $this->akeneoApi->getMediaFiles($page, $limit); $itemsCount = $mediaFiles['items_count']; @@ -410,6 +431,7 @@ protected function importMediaFiles(): void unset($this->productMediaFiles[$mediaFile['code']]); continue; } + $this->saveMediaFile($mediaFile); } } while ($page * $limit < $itemsCount); diff --git a/src/Models/AbstractAkeneoTranslateable.php b/src/Models/AbstractAkeneoTranslateable.php index 83041cc..633823b 100644 --- a/src/Models/AbstractAkeneoTranslateable.php +++ b/src/Models/AbstractAkeneoTranslateable.php @@ -3,22 +3,24 @@ namespace WeDevelop\Akeneo\Models; use SilverStripe\Control\Controller; +use SilverStripe\Control\NullHTTPRequest; use SilverStripe\i18n\i18n; use SilverStripe\ORM\DataObject; use SilverStripe\ORM\HasManyList; /** - * @method HasManyList LabelTranslations - * @property string Code + * @method HasManyList LabelTranslations() */ class AbstractAkeneoTranslateable extends DataObject implements AkeneoTranslateableInterface { - /** @config */ + /** + * @config + * @var array + */ private static array $has_many = [ 'LabelTranslations' => LabelTranslation::class, ]; - public function getTitle(): string { return $this->getLabel(); @@ -50,11 +52,14 @@ public function updateLabels(array $akeneoItem): void } foreach (array_keys($akeneoItem['labels']) as $locale) { - $label = $this->LabelTranslations()->find('Locale.Code', $locale) ?? new LabelTranslation(); + /** @var LabelTranslation|null $label */ + $label = $this->LabelTranslations()->find('Locale.Code', $locale); + $label ??= LabelTranslation::create(); + /** @var Locale|null $localeModel */ $localeModel = Locale::get()->find('Code', $locale); - if (!$localeModel) { - $localeModel = new Locale(); + if ($localeModel === null) { + $localeModel = Locale::create(); $localeModel->Code = $locale; $localeModel->write(); } @@ -67,15 +72,13 @@ public function updateLabels(array $akeneoItem): void public function getLocaleFromRequest(): string { - $controller = Controller::curr(); - - if (!$controller) { + if (!Controller::has_curr()) { return i18n::get_locale(); } - $request = $controller->getRequest(); + $request = Controller::curr()->getRequest(); - if (!$request) { + if ($request instanceof NullHTTPRequest) { return i18n::get_locale(); } diff --git a/src/Models/AkeneoImportInterface.php b/src/Models/AkeneoImportInterface.php index 62fcade..9695890 100644 --- a/src/Models/AkeneoImportInterface.php +++ b/src/Models/AkeneoImportInterface.php @@ -1,8 +1,12 @@ DisplayGroups - * @method ManyManyList|UnsavedRelationList ParentDisplayGroups + * @method ManyManyList|UnsavedRelationList DisplayGroups() + * @method ManyManyList|UnsavedRelationList ParentDisplayGroups() */ class DisplayGroup extends DataObject { @@ -30,19 +31,28 @@ class DisplayGroup extends DataObject /** @config */ private static string $plural_name = 'Display Groups'; - /** @var array @config */ + /** + * @config + * @var array + */ private static array $db = [ 'Title' => 'Varchar', 'IsRootGroup' => 'Boolean(0)', ]; - /** @var array @config */ + /** + * @config + * @var array + */ private static array $many_many = [ 'ProductAttributes' => ProductAttribute::class, 'DisplayGroups' => DisplayGroup::class, ]; - /** @var array> @config */ + /** + * @config + * @var array> + */ private static $many_many_extraFields = [ 'ProductAttributes' => [ 'SortOrder' => 'Int', @@ -52,15 +62,19 @@ class DisplayGroup extends DataObject ], ]; - /** @var array @config */ + /** + * @config + * @var array + */ private static array $belongs_many_many = [ 'ParentDisplayGroups' => DisplayGroup::class . '.DisplayGroups', ]; private const RECURSION_MAX_DEPTH = 20; + private static int $RECURSION_COUNTER = 0; - public function getCMSFields() + public function getCMSFields(): FieldList { $fields = parent::getCMSFields(); @@ -139,12 +153,12 @@ public function getHierarchyHTML(): string $DisplayGroups = $this->getDisplayGroups(); $html = '
    '; - $html .= '
  • ' . $this->getTitle() . '
  • '; + $html .= sprintf('
  • %s
  • ', $this->getCMSEditLink(), $this->getTitle()); $html .= '
      '; /** @var ProductAttribute $attribute */ foreach ($attributes as $attribute) { - $html .= '
    1. ' . $attribute->getLabel() . '
    2. '; + $html .= sprintf('
    3. %s
    4. ', $attribute->getLabel()); } $html .= '
    '; @@ -152,23 +166,15 @@ public function getHierarchyHTML(): string /** @var DisplayGroup $DisplayGroup */ foreach ($DisplayGroups as $DisplayGroup) { if (self::$RECURSION_COUNTER > self::RECURSION_MAX_DEPTH) { - $html .= '
'; - - return $html; + break; } - self::$RECURSION_COUNTER++; - - $hierarchyHTML = '
  • ' . $DisplayGroup->getHierarchyHTML() . '
  • '; + ++self::$RECURSION_COUNTER; - if ($hierarchyHTML) { - $html .= $hierarchyHTML; - } + $html .= sprintf('
  • %s
  • ', $DisplayGroup->getHierarchyHTML()); } - $html .= ''; - - return $html; + return $html . ''; } private function getHierarchyLiteralField(): LiteralField @@ -190,7 +196,7 @@ public static function getNonRootGroups() ]); } - public function onBeforeWrite() + protected function onBeforeWrite() { $this->IsRootGroup = $this->ParentDisplayGroups()->count() === 0; diff --git a/src/Models/Family.php b/src/Models/Family.php index 4c429e2..3477b33 100644 --- a/src/Models/Family.php +++ b/src/Models/Family.php @@ -19,29 +19,44 @@ class Family extends AbstractAkeneoTranslateable implements AkeneoImportInterfac /** @config */ private static string $plural_name = 'Families'; - /** @config */ + /** + * @config + * @var array + */ private static array $db = [ 'Code' => 'Varchar(255)', 'Updated' => 'Boolean', ]; - /** @config */ + /** + * @config + * @var array + */ private static array $has_one = [ 'AttributeAsLabel' => ProductAttribute::class, 'AttributeAsImage' => ProductAttribute::class, ]; - /** @config */ + /** + * @config + * @var array + */ private static array $has_many = [ 'Variants' => FamilyVariant::class, ]; - /** @config */ + /** + * @config + * @var array + */ private static array $many_many = [ 'Attributes' => ProductAttribute::class, ]; - /** @config */ + /** + * @config + * @var array + */ private static array $cascade_deletes = [ 'Variants', ]; @@ -57,7 +72,9 @@ public function populateAkeneoData(array $akeneoItem, array $relatedObjectIds = $attributeAsLabelCode = $akeneoItem['attribute_as_label']; $attributeAsImageCode = $akeneoItem['attribute_as_image']; + /** @var ProductAttribute|null $attributeAsLabel */ $attributeAsLabel = ProductAttribute::get()->find('code', $attributeAsLabelCode); + /** @var ProductAttribute|null $attributeAsImage */ $attributeAsImage = ProductAttribute::get()->find('code', $attributeAsImageCode); $this->AttributeAsLabelID = $attributeAsLabel?->ID; $this->AttributeAsImageID = $attributeAsImage?->ID; @@ -71,30 +88,25 @@ public function populateAkeneoData(array $akeneoItem, array $relatedObjectIds = /** * @param Member $member - * - * @return bool */ - public function canEdit($member = null) + public function canEdit($member = null): bool { return false; } /** * @param Member $member - * - * @return bool */ - public function canDelete($member = null) + public function canDelete($member = null): bool { return false; } /** * @param Member $member - * @param array $context - * @return bool + * @param array $context */ - public function canCreate($member = null, $context = []) + public function canCreate($member = null, $context = []): bool { return false; } diff --git a/src/Models/LabelTranslation.php b/src/Models/LabelTranslation.php index 14c940c..cea6cb8 100644 --- a/src/Models/LabelTranslation.php +++ b/src/Models/LabelTranslation.php @@ -2,31 +2,42 @@ namespace WeDevelop\Akeneo\Models; +use SilverStripe\Forms\FieldList; use SilverStripe\ORM\DataObject; /** - * @property string $Label + * @property ?string $Label */ class LabelTranslation extends DataObject { /** @config */ private static string $table_name = 'Akeneo_Label_Translations'; - /** @config */ + /** + * @config + * @var array + */ private static array $db = [ 'Label' => 'Varchar', ]; - /** @config */ + /** + * @config + * @var array + */ private static array $summary_fields = [ - 'ID', - 'Label', + 'ID' => 'ID', + 'Label' => 'Label', 'Locale.Code' => 'Locale', ]; + /** @config */ private static string $default_sort = 'Label'; - /** @config */ + /** + * @config + * @var array + */ private static array $has_one = [ 'Locale' => Locale::class, 'Family' => Family::class, @@ -41,7 +52,7 @@ class LabelTranslation extends DataObject 'ProductImage' => ProductImage::class, ]; - public function getCMSFields() + public function getCMSFields(): FieldList { $fields = parent::getCMSFields(); diff --git a/src/Models/Product.php b/src/Models/Product.php index facacd6..1c8ad83 100644 --- a/src/Models/Product.php +++ b/src/Models/Product.php @@ -3,6 +3,7 @@ namespace WeDevelop\Akeneo\Models; use SilverStripe\Control\Controller; +use SilverStripe\Control\NullHTTPRequest; use SilverStripe\Forms\GridField\GridField; use SilverStripe\Forms\GridField\GridFieldConfig_RecordViewer; use SilverStripe\i18n\i18n; @@ -29,38 +30,56 @@ class Product extends DataObject implements AkeneoImportInterface /** @config */ private static string $plural_name = 'Products'; - /** @config */ + /** + * @config + * @var array + */ private static array $db = [ 'SKU' => 'Varchar(255)', 'Enabled' => 'Boolean', 'Updated' => 'Boolean', ]; - /** @config */ + /** + * @config + * @var array + */ private static array $has_one = [ 'Family' => Family::class, 'ProductModel' => ProductModel::class, ]; - /** @config */ + /** + * @config + * @var array + */ private static array $has_many = [ 'AttributeValues' => ProductAttributeValue::class, 'Associations' => ProductAssociation::class . '.Product', ]; - /** @config */ + /** + * @config + * @var array + */ private static array $many_many = [ 'Categories' => ProductCategory::class, ]; - /** @config */ + /** + * @config + * @var array + */ private static array $summary_fields = [ - 'SKU', + 'SKU' => 'SKU', 'Family.Name' => 'Family', 'LabelFromAttribute' => 'Label', ]; - /** @config */ + /** + * @config + * @var array + */ private static array $searchable_fields = [ 'ID', 'SKU', @@ -68,15 +87,13 @@ class Product extends DataObject implements AkeneoImportInterface public function getLocaleFromRequest(): string { - $controller = Controller::curr(); - - if (!$controller) { + if (Controller::has_curr()) { return i18n::get_locale(); } - $request = $controller->getRequest(); + $request = Controller::curr()->getRequest(); - if (!$request) { + if ($request instanceof NullHTTPRequest) { return i18n::get_locale(); } @@ -98,7 +115,10 @@ public function getCMSFields() $fields->makeFieldReadonly($field); } - $fields->addFieldToTab('Root.AttributeValues', new GridField('AttributeValues', 'AttributeValues', $this->AttributeValues(), GridFieldConfig_RecordViewer::create())); + $fields->addFieldToTab( + 'Root.AttributeValues', + GridField::create('AttributeValues', 'AttributeValues', $this->AttributeValues(), GridFieldConfig_RecordViewer::create()) + ); return $fields; } @@ -166,7 +186,9 @@ public function getLabel(): string public function getLabelFromAttribute(): string { $attributeAsLabelCode = $this->Family()->AttributeAsLabel()->Code; - return $this->AttributeValues()->find('Attribute.Code', $attributeAsLabelCode)?->getValue() ?? 'unknown'; + /** @var ProductAttributeValue|null $attributeValue */ + $attributeValue = $this->AttributeValues()->find('Attribute.Code', $attributeAsLabelCode); + return $attributeValue?->getValue() ?? 'unknown'; } public function getLocalisedAttributeValues(?string $locale = null): DataList diff --git a/src/Models/ProductAssociation.php b/src/Models/ProductAssociation.php index 0ed7d76..640b851 100644 --- a/src/Models/ProductAssociation.php +++ b/src/Models/ProductAssociation.php @@ -4,6 +4,9 @@ use SilverStripe\Security\Member; +/** + * @property ?string $Type + */ class ProductAssociation extends AbstractAkeneoTranslateable { /** @config */ diff --git a/src/Models/ProductAttribute.php b/src/Models/ProductAttribute.php index cec0476..2752e98 100644 --- a/src/Models/ProductAttribute.php +++ b/src/Models/ProductAttribute.php @@ -8,6 +8,7 @@ use SilverStripe\ORM\DataQuery; use SilverStripe\ORM\HasManyList; use SilverStripe\Security\Member; +use WeDevelop\Akeneo\Filters\TranslationLabelFilter; /** * @method HasManyList Options() @@ -64,10 +65,11 @@ class ProductAttribute extends AbstractAkeneoTranslateable implements AkeneoImpo private static array $searchable_fields = [ 'LabelByLocale' => [ 'title' => 'Label', - 'filter' => \WeDevelop\Akeneo\Filters\TranslationLabelFilter::class, + 'filter' => TranslationLabelFilter::class, 'relation' => 'LabelTranslations', ], ]; + /** @config */ private static string $default_sort = 'Sort'; @@ -86,7 +88,10 @@ public function getCMSFields() } if ($this->Options()->count() > 0) { - $fields->addFieldToTab('Root.Options', new GridField('Options', 'Options', $this->Options(), GridFieldConfig_RecordEditor::create())); + $fields->addFieldToTab( + 'Root.Options', + GridField::create('Options', 'Options', $this->Options(), GridFieldConfig_RecordEditor::create()) + ); } return $fields; @@ -157,9 +162,10 @@ public function getLabelByLocale(): string $locale = i18n::get_locale(); } + /** @var LabelTranslation|null $labelTranslation */ $labelTranslation = $this->LabelTranslations()->filter('Locale.Code', $locale)->first(); - return $labelTranslation ? $labelTranslation->Label : ''; + return $labelTranslation !== null ? $labelTranslation->Label : ''; } public static function filterByLabel(DataQuery $query, string $value, string $locale = 'nl_NL') @@ -167,18 +173,19 @@ public static function filterByLabel(DataQuery $query, string $value, string $lo $labelTranslationIDs = LabelTranslation::get()->filter(['Label:PartialMatch' => $value])->column('ID'); $query->leftJoin('Akeneo_Label_Translations', '"Akeneo_Label_Translations"."ProductAttributeID" = "Akeneo_ProductAttribute"."ID"'); - $locale = \WeDevelop\Akeneo\Models\Locale::get()->filter(['code' => $locale])->first(); + /** @var Locale|null $locale */ + $locale = Locale::get()->filter(['code' => $locale])->first(); - if (empty($labelTranslationIDs) || (!$locale)) { + if (empty($labelTranslationIDs) || ($locale === null)) { $query->where("0 = 1"); } else { $idsString = implode(',', $labelTranslationIDs); - $query->where("\"Akeneo_Label_Translations\".\"ID\" IN ({$idsString}) and \"Akeneo_Label_Translations\".\"LocaleID\" = {$locale->ID}"); + $query->where(sprintf('"Akeneo_Label_Translations"."ID" IN (%s) and "Akeneo_Label_Translations"."LocaleID" = %d', $idsString, $locale->ID)); } $query->selectField('"Akeneo_Label_Translations"."Label"', 'SearchLabel'); - $query->sort([]); + $query->sort(); return $query; } } diff --git a/src/Models/ProductAttributeGroup.php b/src/Models/ProductAttributeGroup.php index ae1430a..62cd8ba 100644 --- a/src/Models/ProductAttributeGroup.php +++ b/src/Models/ProductAttributeGroup.php @@ -51,7 +51,10 @@ public function getCMSFields() } if ($this->Attributes()->count() > 0) { - $fields->addFieldToTab('Root.Options', new GridField('Attributes', 'Attributes', $this->Attributes(), GridFieldConfig_RecordEditor::create())); + $fields->addFieldToTab( + 'Root.Options', + GridField::create('Attributes', 'Attributes', $this->Attributes(), GridFieldConfig_RecordEditor::create()) + ); } return $fields; diff --git a/src/Models/ProductAttributeValue.php b/src/Models/ProductAttributeValue.php index af16350..e9b040f 100644 --- a/src/Models/ProductAttributeValue.php +++ b/src/Models/ProductAttributeValue.php @@ -51,33 +51,35 @@ public function getValue() if (($value = $this->getField('Value')) === null) { return null; } + $attribute = $this->Attribute(); return match (ProductAttributeType::tryFrom($attribute->Type)) { - ProductAttributeType::BOOLEAN => (bool) $value ? _t(__CLASS__.'.Yes', 'Yes') : _t(__CLASS__.'.No', 'No'), + ProductAttributeType::BOOLEAN => (bool)$value ? _t(self::class.'.Yes', 'Yes') : _t(self::class.'.No', 'No'), ProductAttributeType::DATE => DBDatetime::create()->setValue($value)->Nice(), ProductAttributeType::FILE, ProductAttributeType::IMAGE => ProductMediaFile::get()->find('Code', $value)?->getAttributeValue(), ProductAttributeType::METRIC => DBField::create_field('HTMLText', AttributeParser::MetricTypeParser($this)), ProductAttributeType::MULTISELECT => DBField::create_field('HTMLText', AttributeParser::MultiSelectParser($this)), ProductAttributeType::PRICE_COLLECTION => DBField::create_field('HTMLText', AttributeParser::PriceCollectionParser($this)), ProductAttributeType::SIMPLESELECT => $attribute->Options()->filter('Code', $value)->first()->Name, - ProductAttributeType::TEXT => DBField::create_field('HTMLText', strval($value)), + ProductAttributeType::TEXT => DBField::create_field('HTMLText', (string)$value), ProductAttributeType::TEXTAREA => DBField::create_field('HTMLText', nl2br($this->getField('TextValue') ?? $value)), - default => strval($value), + default => (string)$value, }; } - public function onAfterDelete() + protected function onAfterDelete() { parent::onAfterDelete(); - switch(ProductAttributeType::tryFrom($this->Attribute()->Type)) { - case ProductAttributeType::FILE: - File::get()->byID($this->Value)?->delete(); - break; - case ProductAttributeType::IMAGE: - Image::get()->byID($this->Value)?->delete(); - break; + $productAttributeFile = match (ProductAttributeType::tryFrom($this->Attribute()->Type)) { + ProductAttributeType::FILE => File::get()->byID($this->Value), + ProductAttributeType::IMAGE => Image::get()->byID($this->Value), + default => null, + }; + + if ($productAttributeFile instanceof DataObject) { + $productAttributeFile->delete(); } } diff --git a/src/Models/ProductImage.php b/src/Models/ProductImage.php index a120948..06cbf76 100644 --- a/src/Models/ProductImage.php +++ b/src/Models/ProductImage.php @@ -29,7 +29,7 @@ class ProductImage extends Image public static function createFromAkeneoData(array $data, string $content): self { $productImage = new self(); - $productImage->setFromString($content, sprintf('%s_%s', base64_encode($data['code']), $data['original_filename'])); + $productImage->setFromString($content, sprintf('%s_%s', base64_encode((string)$data['code']), $data['original_filename'])); $productImage->Code = $data['code']; $productImage->Title = $data['original_filename']; diff --git a/src/Service/AkeneoApi.php b/src/Service/AkeneoApi.php index 9c16d02..064417d 100644 --- a/src/Service/AkeneoApi.php +++ b/src/Service/AkeneoApi.php @@ -10,17 +10,24 @@ class AkeneoApi { private const URI = 'api/rest/v1/'; + private const TOKEN_URI = 'api/oauth/v1/'; - private string $host; - private string $clientId; - private string $secret; - private string $username; - private string $password; + private readonly string $host; + + private readonly string $clientId; + + private readonly string $secret; + + private readonly string $username; + + private readonly string $password; + private ?string $channel; - private Client $apiClient; - private Client $tokenClient; + private readonly Client $apiClient; + + private readonly Client $tokenClient; private ?Token $token = null; @@ -169,7 +176,7 @@ public function getChannels(): array */ private function request(string $uri, array $options = [], bool $withCount = true) { - if (!$this->token) { + if (!$this->token instanceof \WeDevelop\Akeneo\Service\Token) { $this->authorize(); } @@ -180,9 +187,9 @@ private function request(string $uri, array $options = [], bool $withCount = tru try { $response = $this->apiClient->get($uri, $options); - } catch (\Exception $e) { - Injector::inst()->get(LoggerInterface::class)->error($e->getMessage()); - throw $e; + } catch (\Exception $exception) { + Injector::inst()->get(LoggerInterface::class)->error($exception->getMessage()); + throw $exception; } if ($response->getHeader('Content-Type')[0] !== 'application/json') { @@ -191,9 +198,9 @@ private function request(string $uri, array $options = [], bool $withCount = tru try { $responseData = json_decode($response->getBody()->getContents(), true, 512, JSON_THROW_ON_ERROR); - } catch (\Exception $e) { - Injector::inst()->get(LoggerInterface::class)->error($e->getMessage()); - throw $e; + } catch (\Exception $exception) { + Injector::inst()->get(LoggerInterface::class)->error($exception->getMessage()); + throw $exception; } return $responseData; @@ -218,9 +225,9 @@ private function requestToken(): Token 'auth' => $auth, 'body' => json_encode($body), ]); - } catch (\Exception $e) { - Injector::inst()->get(LoggerInterface::class)->error($e->getMessage()); - throw $e; + } catch (\Exception $exception) { + Injector::inst()->get(LoggerInterface::class)->error($exception->getMessage()); + throw $exception; } return Token::createFromResponse($response); @@ -244,9 +251,9 @@ private function refreshToken(): Token 'auth' => $auth, 'body' => json_encode($body), ]); - } catch (\Exception $e) { - Injector::inst()->get(LoggerInterface::class)->error($e->getMessage()); - throw $e; + } catch (\Exception $exception) { + Injector::inst()->get(LoggerInterface::class)->error($exception->getMessage()); + throw $exception; } return Token::createFromResponse($response); diff --git a/src/Service/RequestToken.php b/src/Service/RequestToken.php index daae0da..77e7e08 100644 --- a/src/Service/RequestToken.php +++ b/src/Service/RequestToken.php @@ -9,9 +9,9 @@ class RequestToken { public function __construct( - private string $accessToken, - private int $expiresIn, - private string $refreshToken + private readonly string $accessToken, + private readonly int $expiresIn, + private readonly string $refreshToken ) { } @@ -19,9 +19,9 @@ public static function createFromResponse(ResponseInterface $response): self { try { $responseData = json_decode($response->getBody()->getContents(), true, 512, JSON_THROW_ON_ERROR); - } catch (\Exception $e) { - Injector::inst()->get(LoggerInterface::class)->error($e->getMessage()); - throw $e; + } catch (\Exception $exception) { + Injector::inst()->get(LoggerInterface::class)->error($exception->getMessage()); + throw $exception; } return new self( diff --git a/src/Service/Token.php b/src/Service/Token.php index c32eb7d..127e9cd 100644 --- a/src/Service/Token.php +++ b/src/Service/Token.php @@ -9,9 +9,9 @@ class Token { public function __construct( - private string $accessToken, - private int $expiresIn, - private string $refreshToken + private readonly string $accessToken, + private readonly int $expiresIn, + private readonly string $refreshToken ) { } @@ -19,9 +19,9 @@ public static function createFromResponse(ResponseInterface $response): self { try { $responseData = json_decode($response->getBody()->getContents(), true, 512, JSON_THROW_ON_ERROR); - } catch (\Exception $e) { - Injector::inst()->get(LoggerInterface::class)->error($e->getMessage()); - throw $e; + } catch (\Exception $exception) { + Injector::inst()->get(LoggerInterface::class)->error($exception->getMessage()); + throw $exception; } return new self( diff --git a/src/Tasks/AkeneoImportTask.php b/src/Tasks/AkeneoImportTask.php index dda4f47..8ad1e22 100644 --- a/src/Tasks/AkeneoImportTask.php +++ b/src/Tasks/AkeneoImportTask.php @@ -22,6 +22,6 @@ public function run($request) /** @var AkeneoImport $import */ $import = Injector::inst()->get('AkeneoImport'); $imports = $request->getVar('import'); - $import->run($imports ? explode(',', $imports) : []); + $import->run($imports ? explode(',', (string)$imports) : []); } } diff --git a/src/Util/AttributeParser.php b/src/Util/AttributeParser.php index dc3f985..1490185 100644 --- a/src/Util/AttributeParser.php +++ b/src/Util/AttributeParser.php @@ -22,14 +22,14 @@ public static function MetricTypeParser(ProductAttributeValue $value): ?string } /** @var array{amount: string, unit: string}|mixed $decodedValue */ - $decodedValue = json_decode($jsonValue, true); + $decodedValue = json_decode((string)$jsonValue, true); if (!is_array($decodedValue)) { return null; } return vsprintf('%s %s', [ - round(floatval($decodedValue['amount']), 2), - _t(__CLASS__ . '.' . strtoupper($decodedValue['unit']), ucfirst(strtolower($decodedValue['unit']))), + round((float)$decodedValue['amount'], 2), + _t(self::class . '.' . strtoupper((string)$decodedValue['unit']), ucfirst(strtolower((string)$decodedValue['unit']))), ]); } @@ -45,14 +45,12 @@ public static function MultiSelectParser(ProductAttributeValue $value): string } try { - $parsedJSON = json_decode($jsonValues, true, 512, JSON_THROW_ON_ERROR); + $parsedJSON = json_decode((string)$jsonValues, true, 512, JSON_THROW_ON_ERROR); } catch (\Exception) { return ''; } - $attributeNames = array_map(static function (ProductAttributeOption $option) { - return $option->getName(); - }, $value->Attribute()->Options()->filter('Code', $parsedJSON)->toArray()); + $attributeNames = array_map(static fn (ProductAttributeOption $option) => $option->getName(), $value->Attribute()->Options()->filter('Code', $parsedJSON)->toArray()); return implode(', ', $attributeNames); } @@ -63,10 +61,10 @@ public static function PriceCollectionParser(ProductAttributeValue $value): stri throw new \RuntimeException('Not a price collection attribute value'); } - /** @var array|mixed $decodedValue */ + /** @var array|mixed $jsonValue */ $jsonValue = $value->getField('Value'); if (!is_array($jsonValue) || empty($jsonValue[0])) { - return null; + return ''; } return $jsonValue[0]['currency'] . ' ' . $jsonValue[0]['amount'];