気ままなタンス*プログラミングなどのノートブック

プログラミングやRPGツクール、DTM等について、学んだことや備忘録をアウトプットとして残し、情報を必要としている誰かにとって「かゆいところに手が届く」ブログとなることを願いながら記事を書いています。

【intra-mart】intra-martの検証環境をDockerを使って爆速で構築する(docker-for-intra-martで環境構築)

こんにちは。

intra-martの環境構築ってすごく面倒くさいですよね。

TL;DR

resin-proとimart.warさえ準備すれば、1時間かかってた環境構築作業が15分で終わって、 いい感じにDockerコンテナ上でintra-martが動くので、検証作業が楽になって幸せ。

目次

intra-mart(イントラマート)とは

「そもそもintra-martって何?」という感じかもしれません。

intra-martはNTTデータイントラマート社が提供する、企業向けのシステム共通基盤です。

個別最適化された企業内のIT環境を一つに集約し、業務アプリケーションの全社横断的な運用を可能にするシステム共通基盤(開発・運用フレームワーク)です

「システム共通基盤/PaaS | NTTデータイントラマート」ページ説明より引用

稟議や手続きなどの承認フローの作成機能や、Webブラウザ上でのドラッグ&ドロップによる画面開発機能、 その他グループウェア、コラボレーション等の機能が提供されています。

勤め先でこの基盤が導入されている方であればご存知かもしれません。

企業競争力を強化する製品・サービス群 | システム共通基盤の株式会社NTTデータイントラマート

環境構築時の課題

僕は、現在勤めている会社でintra-martを利用して、アプリ開発・保守や技術検証等を実施しています。

つまり新機能の検証なども実施するため・・・ 例えばアップデートによって追加された新機能をちょっと試したいけど、既存の環境は汚したくないという場合には セットアップ直後のまっさらの状態の環境が必要になります。

しかしintra-martは環境構築作業だけでかなりの時間がとられてしまうのです。

環境構築作業一覧

下記に、環境構築時に実施する必要のある作業を一覧化してみましたが・・・多すぎる

  • JDKのインストール
  • resin-proのセットアップ
    • intra-martのダウンロードライブラリから、resin-pro-x.x.x.tar.gzをダウンロードし展開
    • プロパティの設定
    • Redhat EnterpriseやCentOSのインストール
    • resinのmakeに必要なパッケージ・ライブラリのインストール
    • make実行
  • Jugglingプロジェクトの作成、warファイル出力
  • データベースのセットアップ
    • インストーラのダウンロード
    • ミドルウェアのインストール
    • データベース作成
    • スキーマ、ユーザ作成
    • 権限付与
  • warファイルのデプロイ
  • intra-martのテナント環境セットアップ

慣れていれば1時間もかからず作業を終えてしまえるのですが、そうでない場合は3時間程度はかかってしまうこともあります。 そしてとにかく、ダウンロードやインストール、セットアップ等々、手動で何度も繰り返し実施するのは非常に面倒です。

改善方法の検討→環境構築を部分的に自動化

とにかく、手動作業が多すぎて嫌になりました。 そこで今回は最近学習を始めたDockerを利用して、自動かつ爆速(誇張表現?)でintra-mart環境を構築できるようにしてみました。

Dockerプロジェクトの配置先(Githubリポジトリ)

Githubにアップしました。

github.com

Dockerでやること

  • JDKのインストール
  • resin-proのセットアップ
    • CentOSのインストール
    • resinのmakeに必要なパッケージ・ライブラリのインストール
    • make実行
  • データベースのセットアップ
    • インストーラのダウンロード
    • ミドルウェアのインストール
    • データベース作成
    • スキーマ、ユーザ作成
    • 権限付与

手動でやること

  • resin-proのセットアップ
    • intra-martのダウンロードライブラリから、resin-pro-x.x.x.tar.gzをダウンロードし展開
    • プロパティの設定
  • Jugglingプロジェクトの作成、warファイル出力
  • warファイルのデプロイ
  • intra-martのテナント環境セットアップ

Dockerで作成するintra-martのシステム構成

Docker上の環境

ホスト名 コンテナイメージ 目的
ap CentOS 7.5.1804(※1)(library/centos) resin-proの実行及びwarデプロイ
db PostgreSQL 10(library/postgres) intra-martに関するデータの保存
adminer Adminer(library/adminer) dbのデータ参照用のアプリ

(※1 厳密には、NTTデータイントラマート社はintra-martが動作するLinux環境として、Red Hat Enterprise Linux 6.x、7.xのみを動作保証しているため、 本Docker関連ファイルを利用する場合は、あくまでも動作検証用の環境に留めておくことをおすすめします。本記事の内容によって発生した障害等について、一切責任を負いません)

必要なPC環境

  • 下記のいずれか
OS バージョン Docker
Windows7以降 64bit Docker Toolbox
Windows10 64bit Docker for Windows
macOS - Docker for Mac

なお、試してはいませんがVT-xを有効にする等の対応によって 32bit環境でもDockerが利用できるようなので、32bit環境でも実行できるかもしれませんね。 (僕は32bitのPCは持っていないので、試せていません)

利用手順

ここからは、本Dockerプロジェクトを利用して、intra-martの環境を構築する手順を記述いたします。 (この手順は、Windows10 + Docker Toolboxを前提にしています。必要に応じて適宜読み替えをお願いします。)

[1] Docker Toolboxのインストール

  • 下記のURLより、Docker Toolboxをダウンロードし、インストールします docs.docker.com

[2] Dockerのメモリを増やす

  • Dockerで利用するメモリを確保するため、VirtualBoxから、メモリを増やしておきます。
    • 僕のPCはとりあえず5GBを設定しています
  • 下記のURLの記事にメモリを増やす方法が詳しく記載されています qiita.com

  • Docker for Windowsの場合は、下記のURLが参考になります qiita.com タスクトレイのDockerアイコン右クリック->Settingsを開く

  • Docker for Macの場合の場合、下記のURLが参考になります qiita.com Preferences → Advanced にあるMemoryで使用量を調節

[2] Gitのインストール

  • 下記のURLより、Git for Windowsをダウンロードし、インストールします gitforwindows.org

[3] Dockerプロジェクトのダウンロードと初期設定

  • 任意のフォルダで、以下のコマンドを実行し、dockerプロジェクトをダウンロードします
> git clone https://github.com/rinne-grid/docker-for-intra-mart im
> cd im
  • warファイルの配置用フォルダを作成します
> mkdir .\ap\war

[4] Jugglingでwarファイルを作成

プロジェクト名をimartにして、必要なモジュールを選択し、設定を行います。 今回のDocker環境をそのまま利用するためには、下記ファイルの設定を変更する必要があります

  • storage-config.xmlを設定する
  • resin-web.xmlを設定する
  • 出力するwarファイル名をimart.warとする

(Dockerプロジェクトのap/.envファイルを変更することで、別の値を指定することも可能です)

storage-config.xmlの設定

imart/config/storage-config.xml の 19行目付近を以下のとおりに変更します

<root-path-name>/im-data/storage</root-path-name>
resin-web.xmlの設定

imart/resin-web.xml 内容を下記のとおりにします

<web-app xmlns="http://caucho.com/ns/resin" xmlns:resin="urn:java:com.caucho.resin">
    <character-encoding>UTF-8</character-encoding>

    <log-handler name="" class="jp.co.intra_mart.common.platform.log.handler.JDKLoggingOverIntramartLoggerHandler"/>
    <logger name="debug.com.sun.portal" level="warning" />

    <!-- im_service(im_asynchronous) -->
    <resource jndi-name="jca/work" type="jp.co.intra_mart.system.asynchronous.impl.executor.work.resin.ResinResourceAdapter" />
    <jsp>
        <recycle-tags>false</recycle-tags>
    </jsp>
    <database jndi-name="jdbc/default">
        <driver>
            <type>org.postgresql.Driver</type>
            <url>jdbc:postgresql://db:5432/imart</url>
            <user>imart</user>
            <password>imart</password>
            <init-param>
                <param-name>preparedStatementCacheQueries</param-name>
                <param-value>0</param-value>
            </init-param>
        </driver>
        <max-connections>20</max-connections>
        <prepared-statement-cache-size>8</prepared-statement-cache-size>
    </database>
    <session-config>
        <reuse-session-id>false</reuse-session-id>
        <session-timeout>30</session-timeout>
    </session-config>

    <mime-mapping extension=".json" mime-type="application/json"/>
</web-app>
warファイルの出力

imart.warという名称でwarファイルを出力したら、 プロジェクトのim/ap/warフォルダの中に、warファイルをコピーします

[5] intra-martのサイトからLinuxのresin-proをダウンロード

intr-martのサイトにアクセスし、プロダクトファイルダウンロードボタンを押下します。

www.intra-mart.jp

ライセンスキーを入力すると、ダウンロード可能なファイル一覧が表示されます。

なお、intra-martサイトにも書いているとおり、.tar.gzがLinux用のresin-proになります。

https://www.intra-mart.jp/download/product/iap/setup/iap_setup_guide/texts/install/linux/resin_linux.html

最新のResinresin-pro-4.0.xx.tar.gzを入手します。

[6] 7zipをダウンロード、インストール

tar.gz形式のファイルを展開するため、この記事では7zipを利用します。

sevenzip.osdn.jp

  1. resin-pro.4.0.xx.tar.gzを展開します
  2. resin-pro.4.0.xx.tarファイルが作成されます
  3. resin-pro.4.0.xx.tarを展開します
  4. resin-pro.4.0.xxフォルダが作成されます
  5. resin-pro.4.0.xxフォルダの直下に、automake, binといったフォルダが存在することを確認します

f:id:rinne_grid2_1:20180707183550p:plain

[7] Dockerプロジェクトのフォルダにresin-proをコピー

  • 上記の[6]の5のフォルダ「resin-pro.4.0.xx」の名称をresin-proに変更します
  • resin-proフォルダをim/apフォルダにコピーします

[8] プロジェクトのフォルダ構成の確認

  • フォルダを確認し、以下の構成と同じになっていることを確認します
  • ポイント
    • im/ap/resin-proフォルダがあり、直下にautomake等のファイルが存在する
    • im/ap/warフォルダがあり、imart.warファイルが存在する
im
│  .env
│  .gitignore
│  docker-compose.yml
│  README.md
│
└─ap
    │  Dockerfile
    │
    ├─resin-pro
    │  ├─automake  など
    │
    └─war
            imart.war

[9] 必要に応じて、設定ファイルを変更する

  • im/ap/resin-pro/conf/resin.properties の 82行目付近 - jvm_args

-Xmx, -Xmsの値が、初期状態だと8192m(8GB)が設定されているため、自分のPCのメモリ状況に合わせて変更します

jvm_args : -Dfile.encoding=UTF-8 -Djava.io.tmpdir=tmp -Xmx1500m -Xms1500m -XX:+UseConcMarkSweepGC -XX:CMSInitiatingOccupancyFraction=30 -XX:NewSize=512m -XX:MaxNewSize=512m -XX:+CMSClassUnloadingEnabled -verbose:gc -XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+HeapDumpOnOutOfMemoryError -Xloggc:log/gc.log -XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=5 -XX:GCLogFileSize=10M
  • HTTPプロキシの設定 im/.env

社内ネットワーク等で、プロキシサーバーを経由する必要がある場合、.envのHTTP_PROXY、HTTPS_PROXYに値を設定します

HTTP_PROXY=http://user:password@server:port/
HTTPS_PROXY=http://user:password@server:port/

[10] Docker Machineを起動

  • Docker Machineを起動していない場合、下記コマンドで起動します
    • Docker Toolbeltという青いアイコンのショートカットを起動すると、自動でDocker Machineを起動してくれます

f:id:rinne_grid2_1:20180707183911p:plain

> docker-machine start

[11] Dockerコンテナの起動

  • プロジェクトフォルダに移動します
> cd any_folder\im
  • docker-composeを利用し、コンテナを起動します
> docker-compose up --build -d

ネットワーク接続環境にもよりますが、早くて5分、おそくとも30分程度で完了します

  • resinのindexページに接続します

http://192.168.99.100:8888

f:id:rinne_grid2_1:20180707184327p:plain

(Docker for WindowsやDocker for Macの場合は、上記IPアドレスではなくhttp://localhost:8888、もしくは自分で設定しているホスト名やIPアドレスにアクセスします。)

  • resinのページが開けることが確認できたら、warファイルをデプロイします
> docker exec im_ap /ap-server/bin/resinctl deploy /war/imart.war

コンテナ内のresin-proの場所は、/ap-serverです。 im/.envファイル内の変数を変更することで、お好きな場所を指定できます。

  • デプロイのコマンドが終了して、数分経ったらintra-martのセットアップページにアクセスします
    • 503 Service Temporarily Unavailableが発生する場合は、もう少しだけ待ってあげてください。

http://192.168.99.100:8888/imart/system/login

  • 無事にテナント設定画面が表示されるので、テナント環境セットアップを実行します

f:id:rinne_grid2_1:20180707184846p:plain

  • テナントIDはimartを指定します

f:id:rinne_grid2_1:20180707184958p:plain

  • リソース参照名は一覧に表示されたものを選択します

f:id:rinne_grid2_1:20180707185043p:plain

  • テナント登録を行い、しばらく待ちます

f:id:rinne_grid2_1:20180707185142p:plain

  • テナント環境セットアップが適切に動作しているかどうかについては、Adminerからテーブル作成状況を参照することで確認できます
情報名 入力情報
データベース情報 PostgreSQL
サーバ db
ユーザ名 imart
パスワード imart
データベース imart
  • テーブルの作成状況が確認できます(だいたい500テーブルくらいができたら、処理完了です)

f:id:rinne_grid2_1:20180707185642p:plain

f:id:rinne_grid2_1:20180707185835p:plain

  • データベースやストレージ情報はDocker Volumeに保存しているため、データは永続化されています
    • 一度、docker-compose downで終了し、もう一度docker-compose upを試して、システムログイン画面にアクセスすると、ダッシュボードが表示されることがわかります

f:id:rinne_grid2_1:20180707191944p:plain

事象別のコマンドリファレンス

事象 コマンド
Dockerコンテナを開始したい docker-compose up
Dockerコンテナをビルドして開始させたい docker-compose up --build
Dockerコンテナを終了させたい docker-compose down
DBデータやストレージを削除して、
新しくテナント環境セットアップから始めたい(永続化しているコンテナのがデータ全部消えるので要注意)
docker-compose up
docker-compose down -v
docker-compose up
warファイルをアンデプロイしたい docker exec im_ap /ap-server/bin/resinctl undeploy /war/imart.war
warファイルをデプロイしたい docker exec im_ap /ap-server/bin/resinctl deploy /war/imart.war

コンテナごとの接続情報

  • APサーバー(CentOS)
コンテナ名 ホスト名 ポート番号(ホスト) ポート番号(コンテナ)
im_ap ap 8888 8080
  • PostgreSQL
コンテナ名 ホスト名 DB名 ユーザ名 パスワード ポート番号(ホスト) ポート番号(コンテナ)
im_db db imart imart imart 5432 5432
  • Adminer
コンテナ名 ホスト名 ポート番号(ホスト) ポート番号(ゲスト)
im_ap adminer 8889 8080

まとめ

resin-proの設定とwarファイルさえ作成してしまえば、 あとは数コマンド叩くだけで、すぐに使えるintra-martの検証環境が手に入りました。 1時間かかっていた構築作業が、15分程度で済みそうです。

また、慣れていない人に利用してもらう場合も、3時間から30分程度に削減できそうです。

【雑記】Vue.js学習時の思考パターンに関するメモ

最近はKindleの「速習Vue.js 速習シリーズ」書籍を利用して、Vue.jsの学習を行っていました。

速習Vue.js 速習シリーズ

速習Vue.js 速習シリーズ

ふと、自分自身が学習時にどんなことを考えているのか(思考パターン)が気になってしまい、 その時点の思考についてまとめてみることにしました。

「テンプレート側でデータオブジェクトにアクセスするには、{{ ... }}(Mustache)構文を利用します」という文章を読んだ時の思考

  • 確かマスタッシュって名前の式構文だったなー。口ひげみたいな形だから、そう呼ばれているんだったか。
    • Angularでも同じ書き方するよな。
    • Django Templateもコンテキスト変数を表示するときにマスタッシュ記号使うし、Vue.jsやAngularをDjangoTemplateと一緒に利用しようとする競合するね
  • あれ、Django Template以外にもJinja2ってあったような気がする。それもマスタッシュ構文使うんだっけ?
  • 調査後、同じ記号利用することが判明
  • Angularでいうところのパイプとか、Djangoでいうフィルターみたいに、テンプレートの変数に対して加工を行うこともできそうな気がする。
    • 調べたら出てくるだろうけど、本で取り上げてない内容を先に確認するのはなんか気が引ける→とりあえず保留で。

{{ ... }}(Mustache)式の中でJavaScript組み込みオブジェクトが利用できるという文章を読んだときの思考

  • ということは、自作オブジェクトも登録するって方法もあるんだろうか?
    • なんなくあまりやらない方が良さそうな気がするけど方法は気になる。
  • きちんとした方法が後のページど出てくるかもしれないから、その時に振り返ればいいか
    • Vue.jsで定義されている何らかのグローバル変数に対して、追加したいオブジェクトを指定してあげる感じなんだろうか
  • それにしても、JavaScript組み込みオブジェクトといえば・・・サイ本購入したのは良いけど、最後まで読めてない気がする。何度も読み直すくらいしなきゃいけないな

JavaScript 第6版

JavaScript 第6版

コンポーネントに関する文章を読んだ時の思考

Vue.component('my-hello', {
  template: '<div>こんにちは、コンポーネント</div>'
});
  • Vue.component関数にコンポーネント名とオブジェクトを指定するのか
    • templateプロパティで、HTMLタグを指定しているなー
    • ちょっと前に書いたJSXを思い出す気がする
    • Angularの@Componentデコレータの中身に雰囲気が似ている気がする
    • こんな感じのやつ
@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
  • コンポーネントの利用方法は、指定したIDをHTMLのコードの中に記載すれば良いとのこと
    • コンポーネントの利用方法については、AngularやReactと同じような感じに見える
    • ということは、コンポーネントに親子関係も発生しそうだし、なんとなくReactのpropsやstateみたいな、値の定義とその値を親子間で受け渡す方法がほぼほぼ提供されてそうな気がする

vue-cliに関する説明文章を読んだときの思考

  • Vueにもcliがあるのか
  • Angularだと、ng newで雛形が作成されていたけど、vueの場合はどんな感じなんだろう
    • vueの場合も同じように雛形が作成されるけど、最初に色々と設定を聞かれるのね
    • .vueファイルってのができてるけど、これは・・・?
      • テンプレートとスクリプト、スタイルをまとめて記載したものらしい
      • style scopedって感じで、styleタグにscopedが追加されているけど、これはいったい何を示しているのか?
        • スコープって名前から推測すると、まずAngularのコンポーネントにおけるスタイルの適用範囲を思い出す
        • Angularの場合、そのコンポーネントで指定したスタイルはそのコンポーネント(と配下も?)のみに適用されたような気がする
        • ということは、Vue.jsも同じような感じで、適用範囲が現在のコンポーネントに絞られるのかもしれない

まとめ

今回学習しているVue.jsは、今までに書いたコードがある既存のフレームワークと似通っている部分が多かったため、 「比較」と「推測」が主な思考パターンになっているようでした。

基本的には、今までに体験・経験したことがあるかどうかを考えているようです。 該当するものがなければ、"そういうものだ"と、一時的に認識しておき、あとで整理するような感じになってそうです。

今まで見たことも経験したこともないものを学習する場合、 いったいどんな思考パターンになるのか、今後機会があれば試してみようと思います。

【Angular】JSONPの取得方法がわかったのでサンプルを作った話(HttpClient jsonp)

こんばんは。今回はAngularの話です。

今日は休日ということもあり、以前購入した「Angularアプリケーションプログラミング」の本を読みすすめていました。

Angularアプリケーションプログラミング

Angularアプリケーションプログラミング

その中で、はてなブックマークのAPIを使って、 ブックマークエントリー情報をJSONP形式で取得するという内容があったのですが・・・書籍の発売当時のAngularバージョンを元にして書かれているため、 うまく動きません。

そこで、JSONP形式による通信方法について調査し、現行バージョンでの取得サンプルを作成しました。

目次

サンプルプログラムのページ

はてなブックマークエントリ情報APIで、指定したURLのブックマーク情報を取得します。 初期URLは僕が作成した、ツクールMVのプラグインURLが指定されています。

https://rinne-grid.github.io/index.html

(スクリーンショット)

  • 初期画面 f:id:rinne_grid2_1:20180527212431p:plain

  • ブックマークエントリー取得 f:id:rinne_grid2_1:20180527212442p:plain

サンプルプログラムの置き場所

Githubに配置しました。

github.com

  • 今回、@angular/materialを使って画面を作成していますが、この記事の本筋ではないので、コードの記載は省略します。
  • 全ソースが上記にありますので、git cloneして動作確認等にご利用ください。

Angularアプリの作成

Angularアプリケーションの作成については、公式ドキュメントをご参照ください。

ざっくりと下記のような感じです

  • npmの場合
$ npm i -g @angular/cli
$ ng new appname
  • yarnの場合
$ yarn global add @angular/cli
$ ng new appname

app.modules.tsの記述

  • ポイント
    • @angular/common/HttpClientを利用するため、HttpClientModuleをインポートする
    • HttpClientのjsonpを利用するため、HttpClientJsonpModuleをインポートする
import { BrowserModule } from '@angular/platform-browser';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { NgModule } from '@angular/core';
import { HttpClientModule, HttpClientJsonpModule } from '@angular/common/http';
import { FormsModule } from '@angular/forms';
import { AppComponent } from './app.component';

@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    // @angular/core
    BrowserModule,
    HttpClientModule,
    HttpClientJsonpModule,
    BrowserAnimationsModule,
    FormsModule,

  ],
  providers: [
  ],
  bootstrap: [AppComponent]
})
export class AppModule { }

app.component.htmlの記述

  • ポイント
    • はてなブックマークエントリーAPIを呼び出す契機となるボタンを配置する
    • 取得するブックマークエントリの対象となるURLを入力するテキストボックスを配置する(コンポーネントのtargetUrlと双方向バインド)
<input name="targetUrl" [(ngModel)]="targetUrl">
<button name="getBookmarks"  (click)="getBookmark()">取得</button>

app.component.tsの記述

import { Component } from '@angular/core';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent {

  targetUrl = 'https://github.com/rinne-grid/tkoolmv_plugin_RecollectionMode'
  
  constructor(){}
  getBookmark() {
    console.log(this.targetUrl);
  }
}

サービスクラス(hatena-bookmark.service.ts)の作成

ng generateの利用

  • コンソール、ターミナルでcliを実行し、サービスクラスのテンプレートを作成
$ ng generate service hatena-bookmark

サービスクラスの記述

import { Injectable } from '@angular/core';
import { HttpClient, HttpParams } from '@angular/common/http';

import { Observable } from 'rxjs';

@Injectable()
export class HatenaBookmarkService {
    constructor(private http: HttpClient){}
    // はてなブックマークエントリ情報取得用のAPIエンドポイント
    private apiEndPoint: string = "https://b.hatena.ne.jp/entry/json/";

    getBookmarks(targetUrl:string): Observable<any> {
        console.log(targetUrl);
        // API仕様によると、urlとcallbackを指定するとjsonpを戻すとのこと
        // callbackはjsonpの第2引数で指定する
        let httpParams = new HttpParams()
            .set("url", targetUrl);
        
        // httpParamsにパラメータを設定した状態でtoStringを実行すると、key=value形式で返してくれる
        // https://angular.jp/api/common/http/HttpParams#toString
        console.log(`${this.apiEndPoint}?${httpParams.toString()}`)
        return this.http.jsonp<any>(`${this.apiEndPoint}?${httpParams.toString()}`, "callback");
    }
}

サービスクラスをapp.module.tsのprovidersに追加

import { BrowserModule } from '@angular/platform-browser';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { NgModule } from '@angular/core';
import { HttpClientModule, HttpClientJsonpModule } from '@angular/common/http';
import { FormsModule } from '@angular/forms';
import { AppComponent } from './app.component';

//-------------------------------------------------------
// 追加
//-------------------------------------------------------
import { HatenaBookmarkService } from './hatena-bookmark.service';

@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    // @angular/core
    BrowserModule,
    HttpClientModule,
    HttpClientJsonpModule,
    BrowserAnimationsModule,
    FormsModule,

  ],
  providers: [
//-------------------------------------------------------
// 追加
//-------------------------------------------------------
    HatenaBookmarkService
  ],
  bootstrap: [AppComponent]
})
export class AppModule { }

app.component.tsの修正

  • ポイント
    • HatenaBookmarkServiceをDIする
    • getBookmarkメソッドから、HatenaBookmarkServiceのgetBookmarksメソッドを呼び出す
    • サービスクラスのgetBookmarksの戻り値(Observable)をsubscribeし内容を取得する
import { Component } from '@angular/core';
import { HatenaBookmarkService } from './hatena-bookmark.service';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent {
  targetEntry: any = null;
  targetUrl = 'https://github.com/rinne-grid/tkoolmv_plugin_RecollectionMode'
  
  constructor(private hatenaService: HatenaBookmarkService){}
  getBookmark() {
    console.log(this.targetUrl);
    this.hatenaService.getBookmarks(this.targetUrl).subscribe(
      (entries) => {
        this.targetEntry = entries;
        console.log(entries);
      }
    );
  }
}

アプリを実行

$ ng serve

http://localhost:4200 に接続する

この記事内のソースコードは@angular/materialに関する記述を除いているため、 リンク先のようなサンプルにはならないのですが、HttpClient.jsonpを利用して、 JSONP形式のデータを取得できたことがわかるかと思います。

StackBlitzに本記事のソースコードのみで作ったサンプルを配置しましたので、こちらもご利用ください

(StackBlitz)本記事内のソースコードのみのバージョン

stackblitz.com

ハマったところ(あまりJSONP関係ない)

  • Github Pagesにサンプルを上げたとき、はてなブックマークAPIのURLがhttpで、GithubPagesがhttpsだったのでMixedコンテンツになってしまってうまく動かなかった
    • APIがhttpsも対応していたため、httpsに変更
    • 同様に、StackBlitzからのリクエストもhttpsに変更したら動いた
  • HttpParamsにパラメータをセットする際にインスタンスを作成した後にappendやsetを実行してもうまくセットされなかった
// ダメだった
let httpParams = new HttpParams(); 
httpParams.set("hoge", "foo");
// うまくいった
let httpParams = new HttpParams().set("hoge", "foo");

AngularによるJSONP取得ですが、だいぶ簡単に書くことができて、非常に便利だと思いました。 HttpClient.get, post, put等だとCORS対策等でハマりがちなのですが、 JSONPの場合は(開発の時点では)あまりハマることなくすんなりいけたように思います。

それでは、皆さんも楽しいAngularプログラミングを。

【intra-mart】横配置・縦配置ノードに展開されたノードIDから、展開元のノードIDを取得する方法

こんにちは。

今回は、intra-martに関する記事です。

縦配置・横配置ノード

横配置・縦配置ノードには、複数の処理対象者を設定することができます。 (横配置の場合は、必ず設定順に処理が流れ、縦配置の場合は設定した対象者が全て処理を完了した時点で次の処理に流れるということが実現できます。)

つまり、稟議書のワークフロー等、複数の承認者が必要な場合には必須となってくるノードかと思います。

ノードとステータスの紐づけの要件があったら

例えば、業務ワークフローにおいて、各ノードが終了する度に、ステータスを更新するような要件が発生したとします。

  • 0: 未申請(申請前)
  • 1: 申請OK(申請終了)
  • 2: 複数承認OK(横配置終了)
  • 3: 同期承認OK(縦配置終了)
  • 4: 申請業務終了(案件終了)

その場合、ルート定義で設定したノードIDを元に、LogicDesigner内で分岐して、 対象のノードに到達した時点でDBにステータス更新を行えば良い・・・という感じに思ってしまうのですが・・・

横配置・縦配置それぞれが実際にワークフローとして動作する際には、 ルート定義で設定したノードIDではなく、動的に割り当てられたIDがセットされる仕様になっています。

そこで今回は、下記の機能を利用し、動的に割り当てられたノードIDから、ルート定義で設定したノードIDを取得する方法についてまとめます。

利用する機能

  • IM-BIS
  • IM-FormaDesigner
  • IM-LogicDesigner
    • ユーザ定義(スクリプト)
  • IM-Workflow
    • 到達処理
  • データソース定義

IM-BIS定義を作成し、ルート定義の設定とフォーム設定を行う

ルート定義

横配置、縦配置の順にノードを配置し、それぞれのノードIDは「yoko_node」「tate_node」に設定しました。

f:id:rinne_grid2_1:20180520084741p:plain

f:id:rinne_grid2_1:20180520084800p:plain

フォーム設定

IM-BISからIM-FormaDesignerを開き、簡易的なフォームを配置します。

f:id:rinne_grid2_1:20180520085154p:plain

フォーム共有で横配置・縦配置それぞれに共有し、「定義の反映」実行

共有

  • フォームを共有

f:id:rinne_grid2_1:20180520085450p:plain

貼り付け

  • 横配置・縦配置にそれぞれフォームを貼付

f:id:rinne_grid2_1:20180520085638p:plain

定義の反映

  • 忘れてはいけない、定義の反映を実行

f:id:rinne_grid2_1:20180520085834p:plain

LogicDesignerのフロー定義を作成

入出力設定

  • 到達処理で、動的に割り当てられたノードIDを取得するため、暗黙的なパラメータをセットします
    • 入力:システム案件IDとノードIDを設定
    • 出力:メール送信可否フラグを設定(mailSendFlagはboolean)

f:id:rinne_grid2_1:20180520094556p:plain

到達処理で割当可能なパラメータについては、下記のintra-martのリンクをご参照ください。

5.4. 到達処理 — IM-Workflow プログラミングガイド   第16版 2016-08-01   intra-mart Accel Platform

定数設定

  • 到達処理では、必ずメール送信可否フラグを返却する必要があるため、今回は定数でtrueを設定しておきます

f:id:rinne_grid2_1:20180520090943p:plain

ユーザ定義(JavaScript定義)を作成します

f:id:rinne_grid2_1:20180520091156p:plain

パラメータの設定

  • 到達処理で受け取ったパラメータをJavaScript定義に渡すため、入力パラメータにシステム案件IDとノードIDを設定します

f:id:rinne_grid2_1:20180520092551p:plain

スクリプトの作成

  • JavaScriptを記述します
    • ロケールコードとシステム案件IDを利用し、未完了案件ノードを取得
    • 未完了案件ノードから、ノード設定情報を取得
    • 取得できた場合、ノード設定情報から、展開元のノードIDを取得
function run(input) {

    var systemMatterId = input.systemMatterId;
    var nodeId = input.nodeId;
    
    var actvMatterNode = new ActvMatterNode("ja", systemMatterId);
    var config = actvMatterNode.getExecNodeConfig(nodeId);
    if(config.resultFlag && config.data !== undefined) {
      var originalNodeId = config.data.expansionOriginalNodeId[0];
      Debug.console("展開ノードID:" + nodeId + " 展開元ノードID:" +originalNodeId);
    }

    return {};
}

上記LogicDesigner定義でデータソース定義を作成

f:id:rinne_grid2_1:20180520093139p:plain

f:id:rinne_grid2_1:20180520093251p:plain

f:id:rinne_grid2_1:20180520093347p:plain

コンテンツ定義で、到達処理を設定

f:id:rinne_grid2_1:20180520093522p:plain

f:id:rinne_grid2_1:20180520093727p:plain

サンプルデータの青柳さんでログインして、申請を実施

(ロール:BIS担当者を付与していないと、申請時に権限エラーで落ちるので、注意しましょう)

f:id:rinne_grid2_1:20180520100757p:plain

f:id:rinne_grid2_1:20180520100909p:plain

横配置ノードに到達し、到達処理が動く

  • ノードIDが取得できたことが確認 (e Builderのコンソールより)

f:id:rinne_grid2_1:20180520101200p:plain

これで、現在ノードIDの展開元が何なのかを知ることができました。

ただし、「動的承認ノード」でも到達処理が動いてしまうため、動的承認ノードが存在する場合は、 ルート定義のIDを用いて、別途分岐を行う必要があります。 (動的承認ノードの場合は、ルート定義のノードIDがそのまま渡されるため、分岐に利用できます)

function run(input) {

    var systemMatterId = input.systemMatterId;
    var nodeId = input.nodeId;
        var dynamicNodeList = ["douteki_node1", "douteki_node2"];

        // 到達処理で渡されたノードIDが動的承認ノードのものではない場合、横配置・縦配置として展開元IDを取得
        if(dynamicNodeList.indexOf(nodeId) < 0) {
        var actvMatterNode = new ActvMatterNode("ja", systemMatterId);
        var config = actvMatterNode.getExecNodeConfig(nodeId);
        if(config.resultFlag && config.data !== undefined) {
          var originalNodeId = config.data.expansionOriginalNodeId[0];
          Debug.console("展開ノードID:" + nodeId + " 展開元ノードID:" +originalNodeId);
        }
        } else {
            // 動的承認ノードの処理
        }
    return {};
}

以上で終わります。 皆様も素敵なintra-martライフを。

【2018年最新】【体験レポート】[審査に落ち続けてる人必見]Googleアドセンスの審査に合格したので、対応内容を時系列で詳しく書きます

こんばんは。長かったゴールデンウィークはあっという間に過ぎ去ってしまいました!

GW最終日の昨日、ようやくGoogle Adsence審査を通過することができました。

そして、申請回数は・・・計7回です!

申請前にGoogleアドセンスについて調査していた際、 「一発OKだった」や、「ほんの数時間で通過連絡が来た」という形で書いてあったブログが多く出てきたので、 「なんとか大丈夫だろう」と、少々甘く考えていたのですが、現実は厳しかったです。

そして何より、1回目の申請で通過する方々は、凄すぎる・・・!

「何で自分のブログは駄目なのだろうか」と、計6回も落ち込んだのですが、最終的には審査通過し、通過メールと一緒にやってきたポップコーンの彼(黄色背景の画像)と対面することができました。

f:id:rinne_grid2_1:20180508010549p:plain

(通過メールには、上記画像がgif形式で貼付されているので、gifアニメで動いている姿を見てびっくりしました)

さて、今日の記事の本題です。 Googleアドセンスに関して、6回も不承認連絡が来てしまい、とてもスマートとは言えず、非常に泥臭い感じがありますが・・・ 1回目の申請から通過まで間で、必死に試行錯誤を繰り返した内容を記事としてまとめました。

僕のように、なかなか審査通過ができない方や、今後Googleアドセンスの申し込みを実施される方のご参考になりましたら幸いです。

申請前のページ状態

f:id:rinne_grid2_1:20180508010234p:plain

1回目

記事に関する情報

記事数
550記事
各記事の文字数
50文字~1500文字の記事が混在

対応内容

  • はてなブログ無料会員から、有料会員(はてなブログPro)に変更(独自ドメインを設定するため)
  • 独自ドメインの設定
  • 問い合わせページ、プライバシーポリシーページの作成
  • ブログ紹介文の変更(対応後イメージ中の[1])
  • サイドメニューへの追加
    • プロフィールメニュー(対応後イメージ中の[2])
    • サイドメニューに問い合わせページとプライバシーポリシーページへのリンク(対応後イメージ中の[3])

対応後のイメージ

f:id:rinne_grid2_1:20180508010256p:plain

申請の時系列

  • 2018/03/27(火) YOUTUBEの審査基準(動画10,000再生)を満たしていないにもかかわらず、パートナープログラム経由でアドセンス申請し、広告設定。この時、いくつかの記事にアドセンスのコードを貼り付ける(そして翌日には貼付したこと自体を忘れる)
  • 2018/03/28(水) パートナープログラム経由でのアドセンスアカウントをキャンセルするため、キャンセルリクエスト送信
  • 2018/03/29(木) 改めて、通常の方法でアドセンス申請を実施
  • 2018/04/04(水) 結果連絡がなかなか来ないため、対応内容を振り返る。パートナープログラム経由で設定した広告のコードをいくつかの記事に貼付していたことに気づき、急ぎ削除。その直後、アドセンス不承認通知がメールで送られてくる

不承認メール内容(僕の場合は1回目~6回目まで全て同じ内容でした)

  • Googleからのメールを引用

Google のポリシーに準拠していないサイト: お客様のサイトは、Google AdSense のプログラム ポリシーに準拠していないか、ウェブマスター向けの品質に関するガイドラインに準拠していないため、現時点では AdSense のお申し込みを承認できませんのでご了承ください。Google の目標は、質が高く有用なコンテンツと価値あるユーザーに関連性の高い広告を配信できるサイトを広告主様に提供することです。お客様のサイトは、現在この基準を満たしていないと判断いたしました。

次のヒントを参考に、サイトでのユーザーの利便性を高め、AdSense の要件を満たすようにしてください。

• Google 広告を掲載するサイトでは、ユーザーに価値ある情報を提供することが重要です。サイトに掲載するコンテンツは、ユーザーが真っ先にアクセスしたくなるような、独自性と関連性の高いものにしてください。

• 自動生成されたページや独自のコンテンツがほとんどないページには、広告を掲載しないでください。

• サイトは操作しやすい構成にして、ユーザーの利便性を高めることが重要で、クリック操作でページを移動して、探している情報を簡単に見つけられるようにする必要があります。

2回目

不承認メール内容と、「Google Adsenceのプログラムポリシー」や「ウェブマスター向けの品質に関するガイドライン」を確認しつつ、メールにあった 「サイトは操作しやすい構成に」や「探している情報を簡単に見つけられるように」というヒントを元に、対応方針を決めました。

  • ナビゲーションメニューをPC版に追加する

記事に関する情報(1回目と同様)

記事数
550記事
各記事の文字数
50文字~1500文字の記事が混在

対応内容

  • ブログ記事のカテゴリの見直しを実施(カテゴリの付与されていない記事が多くあった)
  • 画面上部にPC用のナビゲーションメニューを追加し、ファーストビューでカテゴリごとの記事にアクセスできるようにした(対応後イメージ中の[1])

対応後のイメージ

f:id:rinne_grid2_1:20180508010314p:plain

申請の時系列

  • 2018/04/05(木) アドセンス再申請
  • 2018/04/06(金) アドセンス不承認

3回目

スマホ版については、記事の検索ボックスがページ上にあるため、ナビゲーションメニューは不要だろうと勝手に考えてしまっていたのですが・・・

ウェブマスター向けの品質に関するガイドライン」を改めて確認し

パソコン、タブレット、スマートフォンを含む、あらゆる種類やサイズの端末向けにサイトをデザインします。

という記載を読み・・・スマホ版のナビゲーションメニュー追加を決意しました。

記事に関する情報(1回目と同様)

記事数
550記事
各記事の文字数
50文字~1500文字の記事が混在

対応内容

  • Google Search Consoleとの連携を実施し、sitemap.xmlを登録
  • スマホ版のナビゲーションメニューを作成(対応イメージ中の[1])
    • Bootstrap(CSSフレームワーク)を利用(CDNからcss、jsを読み込み)
  • 記事の投稿
  • カテゴリ表示数の削減(1ページに100種類ものカテゴリを表示していた)

対応イメージ

  • スマホ版:対応前

f:id:rinne_grid2_1:20180508010521p:plain

  • スマホ版:対応後

f:id:rinne_grid2_1:20180508010530p:plain

申請の時系列

  • 2018/04/07(土)アドセンス再申請
  • 2018/04/11(水)アドセンス不承認

4回目

プライバシーポリシー等の必要ページを準備しつつ PC、スマホともにナビゲーションメニューの追加を行ったにも関わらず 不承認通知が来たため、ようやくここに来て記事内容の 見直しを行うことにしました。

記事に関する情報

記事数
300記事
各記事の文字数
大半が500文字以上

対応内容

  • 内容の薄い記事(500文字未満)や、プログラムソースが記事の大半を占めており、自分なりの解説やソースコメントを記載していない記事を「非公開」設定に変更

申請の時系列

  • 2018/04/12(木) アドセンス再申請
  • 2018/04/13(金) アドセンス不承認

5回目

さらに記事の見直しを行いました。

記事に関する情報

記事数
100記事
各記事の文字数
大半が600文字以上

対応内容

  • 内容の薄い記事(600文字未満)のものを「非公開」設定に変更
  • 過去記事(2011年頃)の記事で、体裁の整っていないものを「非公開」に設定(ex. 画像リンク切れ、レイアウト崩れ等)
  • 白背景+白文字等、クリック反転しないと視認できない文字の削除(自作ゲームの攻略記事に、行き詰まった人向けに、隠しテキストでクリア後のマップ用パスワードを記載していたのですが、念のため削除しました)

申請の時系列

  • 2018/04/14(土)アドセンス再申請
  • 2018/04/27(日)アドセンス不承認

6回目

ここまで来ると、わりと心が折れそうだったのですが・・・ 改善点を必死に探して申請しました

記事に関する情報

記事数
50記事
各記事の文字数
大半が800文字以上

対応内容

  • 読書メモ等の記事において、「引用」のみがメインとなっているものを「非公開」に設定
  • プロフィールアイコンを変更(RPGツクール用に作成されたドット絵を利用していたため。)

申請の時系列

  • 2018/04/28(月) アドセンス再申請
  • 2018/04/29(火) アドセンス不承認

7回目

記事に関する情報

記事数
25記事
各記事の文字数
800文字以上

対応内容

  • 記事のタイトルを改善([中級者向け]や、「便利」など)
  • クリックを誘導するかのように、誤解を与える文言が存在する記事があったため、削除(ex. 導入手順については下記のURLをクリックし・・・等)

申請の時系列

  • 2018/05/01(火) アドセンス再申請
  • 2018/05/06(日) アドセンス承認

まとめ

最終的に審査OKとなった理由は、断定はできませんが 下記のような対応が功を奏したのではないかと考えています。

  • プライバシー・ポリシーページ、問い合わせページの設置
  • PC、スマホへのナビゲーションメニューの追加
  • 記事タイトルの改善
  • URLクリックに関する文言の削除
  • 内容の薄い記事、極端に文字数の少ない記事の排除
  • レイアウト崩れ、画像リンク切れへの対応
  • カテゴリ表示数の削減

不承認続きで、心が折れかけましたが、諦めずに改善を続けて申請して本当に良かったと思います。

【備忘録】マイクロフレームワーク「Slim」とEloquentを使って、某小規模企画サイトのバックエンドを作ったので実装の流れを備忘録として残す

こんばんは。 GW後半が始まりましたが、皆様はいかがお過ごしでしょうか。

僕はというと、タイトル通り、某企画サイトのバックエンド側を実装しておりました。 (仕事ではなく、プライベートでの趣味の一環です。とある方から依頼を受けました。)

本番環境の制約

依頼元の本番環境のサーバーがレンタルサーバーということもあり、 利用できる言語はPHPのみで、その中でも利用可能なWebフレームワークが限られていました。

特にサーバーの制約がなければ、比較的慣れている「Django」を 利用していたと思うので、普段使わないものを使うきっかけができて、逆に良かったと思います。

今回の要件

  • ユーザの識別
    • ただしメールアドレス等の個人情報は収集しない
  • 識別したユーザごとの状態管理、ユーザ自身の操作による状態更新

要件の実現方法

  • Twitter OAuth認証の導入
  • ユーザの状態をDBテーブルで管理する
  • ユーザの状態更新のために、然るべきAPIを準備し、HTTPリクエストによって処理を実行する

今回利用した各フレームワーク、ライブラリのバージョン

  • Slim 3.1
  • illuminate/database(Eloquent ORM) 5.5
  • Twig 2.3

開発環境

  • 仮想環境のUbuntu 14.04 - gistで提供されていたVagrantfile及びprovisionシェル

Twitter OAuth認証によるユーザ識別

実装を始める前には、Twitter認証のやり方が全くわからなかったのですが、こちらの記事を参照して なんとか実装することができました。

syncer.jp

大変わかりやすい記事で、本当に助かりました。 誠にありがとうございました。

Slimの導入

ディレクトリ構成の検討

Slimを利用するにあたって、ディレクトリ構成を決める必要があります。 自分で適当に決めることもできるようです。

個人的なアプリ開発ならば適当に決めたものを利用するのですが、 一応「依頼を受けての開発」なので、素直にテンプレート(Slim-Skeleton)を利用することにしました。

github.com

Slim Skeletonプロジェクトの作成

Composerを利用することで、すばやくSlimアプリの作成ができます

$ composer create-project slim/slim-skeleton [appname]

作成後のディレクトリ構成

こんな感じのプロジェクトが作成されます。 public配下のindex.phpには各種設定ファイルの読み込みに関する処理が記述されています。

appname
├── composer.json
├── composer.lock
├── CONTRIBUTING.md
├── docker-compose.yml
├── .gitignore
├── logs/
│ └── README.md
├── phpunit.xml
├── public/
│ ├── .htaccess
│ └── index.php
├── README.md
├── src/
│ ├── dependencies.php
│ ├── middleware.php
│ ├── routes.php
│ └── settings.php
├── templates/
│ └── index.phtml
└── tests/
└── Functional/
├── BaseTestCase.php
└── HomepageTest.php

データベース接続

「Slim DB」で検索してみると、PDO(PHP Data Objects)を利用した データベース接続の解説記事が出てきました。 しかし、「DBテーブルごとに、紐づくモデルクラスを作り、SQLを書く必要がありそうで面倒くさそう」的なことや 「DB接続に利用するコネクションオブジェクトの破棄等をうっかりと忘れてしまいそう」といったことを 考えてしまい、もっと別の方法はないか探すことにしました。

すると、公式ドキュメントに「Eloquent」という文字があったのです!
Using Eloquent with Slim - Slim Framework

Eloquent ORM(オブジェクト関係マッパー)は人気のPHPフレームワーク「Laravel」のORMとして提供されている アクティブレコードによるデータ操作の実装です。

(なお、Eloquentという言葉に反応した理由は下記の記事にもあるとおり、最近Laravelに入門していたからです。) www.rinsymbol.net

フレームワーク「Laravel」を使っていなくても、 データ操作部分に関係するモジュールを導入すれば、Slimでも使えるようになるとのことでした。

Eloquent ORMの導入(と、Twigの導入)

下記のURL先の記述従い、必要な設定を追記していきます。
Using Eloquent with Slim - Slim Framework

  • Eloquentモジュールの導入

リンク先は5.1になっていますが、僕は5.5が使いたかったので、5.5を指定しました。 (5.5を指定した理由ですが、LaravelのLTS(Long Term Support)が5.5らしく、とりあえずそれに合わせといた方が良いのでは、と短絡的に考えたためです)

$ composer require illuminate/database=5.5.*
  • 必要に応じて、twigモジュールの導入

今回、ビュー側のテンプレートエンジンはtwigを利用したかったので、僕はここでtwigも導入しました。

$ composer require twig/twig
  • src/settings.phpファイルへのデータベース設定の追記

settings.phpファイルには、ログやレンダラに関する設定が連想配列で記載されています。 この中に、データベースに関する設定を追加します。('db'から始まる行~timezoneまで)

<?php
'settings' => [
    'displayErrorDetails' => true, // set to false in production
    'addContentLengthHeader' => false, // Allow the web server to send the content-length header
    'determinateRouteBeforeAppMiddleware' => false,
    // Renderer settings
    'renderer' => [
        'template_path' => __DIR__ . '/../templates/',
    ],
    // ここから↓
    'db' => [
        'driver' => 'mysql',
        'host' => 'localhost',
        'database' => 'database_name',
        'username' => 'user_name',
        'password' => 'password',
        'charset' => 'utf8',
        'collation' => 'utf8_unicode_ci',
         'prefix' => '',
        'timezone' => '-09:00'
    ], // ここまで↑
// Monolog settings
    'logger' => [
        'name' => 'appname',
        'path' => isset($_ENV['docker']) ? 'php://stdout' : __DIR__ . '/../logs/app.log',
        'level' => \Monolog\Logger::DEBUG,
    ],
  • src/dependencies.phpファイルにサービスファクトリ情報を追記する
<?php
// DIC configuration

$container = $app->getContainer();

... 中略

// ここから↓
$container['db'] = function($container) {
    $capsule = new \Illuminate\Database\Capsule\Manager;
    $capsule->addConnection($container['settings']['db']);

    $capsule->setAsGlobal();
    $capsule->bootEloquent();

    return $capsule;
};

//コントローラから、$this->container['logger']->debug()といった形で呼び出すため、インスタンス情報を取得する
$container->get('logger');
$container->get('db');

// twigモジュールを入れている場合
$container->get('view');
// ここまで↑

DBテーブルとモデルクラスの作成

DBテーブルを作成し、Eloquentの機能を利用するための、モデルクラスを作成します。

今回は、「users」テーブルと対応するモデルクラス「User」を作成しました。

  • userテーブル
create table users (
  id int(5) auto_increment primary key,
  token varchar(100) not null,
  hash_value varchar(100) not null,
  created_at datetime,
  updated_at datetime
)
  • Userクラス src/models/user.php
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;

class User extends Model {
    protected $table = 'users';
    protected $dates = ['created_at', 'updated_at'];

    public function getAttribute($key) {
        return parent::getAttribute(\snake_case($key));
    }
    public function setAttribute($key, $value){
        return parent::setAttribute(\snake_case($key), $value);
    }
}

モデルの作成については下記の記事を参考にしました。 ピンポイントで求めていた情報でした!ありがとうございました!

taka512.hatenablog.com

コントローラ作成

リクエストを処理するため、コントローラを作成します。

配置するディレクトリはどこでも良いのですが、僕はsrc/app/controllersディレクトリを作成し、 hoge_controller.phpファイルを作成しました。

  • src/app/controllers/hoge_controller.php
<?php
namespace App\Controllers;
use Psr\Container\ContainerInterface;
use Illuminate\Database\Eloquent\ModelNotFoundException;

require __DIR__ . "/../models/user.php";

class HogeController {
    protected $container;

    public function __construct(ContainerInterface $container) {
        $this->container = $container;
    }

    public function index($request, $response, $args) {
        $users = User::all();
        return $this->container['view']->render($response,
        'index.twig',
        ['users' => $users]);
    }
}

Controllerクラスを作成し、コンストラクタでコンテナ(dependencies.phpで設定したオブジェクト)を 注入(依存性注入:DI?)している感じがしますが、詳細については調べきれていません。

ルーティングの設定がまだなのですが・・・indexにアクセスした場合 ユーザの変数をindex.twigに渡すようにしています。

僕の場合、コントローラでUserモデルクラスをわざわざ読み込んでいるのですが、 おそらく、下記の記事のようにindex.phpで読み込む形式が良いのだと思います。(ブログ投稿にあたって、改めて調べていた時にこの記述を見つけました。Eloquent\Modelを継承したモデルクラスにも適用できるかどうかは試せていません。。) qiita.com

ルーティングの設定

Slim Skeletonのプロジェクトテンプレートを作成した時から存在するsrc/app/routes.phpに設定を追記し、 /indexでアクセスした時に、HogeControllerのindexメソッドが呼ばれるようにマッピングを行います

<?php

use \App\Controllers\HogeController;
$app->get('/index', HogeController::class . ':index');

HogeController::class . ":index"という書き方で、 .(ドット)と,(カンマ)を間違えているのではないかと疑問に思ったのですが、 こちらは.(ドット)であっています。

これで、/indexでアクセスした時、HogeControllerのindexメソッドが呼ばれるようになります。

ビューの設定

今度は、src配下ではなく、appname配下のtemplatesフォルダにtwigファイルを作成します (今まではすべてsrc配下のファイルを更新していました)

  • appname/templates/index.twigファイルを作成する
<!DOCTYPE html>
<html lang="ja">
<head>
<title>index</title>
</head>
<body>
{% for user in users %}
  user.id - user.token
{% endfor %}
</body>
</html>

上記の例では、/indexにアクセスすると、userのid列、token列が表示されます twigで利用できるタグやフィルタについては公式ドキュメントをご参照ください。

Documentation - Twig - The flexible, fast, and secure PHP template engine

静的ファイルの配置

JavaScriptやCSSといった静的ファイルは、src/public配下に配置すると良さそうです。

ちなみに僕は、開発時にはapache2を利用していたのですが、 下記のような構成にしていました。

Apache2の設定 /etc/apache2/sites-enabled/000-default.conf

<VirtualHost *:80>
  DocumentRoot "/var/www"
  ServerName app.dev
  ServerAlias app.dev
  <Directory "/var/www">
    AllowOverride All
    Require all granted
  </Directory>
</VirtualHost>

したがって、開発サーバーでWebアプリに接続する際には下記のような形となっていました。 http://localhost:8000/appname/public/index
(ポートフォワーディングで、ゲスト80番をホストの8000番に設定していることが前提となります)

最後に、その他感想

普段利用しないスキルセットで対応する機会がもらえたり、 TwitterのOAuth等、そもそも知見のないことに挑戦できて、すごく充実した日々でした。

【雑記】会議の段取り力が不足していることに危機感を覚えた話

簡単な仕事、難しい仕事

おそらく誰しも、ゴール地点や方針が決まっているものに対して仕事に取り組む場合、 さほど困難はなく最後までやりとげることが可能だろうと思う。

ただ、その逆で、雲をつかむような状態・・・とは言いすぎだが、 方針が定まっていない状態であったらどうだろうか。

仮にコンサルティングや管理職・経営者の立場であれば、それが日常茶飯事である可能性が高く、別段苦手意識は存在しないかもしれない。

しかし、僕の現在の立場であるエンジニアとしてシステムに関する仕事をしていく中で、 その「状態」に出くわし、苦手意識の表出と同時にスキル不足を実感してしまった。

具体的に、どんなスキルが足りなかったのだろうか? 一言で表すとするならば、段取り力だろう。

段取り力不足

こちらの役割として伝えたいことは、資料を用いて(下手なりに)説明することができた。しかし、資料説明後に沈黙が続いてしまった。

質問はないか、課題事項は何か、次回までのタスクは何か。 打ち合わせの本題ともいえる議論に繋げるための司会進行が全く機能していなかったのである。

打合せ結果としては問題なく、現状の整理と次回までのタスクを共有することができ、建設的な議論ができた。 しかしそれは、上司のフォローがあったからであるとともに、社内の人物が相手だったためである。 自分一人では到底その場を乗り切ることはできなかったと感じる。 また、社外のユーザ相手であれば、おそらく苦言を呈されていることだろう。

打ち合わせの目的は乗り切ることか?

・・・僕自身の場当たり的な思考を示すため、あえて「乗り切る」という言葉を使った。

つまりはこういうことである。 「未確定な状態からいち早く抜け出すためには、今回の打ち合わせで絶対に説明を終えなければならない」と焦り、その超短期的なタスクに気を取られすぎ、打ち合わせを乗り切ることに躍起になり、視野が狭くなってしまったのだ。

打ち合わせの目的は、資料内容を全て説明することではなく、 新たなシステムを導入する上で、各々の認識を一致させることである。

説明がわかりづらい等の指摘は誰からも出てこない。 しかし、それは打ち合わせメンバーが優しい眼差しで見守ってくれている(もしくは無関心)からであり、 振り返れば振り返るほどと言葉に詰まったり、曖昧な表現が発生したりと、至らない点が多くあったことを痛感する。

ゴール地点が決まっている場合であれば焦りは少なく、一時しのぎ的な対応にもならなかった可能性はある。 しかし段取り力が不足しているという事実にも気づくことができなかったかもしれない。

失敗を経験し、認識し、今後に活かすにはどうすべきか

この失敗を経て、このままではまずいと危機感を覚え、行動を起こすために計画を練った。

なんだ、そんな当たり前なことか、と思うかもしれないが、 打ち合わせの冒頭で、説明したい内容とその議題にかける目安時間を予め1枚のスライドで伝えることにしたのだ。

打ち合わせ中にアドリブで段取りができないなら、事前に準備するに越したことはない。

失敗を経験する前は、"説明するタスクを終わらせること"ばかりに意識が向いていたが、 相手に説明内容が伝わることに意識が向くようになった。

・・・さて、その結果はどうだったのだろうか?

実は、今日の打ち合わせが練りに練った計画を実行するタイミングだった。

時間配分が多少前後したり、言葉に詰まったりということはあったものの、打ち合わせの目的である「認識の一致」を達成することができた

なお・・・認識が一致したことで、システム化する対象が変わったのは、また別の話である。