この記事は、concrete5 Japan Advent Calendar 2016の7日目の記事です。
6日目の菱川拓郎さんの「concrete5 サイトをAMPに対応させる」からバトンを引き継いでいます。
コンクリートファイブジャパン代表取締役の次の日にまともな技術記事を書くのは若干緊張しますが、私なりに頑張って書いていきたいと思います。
先日、とある地元企業様のWebサイトをconcrete5.7で制作させていただいたのですが、「外部フォーム」ブロックを使ったフォームの作成で、思いのほか苦労してしまいました。
特に変わったフォームを作ろうとしたわけではなく、WordPressの「Contact Form 7」でさらっと作ることができるようなレベルのフォームを作ろうとしただけなのです。
多分、私以外にも、同様なことで行き詰まっておられたり、機能の実装をあきらめてしまった方がおられるのではないかと思いましたので、私なりに頑張って調べた内容を記事にまとめておきます。
(実はつい先日、バージョン8がリリースされてしまいました。外部フォームについて目立ったアップデートの話が出てきていないので大丈夫だと思うのですが、検証はしていません。もし問題があるようでしたら教えてください)
目次
デフォルトの「フォーム」ブロックは、無料配布のフォームスクリプトの代替にならない
concrete5にはデフォルトで「フォーム」ブロックが用意されています。
GUIでフォームが作成できますし、いろいろなタイプのフィールドが用意されていていますし、管理画面から送信された内容が確認できるのでとても便利なのですが、WordPressの「Contact Form 7」や「MW WP Form」のような機能を期待していると、意外にできないことが多くてガッカリします。
特に、従来のサイトのリプレースで、リプレース前のサイトに汎用のフォームライブラリ(よく無料で公開されているやつです)を使用したフォームが設置されている場合、それらと同等の機能が「フォーム」ブロックでは実現できないケースが非常に多いです。
私が一番困ったのが、フォーム部品のそれぞれに異なったidやclassを指定できないこと。
これができないと、細かいデザインカスタマイズが難しくなります。
そして痛いのが、郵便番号入力で自動で住所を入力するJavaScriptライブラリが使えないこと。(郵便番号・住所などのフィールドのidまたはclassを指定する必要があるため)
また、数値のみ入力可能にするといった、詳細な入力内容の制限やバリデーションの機能が弱いのもつらいです。
「外部フォーム」は、とにかく情報が少ない
そこで登場するのが「外部フォーム」ブロックなのですが、こちらはブロックの中身をPHPで作成することになるので、いきなり技術面のハードルが上がります。
そして最も問題なのが、まとまったドキュメントがないことです。
5.6は日本語の書籍がありますが、5.7に対応した書籍はまだありません。
ネットで探すと、いくつかブログ記事が見つかります。
導入部分がわかりやすくまとまっているものもあって非常に助かったのですが、肝心のフォーム部分に関しては「簡単な例」しかなくて、特にバリデーションの方法が全然わかりません。
普通にPHPの関数を使ってバリデーションしていたり、フォームブロック用ではないところからバリデーション機能を引っ張ってきている記事はありました。(別にこれらは間違った実装ではありません)
でも、「concrete5にはフォームブロック用のライブラリがちゃんと用意されているはずだ!」と思った私は、英語やその他の言語で書かれた記事も探してみました。
そこでわかったのが、実は、本家のconcrete5のドキュメントにも、あまりちゃんとした記述がないということでした。
「外部フォーム」を使うメリットは?
こんなに扱いづらいのなら、concrete5にこだわらずPHPでフォームを作成してもいいのではないかと思われるかもしれませんが、「外部フォーム」には大きなメリットがあります。
一つ目は、concrete5のブロックになるので、GUIで配置・移動ができるようになることです。
二つ目は、ヘルパーが用意されていることです。
フォームヘルパーで出力したフォームのタグには、適切なclassが付加されます。bootstrapに対応しているclassも付加されるので、すぐに形になります。
バリデーションヘルパーは、エラー処理が統一されていて扱いが簡単です。
メールヘルパーは、必要なパラメーターを設定するだけで、フォームからメールが送信できます。
三つ目は、CSRF(クロスサイトリクエストフォージェリ)対策済みだということです。
CSRFの詳しい説明はここでは省きますが、簡単に言うと入力画面を通さずにフォームにデータを送りつけられてしまう脆弱性です。
フルスクラッチでフォームを作るとわかりますが、XSSとかCSRFとかのセキュリティ対策をするのは結構大変ですし、将来的に新たな脅威が発生した場合の対応も大変です。
「外部フォーム」ブロックを使えばこういった技術的な部分をクリアすることができますし、もし何かあっても世界中のconcrete5ユーザーコミュニティによって対策が講じられますので心強いです。
(参考)IPA ISEC セキュア・プログラミング講座:Webアプリケーション編 第4章 セッション対策:リクエスト強要(CSRF)対策
そして最大のメリットは、concrete5 japan 公式サイトのフォーラムで、堂々と質問できることです。
外部フォームに必要なファイルの構成
外部フォームを使用する場合は、まず、「application/blocks/external_form/form
」フォルダに必要なファイルを作成します。(フォルダが無い場合は新規作成)
最低限必要なファイルは、見た目を記述するビューと、動作を記述するコントローラーの2つです。
例えば「shiba_form」というフォームを作成する場合は、フォーム名と同名のファイルを、formフォルダ直下とcontrollerフォルダに作成します。
application/blocks/external_form/form/
shiba_form.php
:ビューcontroller/
shiba_form.php
:コントローラー
異なる外部フォームを追加作成する場合は、別の名前でビューとコントローラーのセットを作成します。
例えば、「shiba_form」、「form01」と2つのフォームを作成する場合は、下記のようなファイル構成になります。
application/blocks/external_form/form/
shiba_form.php
form01.php
controller/
shiba_form.php
form01.php
外部フォームを設置する際は、カスタムテンプレートでこのフォーム名(例:「shiba_form」)を選択します。
※5.6の場合はフォルダ名が「application/blocks/external_form/forms
」と、「application/blocks/external_form/forms/controllers
」になっていますが、5.7の場合はフォルダ名に「s」がつきません。フォルダ名を5.6と同じにすると外部フォームブロックに正しく認識されませんので注意してください。
必要であれば、自動返信メールのテンプレートを下記に作成します。(メールの内容が簡単な場合は、コントローラー内に書いてしまっても構いません)
application/mail/(メールテンプレート名).php
(メールテンプレート名は自由です)
それでは、「shiba_form」という外部フォームを作成してみましょう。
ビューのつくり方
ビューは、application/blocks/external_form/form/shiba_form.php
となります。
ビューには、フォームの見た目を指定します。
入力フォームと完了画面を記述し、ステータスを見て条件分岐で出しわけます。
また、バリデーションエラー時には、フォームに入力済みの値とエラー内容を表示します。
if (isset($response)) {
// コントローラー処理完了後の処理(サンクスメッセージなど)
・・・
} else {
// フォーム表示(初回入力・再入力共通)
if(isset($error)) {
// エラー表示
・・・
}
// フォーム表示
<form>
・・・
</form>
}
$response
、$error
には、コントローラー内で値をセットします。
コントローラーの処理が終わるとビューに戻ってきますので、セットされた値で条件分岐して、再度入力させたり処理完了メッセージを出したりします。
フォーム部分の作成
まず、フォーム表示を作っていきます。
フォーム部品は、concrete5のフォームヘルパーを使って記述していきます。
$form = Core::make('helper/form');
手順としては、まずHTMLでフォームを作成し、ヘルパーに置き換えていくとよいと思います。
<?php
defined('C5_EXECUTE') or die(_("Access Denied."));
// ヘルパー読み込み
$form = Core::make('helper/form');
・・・
if (isset($response)) {
// コントローラー処理完了後の処理(サンクスメッセージなど)
・・・
} else {
// フォーム表示(初回入力・再入力共通)
if(isset($error)) {
// エラー表示
・・・
}
// フォーム表示
?>
<form method="post" action="<?php echo $view->action('○○○')?>"> ・・・ </form> <?php }
formタグのactionで指定する$view->action('○○○')
の○○○の部分は、コントローラーで記述するフォームの処理を行う関数の名前を指定します。
例えば、$view->action('send_mail')
であれば、コントローラーの処理関数名は、public function action_send_mail()
と、先頭にaction_をつけたものになります。
フォーム部品として使えるヘルパーのメソッドは、concrete5のドキュメントにあります。
ただ、ここを見ても実際に使えるメソッドがわかりません。
API Reference からたどっても、具体的な記述に全然たどり着かないのです。
で、いろいろ探して、コミュニティに詳しい説明をされている記事を見つけました。
How to use the form widget in concrete5 5.7
上記の記事の内容を、私が使用した経験も含めて簡単に日本語で説明します。
フォームヘルパーのメソッド
5.7では、HTML5のinputタグのtype属性に一部対応しています。
HTML5のtype属性に対応しているブラウザでは、入力補助や妥当性チェックが行われます。
※あくまでも入力補助機能ですので、必ずサーバー側で入力値のバリデーションを行ってください。
テキスト
$form->text($name, $value, $tagAttributes)
- $name:name属性、id属性
- $value:初期値
- $tagAttributes:タグの属性(classなど)配列渡し
パスワード
$form->password($name, $value, $tagAttribute)
数字(HTML5)
$form->number($name, $value, $tagAttributes)
電話番号(HTML5)
$form->telephone($name, $value, $tagAttributes)
メールアドレス(HTML5)
$form->email($name, $value, $tagAttributes)
URL(HTML5)
$form->url($name, $value, $tagAttributes)
非表示テキスト
$form->hidden($name, $value, $tagAttributes)
テキストエリア
$form->textarea($name, $value, $tagAttributes)
ファイル
$form->file($name, $tagAttributes)
チェックボックス
$form->checkbox($name, $value, $isChecked, $tagAttributes)
- $isChecked = true:checked = “checked”
ラジオボタン
$form->radio($name, $buttonValue, $value, $tagAttributes)
- $buttonValue:ボタンの値
- $value = true:checked = “checked”
セレクトボックス
$form->select($name, $options, $value, $tagAttributes)
- $options:配列でoptionを指定
array(‘値’ => ‘表示’, ・・・) - $value:初期値
$value にセットされている $options の値が、selected になる
- $options:配列でoptionを指定
セレクトボックス(複数選択)
$form->selectMultiple($name, $options, $value, $tagAttributes)
- セレクトボックスと同じだが、初期値($value)を配列で与える
ボタン
$form->button($name, $value, $tagAttributes, $additionalClasses)
送信
$form->submit($name, $value, $tagAttributes, $additionalClasses)
ラベル
$form->label($forname, $value, $tagAttributes)
- $forname:ラベルを付ける対象の$name
- $value:ラベル(HTML使用可)
PHPのコードが読める方は、下記のファイルがヘルパーの正体ですので読んでみてください。
/concrete/src/Form/Service/Form.php
フォームに付加されるbootstrapに対応したclass
フォームヘルパーを使ってフォーム部品を作ると、bootstrapに対応したclassが付加されます。
bootstrapを読み込んでいる場合は、フォーム部品にCSSが適用されるので、そのままでもある程度の見栄えになります。
- label:
control-label
- button, submit:
btn
- その他:
form-control
(ヘルパーと実際に出力されるHTMLの例)
echo $form->label('form_name', 'お名前', array('class' => 'col-sm-4'));
echo $form->text('form_name', array('class' => 'col-sm-8'));
↓
<label id="form_name" class="col-sm-4 form-control control-label">お名前</label>
<input type="text" id="form_name" name="form_name" value="" class="col-sm-8 form-control ccm-input-text">
フォーム処理完了後の処理
フォームの送信ボタンをクリックすると、コントローラーにデータが送られ、処理が行われます。
コントローラーで問題なく処理が完了すると、$responce
に何らかの値がセットされて、ビューに処理が戻ってきます。
そこで、$responce
がセットされていればフォーム処理完了後にサンクスメッセージなどを表示するようにしておきます。
if (isset($response)) {
// コントローラー処理完了後の処理(サンクスメッセージなど)
?>
<div id="thanks">
<h3>お問い合わせが完了いたしました</h3>
<p>お問い合わせありがとうございます。<br>自動返信メールを送信いたしました。</p>
</div>
<?php
} else {
・・・
}
エラー時の処理
コントローラーでフォームの入力内容に問題が見つかった場合などは、$error
にエラーの内容がセットされて、ビューに処理が戻ってきます。
そこで、$responce
がセットされておらず、$error
がセットされていれば、エラーの内容を表示してから再度フォームを表示します。
ビューの例
上記をまとめると、ビューはこのようになります。
<?php
defined('C5_EXECUTE') or die(_("Access Denied."));
// ヘルパー読み込み
$form = Core::make('helper/form');
if (isset($response)) {
// コントローラー処理完了後の処理(サンクスメッセージなど)
?>
<div id="thanks">
<h3>お問い合わせが完了いたしました</h3>
<p>お問い合わせありがとうございます。<br>自動返信メールを送信いたしました。</p>
</div>
<?php
} else {
// フォーム表示(初回入力・再入力共通)
if(isset($error)) {
// エラー表示
?>
<div class="has-error">
<p class="help-block">入力内容に誤りがあります。</p>
<?php
foreach($error as $errmsg) {
echo '<p>'.$errmsg.'</p>';
}
?>
</div>
<?php
}
// フォーム表示
?>
<form class="form-horizontal" method="post" action="<?php echo $view->action('send_mail')?>">
<?php
// テキスト
echo '<div class="form-group">';
echo $form->label('name', 'お名前', array('class' => 'col-sm-4'));
echo '<div class="col-sm-4">'.$form->text('name', array('placeholder' => '必須項目')).'</div>';
echo '</div>';
// メールアドレス
echo '<div class="form-group">';
echo $form->label('email', 'メールアドレス', array('class' => 'col-sm-4'));
echo '<div class="col-sm-4">'.$form->email('email', array('placeholder' => '必須項目')).'</div>';
echo '</div>';
// セレクトボックス
$shibacolors = array(
'' => '選択してください',
'red' => '赤柴',
'black' => '黒柴',
'goma' => '胡麻柴',
'other' => 'その他',
'all' => '全部好き'
);
echo '<div class="form-group">';
echo $form->label('like', '好きな柴犬は?', array('class' => 'col-sm-4'));
echo '<div class="col-sm-4">'.$form->select('like', $shibacolors, '').'</div>';
echo '</div>';
// テキストエリア
echo '<div class="form-group">';
echo $form->label('reason', '好きな理由は?', array('class' => 'col-sm-4'));
echo '<div class="col-sm-4">'.$form->textarea('reason', '', array('rows' => 3)).'</div>';
echo '</div>';
// 送信ボタン
echo '<div class="form-group">';
echo '<div class="col-sm-offset-4 col-sm-4">'.$form->submit('submit', '送信', array('class' => 'btn-primary')).'</div>';
echo '</div>';
?>
</form>
<?php
}
コードが複雑になるのでこの記事には書きませんが、コントローラーでエラーメッセージを”フィールド名をキーにした連想配列”に格納して、ビューでフォームの各フィールドに対応させて表示するようにすると、ユーザーにわかりやすいと思います。
コントローラーのつくり方
コントローラーは、application/blocks/external_form/form/controller/shiba_form.php
となります。
コントローラーは、ビューで入力された値を受け取り、内容をチェックし、問題が無ければメール送信を行います。
問題が見つかった場合は、エラー情報を設定します。
コントローラーでの処理が終わると、ビューに戻ります。
コントローラーは次のような形になります。
<?php
namespace Application\Block\ExternalForm\Form\Controller;
use Concrete\Core\Controller\AbstractController;
use Core;
use UserInfo;
class ShibaForm extends AbstractController {
public function action_send_mail($bID = false)
{
if ($this->bID == $bID) {
// バリデーション
・・・
if (!$val->test()) {
// エラーあり
・・・
} else {
// エラーなし
・・・
return true;
}
}
}
5.6 とはかなり書き方が変わっています。
先頭の4行はお決まりです。
コントローラーのクラスは、ファイル名の各単語の先頭を大文字にしてアンダーバーを抜いたものになります。
ファイル名が shiba_form.php
ですので、class ShibaForm
となります。
メインの処理を行う関数名は、ビューのformタグのaction属性 $view->action('○○○')
の値の前に、action_をつけたものになります。
先ほどビューで$view->action('send_mail')
としましたので、関数名は、action_send_mail
となります。
バリデーション
バリデーションは、フォーム用のヘルパーを呼び出して使います。
$val = Core::make('helper/validation/form');
まず、各フィールドに対してバリデーションの内容とエラーメッセージを設定します、$val->test()
でバリデーションを行い、エラーがあった場合は、$val->getError()->getList()
でエラーメッセージを一括して取得できます。$this->set()
で配列にセットできます。
// validation/form ヘルパーを呼び出し
$val = Core::make('helper/validation/form');
$val->setData($this->post());
// バリデーション設定
$val->addRequired('name', '名前を入力してください。');
$val->addRequiredEmail('email', '有効なメールアドレスを入力してください。');
$val->addRequired('like', '好きな柴犬を選択してください。');
if (!$val->test()) {
//エラーあり
$errorArray = $val->getError()->getList();
$this->set('error', $errorArray);
} else {
//エラーなし
・・・
}
バリデーション(フォーム用)ヘルパーのメソッド
よく使いそうなものを取り上げて説明します。
ビューから送信されたPOSTデータをセット
$val->setData($this->post())
必須項目に入力されているか?
$val->addRequired($forname, $errorMsg)
- $forname:対象フィールドの$name
- $errorMsg:エラーメッセージ(フィールド名をキーにした配列で渡すこともできる)
入力項目が整数か?
$val->addInteger($forname, $errorMsg, $emptyIsOk)
- $emptyIsOk:空でも良いか?(デフォルトはtrue/falseで入力必須)
メールアドレスが入力されているか?
$val->addRequiredEmail($forname, $errorMsg)
- 必須チェックのみで、メールアドレスとして正しい書式かどうかのチェックはされていないようです。
イメージがセットされているか?
$val->addUploadedImage($forname, $errorMsg, $emptyIsOk)
ファイルがセットされているか?
$val->addUploadedFile($forname, $errorMsg, $emptyIsOk)
バリデーションエラーメッセージを配列で取得
$val->getError()->getList())
PHPのコードが読める方は、下記のファイルがヘルパーの正体ですので読んでみてください。
/concrete/src/Form/Service/Validation.php
必要なバリデーションの方法がヘルパーに無い場合、フォーム用以外のバリデーションヘルパーを利用するか、自分でバリデーションプログラムを組むことになります。
そこでエラーがあった場合は、バリデーションヘルパーが出力するエラーメッセージと同様の形でエラーメッセージを作成し、array_merge
などを用いて追記してください。
メール送信
バリデーションで問題が無ければメール送信を行います。
メール送信は、メールヘルパーを呼び出して設定していくだけです。
$mh = Core::make('helper/mail');
メールヘルパーは、concrete5のドキュメントにありました。
上記の記事の内容を、簡単に日本語で説明します。
メールヘルパーのメソッド
これ以外にもありますが、よく使いそうなものを取り上げて説明します。
From, Reply to
$mh->from($email, $name)
$mh->replyto($email, $name)
- $email:送信元メールアドレス
- $name:送信元の名前(省略可)
(例)$mh->from('kanoko@cherrypieweb.com', '柴犬かのこ')
$this->post('○○○')
として、○○○にビューで入力された項目の$name
を指定すれば、その値が設定される
(例)$mh->from($this->post('from'), $this->post('from_name')
To
$mh->to($email, $name)
- toはカンマ区切りで複数設定可
(例)'kanoko@cherrypieweb.com, yomogi@cherrypieweb.com'
- toはカンマ区切りで複数設定可
Cc, Bcc
$mh->cc($email)
$mh->bcc($email)
題名
$mh->setSubject($subject)
- $subject:メールの題名
- これを使わず、メールテンプレートで設定することもできる
本文
$mh->setBody($body)
$mh->setBodyHTML($body)
- $body:メールの本文
setBodyHTMLでは、HTMLタグが使える - メールの本文が長くなる場合はこれを使わず、addParameterとloadを用いてメールテンプレートにセットする
- $body:メールの本文
添付ファイル
$mh->addAttachment($attachment)
- $attachment:添付ファイルのパス
(例)$attachment = \Concrete\Core\File\File::getByID(123);
- $attachment:添付ファイルのパス
メールテンプレートのパラメーター設定
$mh->addParameter($key, $value)
- $key:メールテンプレートのパラメーター名
- $value:設定値
$this->post('○○○')
として、○○○にビューで入力された項目の$name(例:$this->post('name')
)を指定すれば、その値が設定される
メールテンプレートの読み込み
$mh->load($template)
- $template:メールテンプレート名
メールテンプレートは、application/mail/ に置く
(例)メールテンプレートファイル:application/mail/thanksmail.php
→ メールテンプレート名:thanksmail
- $template:メールテンプレート名
メール送信
$mh->sendMail()
メールテンプレートの例
メールテンプレートでは、下記が設定できます。
他のメソッドで指定できるものもあるので、重複しないようにします。
- $subject:題名
- $from:送信元
- $body:本文
<?php
defined('C5_EXECUTE') or die(_("Access Denied."));
// タイトル
$subject = '【自動返信】お問い合わせありがとうございます';
// 本文
$body = "(お問い合わせ内容)\r\n";
$body .= $name." 様\r\n";
$body .= "メールアドレス:".$email."\r\n";
$body .= "好きな柴犬:".$like."\r\n";
$body .= "好きな理由:\r\n";
$body .= $reason."\r\n";
コントローラーの例
上記をまとめると、コントローラーはこのようになります。
<?php
namespace Application\Block\ExternalForm\Form\Controller;
use Concrete\Core\Controller\AbstractController;
use Core;
use UserInfo;
class TestForm extends AbstractController {
public function action_send_mail($bID = false)
{
if ($this->bID == $bID) {
// validation/form ヘルパーを呼び出し
$val = Core::make('helper/validation/form');
$val->setData($this->post());
// バリデーション設定
$val->addRequired('name', '名前を入力してください。');
$val->addRequiredEmail('email', '有効なメールアドレスを入力してください。');
$val->addRequired('like', '好きな柴犬を選択してください。');
// バリデーション実行と結果の取得
if (!$val->test()) {
// エラーあり
$errorArray = $val->getError()->getList(); // エラーメッセージの取得
$this->set('error', $errorArray); // $error にエラーメッセージを格納
} else {
// エラーなし
$mh = Core::make('helper/mail'); // メールヘルパー呼び出し
// メールヘッダーを設定
$mh->to($this->post('email')); //登録者への自動返信
$mh->bcc('kanoko@cherrypieweb.com'); //メール管理者へbcc送信
$mh->from('kanoko@cherrypieweb.com', '柴犬かのこ'); //メール送信元
// メールの内容を取得
$mh->addParameter('name', $this->post('name'));
$mh->addParameter('email', $this->post('email'));
$mh->addParameter('like', $this->post('like'));
$mh->addParameter('reason', $this->post('reason'));
// メール送信
$mh->load('thanksmail'); // メールテンプレートの読み込み
$mh->sendMail(); // メール送信
$this->set('response', true); // ステータスを設定
return true;
}
}
}
}
作成した「外部フォーム」ブロックの配置
ビューとコントローラーができたら、「外部フォーム」ブロックとして配置できるようになります。
ブロックから「外部フォーム」をページに配置し、[編集]をクリックし、[読み込むファイル]で配置したいフォームのファイル名を選択します。
「Test Form」というのが出てきますが、これは本体に含まれているサンプルです。
外部フォームを配置して編集モードから抜ければフォームが使えるようになっていますので、まずは、正しくフォームが機能するか試してみてください。
入力内容にエラーがあると、下記のような表示になります。
正常に送信できると、下記のような完了画面が表示されます。
残念ながら外部フォームでは、管理画面の「フォーム一覧」から送信内容を確認することはできません。
ただ、「レポート」ー「ログ」の、「送信済みメール」チャンネルにログが残りますので確認は可能です。
もし自動返信メールが届かないときは、ログを確認してフォーム自体に問題があるのかどうかの切り分けをしましょう。
単に送信先のメールアドレスが間違っているだけということもあります。
さいごに
軽くまとめようと思ったら、とても長い記事になってしまいました。
私はPHPの上級者ではないし、concrete5についてもフォーラムで質問してばかりですので、間違っているところがあったり、もっと良いやり方があるかもしれません。
上級者の方の愛のあるマサカリを期待しています(笑)
明日は、遠藤 敏明さんです。
よろしくお願いします!
この記事を書いた人
-
FAシステムメーカー、国内最大手印刷会社製版部、印刷・ウェブ制作会社を経て、家庭の事情で実家に帰省して独立
現在はフリーランスと制作会社シニアディレクターのマルチワーク
ウェブ制作のほぼ全般を見渡せるディレクター業務が主だが、デザイン・コーディングも好き
1997年ブログ開設
WordPressコミュニティには2011年から参加
WordCamp Kansai 2016 セッションスピーカー
WordCamp Tokyo 2023 パネルディスカッションパネラー
WordBench京都、WordBench神戸、WordPress Meetup八王子など登壇多数
最新の投稿
ご質問・ご相談などありましたら
お気軽にお問い合わせください
資料請求・お問い合わせにはメールアドレスが必要です
コメントを残す