dynamic / silverstripe-salsify
SilverStripe网站的Salsify集成。
Requires
- halaxa/json-machine: ^1.0
- silverstripe/framework: ^4.10
Requires (Dev)
README
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
添加为任何映射对象的扩展。它将添加一个 SalsifyID
和 SalsifyUpdatedAt
字段,这些字段可以映射。用于单个对象更新的 SalsifyID
字段。
MyObject: extensions: - Dynamic\Salsify\ORM\SalsifyIDExtension
SalsifyID
和 SalsifyUpdatedAt
字段仍然需要在映射器配置中显式映射。
Dynamic\Salsify\Model\Mapper.example: mapping: \Page: SalsifyID: salsifyField: 'salsify:id' unique: true SalsifyUpdatedAt: 'salsify:updated_at'
SalsifyFetchExtension
SalsifyFetchExtension
自动添加到左侧和主界面。它将为具有Salsify映射的数据对象提供一个按钮来重新获取。
要使按钮强制更新,可以将 refetch_force_update
和 refetch_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
唯一字段将被添加到数组中,每个产品的值将用作查找现有记录的过滤器。这允许有多个复合唯一字段。
字段类型
内置的字段类型有Raw
、Literal
、File
、Image
、HasOne
、HasMany
。还有一些特殊字段类型,如SalsifyID
、SalsifyUpdatedAt
、SalsifyRelationsUpdatedAt
,它们用于映射到特定字段而不进行修改。还可以添加更多类型。
原始
默认情况下使用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关系可以大致相同。HasOne
的salsifyField
不重要。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_many
和many_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模块。
请直接向模块维护者报告安全问题。请不要在错误跟踪器中提交安全问题。
开发和贡献
如果您想为此模块做出贡献,请确保您提出一个拉取请求,并与模块维护者讨论。