BBB bit2byte blog

bit2byte

shopifyのカスタマイズ – 会員登録フォーム項目追加 Webhookを使ったメタフィールドの更新

2024.09.03.火

井上 飛鳥

Engineer

クライアント要望

  • 会員登録項目をカスタマイズしたい(増やしたい)
  • 登録した会員情報をマイページで編集できるようにしたい
  • 会員情報とは別にお届け先住所を登録する

対応したこと

  1. メタフィールドの定義
  2. 会員登録フォームでCustomer noteを使用
  3. Webhookの顧客作成後のイベントでCustomer noteのデータをメタフィールドに登録
  4. 入力した住所情報を配送先住所に設定する

メタフィールドで項目追加した顧客の画面イメージ

1.メタフィールドの定義

まず、shopify管理画面 設定 > カスタムデータ > 顧客 を選択し、「お客様のメタフィールド定義」の画面を開きます。

「定義を追加する」をクリックすると顧客画面に表示するカスタムフィールドが登録できます。

2.会員登録フォームでCustomer noteを使用

Customer noteを使用したinputタグの例:

<input type="text" name="customer[note][phone]">

会員登録フォームで以下のようにタグを記述すれば顧客のメモ欄に登録する事が可能です。

ただし、メモ欄に表示させるだけでは以下の要件が実現できません。

  • 登録した会員情報をマイページで編集できるようにしたい

そこで今回はアプリを使用せず、このCustomer noteをうまく使ってメタフィールドに追加する方法をとります。

3.Webhook「お客様の作成」のイベントでCustomer noteのデータをメタフィールドに登録

具体的には会員登録画面で新規にinputタグなどを追加し、customer[note][phone]のようにname属性を指定します。

登録を実行をするとnoteの値がshopifyに保存されます。
その後、Webhookでnoteを含めた顧客情報を受け取ることができます。

Webhookの設定方法

Shopify管理画面上の「設定 > 通知 > Webhook」で設定が可能です。
自前のサーバーを用意し、httpsでアクセス可能なプログラムを準備し、「お客様の作成」イベントで実行させます。

Webhookハンドラの実装コード(Laravelコントローラーの処理)

処理としてはnoteを内容を解析し、顧客のメタフィールドを作成します。

注意としてメタフィールドは定義済みで顧客画面にも追加されたメタフィールドが空の状態で置かれてますが、顧客に紐づくメタフィールドが作成されているわけではありません。
そのため、上書きを行うのではなく新規にメタフィールドを作成する必要があります。

public function handleWebhook(Request $request)
{
    $data = $request->all();
    Log::info('Received webhook data:', $data);

    $customerId = $data['id'] ?? null;
    $note = $data['note'] ?? '';

    if ($customerId && $note) {
        $metafieldsData = $this->parseNoteToMetafields($note);
        Log::info('Parsed metafields data:', $metafieldsData);

        $this->processMetafields($customerId, $metafieldsData);

        $defaultAddressId = $this->getDefaultAddressId($customerId);
        if ($defaultAddressId) {
            $this->updateDefaultAddress($customerId, $defaultAddressId, $note);
        } else {
            Log::error('Default address ID not found.');
        }
    } else {
        Log::error('Customer ID or note is missing.');
    }
}
protected function parseNoteToMetafields($note)
{
    $metafields = [];
    $customerConfig = config('metafields.customer');
    $namespace = $customerConfig['namespace'];
    $types = $customerConfig['fields'];

    $lines = explode("\n", $note);
    foreach ($lines as $line) {
        if (preg_match('/^(\w+):\s*(.*)$/', trim($line), $matches)) {
            $key = $matches[1];
            $value = trim($matches[2]);

            if (empty($value) || !isset($types[$key])) {
                Log::warning('Ignoring unrecognized or empty metafield key.', ['key' => $key]);
                continue;
            }

            $metafields[] = [
                'namespace' => $namespace,
                'key' => $key,
                'value' => htmlspecialchars(str_replace(["\r", "\n"], ' ', $value), ENT_QUOTES, 'UTF-8'),
                'type' => $types[$key]['type'],
            ];
        }
    }
    return $metafields;
}

protected function processMetafields($customerId, $metafieldsData)
{
    foreach ($metafieldsData as $metafield) {
        $this->createMetafield($customerId, $metafield);
    }
}

protected function createMetafield($customerId, $metafield)
{
    $url = "https://{$this->shopName}/admin/api/2024-07/customers/{$customerId}/metafields.json";

    try {
        $response = $this->client->post($url, [
            'headers' => [
                'Content-Type' => 'application/json',
                'X-Shopify-Access-Token' => $this->accessToken,
            ],
            'json' => [
                'metafield' => [
                    'namespace' => $metafield['namespace'],
                    'key' => $metafield['key'],
                    'value' => $metafield['value'],
                    'type' => $metafield['type']
                ]
            ]
        ]);

        if ($response->getStatusCode() == 201) {
            Log::info('Metafield created successfully.');
        } else {
            Log::error('Failed to create metafield', ['status' => $response->getStatusCode(), 'response' => $response->getBody()->getContents()]);
        }
    } catch (\Exception $e) {
        Log::error('Error creating metafield: ' . $e->getMessage());
    }

    Log::info('Request Payload:', [
        'url' => $url,
        'payload' => $metafield
    ]);
}

4.入力した住所情報を配送先住所に設定する

Webhook実行前にShopifyの顧客登録処理は済んでいるため、その時点で配送先情報は自動的に作成された状態になっています。getDefaultAddressIdにより現在の配送先住所を取得し、noteに格納された住所データを取得し、Customer Address APIを使用して顧客の配送先住所に上書きします。

protected function getDefaultAddressId($customerId)
{
    $url = "https://{$this->shopName}/admin/api/2024-01/customers/{$customerId}/addresses.json";
    $response = $this->client->get($url, [
        'headers' => [
            'Content-Type' => 'application/json',
            'X-Shopify-Access-Token' => $this->accessToken,
        ]
    ]);
    if ($response->getStatusCode() == 200) {
        $addresses = json_decode($response->getBody()->getContents(), true);
        foreach ($addresses['addresses'] as $address) {
            if ($address['default']) {
                return $address['id'];
            }
        }
    }
    return null;
}
protected function updateDefaultAddress($customerId, $addressId, $note)
{
    preg_match('/zip:\s*([^\r\n]+)/', $note, $zipMatch);
    preg_match('/address1:\s*([^\r\n]+)/', $note, $address1Match);
    preg_match('/address2:\s*([^\r\n]+)/', $note, $address2Match);
    preg_match('/city:\s*([^\r\n]+)/', $note, $cityMatch);
    preg_match('/province:\s*([^\r\n]+)/', $note, $provinceMatch);
    preg_match('/phone:\s*([^\r\n]+)/', $note, $phoneMatch);
    $addressData = [
        'address1' => $address1Match[1] ?? '',
        'address2' => $address2Match[1] ?? '',
        'city' => $cityMatch[1] ?? '',
        'province' => $provinceMatch[1] ?? '',
        'phone' => $phoneMatch[1] ?? '',
        'zip' => $zipMatch[1] ?? '',
        'country' => 'Japan',
        'default' => true
    ];
    Log::info('Address Info:', $addressData);
    $url = "https://{$this->shopName}/admin/api/2024-01/customers/{$customerId}/addresses/{$addressId}.json";
    try {
        $response = $this->client->put($url, [
            'headers' => [
                'Content-Type' => 'application/json',
                'X-Shopify-Access-Token' => $this->accessToken,
            ],
            'json' => ['address' => $addressData]
        ]);
        if ($response->getStatusCode() == 200) {
            Log::info('Default address updated successfully.');
        } else {
            Log::error('Failed to update default address', ['response' => $response->getBody()]);
        }
    } catch (\Exception $e) {
        Log::error('Error updating address: ' . $e->getMessage());
    }
}

井上 飛鳥代表取締役

<Web業界との関わり>
Webは20歳のころ自分のHPを作成する事から始まり、当時はHTMLよりもFlash MXでFlashサイトを作ったりしていました。
その後サーバーサイドに興味を持ち、様々な企業様のWeb制作・システム開発に携わらせていただきました。

<会社名Bit2Byteの由来>
企業前からイメージしていたものです。
昔から仕事や悩みなどを1人で抱え込む事が多かったのですが、何事も1人だけではできず、皆の協力で成り立っているものだと深く思い、1人から皆に繋げていく意味を込めて名付けました。

お客様の立場になり問題を解決できるよう会社一丸となって本当に感謝される事を目的としています。

Recommend