株式会社クイックのWebサービス開発blog

HAPPYなサービスプランナー・エンジニア・デザイナーのブログです。

【WordPress】TinyMCEに自作ボタンを追加して定型文をgit管理したい!

こんにちは。ソフトウェアエンジニアのぽんです。
WordPressでボタンやアコーディオンといったパーツを実装することってありますよね。
TinyMCE TemplatesやAddQuickTagといったプラグインを使用し、定型文を作って実装する方法がよく紹介されていますが
WordPress上にマークアップを記載していくことになるため、git上で管理できないのが難点です。
今回は、gitで管理できる定型文の実装方法をお話ししていこうかなと思います!

完成系

こんな感じになります。

f:id:aimstogeek:20200521090926p:plain
※ ビジュアルエディタを実際の記事と同じ見た目になるようカスタマイズしています。 こちらの記事も合わせてどうぞ! aimstogeek.hatenablog.com

必須プラグイン

今回はTinyMCEの機能を使うため、こちらのプラグインが必須となります。

  • Classic Editor

WordPress5.0から標準エディタが「Gutenberg」となっているため、旧エディタをTinyMCEに変更するプラグインです。

実装方法

TinyMCEプラグインを作成

JavaScriptファイルを作成します。
ファイルの作成場所は任意ですが、今回はテーマディレクトリ配下にinculudesというディレクトリを作成しその中にsetting-template.jsという名前でJSファイルを作成しました。
ここにボタンを追加するTinyMCEプラグインを書いて行きます。

(function() {
  tinymce.create('tinymce.plugins.original_tinymce_button', {
    init: function(ed, url) {
      ed.addButton('insert_template', {
        // text : ボタンの表示名
        text: 'テンプレートの挿入',
        // type: 'menubutton'にすると、プルダウンのようなメニューボタンを作成することができます。
        type: 'menubutton',
        menu: [
          {
            text: 'ボックス',
            menu: [
              {
                text: 'デフォルト',
                onclick: function() {
                  // insertContentでカーソルのある位置に要素を追加します
                  ed.insertContent('<div class="box"><h3>見出し</h3><p>テキスト</p></div>');
                }
              },{
                text: 'コラム',
                onclick: function() {
                  ed.insertContent('<div class="box box-clm"><h3>見出し</h3><p>テキスト</p></div>');
                }
              }
              ]
          }, {
            text: 'ダウンロードボタン',
            onclick: function() {
              ed.insertContent('<p class="btn-dl><a href="dummy">ダウンロード</a></p>');
            }
          }
        ]
      });
    },
    createControl : function(n, cm) {
      return null;
    },
  });
  tinymce.PluginManager.add('original_tinymce_button_plugin', tinymce.plugins.original_tinymce_button);
})();

今回は
プラグイン名: original_tinymce_button_plugin
ボタン名: insert_template
で作成しました。
こちらをfunction.phpで設定していきます。

function.phpに追記

先ほど作成したTinyMCEプラグインをボタンとして登録します。

<?php
// 作成したプラグインを登録
add_filter( 'mce_external_plugins', function ( $plugin_array ) {
  $plugin_array[ 'original_tinymce_button_plugin' ] = get_template_directory_uri() . '/includes/setting-template.js';
  return $plugin_array;
});
// プラグインで作ったボタンを登録
add_filter( 'mce_buttons', function ( $buttons ) {
  $buttons[] = 'insert_template';
  return $buttons;
});

こちらで完成です!

まとめ

WordPressでの定型文の作り方やTinyMCEの自作ボタンの作り方は星の数ほど紹介しているサイトが多いのですが、 私の中でこの2つが結びつかなくて実装する際にすごく時間がかかってしまったので今回紹介してみました。

今の状態だとただ要素をエディタに貼り付けるだけなので、ボタンを押したらポップアップが表示されてそこで文章編集やリンクを編集できるように改良したらさらに使いやすくなりそうだなと思いました。
まだまだ改善の余地ありですね・・・!!


\\『真のユーザーファーストでマーケットを創造する』仲間を募集中です!! // 919.jp

LaravelでDoctrineを使ってみた

こんにちは、ソフトウェアエンジニアのissyです。

現在のプロジェクトでは、Laravel+Doctrine+クリーンアーキテクチャで開発を行なっています。 本来Laravelを使う場合には、ORMはEloquentを使いますが、Doctrineを使っています!

なぜ?

アクティブレコードを使った場合、データベースとエンティティが密結合になってしまうので、 データマッパーのORMを使う事で分離したかったからです。 PHPのORMはほとんどがアクティブレコード実装のため、データマッパーだとDoctrine一択でした。

現在開発中のシステムは業務システムであるため、複雑なビジネスロジックもあり、アクティブレコードは不向きだと判断しました。

どうやって?

Laravel Doctrineを使っています。

インストールの方法などは公式を参照して下さい。

github.com

実装

以下のようなディレクトリ構成で設計しました。

  • app/

Laravelの標準のディレクトリ構成。

  • packages/Domain

エンティティ、値オブジェクトなどのビジネスルールを格納する。(User,Addressクラスなど。)

  • packages/Infrastructure

技術的関心事の処理を行うクラスを格納する。(UserRepositoryクラス、マッピングファイルなど。)

  • packages/Usecase

ユースケースクラスを格納する。

マッピング

  • xmlマッピングがオススメです。 yamlは2.7で非推奨、アノテーションはinfrastructure層とdomain層の関心事が混ざり、変更のコストが高くなりがちです。

  • Embeddedが便利。 xmlなどのマッピングファイルを記述する事で、value objectを生成できます。 以下では、「address」に使用しています。

<?xml version="1.0" encoding="utf-8"?>
<doctrine-mapping xmlns="http://doctrine-project.org/schemas/orm/doctrine-mapping" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://doctrine-project.org/schemas/orm/doctrine-mapping https://www.doctrine-project.org/schemas/orm/doctrine-mapping.xsd">
  <entity name="packages\Domain\Model\User" table="users">
    <id name="id" type="bigint" column="id">
      <generator strategy="IDENTITY"/>
    </id>
    <field name="name" type="string" column="name" length="255" nullable="false">
      <options>
        <option name="fixed"/>
      </options>
    </field>
      <embedded
          name="address"
          class="packages\Domain\Model\Address"
          use-column-prefix="false"
      />
  </entity>
</doctrine-mapping>
<?xml version="1.0" encoding="utf-8"?>
<doctrine-mapping xmlns="http://doctrine-project.org/schemas/orm/doctrine-mapping" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://doctrine-project.org/schemas/orm/doctrine-mapping https://www.doctrine-project.org/schemas/orm/doctrine-mapping.xsd">
    <embeddable name="packages\Domain\Model\Address">
        <field name="street" type="string" />
        <field name="postalCode" type="string" />
        <field name="city" type="string" />
        <field name="country" type="string" />
    </embeddable>
</doctrine-mapping>

エンティティ

xmlマッピングする事で、infrastructure層に依存しない形で実装できました!

<?php

declare(strict_types=1);

namespace packages\Domain\Model;

class User
{
    private int $id;
    private string $name;
    private Address $address;

    public function __construct(int $id, string $name, Address $address)
    {
        $this->id = $id;
        $this->name = $name;
        $this->address = $address;
    }

    /**
     * @return int
     */
    public function getId(): int
    {
        return $this->id;
    }

    /**
     * @return string
     */
    public function getName(): string
    {
        return $this->name;
    }

    /**
     * @return Address
     */
    public function getAddress(): Address
    {
        return $this->address;
    }
}


値オブジェクト

<?php

declare(strict_types=1);

namespace packages\Domain\Model;

class Address
{
    private string $street;

    private string $postalCode;

    private string $city;

    private string $country;

    // 以下略...
}

リポジトリ

ドメインオブジェクトを返却するリポジトリクラスです。

ORマッパーやSQLは、データベースに依存しないようにリポジトリに記述しています。

<?php

declare(strict_types=1);

namespace packages\Infrastructure\Doctrine;
use Doctrine\ORM\EntityRepository;
use packages\Domain\Model\User;
use packages\Domain\Model\UserId;
use packages\Infrastructure\UserRepository;

class DoctrineUserRepository extends EntityRepository implements UserRepository
{

    public function findUser(UserId $userId): User
    {
        return $this->find($userId->value());
    }

    public function add(User $user): void
    {
        $em = $this->getEntityManager();
        $em->persist($user);
        $em->flush();
    }
}

ユースケース

リポジトリから取得したエンティティをプレーンオールドなオブジェクトに詰めて返却しています。

<?php


namespace packages\Usecase;


use packages\Domain\Model\UserId;
use packages\Infrastructure\UserRepository;

class UserGetInteractor implements UserGetUsecase
{
    private UserRepository $userRepository;

    public function __construct(UserRepository $userRepository)
    {
        $this->userRepository = $userRepository;
    }
    public function execute(int $userId) : UserGetOutputData
    {
       $user = $this->userRepository->findUser(new UserId($userId));
       return new UserGetOutputData(
           $user->getId(),
           $user->getName(),
           $user->getAddress()->street(),
           $user->getAddress()->postalCode(),
           $user->getAddress()->city(),
           $user->getAddress()->country(),
       );


    }
}

所感

Doctrineのおかげでdomainとinfrastructureが分離した綺麗な設計が出来たと思います! リポジトリパターンを使えば、Eloquentに切り替えることも容易にできます。

コードの品質を高めることで、ユーザーに素早く価値を届けられるように 現在のプロジェクトでは他にも様々な取り組みを行なっていますが、それはまた次回に書こうと思います!


\\『真のユーザーファーストでマーケットを創造する』「ありがとう」で溢れる仲間を募集中です!! // 919.jp

リモートユーザーテストの実践法

f:id:aimstogeek:20200416175919p:plain

こんにちは、デザイナーのachanです。

クイックでは新型コロナウイルスの感染拡大防止のため、4月より原則出社禁止、リモートワークになっています。 その影響でユーザーと直接会えず対面でのユーザーテストやインタビューを中止せざるを得なくなっています。これをきっかけに、リモート環境でのユーザーテストの実践法をまとめてみたいと思います。

リモートユーザーテストとは

リモートユーザーテストとは、ユーザーに自宅のパソコンや携帯からWebサイトやアプリを利用してもらい、その行動や発言の観察を通して「ユーザー心理」と「Webサイトやアプリの課題」を見つける調査手法です。

また実施形式の「進行」(モデレーター役)という観点から2種類に分けられます。

  • 進行あり:同期型リモートユーザーテスト
  • 進行なし:非同期型リモートユーザーテスト

利用するツール・環境

対面で行うテストのように操作中の行動や発言(思考発話・ひとりごと)の録画や、閲覧したページの記録、アンケート形式の回答データの取得など目的に応じてツールの準備が必要です。

基本、リモート環境では必要なツールは以下に分けられます。

ちなみに、ビデオチャットツールは以下の項目に注意して選定する必要があります。

ユーザー側
- ソフトインストールが必要かどうか
- 画面共有ができるか
- スマホ画面のミラーリングができるか

実施側
- 録画・録音ができるか

上記すべて満たすものとしては個人的にzoomがお勧めです(アカウント作成が必要)。

テストの準備

1. 目標を定義し、ユーザーを募集する

効果的なユーザーテストを行うためには、テストするサイトやアプリの検証目標(タスクベースなど)を定義し、ターゲット条件に限りなく近いユーザーを募集しましょう。例えば、

① 似たものの利用経験の有無
② 製品・サービスへの興味の有無
③ ITスキルの高低

また、人数に関しては、ユーザーテストは定量調査の目的ではなく定性調査を目的に実施するため、1検証に対しておよそ5人の実施で有効な検証結果を得ることが出来るとされています。

一方で進行なしのユーザーテストではユーザー自身に一人で操作を進めてもらうため、発生した行動や回答に対する深掘りが難しいという欠点があるのと、指示されているタスクや質問の意味を取り違えてしまったりすることがあります。それを防ぐためにはテスト実施人数を想定より1~2名多めにしたほうがいいでしょう。

2. タスクを設計し、テストスクリプトを作成する

「ユーザーテストはタスク設計がカギ」と言っても過言ではありません。適切なタスク(作業課題)を設計するための基本原則が4つあります。

① テストの実施目的に沿った主要なタスクに絞り込む
② ユーザーの視点で、その操作を行う目的や意図、状況を想定する
③ タスクのスタートとゴールを定義する
④ タスクを行う状況設定として、シナリオ化する

テストスクリプトを作成する際に、あくまで経験談となりますが例えば、

  • 指示文を短くする
    - 長文だと「何」を「どうする」のか、関係性が分かりづらくなる
  • 社内用語を使わない
  • 感想を言うときはなるべくその画面に移動してもらうように指示文に書き込む
    - 「つまづいたところを教えてください」だと何の画面、何の内容なのかわからない。そこで、指示文を「該当の画面を表示して教えてください」に変更すれば明確になる

テストの実施

1. 事前説明

タスクを実行してもらう前に、ユーザーに伝えておくべきポイントは主に以下3つです。

① ユーザー自身のテストではない
- 場合によっては、ユーザーが指示されたタスクをクリアできないことがあります。その際、ユーザーは自分が悪いと誤解しやすく、プレッシャーを感じてしまいテストに非協力的になってしまいます。それを防ぐため、事前に伝えておくとショックが和らぎます。

② 操作しながら思っていることを口に出してもらう
- これは製品の課題やユーザーの心理を抽出するための重要な手法(思考発話)です。なるべくユーザー自身で該当画面に移動してもらい、操作しながら感想をもらいましょう。

③ 質問には答えられない
- テストでは操作に関わるすべての判断はユーザー自身が行わないと意味がありません。そのため、操作上の疑問点があったとしてもインタビュアーが返答できかねることを事前に伝えましょう。

2. テストの進行

前述の進行の有無で2パターンがあります。

① 進行あり- 同期型リモートユーザーテストの場合

この場合では、ユーザーに提示したタスクを実行してもらい、それを観察(記録)します。

モデレーターとユーザーが離れた環境にいるので、可能な限り快適で「リアルな」雰囲気を作ります。例えば、

  • 情報の取り扱いを伝える
  • アイスブレイクをいつもよりやや長めに
  • 声を明るく、大きく
  • オウム返し
  • 同席者(例: 記録係)がいる場合こちらの状況をしっかり説明する

ユーザーがタスクを実行している間、モデレーターは観察を続けます。途中で質問されても直接回答せずに、質問で返答してください。例えば、「次にどこへ行けばいいですか」と尋ねられた場合、「どこへ行けば良いと思いますか」などと返答しましょう。

もし記録係がいる場合、ユーザーが成功したタスクと必要な時間を漏れなくメモしましょう。記録係がいない場合、なるべくユーザーの発話を多く引き出し、動画に記録を残すようにしましょう。

② 進行なし - 非同期型リモートユーザーテストの場合

この場合では、事前にユーザーにタスクを送り、操作している動画を納品してもらいます。そのためユーザーが好きな時間、好きな場所において各自でテストを進めることができます。

非同期型では、テスト経験のあるユーザーが必要なので、選定する際に気をつけましょう。

テストの結果分析

テストの目的に合わせて簡易分析と詳細分析を使い分けます。

簡易分析

セッションの直後、記憶が鮮明なうちに、調査で気づいた大きな問題やその他の観察内容を書き留め、簡易リストを作成します。メモや記録を詳しく調べるまでもなく、傾向は一目瞭然です。

簡易分析は、時間の制約がある場合や、デザインの修正をすぐに行う必要がある場合に特に有用です。

詳細分析

時間と意欲があれば、「インパクト分析法」を用いて、発見された問題点を解決する際の優先順位を付けることができます。

そこで、下図のように「問題の質」「発生頻度」 の2軸を使って判断します。

f:id:aimstogeek:20200416175923p:plain

問題の質
- 発見された問題を「効果・効率・満足度」の3つに分けます。
・効果: 独力でタスクを完了できていない問題
・効率:完了できても、途中で戸惑ったり、無駄な操作を行ったりするような問題
・満足度: 完了までの過程でユーザーの不安や不満が口や態度に出るような問題

発生頻度
- 同じ問題を起こしたユーザーの人数で評価します。
・発生頻度高: (ほぼ)全員
・発生頻度中:複数人
・発生頻度低: 1人
※ 人数の境界値は厳密なルールがないが、問題点の発生頻度やテストの検証目的に合わせて方針を決めてください。

さいごに

対面でのテストと完全に同じクオリティや効果を得るのは難しいかと思いますが、コストを削減できるためリモートユーザーテストはますます人気が高まっています。 状況によって使い分ければ、効率よくユーザーに価値を提供できると信じています。

参考資料
樽本徹也著, 『ユーザビリティエンジニアリング第2版』
How to run a remote usability testing - UX Collective
Remote Usability Testing 101 & Getting Started | Adobe XD Ideas
Remote Usability Tests: Moderated and Unmoderated


\\一緒に良いサービスを作って成長したい、そんなメンバーを募集中です! //
919.jp

傾聴力とは?「話聞いてない!」って怒られたことのある人たちへ

こんにちは。

最近、電車や駅のホームにおいて、
「おい!話聞いてんの!?」と鬼の形相になっている人を何度か見かけました。

そればかりか家に帰ってからも、
「話全然聞いてないじゃん、ふざけてんの?ねぇ!!」と鬼の形相になっている人を見かけます。

サービスプランナーのコッティです。

さて、こんなありきたりな場面、
怒られている側は話を聞いていない訳ではありません。
内容もきっと覚えてます。
でも怒っている側は話を聞いていないと感じている。
そして、気付けば相手の信頼を失っている・・

一体そのギャップは何が原因になっていて、
どうすれば信頼を失う事態を防ぐことが出来るのでしょうか。

主な原因:興味を持たれていないと感じている

原因は他にもいろいろあるかもしれませんが、
まとめると話し手が聞き手に対して、
「興味持ってない」ように捉えられる事が多いと思います。
例えば、こんな話の聞き方をした事はありませんか?

- スマホをいじりながら、適当な相槌を打つ
- 単純な相槌だけを打ってあたかも聞いている風を装う
- 気を利かせて適当に返答したけど、何かずれてる

ついついこういう行動ってしてしまいますよね。
たとえ、心では真面目に話を聞こうとしていたとしても、
上述の行動をとった瞬間、
あなたは「私の話に興味を持たない人」というレッテルを貼られることになります。
ビジネスシーンでこんな事になったら信頼失いますよね。
日常生活でも同じことです。

解決策

こんなときに役立つのが「傾聴力」というスキルです。
最近ではビジネスシーンにおいても重要性が叫ばれていますよね。
普段から意識出来る傾聴の基本を少しご紹介します。

1.態度や姿勢を相手に向ける

まずは、態度や姿勢を改めて、
「あなたの話に興味があります。」という姿勢を見せましょう。
その際に腕組みをしたり、
時計をチラチラ見る仕草は、出来るだけ止めましょう。
相手は拒絶されていると感じてしまいます。
スマホをいじりながら話聞くとか論外ですね。
相手の話を遮らない、というのも大事なポイントです。

相手の話を途中まで聞いて、何となく全容を掴んだ気になってしまい、
話を遮って自分の考えや理解を喋り出す。こんなことやってませんか?

2.相手を真似する

相手の仕草や姿勢、声のトーン等を相手に合わせてみましょう。
ミラーリング」と呼ばれる手法です。
食事や会食の際に、食べ物を口に入れるペースや、
飲み物を飲むペースを合わせると親近感が増すと言われますよね。
あれと同じことです。

真似する関係で言えばもう一つ、
相手が話した事を繰り返す事も傾聴の手法です。
しっかり理解してくれる、共感してくれるという印象を与える効果があり、
「バックトラッキング」と呼ばれています。
例えば、こんな流れでしょうか。
ほぼ相手の会話を繰り返しているだけなのにポジティブなやり取りが飛び交ってます。

A:「今日ね、恵比寿に飲みに行くの~」

B:「え、今日恵比寿に飲みに行くの~?」

A:「そう、美味しい餃子のお店に行くんだ~」

B:「美味しい餃子のお店に行くんだ~良いね!」

ここで最初の返しで、こんな返答をしてしまうとどうでしょうか?

B:「恵比寿か、オシャレなイタリアン行くの?良いね!」

本当は美味しい餃子のお店に行くことを嬉しく伝えたいのに、
何だか興ざめして、会話終わらせてしまうこともありますよね。


3.相手の言葉を言い換えてみる

少し難易度が上がってしまいますが、
相手の話したことを自分の言葉に変換して返す手法です。

これが出来ると、相手はきちんと話を聞いてくれて、
しかも理解してくれたと信頼がグッと増します。

相手の話を要約して返せると理想的かもしれませんが、
相手の話の逆説を言うだけでも立派な言い換えになります。
上手だなーと思う人は「逆に〇〇ならこうなりますか?」、
といった言い方をしている場合もあります。

最後に

話を聴くというのはコミュニケーションの基本です。
コミュニケーション能力の高い人というのは、
聴く能力の高い人だと個人的には思っております。

今回記載した内容はビジネスでも日常でも、
少し意識すれば出来ることかと思います。
私はプランナーという職種柄、いろんな人にヒアリングや提案する事も多いです。
聴く姿勢をとることで、相手から信頼を得るというのは非常に大切な事だと思っています。

ちなみに私の場合は、業務でのヒアリングを実施する際に、
「バックトラッキング」と「相手の言葉を言い換えてみる(要約)」を抱き合わせて、
使っていることが多いです。

反復、反復、反復(反復の間に情報整理して)、要約で確認するという流れです。
こちらから確認を挟むことで、相手も改めて内容を整理して、
誤りや追加情報があると色々と話しをしてくれるという副次効果もあったりします。

「話聞いてないじゃん!」と怒られる人が少しでも減りますように。



\\最高のサービスを一緒に作る仲間を超募集!! // 919.jp

業務をヒアリングする際に心に留めているちょっとしたこと

こんにちは、サービスプランナーのいのっちです。

弊社ではプランナーの仕事の1つとして、業務をシステムを用いてどのように改善するか企画したり、その推進をしたりします。

今回は、現状の業務をヒアリングする際、私が気にかけていることをいくつかご紹介したいと思います。

1.業務がかたいのか、やわらかいのかを考えながら聞く

業務のシステム化による効率化はとても効果があり、今まで何時間もかけていた仕事が一瞬で終わることも少なくありません。

しかし、一方でシステム化するということは、手作業に備わっている柔軟性を失うことになります。

立ち上げたばかりの事業など、事業や仕事自体が手探りであるときは、日々、仕事のやり方を変更していくことも珍しくありません。これは業務がやわらかい状態です。

この状態の時は、「今、システム化すべきなのか?まだ、しない方が良いかも知れない」という事を心に留めつつ聞いていきます。

2.ミスへの対応をどうしているか引き出す

担当者から業務の流れや詳細を伺うときは、多くの場合、定められた業務手順を説明されると思います。

それをそのままシステムにしてしまうと大きな落とし穴にハマってしまいますので注意が必要です。

なぜならば、人が作業をすると、どうしても何かしらミスをしてしまうからです。
選択ミスをして送ってはいけない宛先にデータを送ろうとしていたり、誤ったデータを登録していたり様々な箇所でミスをしてしまいます。

ミスに対応できるシステムでなければ業務に耐えることはできません。
ミスを対応できるようにするには、ミスに気がつくことができることと、ミスを修正しリカバリーすることができる必要があります。

手作業で業務を行っていると、意識をしていなくても、データや操作の間違いを発見することが多々あります。これは業務手順として定義されないですが、データをチェックするポイントとなっています。

これらは当事者が意識していないため説明されないことが多いですが、うまく捉える必要があります。

そのような時は次のように、過去のトラブル事例を伺うような形で聞くと、うまく引き出せます。

「過去、間違いが入ったまま(プロセスを)進めてしまい、後で気がついて何とかしたことってありますか?

そうすると、何かしら過去のことを語ってもらえると思います。
さらに下記のことも深堀りして聞いていきましょう。

  • 誰が気がついたか
  • 何を見て気がついたか
  • どうやってリカバリーしたのか

3.改善ポイントが無いか探すためアレコレ聞く

業務の手順は増やす事は多いですが、業務をやりながら自ら減らすことはなかなかできないものです。

折角の機会ですから、今の業務を無くして省力化できないか考えていきたいですね。
是非、業務担当者とディスカッションして、改善のヒントを引き出しましょう。

私の場合、下記のような問いかけをキッカケにしたりします。
今やっている○○のチェック。もしやらなかったらどうなると思います?

そうすると、例えば、「入力時の担当者がコピペミスを意外としていて、そのまま出すとクレームとなるので怖い」というような意見をもらえたりします。

この場合、前プロセスの入力画面のUIが問題かも知れませんし、同じ概念のデータ項目があり混乱を招いているのかも知れません。

さらに深堀りをする価値がありそうですね。

最後に

業務の流れを担当から教えてもらう機会は改善の大チャンスです!
積極的に質問して過去の経緯・背景や将来の希望を引き出しましょう。
きっと良くするポイントが見つかるはずです。



\\最高のサービスを一緒に作る仲間を超募集!! // 919.jp

記事がバズってサイトがダウン!?の舞台裏

サービスプランナーのyumeです。
私は、数千件の記事を扱うメディアサイトを担当しているのですが、嬉しいことにたまに突然アクセスが集中することがあります。いわゆる「バズ」ですね。

このとき、開発運用チームでは「編集チームが丹精込めて作った記事が日の目を見るチャンス!1人も逃したくない!」と思い対応するわけですが、具体的には何をしているのか?という流れを紹介したいと思います!

状況)サイトが重くなる

自分たちの施策やイベント等、予め対策ができる場合もあるのですが、だいたいは準備していないときに限ってサイトダウンは訪れるんですよね。 事象としては、ページの表示が異常に遅い、ステータス500を連発する、という感じです。

1)アラートが飛ぶ

私たちが運用しているサイトはすべて監視ツールを入れて負荷状況を見ています。
負荷が高まるとチャットにアラートが飛ぶ仕組みです。

業務時間外でチャットを見ていない場合もあるので、気づいた人が担当者に連絡したり、可能な範囲で状況をチェックしたりします。

2)駆けつける

ミーティングから抜け出したり、飲み会から抜け出したり、帰りの電車に乗っていたところを折り返したり。
このときはめんどくさいというより使命感で燃えまくっています。

今回は、インフラエンジニア(弊社ではSREチーム)×サービスプランナーで対応した場合の、プランナー目線の流れを記載します!

3)アクセス集中ページの特定

まず、GoogleAnalyticsのリアルタイムを確認

f:id:aimstogeek:20200312142514p:plain
GAリアルタイム

サーバが完全に死んでいない場合は、このページを見ると、見慣れないページにアクセスが集まっていることに気づけます。
※サーバが完全にお亡くなりになった場合は確認ができなくなるのでそのときはアクセスログを見るしかないです。

各ページの仕様をだいたい把握しているので、ページが分かると、Web/DB/その他のどこに負荷がありそうか推測が可能。

私がこのように対応しているとき、SRE側でもサーバ負荷チェックやアクセス集中パスの調査を行ってくれます。
結果、「とりあえずDBを増やそう」などの一旦の対応方針を決めることができます。

4)原因の特定

流入経路の確認

引き続きGoogleAnalyticsのリアルタイムで、該当ページへのアクセス元をざっくり割り出し

f:id:aimstogeek:20200312152614p:plain
GAリアルタイム_トラフィック
担当サイトのケースでは、

  • ソーシャル
  • オーガニック

のどちらかで、だいたいオーガニックです。
要は、「話題になったキーワードでSEO上位を獲得していた」ケースですね。

今回も、ここで「オーガニックの流入が多いぞ!」となったパターンで紹介します。

検索キーワードの確認

ページと流入経路が特定できたので、どのキーワードでアクセスが集まっているのかを確認します。
これには、SearchConsoleを使用。リアルタイムの情報は見れませんが、流入キーワードの傾向はわかるので結構役に立ちます。

こんな感じで、ページのURLを指定してフィルタをかけて…

f:id:aimstogeek:20200312152621p:plain
SearchConsoleでページにフィルタ設定
キーワード傾向を抽出。
f:id:aimstogeek:20200312152632p:plain
通常時のこのページの流入キーワードが分かる
こうすることで、検索数が急激に増えたキーワードのあたりをつけることができます。

SNSで話題調査

キーワードのあたりがついたので、今度はそのキーワードがSNSでどのように言及されているかを調査。
これには、「Yahoo!リアルタイム」が便利です。
https://search.yahoo.co.jp/realtime

塗りが多くてもはやなにを表しているか分からないと思いますが、SNSの検索結果に加えて、右側に言及数の推移も表示されて便利です。

f:id:aimstogeek:20200312152636p:plain
わかりにくいけど非常に便利だぞ!

これらをかけ合わせて、以下を推測・予想!

  • どうしてそのキーワードが話題になったのか
  • いつから話題になったのか
  • それがどのくらい継続しそうか

担当サイトのケースでは、テレビで特集が組まれたキーワードだったとか、医療ドラマで意味深につぶやかれたキーワードだったとかが多いです。
ちなみに、医療ドラマの場合は編集チームの誰かが絶対見ているので、「〇〇ってワードでバズったんだけどなんかあった?」と聞けば一発で答えてくれます。笑

原因が分かったので、今後の予測をたて、対応を検討!

例:テレビ関係

  • 放送された瞬間とその後のCMがタイミングとしてはピーク
  • そこからは割と急速にアクセスが落ちるので、その時点の負荷に耐えられる構成にして一旦の対応を完了

例:Yahoo!ニュースに載ったケース

  • 数時間単位でコンスタントにアクセスが来る
  • どこでピークがくるか不明。余裕を持ったインフラ構成にしておく

この辺の予測はもう経験と勘です。他社の事例とか超知りたい。

5)対応

実際の対応はインフラがメインなので、SREチームがやってくれます。臨機応変に対応してくれるのでとても心強い。

実際の指示も、「Webサーバ3台増やして」なんて具体的なことではなく、「医療ドラマでこのページがバズってるみたいなんだけどどうしよう!」というような情報を共有して、一緒に対応を考えることが多いです。

まとめ

バズは嬉しいものですが、対応できなければせっかく来てくれたユーザーとの縁が持てなくなってしまいます。
私たちも多くの失敗を経て、現時点ではこのような形で対応していますが、まだまだ対応しきれないことも多く。。
オートスケールもしたいし、そもそももっとキャッシュとか活用して負荷も減らせるのでは?とか、考えることはいっぱいあると思います。日々精進ですね!



\\世の中とユーザーの動きを見ながらアレコレ動きたい仲間を募集!! // 919.jp

Laravelの.envについて調べてみた

どうもソフトウェアエンジニアのぱふゅーむです。
最近Laravelの.env周りで少しトラブったので復習の意味も込めて、
Laravelの.envの仕様について調べてみました。

今回は下記2点を中心に.envについて説明していきます!

  • .envファイルがどのように環境変数として設定されるか
  • .envファイルの値を取得する際に気をつけなければならないこと

.envファイルがロードされるタイミング

.envはIlluminate\Foundation\Bootstrap\LoadEnvironmentVariablesのbootstrapメソッドでロードされています。
.envをロードする前に$app->configurationIsCached()でキャッシュの存在をチェックしているようです 。

<?php

/**
 * Bootstrap the given application.
 *
 * @param  \Illuminate\Contracts\Foundation\Application  $app
 * @return void
 */
public function bootstrap(Application $app)
{
    if ($app->configurationIsCached()) {
        return;
    }

    $this->checkForSpecificEnvironmentFile($app);

    try {
        (new Dotenv($app->environmentPath(), $app->environmentFile()))->load();
    } catch (InvalidPathException $e) {
        //
    }
}

ちなみにbootstrapメソッドが呼ばれるタイミングはpublic/index.phpの以下の部分です。
メッセージがイカしてます笑

<?php

/*
|--------------------------------------------------------------------------
| Turn On The Lights
|--------------------------------------------------------------------------
|
| We need to illuminate PHP development, so let us turn on the lights.
| This bootstraps the framework and gets it ready for use, then it
| will load up this application so that we can run it and send
| the responses back to the browser and delight our users.
|
*/

$app = require_once __DIR__.'/../bootstrap/app.php';

キャッシュの存在をチェックするconfigurationIsCachedメソッドが定義されているのはIlluminate\Foundation\Applicationです。

<?php

/**
 * Determine if the application configuration is cached.
 *
 * @return bool
 */
public function configurationIsCached()
{
    return file_exists($this->getCachedConfigPath());
}

/**
 * Get the path to the configuration cache file.
 *
 * @return string
 */
public function getCachedConfigPath()
{
    return $this->bootstrapPath().'/cache/config.php';
}

bootstrap/cache/config.phpが存在する場合は、.envをロードせずbootstrap/cache/config.phpを返しているようです。
bootstrap/cache/config.phpphp artisan config:cacheで作成されるファイルです。
コマンドを叩いて作成されたファイルを確認してみました。

bootstrap/cache/config.php

<?php

return array (
  'app' => 
  array (
    'name' => 'hoge',
    'env' => 'local',
    'debug' => true,
    'url' => 'https://hoge/fuga',
    'timezone' => 'Asia/Tokyo',
    'locale' => 'ja',
    'fallback_locale' => 'ja',
    'key' => '',
    'cipher' => 'AES-256-CBC',
    'log' => 'single',
    'log_level' => 'debug',
    'providers' => 
    array (
      0 => 'Illuminate\\Auth\\AuthServiceProvider',
      1 => 'Illuminate\\Broadcasting\\BroadcastServiceProvider',
      2 => 'Illuminate\\Bus\\BusServiceProvider',
      3 => 'Illuminate\\Cache\\CacheServiceProvider',
      ...
    'aliases' => 
    array (
      ...
  'auth' => array(
    ...

config/配下のファイルが全てキャッシュされているのが分かります。 php artisan config:cacheを叩くと.envを読み込まず、こちらのキャッシュファイルを読み込むようになります。

cacheされたファイルが存在しない場合、.envファイルの値はどのように環境変数に設定されるのでしょうか?
コードを追ってみると以下のように定義されていました。

Dotenv/Loader

<?php

/**
 * Set an environment variable.
 *
 * This is done using:
 * - putenv,
 * - $_ENV,
 * - $_SERVER.
 *
 * The environment variable value is stripped of single and double quotes.
 *
 * @param string      $name
 * @param string|null $value
 *
 * @return void
 */
public function setEnvironmentVariable($name, $value = null)
{
    list($name, $value) = $this->normaliseEnvironmentVariable($name, $value);

    // Don't overwrite existing environment variables if we're immutable
    // Ruby's dotenv does this with `ENV[key] ||= value`.
    if ($this->immutable && $this->getEnvironmentVariable($name) !== null) {
        return;
    }

    // If PHP is running as an Apache module and an existing
    // Apache environment variable exists, overwrite it
    if (function_exists('apache_getenv') && function_exists('apache_setenv') && apache_getenv($name)) {
        apache_setenv($name, $value);
    }

    if (function_exists('putenv')) {
        putenv("$name=$value");
    }

    $_ENV[$name] = $value;
    $_SERVER[$name] = $value;
}

最終的にはputenv環境変数に定義していました。
つまり.envに設定された値は、カレントのリクエストを実行している間のみしか環境変数として存在しません。

cacheした場合に気をつけなければならないこと

Laravelには環境変数を取得するヘルパ、env()が用意されています。
Illuminate\Support\helpers/phpに定義されているので中身を見てみます。

<?php

/**
 * Gets the value of an environment variable.
 *
 * @param  string  $key
 * @param  mixed   $default
 * @return mixed
 */
function env($key, $default = null)
{
    $value = getenv($key);

    if ($value === false) {
        return value($default);
    }

    switch (strtolower($value)) {
        case 'true':
        case '(true)':
            return true;
        case 'false':
        case '(false)':
            return false;
        case 'empty':
        case '(empty)':
            return '';
        case 'null':
        case '(null)':
            return;
    }

    if (strlen($value) > 1 && Str::startsWith($value, '"') && Str::endsWith($value, '"')) {
        return substr($value, 1, -1);
    }

    return $value;
}

基本的にはgetenv()で取得した環境変数を返しているだけです。
つまり、php artisan config:cache時にControllerや、Model等config/配下以外で.envファイルの値をenv()ヘルパを使用して取得しようとすると .envファイルをロードしないため、nullが返ってきてしまいます。
その回避策として.envの値を参照する時はヘルパ、config()を使用します。

Illuminate\Foundation\helpers.phpをみてみます。

<?php

/**
 * Get / set the specified configuration value.
 *
 * If an array is passed as the key, we will assume you want to set an array of values.
 *
 * @param  array|string  $key
 * @param  mixed  $default
 * @return mixed
 */
function config($key = null, $default = null)
{
    if (is_null($key)) {
        return app('config');
    }

    if (is_array($key)) {
        return app('config')->set($key);
    }

    return app('config')->get($key, $default);
}

configの値を取得してくれていますね。
env()はconfig内のみで使用するよう注意が必要です。

.envの運用について

今回の調査で.envファイルの値がどのように環境変数として設定されるか分かり、勉強になりました。
.envは便利なファイルですが、一歩使い方を誤るとクリティカルなバグの温床になってしまいます。
ちなみに私が参画しているプロジェクトでは.envを運用していません。
正しく言うと.envは設定していますが、.envに設定した値を直接apache環境変数に設定しています。

この運用は下記のようなメリットがあげられます。

  • configをキャッシュした時に、上記に記述したようなバグを防げる
  • アプリケーションより下のレイヤーで設定することによって、若干だがパフォーマンスの向上が期待できる

しかし反対に、devやstgなど各環境毎に.envの値をapacheに設定してあげる必要があるため、 煩わしさもあります。

今回の調査を参考に、.envのベストな運用を考えていきたいと思います。

Enjoy developing!



\\『真のユーザーファーストでマーケットを創造する』「ありがとう」で溢れる仲間を募集中です!! // 919.jp