craftcms/shopify

Craft CMS 的 Shopify

安装次数: 4,589

依赖项: 1

建议者: 0

安全: 0

星标: 45

关注者: 7

分支: 25

开放问题: 16

类型:craft-plugin


README

Shopify icon

Craft CMS 的 Shopify

通过将 Shopify 产品同步到 Craft CMS 来构建一个以内容驱动的店面。

主题

  • 📦 安装: 设置插件并与 Shopify 连接。
  • 🗃️ 产品操作: 了解可用的数据类型以及如何访问它们。
  • 📑 模板: 在 Twig 中使用产品的技巧和窍门。
  • 🍃 升级: 利用新功能和性能改进。
  • 🔭 高级功能: 深入您的集成。

安装

Shopify 插件需要 Craft CMS 5.0.0 或更高版本。

要安装插件,请访问您的 Craft 项目中的 插件商店,或按照以下说明操作。

  1. 在新终端中导航到您的 Craft 项目

    cd /path/to/project
  2. 使用 Composer 需求包

    composer require craftcms/shopify -w
  3. 在控制面板中,转到 设置插件,然后点击 Shopify 的“安装”按钮,或运行

    php craft plugin/install shopify

创建 Shopify 应用

插件与 Shopify 的 自定义应用 系统兼容。

注意

如果您不是 Shopify 商店的拥有者,请让拥有者添加您为协作者或工作人员,并授予您 开发应用 权限

按照 Shopify 的说明 创建私有应用(通过“获取自定义应用的 API 凭证”部分),并在提示时执行以下操作

  1. 应用名称: 选择一个可以识别集成的名称,例如“Craft CMS”。

  2. 管理 API 访问范围: 插件正常工作需要以下范围

    • read_products
    • read_product_listings
    • read_inventory

    此外(在此屏幕底部),“Webhook 订阅”→“事件版本”应为 2023-10

  3. 管理 API 访问令牌: 显示并复制此值到您的 .env 文件中,作为 SHOPIFY_ADMIN_ACCESS_TOKEN

  4. API 密钥和密钥: 显示并/或复制 API 密钥API 密钥 到您的 .env 中的 SHOPIFY_API_KEYSHOPIFY_API_SECRET_KEY

商店主机名

您还需要准备的最后一条信息是您的商店的主机名。这通常是在使用 Shopify 管理员时在浏览器中出现的,它也显示在商店的设置屏幕上

Screenshot of the settings screen in the Shopify admin, with an arrow pointing to the store’s default hostname in the sidebar.

将此值(不要 包含前缀 http://https://)保存在您的 .env 中作为 SHOPIFY_HOSTNAME。此时,您应该有四个 Shopify 特定的值

# ...

SHOPIFY_ADMIN_ACCESS_TOKEN="..."
SHOPIFY_API_KEY="..."
SHOPIFY_API_SECRET_KEY="..."
SHOPIFY_HOSTNAME="my-storefront.myshopify.com"

连接插件

现在您已经拥有了自定义应用程序的凭据,是时候将其添加到Craft中了。

  1. 请访问项目控制面板中的 Shopify设置 页面。
  2. 使用特殊的 配置语法 将四个环境变量分配给相应的设置。
    • API密钥$SHOPIFY_API_KEY
    • API密钥$SHOPIFY_API_SECRET_KEY
    • 访问令牌$SHOPIFY_ACCESS_TOKEN
    • 主机名$SHOPIFY_HOSTNAME
  3. 点击 保存

注意

这些设置存储在 项目配置 中,并将自动应用于其他环境。 Webhooks 仍需要为每个环境进行配置!

设置Webhooks

将凭据添加到Craft后,控制面板的 Shopify 部分将出现一个新的 Webhooks 选项卡。

在Webhooks页面上点击 创建 以将所需的webhooks添加到Shopify。插件将使用您刚刚配置的凭据执行此操作——这也可以作为初始通信测试。

警告

您需要为将插件部署到的每个环境添加webhooks,因为每个webhook都与一个特定的URL相关联。

注意

如果您需要在开发中测试实时同步,我们建议使用 ngrok 创建到本地环境的隧道。DDEV通过 ddev share 命令 使这变得简单。请注意,注册webhooks时使用的是您网站的默认/基本URL,因此您可能需要将其更新以匹配ngrok隧道,然后重新创建您的webhooks。

升级

在升级之前,请确保 管理员API访问范围 与以下 要求 匹配。这确保了插件的全部新功能将正常工作。

升级后,请确保已创建所有必需的webhooks,方法是点击CP中项目控制面板页面的 ShopifyWebhooks 上的“创建”按钮。如果“创建”按钮不可见,则已创建了所有必需的webhooks。

产品元素

您的Shopify商店中的产品在Craft中以产品 元素 的形式表示,可以在控制面板中通过访问 Shopify产品 来找到。

同步

配置插件后,您可以通过命令行执行所有产品的初始同步。

php craft shopify/sync/products

syncProductMetafieldssyncVariantMetafields 设置 控制通过此过程同步的数据。从现在起,您的产品将通过 webhooks 自动保持同步。

对于更大、更复杂的商店,在完整同步过程中可能会遇到 速率限制 问题。在这些情况下,您可以使用 --throttle 选项来减慢同步过程。

注意

只有少量产品的较小商店可以通过 Shopify Sync 工具执行同步。

本地属性

除了标准的元素属性如 idtitlestatus 之外,每个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的字段(如 optionsmetadata)只能以纯文本形式查询。如果您需要进行高级组织或筛选,我们建议在产品 字段布局 中使用自定义分类或标签字段。

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组件,如功能齐全的购物车。原理是相同的

  1. 通过适当的销售渠道在Shopify中使产品可用;
  2. 在前端输出同步的产品数据;
  3. 根据步骤#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 设置 apiKeyapiSecretKeyaccessTokenhostName 将覆盖在应用设置期间通过控制面板设置的 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 查询)可以非线性增长。