dynamic/silverstripe-salsify

SilverStripe网站的Salsify集成。

安装: 417

依赖: 0

建议者: 0

安全: 0

星标: 2

关注者: 7

分支: 4

开放问题: 8

类型:silverstripe-vendormodule

2.0.2 2023-03-24 16:10 UTC

This package is auto-updated.

Last update: 2024-08-24 18:58:10 UTC


README

CI Build Status Scrutinizer Code Quality codecov

Latest Stable Version Total Downloads Latest Unstable Version License

SilverStripe网站的Salsify集成。

需求

  • SilverStripe ^4.0

安装

composer require dynamic/silverstripe-salsify

许可证

许可证

目录

运行任务

任务可以从浏览器窗口或命令行运行。要在浏览器中运行任务,请访问 dev/tasks 并查找 从Salsify导入产品 或访问 dev/tasks/SalsifyImportTask

建议使用命令行,因为任务在浏览器中很容易超时。要在命令行中运行任务,必须安装sake并运行命令 sake dev/tasks/SalsifyImportTask

示例配置

扩展

SalsifyIDExtension

建议将 Dynamic\Salsify\ORM\SalsifyIDExtension 添加为任何映射对象的扩展。它将添加一个 SalsifyIDSalsifyUpdatedAt 字段,这些字段可以映射。用于单个对象更新的 SalsifyID 字段。

MyObject:
  extensions:
    - Dynamic\Salsify\ORM\SalsifyIDExtension

SalsifyIDSalsifyUpdatedAt 字段仍然需要在映射器配置中显式映射。

Dynamic\Salsify\Model\Mapper.example:
  mapping:
    \Page:
      SalsifyID:
        salsifyField: 'salsify:id'
        unique: true
      SalsifyUpdatedAt: 'salsify:updated_at'

SalsifyFetchExtension

SalsifyFetchExtension 自动添加到左侧和主界面。它将为具有Salsify映射的数据对象提供一个按钮来重新获取。

要使按钮强制更新,可以将 refetch_force_updaterefetch_force_update_relations 设置为映射到的数据对象上的true。当设置为true时,refetch_force_update 将强制更新Salsify中的任何非关系字段。当设置为true时,refetch_force_update_relations 将强制更新Salsify中的任何关系字段。

\Page:
  refetch_force_update: true
  refetch_force_update_relations: true

有关更多信息,请参阅单个对象导入部分。

导入器

导入器将运行获取器和映射器。每个导入器需要将其构造函数中的导入器密钥传递。对于其余的README,将使用 example 作为服务。

SilverStripe\Core\Injector\Injector:
  Dynamic\Salsify\Model\Importer.example:
    class: Dynamic\Salsify\Model\Importer
    constructor:
      importerKey: example

获取器

要设置获取器,需要一个API密钥和一个频道ID。API密钥可以在根获取器配置中,也可以在特定服务配置中覆盖。频道ID可以通过访问Salsify中的频道并复制URL的最后一段来找到。https://app.salsify.com/app/orgs/<org_id>/channels/<channel_id> 要查找API密钥,请遵循此链接

Dynamic\Salsify\Model\Fetcher:
  apiKey: 'api key here'

Dynamic\Salsify\Model\Fetcher.example:
    channel: 'channel id'

Dynamic\Salsify\Model\Fetcher.example:
  apiKey: 'api key here'
  channel: 'channel id'

组织ID也可以包含在内,以避免账户访问多个组织。

Dynamic\Salsify\Model\Fetcher:
  organizationID: 'org id'

Dynamic\Salsify\Model\Fetcher.example:
  organizationID: 'org id'

https://developers.salsify.com/docs/organization-id

fetcher还可以更改HTTP请求的超时时间。这不是Salsify生成导出的超时。超时以毫秒为单位,默认为2000毫秒或2秒。与apiKey一样,超时可以在根fetcher配置中设置,并由服务配置覆盖。

Dynamic\Salsify\Model\Fetcher:
  timeout: 4000

Dynamic\Salsify\Model\Fetcher.example:
  timeout: 4000

映射器

要设置一个将Salsify字段映射到SilverStripe的mapper,需要一些配置。

Dynamic\Salsify\Model\Mapper.example:
  mapping:
    \Page:
       SKU:
        salsifyField: SKU
        unique: true
       Title: Product Title

每个mapper实例都需要一个服务配置。在mapping配置下,可以指定一个或多个类以进行导入。每个类可以映射一个或多个字段,并且必须至少有一个是唯一的。所有字段都有映射到SilverStripe字段的键。Title: Product Title将Salsify中的Product Title映射到SilverStripe中的Title

唯一字段

与非唯一字段一样,键是映射到的SilverStripe字段。salsifyField是从Salsify映射的字段。unique可以是true或false,并将其用作检查现有记录的过滤器。

Dynamic\Salsify\Model\Mapper.example:
  mapping:
    \Page:
      SKU:
        salsifyField: SKU
        unique: true

唯一字段将被添加到数组中,每个产品的值将用作查找现有记录的过滤器。这允许有多个复合唯一字段。

字段类型

内置的字段类型有RawLiteralFileImageHasOneHasMany。还有一些特殊字段类型,如SalsifyIDSalsifyUpdatedAtSalsifyRelationsUpdatedAt,它们用于映射到特定字段而不进行修改。还可以添加更多类型。

原始

默认情况下使用Raw类型。它将salsify属性值写入对象字段。

Dynamic\Salsify\Model\Mapper.example:
  example:
    mapping:
      \Page:
        Title: 'Web Title'

SalsifyID, SalsifyUpdatedAt, 和 SalsifyRelationsUpdatedAt

具有这些类型的字段不能修改,但将像原始类型一样处理。

Dynamic\Salsify\Model\Mapper.example:
  example:
    mapping:
      \Page:
        SalsifyID:
          salsifyField: 'salsify:id'
          type: SalsifyID
        SalsifyUpdatedAt:
          salsifyField: 'salsify:updated_at'
          type: SalsifyUpdatedAt
        SalsifyRelationsUpdatedAt:
          salsifyField: 'salsify:relations_updated_at'
          type: SalsifyRelationsUpdatedAt
布尔

对于从salsify映射到布尔数据库类型的Yes/No值字段非常有用。

Dynamic\Salsify\Model\Mapper.example:
  example:
    mapping:
      \Page:
        Obsolete:
          salsifyField: 'obsolete'
          type: Boolean
isTrue

布尔处理器还提供了一个方便的isTrue辅助方法。这在需要对Yes/No格式的属性进行修改或跳过时非常有用。

文字

要设置没有salsify字段的字段,可以使用文字字段。

Dynamic\Salsify\Model\Mapper.example:
  example:
    mapping:
      \Page:
        Author:
          value: 'Chris P. Bacon'
          type: Literal

上面的示例将所有映射页面的作者字段设置为Chris P. Bacon

文件和图片

要从salsify获取图像或文件并将其映射到对象,需要指定一个类型。

Dynamic\Salsify\Model\Mapper.example:
  example:
    mapping:
      \Page:
        FrontImage:
          salsifyField: Front Image
          type: Image

图像和文件也可以通过ID进行映射。

Dynamic\Salsify\Model\Mapper.example:
  mapping:
    \Page:
      FrontImageID:
        salsifyField: Front Image
        type: Image

如果映射指定为图像且不是有效的图像扩展名,Salsify将尝试将文件转换为png。

图片转换

为了减少在访问页面时尝试调整图像大小造成的500错误,Salsify可以转换图像。当配置中的图像转换更新时,它也会重新下载具有新转换的图像。

Dynamic\Salsify\Model\Mapper.example:
  mapping:
    \Page:
      FrontImageID:
        salsifyField: Front Image
        type: Image
        transform:
          - 'c_fit'
          - 'w_1000'
          - 'h_1000'
          - 'dn_300'
          - 'cs_srgb'

上面的示例将下载http://a1.images.salsify.com/image/upload/c_fit,w_1000,h_1000,dn_300,cs_srgb/sample.jpg而不是http://a1.images.salsify.com/image/upload/sample.jpg

要查看Salsify支持的转换,请访问https://getstarted.salsify.com/help/transforming-image-files

建议对所有大文件都这样做。

一对一和一对多

has_one和has_many关系可以大致相同。HasOnesalsifyField不重要。ManyRelation类型需要是一个数组的salsify字段。ManyRelation还可以指定一个排序列。对数据的所有修改都将通过映射关系传递。

HasOne示例
Dynamic\Salsify\Model\Mapper.example:
  mapping:
    \Page:
      Document:
        salsifyField: 'salsify:id'
        type: 'HasOne'
        relation:
          \Documentobject:
            Title: Document Title
ManyRelation示例
namespace {
    use SilverStripe\CMS\Model\SiteTree;

    class Page extends SiteTree
    {

        /**
         * @var array
         */
        private static $many_many = [
            'Features' => Feature::class,
        ];

        private static $many_many_extraFields = [
            'Features' => [
                'SortOrder'=> 'Int',
            ],
        ];
    }
}
<?php

namespace {
    use \Page;
    use SilverStripe\ORM\DataObject;

    /**
     * Class Feature
     *
     * @property string Name
     */
    class Feature extends DataObject
    {
        /**
         * @var string
         */
        private static $table_name = 'Feature';

        /**
         * @var array
         */
        private static $db = [
            'Name' => 'Varchar(100)',
        ];

        /**
         * @var array
         */
        private static $belongs_many_many = [
            'Pages' => Page::class,
        ];

        /**
         * @var array
         */
        private static $indexes = [
            'Name' => [
                'type' => 'unique',
                'columns' => ['Name'],
            ],
        ];
    }
}
<?php

namespace {

    use SilverStripe\Core\Extension;

    /**
     * Class SalsifyExtension
     */
    class SalsifyExtension extends Extension
    {
        /**
         * @param string|\SilverStripe\ORM\DataObject $class
         * @param string $dbField
         * @param array $config
         * @param array $data
         *
         * @return array
         */
        public function featureModifier($class, $dbField, $config, $data)
        {
            $features = [];
            foreach ($this->owner->config()->get('featureFields') as $featuredField) {
                if (array_key_exists($featuredField, $data) && $this->is_true($data[$featuredField])) {
                    $features[] = [
                        'FeatureName' => $featuredField,
                    ];
                }
            }
            $data['Features'] = $features;
            return $data;
        }

        /**
         * @param $val
         * @param bool $return_null
         * @return bool|mixed|null
         *
         * FROM https://php.ac.cn/manual/en/function.boolval.php#116547
         */
        private function is_true($val, $return_null = false)
        {
            $boolval = (bool)$val;

            if (is_string($val)) {
                $boolval = filter_var($val, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE);
            }

            if ($boolval === null && !$return_null) {
                return false;
            }

            return $boolval;
        }
    }
}
Dynamic\Salsify\Model\Mapper.example:
  extensions:
    - SalsifyExtension
  featureFields:
      - "Feature One"
      - "Feature Two"
  mapping:
    \Page:
      Features:
        salsifyField: 'Features'
        type: 'ManyRelation'
        modification: 'featureModifier'
        sortColumn: 'SortOrder'
        relation:
          Feature:
            Name:
              salsifyField: 'FeatureName'
              unique: true
Salsify关系

在salsify中也可以创建产品之间的关系。默认情况下,它将映射到has_manymany_many关系。salsifyField是关系类型的名称。

Dynamic\Salsify\Model\Mapper.example:
  mapping:
    \Page:
      RelatedProducts:
        salsifyField: 'You May Also Like'
        type: 'SalsifyRelation'

要将对象映射到has_one关系,可以返回单个对象。

Dynamic\Salsify\Model\Mapper.example:
  mapping:
    \Page:
      AlternateProduct:
        salsifyField: 'Alternate'
        type: 'SalsifyRelation'
        single: true

字段回退

可以为Salsify指定一个回退字段。当映射对象的数据中没有正常的salsifyField时,将使用回退字段。回退字段可以是字符串或数组。

Dynamic\Salsify\Model\Mapper.example:
  mapping:
    \Page:
      Title:
        salsifyField: 'Product Web Title'
        fallback: 'Product Title'

Dynamic\Salsify\Model\Mapper.example:
  mapping:
    \Page:
      Title:
        salsifyField: 'Product Web Title'
        fallback:
          - 'Product Title'
          - 'SKU'

在没有Salsify字段的情况下保留字段值

默认情况下,如果对象的数据中没有Salsify字段,所有要映射的值将清除。可以修改设置以保留字段的值。

Dynamic\Salsify\Model\Mapper.example:
  mapping:
    \Page:
      Title:
        salsifyField: 'Product Web Title'
        keepExistingValue: true

这将保留标题的先前值,即使字段不再在数据中。

扩展onBeforeMap

onBeforeMap在fetcher运行后,但在mapper开始映射之前运行。它传递来自渠道导出的文件URL以及映射是多个还是单个产品。

Dynamic\Salsify\Model\Mapper.example:
  extensions:
    - ExampleFeatureExtension
<?php
namespace {
    use SilverStripe\Core\Extension;
    use JsonMachine\JsonMachine;

    /**
     * Class ExampleFeatureExtension
     */
    class ExampleFeatureExtension extends Extension
    {
        /**
         * Gets all the attributes that are in a field group and sets them in the mapper's config
         * @param $file
         * @param bool $multiple
         */
        public function onBeforeMap($file, $multiple)
        {
            $attributes = [];
            $attributeStream = JsonMachine::fromFile($file, '/1/attributes');
            foreach ($this->owner->yieldSingle($attributeStream) as $attribute) {
                if (array_key_exists('salsify:attribute_group', $attribute)) {
                    if ($attribute['salsify:attribute_group'] == 'Product Features') {
                        $attributes[] = $attribute['salsify:id'];
                    }
                }
            }

            $this->owner->config()->set('featureFields', $attributes);
        }
    }
}

扩展onAfterMap

onAfterMap在fetcher运行后,但在mapper开始映射之前运行。它传递来自渠道导出的文件URL以及映射是多个还是单个产品。此扩展点非常适合清理不在渠道导出中的产品。

Dynamic\Salsify\Model\Mapper.example:
  extensions:
    - ExampleCleanUpExtension
<?php
namespace {
    use SilverStripe\Core\Extension;
    use JsonMachine\JsonMachine;
    use Dynamic\Salsify\Task\ImportTask;
    use Dynamic\Salsify\Model\Mapper;

    /**
     * Class ExampleCleanUpExtension
     */
    class ExampleCleanUpExtension extends Extension
    {
        /**
         * @param $file
         * @param bool $multiple
         */
        public function onAfterMap($file, $multiple)
        {
            // don't clean up on a single product import
            if ($multiple == Mapper::$SINGLE) {
                return;
            }

            $productStream = JsonMachine::fromFile($file, '/4/products');
            $productCodes = [];

            foreach ($this->owner->yieldKeyVal($productStream) as $name => $data) {
                $productCodes[] = $data['salsify:id'];
            }

            $invalidProducts = Product::get()->exclude([
                'SalsifyID' => $productCodes,
            ]);

            $count = 0;
            foreach ($this->owner->yieldSingle($invalidProducts) as $invalidProduct) {
                /** @var Product $invalidProduct */
                $invalidProduct->doArchive();
                $count++;
            }
            ImportTask::output("Archived {$count} products");
        }
    }
}

扩展beforeObjectWrite

此扩展点非常适合检测哪些字段已更改。如果映射过程中父级已更改,这将有助于创建重定向页面。

Dynamic\Salsify\Model\Mapper.example:
  extensions:
    - ExampleRedirectExtension
<?php
namespace {
    use SilverStripe\Core\Extension;
    use SilverStripe\ORM\DataObject;
    use SilverStripe\RedirectedURLs\Model\RedirectedURL;
    use Dynamic\Salsify\Task\ImportTask;

    /**
     * Class ExampleRedirectExtension
     */
    class ExampleRedirectExtension extends Extension
    {
        /**
         * This will create a redirect if a page's parent changes
         * @param DataObject $object
         */
        public function beforeObjectWrite($object)
        {
            if (!$object instanceof \Page) {
                return;
            }

            if (!$object->isChanged()) {
                return;
            }

            $changed = $object->getChangedFields(false, DataObject::CHANGE_VALUE);

            if (!array_key_exists('ParentID', $changed) && !array_key_exists('URLSegment', $changed)) {
                return;
            }

            $oldParent = $object->ParentID;
            $oldSegment = $object->URLSegment;
            if (array_key_exists('ParentID', $changed)) {
                $parent = $changed['ParentID'];
                $oldParent = $parent['before'];
            }

            if (array_key_exists('URLSegment', $changed)) {
                $segment = $changed['URLSegment'];
                $oldSegment = $segment['before'];
            }

            $this->createRedirect($oldParent, $oldSegment, $object->ID);
        }

        /**
         * @param \Page|int $oldParent
         * @param string $oldSegment
         * @param int $objectID
         */
        private function createRedirect($oldParent, $oldSegment, $objectID)
        {
            if (is_int($oldParent)) {
                $oldParent = \Page::get()->byID($oldParent);
            }

            $redirect = RedirectedURL::create();
            $redirect->RedirectCode = 301;
            $redirect->FromBase = preg_replace('/\?.*/', '', $oldParent->Link($oldSegment));
            $redirect->LinkToID = $objectID;
            $redirect->write();
            ImportTask::output("Created redirect from {$redirect->FromBase} to {$redirect->LinkTo()->Link()}");
        }
    }
}

扩展afterObjectWrite

在映射后发布对象,可以扩展afterObjectWrite方法。它传递已写入的数据对象,如果对象已在数据库中,以及如果对象已发布。如果对象未应用版本化扩展,则$wasPublished将为false。

Dynamic\Salsify\Model\Mapper.example:
  extensions:
    - ExamplePublishExtension
<?php
namespace {
    use SilverStripe\Core\Extension;
    use SilverStripe\Versioned\Versioned;

    /**
     * Class ExamplePublishExtension
     */
    class ExamplePublishExtension extends Extension
    {
        /**
         * This will publish all new mapped objects and mapped objects that are already published.
         * @param DataObject|Versioned $object
         * @param bool $wasWritten
         * @param bool $wasPublished
         */
        public function afterObjectWrite($object, $wasWritten, $wasPublished)
        {
            if ($object->hasExtension(Versioned::class)) {
                if (!$wasWritten || $wasPublished) {
                    $object->publishRecursive();
                }
            }
        }
    }
}

高级

自定义字段类型
跳过对象
修改字段数据
使用属性分配父级
使用Pages创建虚拟页面
重定向
将组中的属性作为DataObjects

单个对象导入

在CMS中添加重新获取按钮需要一些配置。需要一个组织来获取单个产品。单个对象导入还需要一个SalsifyID字段。只需要一个名为single的映射服务,它将像正常的mapper配置一样运行;然而,还可以定义fetcher服务配置来指定组织。

Dynamic\Salsify\Model\Fetcher.single:
  organizationID: 'org id'

Dynamic\Salsify\Model\Mapper.single:
  mapping:
    \Page:
      SalsifyID:
        salsifyField: 'salsify:id'
        unique: true
      SKU: SKU
      Title: GTIN Name
      FrontImage:
        salsifyField: Front Image
        type: Image

要将单个对象映射器作为正常导入器使用,在运行任务时需要一个Importer服务。

SilverStripe\Core\Injector\Injector:
  Dynamic\Salsify\Model\Importer.single:
    class: Dynamic\Salsify\Model\Importer
    constructor:
      importerKey: single

有关更多配置选项,请参阅SalsifyFetchExtension

故障排除

一些字段没有导入

如果一些字段没有导入,请确保它们在数据中显示。偶尔属性会有不同的ID和名称,在这种情况下,数据将显示在属性ID下。

维护者

错误追踪器

在仓库的问题部分跟踪错误。在提交问题之前,请阅读现有的问题以确保您的独特性。

如果问题看起来像是新的错误

  • 创建一个新的问题
  • 描述重现问题的步骤和预期的结果。单元测试、截图和屏幕录像可能在这里有所帮助。
  • 尽可能详细地描述您的环境:SilverStripe版本、浏览器、PHP版本、操作系统、任何安装的SilverStripe模块。

请直接向模块维护者报告安全问题。请不要在错误跟踪器中提交安全问题。

开发和贡献

如果您想为此模块做出贡献,请确保您提出一个拉取请求,并与模块维护者讨论。