craftcms / shopify
Craft CMS 的 Shopify
Requires
- php: ^8.2
- craftcms/cms: ^5.0.0-beta.10
- shopify/shopify-api: ^5.2.0
Requires (Dev)
- craftcms/ecs: dev-main
- craftcms/phpstan: dev-main
- craftcms/rector: dev-main
- craftcms/redactor: *
- vlucas/phpdotenv: ^3.4
- 5.x-dev
- 5.3.x-dev
- 5.2.0
- 5.1.2
- 5.1.1
- 5.1.0
- 5.0.0
- v4.x-dev
- 4.1.2
- 4.1.1
- 4.1.0
- 4.0.0
- v3.x-dev
- 3.2.0
- 3.1.1
- 3.1.0
- 3.0.1
- 3.0.0.1
- 3.0.0
- v2.x-dev
- 2.2.0
- 2.1.1
- 2.0.1
- 2.0.0
- 1.2.1
- 1.1.1
- 1.1.0
- 1.0.5
- 1.0.4
- 1.0.3
- 1.0.2
- 1.0.1
- dev-feature/link-field-support
- dev-main
- dev-dependabot/npm_and_yarn/babel/traverse-7.23.2
- dev-dependabot/composer/craftcms/cms-4.4.15
- dev-dependabot/npm_and_yarn/word-wrap-1.2.4
- dev-feature/multi-store
- dev-docs/prices
- dev-feature/example-templates
This package is auto-updated.
Last update: 2024-09-19 16:13:08 UTC
README
Craft CMS 的 Shopify
通过将 Shopify 产品同步到 Craft CMS 来构建一个以内容驱动的店面。
主题
- 📦 安装: 设置插件并与 Shopify 连接。
- 🗃️ 产品操作: 了解可用的数据类型以及如何访问它们。
- 📑 模板: 在 Twig 中使用产品的技巧和窍门。
- 🍃 升级: 利用新功能和性能改进。
- 🔭 高级功能: 深入您的集成。
安装
Shopify 插件需要 Craft CMS 5.0.0 或更高版本。
要安装插件,请访问您的 Craft 项目中的 插件商店,或按照以下说明操作。
-
在新终端中导航到您的 Craft 项目
cd /path/to/project
-
使用 Composer 需求包
composer require craftcms/shopify -w
-
在控制面板中,转到 设置 → 插件,然后点击 Shopify 的“安装”按钮,或运行
php craft plugin/install shopify
创建 Shopify 应用
插件与 Shopify 的 自定义应用 系统兼容。
注意
如果您不是 Shopify 商店的拥有者,请让拥有者添加您为协作者或工作人员,并授予您 开发应用 权限。
按照 Shopify 的说明 创建私有应用(通过“获取自定义应用的 API 凭证”部分),并在提示时执行以下操作
-
应用名称: 选择一个可以识别集成的名称,例如“Craft CMS”。
-
管理 API 访问范围: 插件正常工作需要以下范围
read_products
read_product_listings
read_inventory
此外(在此屏幕底部),“Webhook 订阅”→“事件版本”应为
2023-10
。 -
管理 API 访问令牌: 显示并复制此值到您的
.env
文件中,作为SHOPIFY_ADMIN_ACCESS_TOKEN
。 -
API 密钥和密钥: 显示并/或复制 API 密钥 和 API 密钥 到您的
.env
中的SHOPIFY_API_KEY
和SHOPIFY_API_SECRET_KEY
。
商店主机名
您还需要准备的最后一条信息是您的商店的主机名。这通常是在使用 Shopify 管理员时在浏览器中出现的,它也显示在商店的设置屏幕上
将此值(不要 包含前缀 http://
或 https://
)保存在您的 .env
中作为 SHOPIFY_HOSTNAME
。此时,您应该有四个 Shopify 特定的值
# ... SHOPIFY_ADMIN_ACCESS_TOKEN="..." SHOPIFY_API_KEY="..." SHOPIFY_API_SECRET_KEY="..." SHOPIFY_HOSTNAME="my-storefront.myshopify.com"
连接插件
现在您已经拥有了自定义应用程序的凭据,是时候将其添加到Craft中了。
- 请访问项目控制面板中的 Shopify → 设置 页面。
- 使用特殊的 配置语法 将四个环境变量分配给相应的设置。
- API密钥:
$SHOPIFY_API_KEY
- API密钥:
$SHOPIFY_API_SECRET_KEY
- 访问令牌:
$SHOPIFY_ACCESS_TOKEN
- 主机名:
$SHOPIFY_HOSTNAME
- API密钥:
- 点击 保存。
注意
这些设置存储在 项目配置 中,并将自动应用于其他环境。 Webhooks 仍需要为每个环境进行配置!
设置Webhooks
将凭据添加到Craft后,控制面板的 Shopify 部分将出现一个新的 Webhooks 选项卡。
在Webhooks页面上点击 创建 以将所需的webhooks添加到Shopify。插件将使用您刚刚配置的凭据执行此操作——这也可以作为初始通信测试。
警告
您需要为将插件部署到的每个环境添加webhooks,因为每个webhook都与一个特定的URL相关联。
注意
如果您需要在开发中测试实时同步,我们建议使用 ngrok 创建到本地环境的隧道。DDEV通过 ddev share
命令 使这变得简单。请注意,注册webhooks时使用的是您网站的默认/基本URL,因此您可能需要将其更新以匹配ngrok隧道,然后重新创建您的webhooks。
升级
在升级之前,请确保 管理员API访问范围 与以下 要求 匹配。这确保了插件的全部新功能将正常工作。
升级后,请确保已创建所有必需的webhooks,方法是点击CP中项目控制面板页面的 Shopify → Webhooks 上的“创建”按钮。如果“创建”按钮不可见,则已创建了所有必需的webhooks。
产品元素
您的Shopify商店中的产品在Craft中以产品 元素 的形式表示,可以在控制面板中通过访问 Shopify → 产品 来找到。
同步
配置插件后,您可以通过命令行执行所有产品的初始同步。
php craft shopify/sync/products
syncProductMetafields
和 syncVariantMetafields
设置 控制通过此过程同步的数据。从现在起,您的产品将通过 webhooks 自动保持同步。
对于更大、更复杂的商店,在完整同步过程中可能会遇到 速率限制 问题。在这些情况下,您可以使用 --throttle
选项来减慢同步过程。
注意
只有少量产品的较小商店可以通过 Shopify Sync 工具执行同步。
本地属性
除了标准的元素属性如 id
、title
和 status
之外,每个Shopify产品元素还包含以下映射到其规范 Shopify产品资源
注意
有关这些属性的预期值类型,请参阅Shopify文档中的 产品资源。
方法
Product::getVariants()
返回属于产品的 变体。
{% set variants = product.getVariants() %} <select name="variantId"> {% for variant in variants %} <option value="{{ variant.id }}">{{ variant.title }}</option> {% endfor %} </select>
Product::getDefaultVariant()
获取属于产品的第一个/默认 变体的快捷方式。
{% set products = craft.shopifyProducts.all() %} <ul> {% for product in products %} {% set defaultVariant = product.getDefaultVariant() %} <li> <a href="{{ product.url }}">{{ product.title }}</a> <span>{{ defaultVariant.price|currency }}</span> </li> {% endfor %} </ul>
Product::getCheapestVariant()
获取属于产品的最低价格 变体的快捷方式。
{% set cheapestVariant = product.getCheapestVariant() %} Starting at {{ cheapestVariant.price|currency }}!
Product::getShopifyUrl()
{# Get a link to the product’s page on Shopify: #} <a href="{{ product.getShopifyUrl() }}">View on our store</a> {# Link to a product with a specific variant pre-selected: #} <a href="{{ product.getShopifyUrl({ variant: variant.id }) }}">Buy now</a>
Product::getShopifyEditUrl()
对于您的管理员,您甚至可以直接链接到Shopify管理界面
{# Assuming you’ve created a custom group for Shopify admin: #} {% if currentUser and currentUser.isInGroup('clerks') %} <a href="{{ product.getShopifyEditUrl() }}">Edit product on Shopify</a> {% endif %}
自定义字段
从Shopify同步的产品具有专用的字段布局,这意味着它们支持Craft的完整内容工具集 。
要编辑产品字段布局,请转到 Shopify → 设置 → 产品,然后滚动到 字段布局。
路由
您可以给同步产品它们自己的网站URL。要设置URI格式(以及当请求产品URL时将加载的模板),请转到 Shopify → 设置 → 产品。
如果您希望客户在Shopify上查看单个产品,请在设置页面清除 产品URI格式 字段,并在模板中使用 product.shopifyUrl
而不是 product.url
。
产品状态
在Craft中,产品的 status
是其 shopifyStatus
属性('active'、'draft' 或 'archived')和启用状态的组合。前者只能从Shopify更改;后者在Craft控制面板中设置。
注意
Craft中的状态通常是多个属性的合成。例如,具有 挂起 状态的条目仅表示它 启用 且具有将来的postDate
。
在大多数情况下,您只需要显示“实时”产品,或者在Shopify中是 活动 的以及在Craft中是 启用 的产品
查询产品
产品可以像系统中任何其他元素一样进行查询。
新的查询从 craft.shopifyProducts
工厂函数开始
{% set products = craft.shopifyProducts.all() %}
查询参数
除了 Craft的标准集 之外,还支持以下元素查询参数。
注意
存储为JSON的字段(如 options
和 metadata
)只能以纯文本形式查询。如果您需要进行高级组织或筛选,我们建议在产品 字段布局 中使用自定义分类或标签字段。
shopifyId
按Shopify产品ID进行筛选。
{# Watch out—these aren't the same as element IDs! #} {% set singleProduct = craft.shopifyProducts .shopifyId(123456789) .one() %}
shopifyStatus
直接查询产品在Shopify中的状态。
{% set archivedProducts = craft.shopifyProducts .shopifyStatus('archived') .all() %}
如果您更喜欢查询 合成状态值,请使用常规的 .status()
参数。
handle
通过产品在Shopify中的handle进行查询。
{% set product = craft.shopifyProducts .handle('worlds-tallest-socks') .all() %}
🚨 这不是获取特定产品的可靠方式,因为其值可能在同步期间更改。如果您想要一个产品的永久引用,请考虑使用Shopify的 产品字段。
产品类型
在Shopify中通过“类型”查找产品。
{% set upSells = craft.shopifyProducts .productType(['apparel', 'accessories']) .all() %}
发布范围
仅显示发布到匹配销售渠道的产品。
{# Only web-ready products: #} {% set webProducts = craft.shopifyProducts .publishedScope('web') .all() %} {# Everything: #} {% set inStoreProducts = craft.shopifyProducts .publishedScope('global') .all() %}
标签
标签以逗号分隔的列表形式存储。您可以使用 the .search()
参数 获得更好的搜索结果。
{# Find products whose tags include the term in any position, with variations on casing: #} {% set clogs = craft.shopifyProducts .tags(['*clog*', '*Clog*']) .all() %}
供应商
通过Shopify的供应商信息进行筛选。
{# Find products with a vendor matching either option: #} {% set fancyBags = craft.shopifyProducts .vendor(['Louis Vuitton', 'Jansport']) .all() %}
图片
图片以JSON blob的形式存储,并且仅用于与加载的产品一起在模板中使用。直接通过 图片资源 进行筛选可能很困难且不可预测——您可以使用 the .search()
参数 获得更好的搜索结果。
{# Find products that have an image resource mentioning "stripes": #} {% set clogs = craft.shopifyProducts .images('*stripes*') .all() %}
选项
选项 以JSON blob的形式存储,并且仅用于与加载的产品一起在模板中使用。您可以使用 the .search()
参数 获得更好的搜索结果。
{# Find products that use a "color" option: #} {% set clogs = craft.shopifyProducts .options('"Color"') .all() %}
以上包括引号("
)字面量,因为它是尝试在JSON数组中找到特定的键,该键始终被双引号包围。
模板
产品数据
在Twig中,产品表现得就像任何其他元素一样。一旦您通过 查询 (或在其模板中引用)加载了一个产品,您就可以输出其本地的 Shopify 属性 和 自定义字段 数据。
注意
一些属性以JSON的形式存储,这限制了嵌套属性的类型。因此,处理日期可能会稍微困难一些。
{# Standard element title: #} {{ product.title }} {# -> Root Beer #} {# Shopify HTML content: #} {{ product.bodyHtml|raw }} {# -> <p>...</p> #} {# Tags, as list: #} {{ product.tags|join(', ') }} {# -> sweet, spicy, herbal #} {# Tags, as filter links: #} {% for tag in tags %} <a href="{{ siteUrl('products', { tag: tag }) }}">{{ tag|title }}</a> {# -> <a href="https://mydomain.com/products?tag=herbal">Herbal</a> #} {% endfor %} {# Images: #} {% for image in product.images %} <img src="{{ image.src }}" alt="{{ image.alt }}"> {# -> <img src="https://cdn.shopify.com/..." alt="Bubbly Soda"> #} {% endfor %} {# Variants: #} <select name="variantId"> {% for variant in product.getVariants() %} <option value="{{ variant.id }}">{{ variant.title }} ({{ variant.price|currency }})</option> {% endfor %} </select>
变体和定价
尽管Shopify的UI可能暗示产品没有价格,但实际上每个产品至少有一个 变体。
您可以通过调用 product.getVariants()
获取一个产品的变体对象数组。产品元素还提供了方便的方法来获取 默认 和 最便宜 的变体,但您可以使用Craft的 collect()
Twig函数进行筛选。
与产品不同,Craft中的变体...
- ...由API表示,返回它们;
- ...除了API返回的属性外,还可以访问
metafields
属性; - ...使用Shopify的属性名下划线约定,而不是暴露 驼峰式等效;
- ...是普通的关联数组;
- ...没有自己的方法;
一旦您有一个变体的引用,您就可以输出其属性
{% set defaultVariant = product.getDefaultVariant() %} {{ defaultVariant.price|currency }}
注意
内置的 currency
Twig过滤器是格式化货币值的好方法。
只有当启用 syncVariantMetafields
设置时,metafields
属性才会被填充。
使用选项
选项是Shopify区分多个轴上的变体的方式。
如果您想让客户从选项中选择而不是直接选择变体,您需要解决给定的组合指向哪个变体。
表单
<form id="add-to-cart" method="post" action="{{ craft.shopify.store.getUrl('cart/add') }}"> {# Create a hidden input to send the resolved variant ID to Shopify: #} {{ hiddenInput('id', null, { id: 'variant', data: { variants: product.variants, }, }) }} {# Create a dropdown for each set of options: #} {% for option in product.options %} <label> {{ option.name }} {# The dropdown includes the option’s `position`, which helps match it with the variant, later: #} <select data-option="{{ option.position }}"> {% for val in option.values %} <option value="{{ val }}">{{ val }}</option> {% endfor %} </select> </label> {% endfor %} <button>Add to Cart</button> </form>
脚本
以下代码可以添加到 {% js %}
标签 中,与表单代码一起使用。
// Store references to <form> elements: const $form = document.getElementById("add-to-cart"); const $variantInput = document.getElementById("variant"); const $optionInputs = document.querySelectorAll("[data-option]"); // Create a helper function to test a map of options against known variants: const findVariant = (options) => { const variants = JSON.parse($variantInput.dataset.variants); // Use labels for the inner and outer loop so we can break out early: variant: for (const v in variants) { option: for (const o in options) { // Option values are stored as `option1`, `option2`, or `option3` on each variant: if (variants[v][`option${o}`] !== options[o]) { // Didn't match one of the options? Bail: continue variant; } } // Nice, all options matched this variant! Return it: return variants[v]; } }; // Listen for change events on the form, rather than the individual option menus: $form.addEventListener("change", (e) => { const selectedOptions = {}; // Loop over option menus and build an object of selected values: $optionInputs.forEach(($input) => { // Add the value under the "position" key selectedOptions[$input.dataset.option] = $input.value; }); // Use our helper function to resolve a variant: const variant = findVariant(selectedOptions); if (!variant) { console.warn("No variant exists for options:", selectedOptions); return; } // Assign the resolved variant’s ID to the hidden input: $variantInput.value = variant.id; }); // Trigger an initial `change` event to simulate a selection: $form.dispatchEvent(new Event("change"));
购物车
您的客户可以直接从Craft网站将产品添加到购物车。
{% set product = craft.shopifyProducts.one() %} <form action="{{ craft.shopify.store.getUrl('cart/add') }}" method="post"> <select name="id"> {% for variant in product.getVariants() %} <option value="{{ variant.id }}">{{ variant.title }}</option> {% endfor %} </select> {{ hiddenInput('qty', 1) }} <button>Add to Cart</button> </form>
JS Buy SDK
目前不支持原生方式管理购物车和结账。
然而,Shopify维护了JavaScript Buy SDK,作为与他们的Storefront API交互的手段,以创建完全定制的购物体验。
注意
使用Storefront API需要不同的访问密钥,并假设您已将产品发布到Storefront应用的销售渠道。
您的公共Storefront API令牌可以与您的其他凭据一起存储在.env
文件中,并使用{{ getenv('...') }}
Twig助手在前端输出——或者直接嵌入到JavaScript包中。请确保其他机密信息安全!这是唯一可以公开的信息。
该插件不对您在前端如何使用产品数据进行任何假设,但提供了连接到SDK所需的工具。以一个例子来说明,让我们看看如何在Twig中渲染产品列表,并连接一个自定义客户端购物车……
Shop模板:templates/shop.twig
{# Include the Buy SDK on this page: #} {% do view.registerJsFile('https://sdks.shopifycdn.com/js-buy-sdk/v2/latest/index.umd.min.js') %} {# Register your own script file (see “Custom Script,” below): #} {% do view.registerJsFile('/assets/js/shop.js') %} {# Load some products: #} {% set products = craft.shopifyProducts().all() %} <ul> {% for product in products %} {# For now, we’re only handling a single variant: #} {% set defaultVariant = product.getVariants()|first %} <li> {{ product.title }} <button class="buy-button" data-default-variant-id="{{ defaultVariant.id }}">Add to Cart</button> </li> {% endfor %} </ul>
自定义脚本:assets/js/shop.js
// Initialize a client: const client = ShopifyBuy.buildClient({ domain: "my-storefront.myshopify.com", storefrontAccessToken: "...", }); // Create a simple logger for the cart’s state: const logCart = (c) => { console.log(c.lineItems); console.log(`Checkout URL: ${c.webUrl}`); }; // Create a cart or “checkout” (or perhaps load one from `localStorage`): client.checkout.create().then((checkout) => { const $buyButtons = document.querySelectorAll(".buy-button"); // Add a listener to each button: $buyButtons.forEach(($b) => { $b.addEventListener("click", (e) => { // Read the variant ID off the product: client.checkout .addLineItems(checkout.id, [ { // Build the Storefront-style resource identifier: variantId: `gid://shopify/ProductVariant/${$b.dataset.defaultVariantId}`, quantity: 1, }, ]) .then(logCart); // <- Log the changes! }); }); });
购买按钮JS
上面的例子可以使用购买按钮JS简化,它提供了一些现成的UI组件,如功能齐全的购物车。原理是相同的
- 通过适当的销售渠道在Shopify中使产品可用;
- 在前端输出同步的产品数据;
- 根据步骤#2中使用的Shopify特定标识符,在事件响应中初始化、附加或触发SDK功能;
结账
虽然存在创建定制购物体验的解决方案,但结账始终会在Shopify平台上进行。这与其说是一个技术限制,不如说是一个政策——Shopify的结账流程快速、可靠、安全,并且许多购物者都熟悉。
如果您希望将客户的整个旅程保持在站内,我们鼓励您尝试我们强大的电子商务插件Commerce。
助手
除了产品元素方法外,该插件还通过craft.shopify
将API公开给Twig。
API服务
警告
在Twig块渲染中使用API调用,并根据流量可能由于速率限制而导致超时和/或失败。考虑使用带有键和特定过期时间的{% cache %}
标签以避免每次渲染模板时都发出请求
{% cache using key "shopify:collections" for 10 minutes %} {# API calls + output... #} {% endcache %}
通过craft.shopify.api
向Shopify Admin API发出请求
{% set req = craft.shopify.api.get('custom_collections') %} {% set collections = req.response.custom_collections %}
每个API资源的模式将不同。有关更多信息,请参阅Shopify API文档。
存储服务
通过craft.shopify.store
提供了一个简单的URL生成器。您可能在上述购物车示例中注意到了它——但它比这更灵活!
{# Create a link to add a product/variant to the cart: #} {{ tag('a', { href: craft.shopify.store.getUrl('cart/add', { id: variant.id }), text: 'Add to Cart', target: '_blank', }) }}
可以将相同的参数传递给产品元素的getShopifyUrl()
方法
{% for variant in product.getVariants() %} <a href="{{ product.getShopifyUrl({ id: variant.id }) }}">{{ variant.title }}</a> {% endfor %}
产品字段
该插件提供了一个Shopify产品字段,它使用熟悉的关系字段UI,允许作者选择产品元素。
使用《Shopify 产品》字段定义的关系在底层使用稳定的元素 ID。当 Shopify 产品被存档或删除时,对应的元素也会在 Craft 中更新,并自然地从您的查询结果中过滤掉——包括通过《Shopify 产品》字段明确附加的元素。
注意
正在升级?查看迁移说明以获取更多信息。
进一步了解
设置
以下设置可以通过在您的 config/
目录中创建一个 shopify.php
文件来控制。
注意
通过 shopify.php
设置 apiKey
、apiSecretKey
、accessToken
和 hostName
将覆盖在应用设置期间通过控制面板设置的 Project Config 值。您仍然可以使用 craft\helpers\App::env()
从配置文件中引用环境值。
事件
craft\shopify\services\Products::EVENT_BEFORE_SYNCHRONIZE_PRODUCT
在产品元素使用新的 Shopify 数据保存之前发出。由于 craft\shopify\events\ShopifyProductSyncEvent
继承自 craft\events\CancelableEvent
,因此设置 $event->isValid
允许您阻止新数据被保存。
事件对象有三个属性
element
:正在更新的产品元素。source
:应用了的产品对象。
use craft\shopify\events\ShopifyProductSyncEvent; use craft\shopify\services\Products; use yii\base\Event; Event::on( Products::class, Products::EVENT_BEFORE_SYNCHRONIZE_PRODUCT, function(ShopifyProductSyncEvent $event) { // Example 1: Cancel the sync if a flag is set via a Shopify metafield: $metafields = $event->element->getMetaFields(); if (metafields['do_not_sync'] ?? false) { $event->isValid = false; } // Example 2: Set a field value from metafield data: $event->element->setFieldValue('myNumberFieldHandle', $metafields['cool_factor']); } );
警告
不要手动保存在此事件处理程序中做出的更改。插件将为您处理这些更改!
元素 API
您可以将同步的产品发布到 Element API 端点,就像任何其他元素类型一样。这允许您设置一个本地的产品 JSON 联播,用 Craft 中添加的任何内容装饰。
use craft\shopify\elements\Product; return [ 'endpoints' => [ 'products.json' => function() { return [ 'elementType' => Product::class, 'criteria' => [ 'publishedScope' => 'web', 'with' => [ ['myImageField'] ], ], 'transformer' => function(Product $product) { $image = $product->myImageField->one(); return [ 'title' => $product->title, 'variants' => $product->getVariants(), 'image' => $image ? $image->getUrl() : null, ]; }, ]; }, ], ];
速率限制
插件通过尊重回复中的头信息(这是第一方 PHP SDK 的一个特性)尽最大努力避免 Shopify 的严格API 速率限制规则。这意味着一系列操作(如同步或循环中的自定义 API 查询)可以非线性增长。