コネヒト開発者ブログ

コネヒト開発者ブログ

swift-formatで自動コード整形

こんにちは!エンジニアの柳村です。

前回SwiftFormatの導入について紹介しましたが、今回はswift-formatについて紹介します。

ここではappleのほうのswift-formatの使い方を紹介していきます。ただこちらはSwift 5.1以上に対応しているので、Swift 5.0向けにgoogleのforkのほうのswift-formatの使い方も紹介しますがこちらはあまりオススメできません。

インストール

各プロジェクト個別にswift-formatをインストールすることを前提にします。もしグローバルにインストールしたい場合はmintもしくはNSHipsterのforkをhomebrewでインストールしてください。

プロジェクトのルートディレクトリで以下を実行してswift-formatを実行可能な状態にします。

※ 事前にxcode-selectなどでXcode11を指定してください

$ git clone git@github.com:apple/swift-format.git swift-format
$ cd swift-format
$ swift build -c release
$ cd ..

設定

以下のコマンドを実行してデフォルトのフォーマットの設定を.swift-formatに書き出します。

$ swift-format/.build/release/swift-format --mode dump-configuration > .swift-format

swift-format実行時にこの.swfit-formatに書いた設定に基づいてフォーマットされます。 デフォルトはこのようになっています。

{
  "blankLineBetweenMembers" : {
    "ignoreSingleLineProperties" : true
  },
  "indentation" : {
    "spaces" : 2
  },
  "lineBreakBeforeControlFlowKeywords" : false,
  "lineBreakBeforeEachArgument" : true,
  "lineLength" : 100,
  "maximumBlankLines" : 1,
  "respectsExistingLineBreaks" : true,
  "rules" : {
    "AllPublicDeclarationsHaveDocumentation" : true,
    "AlwaysUseLowerCamelCase" : true,
    "AmbiguousTrailingClosureOverload" : true,
    "AvoidInitializersForLiterals" : true,
    "BeginDocumentationCommentWithOneLineSummary" : true,
    "BlankLineBetweenMembers" : true,
    "CaseIndentLevelEqualsSwitch" : true,
    "DoNotUseSemicolons" : true,
    "DontRepeatTypeInStaticProperties" : true,
    "FullyIndirectEnum" : true,
    "GroupNumericLiterals" : true,
    "IdentifiersMustBeASCII" : true,
    "MultiLineTrailingCommas" : true,
    "NeverForceUnwrap" : true,
    "NeverUseForceTry" : true,
    "NeverUseImplicitlyUnwrappedOptionals" : true,
    "NoAccessLevelOnExtensionDeclaration" : true,
    "NoBlockComments" : true,
    "NoCasesWithOnlyFallthrough" : true,
    "NoEmptyAssociatedValues" : true,
    "NoEmptyTrailingClosureParentheses" : true,
    "NoLabelsInCasePatterns" : true,
    "NoLeadingUnderscores" : true,
    "NoParensAroundConditions" : true,
    "NoVoidReturnOnFunctionSignature" : true,
    "OneCasePerLine" : true,
    "OneVariableDeclarationPerLine" : true,
    "OnlyOneTrailingClosureArgument" : true,
    "OrderedImports" : true,
    "ReturnVoidInsteadOfEmptyTuple" : true,
    "UseEnumForNamespacing" : true,
    "UseLetInEveryBoundCaseVariable" : true,
    "UseOnlyUTF8" : true,
    "UseShorthandTypeNames" : true,
    "UseSingleLinePropertyGetter" : true,
    "UseSpecialEscapeSequences" : true,
    "UseSynthesizedInitializer" : true,
    "UseTripleSlashForDocumentationComments" : true,
    "ValidateDocumentationComments" : true
  },
  "tabWidth" : 8,
  "version" : 1
}

設定項目についてはdocumentに説明が書いてありますがruleについては詳細にかかれていないのでルール名から察するしかありません。

実行

以下のコマンドでコードをフォーマットできます。

$ swift-format/.build/release/swift-format -r ソースコードのフォルダのパス -i
  • -r で指定したフォルダ内を再帰的にswiftのファイルを探します。個別のファイルを指定したい場合は-rは不要です。
  • -i を指定するとswiftのファイルをフォーマットします。(これを指定しないとコンソールにフォーマット結果が出力されるだけです)

その他のオプションについては以下をご参照ください。

OVERVIEW: Format or lint Swift source code.

USAGE: swift-format [options] <filename or path> ...

OPTIONS:
  --configuration         The path to a JSON file containing the configuration of the linter/formatter.
  --in-place, -i          Overwrite the current file when formatting ('format' mode only).
  --mode, -m              The mode to run swift-format in. Either 'format', 'lint', or 'dump-configuration'.
  --recursive, -r         Recursively run on '.swift' files in any provided directories.
  --version, -v           Prints the version and exists
  --help                  Display available options

POSITIONAL ARGUMENTS:
  filenames or paths      One or more input filenames

実行してみるとわかりますが、プロジェクト全体にかけると結構遅いです。(ママリだと1分かかりました)

また、exclude directoryとかの指定ができないので3rd Partyのソースコードなどがあった場合に特定のディレクトリのみを除外するといったことができません。

これではちょっと頻繁に実行するのはちょっと厳しいです。

そこで以下のように差分のあるswiftのファイルのみをswift-fomatに食わせるようなスクリプトをprojectのbuild phasesやgitのprecommit hookに設定するといい感じになると思います。

$ git diff --name-only | grep .*\.swift$ | xargs swift-format/.build/release/swift-format -i

まとめ

Swift 5.1以降に限定されますがswift-formatでフォーマットできました。

swift-formatは2019年8月現在まだreleaseは一つもされてないのでまだこれからといった感じはしますが、ママリのアプリではXcode11およびSwift 5.1に対応するタイミングくらいに導入しようかと考えています。

------✂------✂------✂------✂------✂------

オマケ:Swift 5.0でswift-formatを使いたい場合

Swift 5.1でやる場合との違いはインストールです。

インストール

googleのswiftのforkの特定のコミットを利用します。

$ git clone git@github.com:google/swift.git swift-format
$ cd swift-format
$ git checkout 74995aa1473b213977a14c0cb477ce89875ee27b
$ git submodule update --init
$ swift build -c release
$ cd ..

あとはSwift 5.1の手順と同じです。

実行結果

ただappleのと比べると実行結果には違いがあります。(5.0で動かすために古いコミットを指定しているのが原因なのですが)

例えばRxSwiftのインデントがこんな感じになってしまいます。。 たぶん不具合でrespectsExistingLineBreaks の指定が効いていないのではないかなと思います。

-        cameraButton.rx.tap
-            .flatMapLatest { [weak self] _ in
-                return UIImagePickerController.rx.createWithParent(self) { picker in
-                    picker.sourceType = .camera
-                    picker.allowsEditing = false
-                }
-                .flatMap { $0.rx.didFinishPickingMediaWithInfo }
-                .take(1)
-            }
-            .map { info in
-                return info[.originalImage] as? UIImage
-            }
-            .bind(to: imageView.rx.image)
-            .disposed(by: disposeBag)
+        cameraButton.rx.tap.flatMapLatest { [weak self] _ in
+            return UIImagePickerController.rx.createWithParent(self) { picker in
+                picker.sourceType = .camera
+                picker.allowsEditing = false
+            }.flatMap { $0.rx.didFinishPickingMediaWithInfo }.take(1)
+        }.map { info in
+            return info[.originalImage] as? UIImage
+        }.bind(to: imageView.rx.image).disposed(by: disposeBag)

あと@unknown default@unknowndefault になってしまうバグがあってコンパイルに通らなくなってしまったりするのでお気をつけください。

コネヒトは技術コミュニティになくてはならない開発組織を目指すために「ス・マイル制度」をはじめました!

こんにちは!CTOの@itoshoです。いきなりですが、今日はコネヒトで新たに発足した「ス・マイル制度」のお披露目をしたいと思います!

tl:dr

  • ス・マイル制度という新しい取り組みをはじめました!
  • 目的は…
    • 技術コミュニティに貢献したい
    • そのためにコネヒトらしい手法でアウトプットの支援を実現したい
  • コネヒトらしい手法とは…
    • アウトプットに対して、マイルという報酬を個人ではなくチームに発生させる
    • マイルはチームの共有資産として誰でも自由に使うことが出来る
    • マイルをどう活用すれば、チームが成長するかを全員で議論する

ス・マイル制度のロゴ

ス・マイル制度とは?

一言で言うと、アウトプット支援的な制度なのですが、従来のアウトプット支援制度と違うのはアウトプットに対して、直接インセンティブを払うのではなく、一度「マイル」という形で開発部*1に貯金される点です。そして、ス・マイル制度ではその貯めたマイルを誰でも自己研鑽のために自由に使うことが出来ます。

例えば、あるエンジニアがOSSにコントリビュートをすると一定のマイルが貯まります。そうすると別のエンジニアはそのマイルを利用して、海外のカンファレンスのチケットを買うことが出来るようになります。

f:id:itosho525:20190821230230p:plain

ス・マイル制度で実現したいこと

このス・マイル制度は、僕がCTOに就任する少し前*2から構想を練り始めた制度です。コネヒトは以前からアウトプットを推奨しており、KotlinやCakePHPのコントリビューターが在籍しています。しかし、推奨はしていたものの、特に会社として目に見える形で支援していたわけではありませんでした。ですので、CTOになったタイミングで会社としてちゃんとアウトプットを支援・評価したいと思ったのが、ス・マイル制度を立ち上げたきっかけです。

また、これまでコネヒトは多くのOSSの恩恵を受けたり、他社の事例を参考にしたりすることで、サービスを成長させてこれたと言っても過言ではありません。ですので、技術コミュニティや他社に「恩返し」したいと強く思いました。そして、コネヒトが得たものを技術コミュニティに還元出来れば、それをもとに技術コミュニティが発展し、技術コミュニティが発展すれば、それにより更にコネヒトも成長するという、Win-Winのループをつくることが出来るのではないか?と考えました。

コネヒトは「人の生活になくてはならないものをつくる」というミッションを掲げています。その中で、開発組織としても技術コミュニティや他社になくてはならない開発組織をつくることがス・マイル制度のゴールになります。

f:id:itosho525:20190821230121p:plain

制度設計時に気をつけたこと

順番が逆

ス・マイル制度で意識したことはコネヒトらしさです。そのために制度は一人で考えず、メンバー全員の意見を聞くようにしました。*3その中で、当初ス・マイル制度は、アウトプットに対して直接金銭的なインセンティブを払うというシンプルなものを考えていました。しかし、何人かのメンバーから「アウトプットを支援すること自体は良いがインセンティブの順番が逆ではないか?」という意見をもらいました。

これはどういうことかと言うと、技術コミュニティに貢献するためには、何らかのアウトプットが必要になります。そして、アウトプットをするためには、学習=インプットが必要になります。しかし、(ものにもよりますが)インプットには一定お金がかかる場合があります。ですので、アウトプット後にお金があるのではなく、アウトプットする前(=インプットをしたい時)にお金があるべきではないかというわけです。

お金のモチベーションは長続きしない

また、ほとんどのメンバーから金銭的なインセンティブは大きなモチベーションにはならないという意見も貰い、確かにダニエル・ピンクのモチベーション3.0ではないですが、お金のモチベーションは長続きしないと考え、最終的に「マイル」という形で、マイルをみんなの共有資産とすることで、お互いを支え合いながら、必要な時に必要な支援が受けられるように設計しました。

更にアウトプットの価値や、組織として何に投資するべきかを全員で議論するために、アウトプットは申請制にし、そもそもアウトプットとして認められるか?認められる場合、どれぐらいの価値があるか?というのをメンバーが承認する形にしています。*4

インプットについては、他薦を認めつつ、こちらもメンバーの申請制にしており、申請すれば自由に使える純粋な福利厚生ではなく、そのインプットによって、期待される成果やアウトプットなどを約束するようにし、その使途を議論出来るようにしました。

こうして、最終的にとてもコネヒトらしい制度が出来たと自負していますが、このような制度をつくる時は、他社の制度やHowをそのまま輸入するのではなく、自分たちのカルチャーにアジャストした形にしないと上手くワークしないということを改めて学びました。

早速盛り上がっているよ!

ス・マイル制度運用を開始してから、1ヶ月弱が立ちましたが早速多くのアウトプットが申請〜承認されています。ちなみに、管理ツールは内製しているのですが、この管理ツールは先日ブログでも紹介した「社内ツールをCakePHP4でつくりました」ツールになります。

ス・マイル制度は制度をつくることがゴールではなく、運用を開始してからが本番だと思います。ですので、運用ルール自体も今後継続的に改善していく必要があると考えています。そのため、運用に関しても僕一人ではなく、コアとなる運用メンバーを中心に開発部全体で行っています。ス・マイル制度の本当の評価や価値は今後生まれてくるものだと思いますので、また時々進捗などをお知らせ出来ればと考えています。

f:id:itosho525:20190821230523p:plain

最後に

現代のソフトウェア開発がOSSや技術コミュニティと切っても切り離せない関係にある中で、企業が金銭的な見返りや打算抜きに技術コミュニティと手を取り合っていくことの重要性はこれからますます高まっていくと僕は考えています。というわけで、ス・マイル制度が、企業と技術コミュニティをお互い持続可能な形で支え合う取り組みに出来るようこれから大切に育てていきたいと思います。そして、コネヒトを技術コミュニティになくてはならない笑顔があふれる*5開発組織にしていくぞ!

*1:開発部はコネヒトでエンジニアとデザイナーが所属する組織です。

*2:CTOに就任したのは2019年の6月。

*3:多数決で決めれば良いものではないので、最終的な意思決定はCTOの僕が行っています。

*4:もちろん、最初は判断基準がないと困ると考えたので、例えば、広報立ち会いの登壇は50000マイルというようなベースとなる基準表を用意しています。

*5:ちなみに、ス・マイルのスにはスペイン語で「あなたの」という意味も込めています。

SwiftFormatで自動コード整形

こんにちは!エンジニアの柳村です。主にママリのiOSアプリの開発を担当しています。

今回はSwiftFormatの導入についてお話します

背景

コネヒトではSwiftはraywenderlichのswift style guideをコーディングのスタイルガイドとしてコードを書いています。

ですが、各開発者が全てのスタイルガイドを覚えているわけではなく、ときどきスタイルガイドからはずれていたりインデントや改行が他のコードと不一致だったりしてコードレビューで指摘したりするといったことがありました。

レビューではコーディングスタイルを気にするよりももっと他のことに注意を払ってレビューすることに力を注ぎたいのでコードフォーマッターを導入してみることにしました。

フォーマッターの選定

Swiftのコードフォーマッターはいくつか存在しています。

まずは試しにcocoapodsに対応していて導入が楽なSwiftFormatにしてみました。

なぜcocoapodsがよかったかというと、フォーマッターのインストールはhomebrewなどを使って各自の環境にインストールするという手段もありますが、複数人で開発を行う場合に各自のインストールしているバージョンに違いが発生する恐れがあります。cocoapodsにしておけばインストールされるバージョンが固定されるので意図しないバージョンが使われてしまうといった恐れがなくなるメリットがあるからです。

設定

cocoapodsでインストールして、Debug Configurationでビルドした際に自動でフォーマットするように設定していきます。

Podfile

   pod 'SwiftFormat/CLI', :configurations => ['Debug']

Project

XcodeのTarget->Build Phasesに以下を追加します

if [ $CONFIGURATION = "Debug" ]; then
"${PODS_ROOT}/SwiftFormat/CommandLineTool/swiftformat" .
fi

.swift

.swift-versionがなければ作成しSwiftのバージョンを記載します。

5.0

.swiftformat

.swiftformatにルールを記載することでデフォルトのルールを変更することができます。

デフォルトのルールのままだとdiffがすごいことになったので以下の一部のデフォルトのルールをdisableにしました。

--disable trailingCommas, strongOutlets, unusedArguments, hoistPatternLet, blankLinesAroundMark

おわりに

このように導入はさっくりとできてしまうので、気軽に試せるのでおすすめです!

次回はswift-formatの使い方について紹介したいと思います、お楽しみに!

機械学習と人が協力してママリのコミュニティを支えているよ、という話をしました

f:id:taxa_program:20190809124049p:plain こんにちは!MLエンジニアの野澤(@takapy0210)です。

今更ですが東京喰種トーキョーグールというアニメを最近見始めました。
内容はもちろん面白いのですが、OPの歌い出し「教えて 教えてよ 〜」部分の声質がとても印象的で、どうにか真似できないかと練習しているところです。

さて、今回はこちらの勉強会でもLTさせていただいた「機械学習と人が協力してコミュニティを支えているよ」という話をしようと思います。

目次

LT資料

下記が資料です。

speakerdeck.com

以降、スライド内容を抜粋しながら進めていきます。

ママリ内での課題

ママリとは、全国のママがお互いに悩みを相談し合うことのできるQ&Aサービスです。
ユーザの熱量がとても高く、運営としてもコミュニティの雰囲気をとても大切にしています。

真摯な悩みを投稿するユーザが多くいる一方で、下記のように不適切な投稿をする人がいるのも、また事実です。

f:id:taxa_program:20190809124215p:plain
不適切な投稿例

このような中でコミュニティを健全な状態に保つためには、不適切な投稿は運営側で検知し、然るべき対応を行う必要があります。しかし、1日の投稿数が数千件を超える中で、全てを人間が目視チェックするのは現実的ではありません。

そこでママリでは、不適切な投稿を検閲するために機械学習を用いています。

f:id:taxa_program:20190809124754p:plain
ママリでの機械学習活用事例

機械学習と人が協力しているってどういうこと?

現在の検閲フローでは、機械学習モデル(以降、モデル)が「不適切な確率50%以上」と推論した投稿に関して、人間が目視チェックするようになっています。 f:id:taxa_program:20190809125736p:plain

なぜ、モデルの推論結果のみで処理せず、最終的に人間がチェックする必要があるのでしょうか。

今回の例に出したような「簡単に稼げる方法教えますよ」等の質問であれば、わざわざ人間がチェックする必要もないと思うのですが、例えば下記のような質問はどうでしょう。

f:id:taxa_program:20190809130101p:plain

この質問では「簡単に稼げる方法教えますよ」という意味の一文が入っているものの、実際は不適切な投稿ではなく、ユーザが抱えている悩みであることが見て取れると思います。

このような投稿をモデルで推論したときに、「誰でも簡単に稼げます!安心安全!今なら初期費用ゼロです!⭐未経験者歓迎⭐」という部分の影響で、不適切な投稿として判断されてしまう可能性があります。

f:id:taxa_program:20190809150943p:plain

そのため、冒頭でも述べた通り「不適切な確率50%以上と推論された投稿については、人間の目視チェックを通して対応する」というフローになっているわけです。

こうすることで、50%未満と推論された投稿については、人間の目視チェックが不要となり(=コスト削減)、機械学習が苦手とする微妙なニュアンスをもった投稿の対応に人間が注力できるようになります。

そして上記のように、機械学習と人それぞれの良いところを生かし合うことで、コストを削減しつつ、コミュニティを健全な状態に保つことが可能になっている、というわけです。

モデルについて

現在運用しているモデルについても少し触れたいと思います。

ママリ内には多くのテキストデータが存在しており、そのデータをコーパスとしてgensimを用いて単語の分散表現を計算しています。

f:id:taxa_program:20190809161150p:plain
単語の分散表現をgensim.word2vecを使用して計算

下記に、gensim.word2vecを用いて言語モデルを作成するコード例を載せておきます。
word2vecのオプションについてはこちらに分かりやすく掲載されているので、気になる方は見てみてください。

from gensim.models import word2vec
import multiprocessing

cpu_count = multiprocessing.cpu_count()  # CPUのコア数を取得する
corpus = word2vec.LineSentence('corpus.txt') # コーパスの読み込み

# 学習
model = word2vec.Word2Vec(corpus, 
                          size=200, 
                          window=5, 
                          hs=1, 
                          min_count=5, 
                          sg=1, 
                          iter=5,
                          workers=cpu_count
                         )

# モデルの保存
model.save('w2v.model')

この言語モデルは、例えば、単語の類似度を測ることが可能だったりします。
試しに、ママリのコーパスで学習させたモデルに対して、「夫」の類似単語TOP10を出力してみます。

# 夫 と類似している単語
model.wv.most_similar(positive=['夫'])

>>>
 ('主人', 0.9211037158966064),
 ('旦那', 0.8511199951171875),
 ('旦那さん', 0.7135452032089233),
 ('ダンナ', 0.6880300641059875),
 ('私', 0.6779747009277344),
 ('妻', 0.642198920249939),
 ('実父', 0.6189703941345215),
 ('わたし', 0.6182470321655273),
 ('父', 0.6118142604827881),
 ('お互い', 0.5914695262908936)

「お互い」という単語が出てくるあたり、コミュニティサービスから作ったモデルならではに感じますね。

そして、上記で得られた単語の分散表現をネットワークの入力として、双方向のLSTMを用いてモデルを構築しています。

f:id:taxa_program:20190809161511p:plain
ニューラルネットワークの概要

下記は実際にモデル構築に使ったコードの一部です。
ネットワーク自体はシンプルな構成になっており、出力層の活性化関数にsigmoidを用いることで確率を出力しています。
また、emb_matrixに上記で学習させた単語の分散表現を渡すことで、ネットワークのEmbedding層に初期設定しています。

# モデルの一部
def build_model(emb_matrix: np.ndarray, input_length: int):
    model = Sequential()
    model.add(Embedding(
        input_dim=emb_matrix.shape[0],
        output_dim=emb_matrix.shape[1],
        input_length=input_length,
        weights=[emb_matrix],
        trainable=False))
    model.add(Bidirectional(LSTM(64, recurrent_dropout=0.15)))
    model.add(Dropout(0.25))
    model.add(Dense(64))
    model.add(Dropout(0.3))
    model.add(Dense(1, activation='sigmoid'))
    return model

今後取り組んでいくこと

先にも述べた通り、現在ママリでは1日に数千件の質問が投稿され、学習に使えるデータは日に日に増加しています。
このような場合、最新のデータでモデルを定期的に更新することは、モデルの精度を維持・向上させるためにとても重要だと考えています。

しかし現在は、このモデル更新を全て手動で実施しているため、「機械学習基盤イケてるでしょ?」とは言い辛い状態です。(とはいえ、全てAWSのサービス内で完結しているため、手順フローを踏襲すれば誰でも/いつでも更新できる状態ではあります)

今後はこの手順を自動化すべく、AWSの各種サービスを組み合わせながら、自動更新フローを構築していく予定です。

最後に

今回はママリでの機械学習活用事例についてお話しました。

機械学習は、解決したい課題によってはとてもインパクトのあるものになり得ますが、課題の種類によっては、機械学習をフル活用するよりも、人間と機械学習お互いの長所を活かし合うことが大切だと考えています。

今後も、機械学習を使うことを目的とするのではなく、あくまでも課題解決の手段として活かせる部分には存分に活かしサービスグロースに貢献していければと思っています!

最後の最後に(告知)

すみません、最後にちょっと告知させてください...!

今月末に開催されるAWSの下記イベントで登壇する予定です。
当日はコミュニティサービスにおいて、どのようにAWS×自然言語処理を活用しているかというお話する予定です。
ご興味のある方いましたら、ご参加お待ちしております!
ml-loft.connpass.com

また、すでに満席となってしまっていますが、下記イベントでもLTする予定ですので、参加者の皆さまよろしくお願いします!
globis.connpass.com

社内ツールをCakePHP4でつくりました

f:id:itosho525:20170430222524j:plain

こんにちは。CTOの@itoshoです。

夏の甲子園が始まっていますね。ランチの時間はSPORTS BULLさんのアプリでバーチャル高校野球を観ています。 というわけで、フレッシュな高校球児に負けないようにフレッシュなCakePHP4で社内ツールをつくった話をしたいと思います。

なぜ、CakePHP4でつくったの?

現在、コネヒトのバックエンドのシステムはCakePHP3系が主流になっています。サービスが拡大していく中で今後他の言語やフレームワークを導入していく可能性はありますが、社内にPHP及びCakePHPのナレッジが蓄積されているので、当面CakePHPがコネヒトの技術スタックの主軸の一つであることは変わらないと考えています。そうすると、CakePHP3からCakePHP4へアップグレードしていくのは当然の流れですので、早めにCakePHP4を触っておきたいと思いました。

しかし、CakePHP4はまだstable版がリリースされておらず、4.0.0 beta1が最新のバージョンになります。*1ですので、もちろんプロダクション環境への導入はリスクがあります。しかし、社内ツールであればある程度そのリスクを許容することが出来ますし、むしろ、バグを踏めばCakePHPにコントリビュート出来るチャンスがあると考え、今回CakePHP4を採用することにしました。

どういうツールをつくったの?

この社内ツールについては、また別の機会に詳しく紹介したいと思いますがつくったものは以下のようなシンプルなwebアプリケーションです。

  • 画面数: 12
  • テーブル数: 5
  • 機能のほとんどがシンプルなCRUD処理

隙間時間で開発していたので完成するまで1ヶ月弱くらいかかりましたが、ガッツリ時間を取ることが出来れば2〜3日で完成出来るボリュームかなと思います。

CakePHP4どう?

上述の通り、ほとんどの機能がbakeの自動生成してくれるコードに毛が生えたレベルで、ガッツリ使い倒したわけではありません。ですので、今後また別の印象を持つかもしれませんが、一つのwebアプリケーションをつくった印象としては「CakePHP3に慣れていれば、すんなり入っていける」と感じました。

そして、CakePHP4.0のロードマップを読むと、冒頭に以下のような記述があります。

CakePHP 4.0 will be a breaking change from 3.x. Unlike 3.0, 4.0 will primarily be a clean-up release. Instead of introducing large backwards incompatible changes, 4.0.0 will focus on removing all the deprecated features we've accumulated throughout the life of 3.x. Any method/feature emitting run-time warnings in 3.6 will be removed from 4.0. 4.0 will not add significant new features as the bulk of the release will be spent on cleanup efforts.

要約すると、4.0は2系から3系の時のような破壊的な変更をせず、非推奨機能の削除を中心に行い、後方互換性のない新機能は次の4.1で行うという方針になっています。つまり、4.0は4系にソフトランディングするためのリリースになっているので「すんなり入っていける」という感想を持つことが出来たというわけです。*2

その上で「おっ!」と思ったところを簡単に紹介します。

  • テンプレートファイルがctpファイルからphpファイルになった
    • おそらく一番最初に気付くのがこの変更ではないでしょうか。拡張子が変わっただけかもしれませんが、PHPStorm使いとしては特別な設定をせずに、PHPファイルとして扱えるようになりました。
  • デフォルトレイアウトにMilligramが採用された
    • Milligramは何度か使ったことがありますが、軽いし、デザイン性も高いし、カスタマイズもしやすいので個人的には嬉しい変更でした。
  • リソースのルーティングのルールが一部変更になった
    • 複数の単語からなる、例えば TodoItemsController を作成した場合、これまでは todo_items というルーティングでしたが todo-items というルーティングになりました。なお、これまで通りアンダースコア形式にしたい場合はオプションで 'inflect' => 'underscore' を指定します。
  • (bakeでコードの自動生成をすると)戻り値の型を宣言するようになった
    • 個人的にはこれが一番嬉しいなと思いました!別に元から書こうと思えば書けたのですが、これまでCakePHP本体が戻り値の型宣言をしていなかったので、それに追従する形でコネヒトではアプリケーションコード側も書いていなかったのですが、CakePHP4からは気にすることなく戻り値の型も宣言出来ることになりそうです。
  • (bakeでコードの自動生成をすると)strictモードが指定されるようになった
    • これもよりタイプセーフなコードを実現するための一環ですね。strictモードの挙動については以前Qiitaにまとめた記事があるので、もし興味があれば併せてご覧ください。
  • newEmptyEntity() メソッドの登場
    • これまで新しくEntityを作成する時は newEntity() メソッドを利用していたのですが、bakeで自動生成されるコードをみると今後は newEmptyEntity() メソッドを利用するほうがベターのようです。文字通り空のEntityを作成するメソッドなのですが newEmptyEntity() メソッドとの違いはEntity生成時にバリデーションを行うか行わないかの違いがあります(newEmptyEntity() メソッドはバリデーションを行わない)。

いますぐCakePHP4を使うには?

ここまで読んで、今すぐ私もCakePHP4を使ってみたい!と思ってくれた方は、以下のコマンドでプロジェクトの作成が可能です。ちなみに、CakePHP4からPHP7.2以上が必要条件になっていますのでご注意ください。

$ composer create-project --prefer-dist cakephp/app:4.x-dev app

冒頭に述べましたが、CakePHP3に慣れていれば、すんなり入っていけますし、上記に挙げたようなよりよい改善も含まれているのでstable版が出たら、プロダクションコードもなるべく早めにアップグレードしていきたい!という気持ちを新たにしました。また、これはCakePHP4に限った話ではないですが、やはりちょっとしたプロトタイプ的なwebアプリケーションをつくりたい時に非常に低コストで出来る点は改めて最高だなと思いました。*3

というわけで、今後もCakePHPと共に「人の生活になくてはならないもの」をつくっていくぞ〜!一緒につくってくれる仲間も絶賛大募集中です!

参考サイト

*1:2019年8月13日現在

*2:言うは易く行うは難しだと思うので、これを実現してくれているCakePHPコアチームの皆さんは本当に凄いと思います。

*3:実は当初Go×ReactでSPAっぽくつくろうと思ったのですが、つくりたいものに対して若干オーバーエンジニアリング感があったので、辞めたという経緯があります。

iOS版ママリの開発環境をXcode10.2/Swift5にアップデートしました

はじめまして、2019年5月に入社したiOSアプリエンジニアのあぼ(@suxisuxido)です。入社後は『既存チーム』と呼ばれる、ママリアプリの既存機能の改良などを行うチームで、iOS側の開発を担当しています。

コネヒトでは先日、iOSの開発環境をXcode10.1/Swift4.2から、Xcode10.2/Swift5にアップデートしました。今回はそのことについて書こうと思います。

ライブラリのAPI変更への対応

各種ライブラリのバージョンアップに伴い、コードの変更が必要です。ワーニング解消も含めると、RxSwiftまわりの変更が一番多かったです。

RxSwift.VariableをRxRelay.BehaviorRelayに変更

便利なVariableはRxSwift4.0.0-rc.0からdeprecatedになっています。今回のタイミングで、同様の使い方ができるRxRelayのBehaviorRelayに変更しました。

- import RxSwift
+ import RxRelay

- let isConnecting = Variable(false)
+ let isConnecting = BehaviorRelay(value: false)

- isConnecting.value = true
+ isConnecting.accept(true)

https://github.com/ReactiveX/RxSwift/releases/tag/4.0.0-rc.0

throttleやdelayオペレーターの引数をTimeInterval からDispatchTimeIntervalに変更

こちらはRxSwift5からdeprecatedになっています。もともとDouble型で計算した結果を使っていた場合はロジックの変更が必要ですね。

- throttle(1.2)
+ throttle(.milliseconds(1200))

- throttle(3)
+ throttle(.seconds(3))

https://github.com/ReactiveX/RxSwift/releases/tag/5.0.0

Swift5の新機能Raw stringsの利用

Swift5の機能のひとつ、Raw stringsを使って、正規表現から"\"のエスケープを削除しました。

- NSRegularExpression(pattern: "^(\\d+)歳")
+ NSRegularExpression(pattern: #"^(\d+)歳"#)

正規表現はどうしてもバックスラッシュが多くなりがちなので、積極的にRaw stringsを使ってスマートに書いていきたいですね!

https://github.com/apple/swift-evolution/blob/master/proposals/0200-raw-string-escaping.md

Xcode10.2ではiOS12.2のデバッグが可能に

じつはこのとき、いくつかの検証端末が意図せずiOS12.2に上がってしまっていたため、Xcode10.1のままではデバッグができず不便でした...。

Xcode10.2にあげたことによってiOS12.2のデバッグが可能になり、弊iOSチームは救われました(´・∀・`)

https://developer.apple.com/documentation/xcode_release_notes/xcode_10_2_release_notes

モンキーテストの実施

一通り対応が終わったら、既存チームでモンキーテストを行いました。

Xcode10.2/Swift5対応がアプリへもたらす影響範囲は大きく、かつ私自身はまだママリアプリの機能について知識が少なくデグレにも気づきづらいので、知識ある人に触ってもらって怪しいところを事前に拾うのが狙いです。

結果的にこのモンキーテストで1つデグレが見つかったので、やっておいてよかったな〜と思っています。

f:id:aboy_perry:20190722160533p:plain
モンキーテストご協力のお願いのようす

おわりに

影響範囲が大きく、気を張って行う必要があるという意味ではそこそこ大変でした。このほかにも、単体テストで保証しやすいのに実際にはテストが書かれていなかった箇所に、単体テストを追加したりしました。

こういう開発環境アップデートは遅くなればなるほどあとからやるのが大変になったりしますし、そのあいだ新機能が使えないままなので、タイミングをみて今回実施できてほんとによかったです!

CakePHP2用のMaster/Replica接続管理プラグインをOSS化しました

こんにちは、サーバーサイドやっております 金城 (@o0h_)です。
「腸よ鼻よ」を応援しています。人間ってたくましい。

腸よ鼻よ(1)

腸よ鼻よ(1)

webでも読めます https://ganma.jp/chohana

さて、掲題のとおりですが、以前にママリのマスプロモーションを実施した際の負荷対策として作成した機構をプラグインとして公開しました。

packagist.org

MySQL用のデータソースで、1つのモデル*1に対して複数のデータベース接続を提供する機構です。 例えば「参照しか走らないリクエストは参照用DBに接続して、更新系はマスターDBに接続する」といったような使い方を想定しています。

簡単に利用方法を紹介します。

  1. Pluginを配置する
  2. 「複数の接続先」をconfig(database.php)に書き込む
  3. (Controller等で)必要に応じて接続先情報を変更するメソッドを実行する

Pluginの配置

Composerによるinstallに対応しています。
これを、plugins もしくは app/Plugin に配置してください。 詳細はCookBookを参考にしてください

https://book.cakephp.org/2.0/ja/plugins/how-to-install-plugins.html#composer また、Pluginを配置した後、プラグインをloadするのをお忘れなく。

接続情報の設定

DATABASE_CONFIGの設定を変更します。 例として、書き込み対応の「master」参照専用の「secondary」という、2つのDB接続を利用するケースを想定します。

  • masterは host:master-db-host; port: 3306; login: root; pass: password
  • secondaryは host:secondary-db-host; port: 3306; login: root; pass: password
  • DB名はどちらも my_app
  • デフォルトでは secondary に接続を向ける

その際には、以下のような記述になります。

<?php
class DATABASE_CONFIG {
    public $default = array(
        'datasource' => 'MasterReplica.Database/MasterReplicaMysql',
        'connection_role' => 'secondary',
        'connections' => array(
            '_common_' => array(
                'login' => 'root',
                'pass' => 'password',
                'database' => 'my_app',
            ),
            'master' => array(
                'host' => 'master-db-host',
            ),
            'secondary' => array(
                'host' => 'secondary-db-host',
            ),
        ),
    );
}
  • まず、datasourceを MasterReplica.Database/MasterReplicaMysql に変更します。
    app/Plugin/MasterReplica にプラグインを配置している想定です
  • connection_role に、「デフォルトでどこに接続させるか」という名前を指定します
  • connections という配列で、「接続名 = key」として、それぞれの接続情報を設定します
    • _common_ は特別なkeyで、全接続先に対する共通設定値をもたせます。今回は login, pass, databaseが共通です
    • master, secondaryに対して「個別に設定したい情報」をもたせます。この内容が、 _common_ と結合されて利用されます
    • なお、接続名(master, secondary)には任意の名前を利用することが可能です

これで接続情報のセットアップができました。

実際に接続先情報を変更する

Datasource経由で、「接続先変更」メソッドを実行することで、接続先の変更ができます。 switchConnectionRole($接続先名) を利用してください。

例えば、以下のコードは実際にプラグインのテストコードとして利用しているスニペットをベースにしたものです。 cf: MasterReplicaMysqlIntegrationTest.php#L78-L95

<?php

$conn = $this->Post->getDataSource();

// masterに向けて書き込みを行う
$conn->switchConnectionRole('master');
$newData = [
    'user_id' => 10,
    'title' => 'Lorem ipsum dolor sit amet',
    'content' => 'Lorem ipsum dolor sit amet',
];
$this->Post->save($newData);

// secondaryに向ける
$conn->switchConnectionRole('replica');

$savedData = $this->Post->read();

実用例①

「ログインしているユーザーはmasterに向ける(データの更新が可能)」という場合は、AppController::beforeFilter()で操作するというのも手です。

<?php

// ログイン処理後
$role = AuthComponent::user() ? 'master' : 'replica';
MasterReplicaMysql::setDefaultConnectionRole($role);

実用例②

IntegrationTest実行時に副作用として「接続先が切り替わったまま」になるので注意してください。
具体的に言うと、「参照DBに向けたままだとtableの作成やtruncateに失敗する」ので、setUp()/tearDown()ができません。

ControllerTestCaseを継承したサブクラスを用意し、 testAction() メソッドを上書きすることで対応が可能です。

<?php

protected function _testAction($url, $options = []) {
        $sourceList = ConnectionManager::sourceList();
        foreach ($sourceList as $source) {
            $obj = ConnectionManager::getDataSource($source);
            if (get_class($obj) !== 'MasterReplicaMysql') {
                continue;
            }
            if (in_array($obj, $this->masterReplicaSources, true)) {
                continue;
            }
            $this->masterReplicaSources[] = $obj;
            // DB再接続時にテーブル情報のキャッシュが副作用をもたらすケースがあるので
            // キャッシュ利用フラグを折っておく
            $obj->cacheSources = false;
        }

        try {
            $result = parent::_testAction($url, $options);
        } finally {
            // _testAction内での副作用を消すため、
            // 初期接続先をmasterにした上でDB接続を初期化する
            if ($this->masterReplicaSources) {
                MasterReplicaMysql::setDefaultConnectionRole('master');
                foreach ($this->masterReplicaSources as $obj) {
                    $obj->switchConnectionRole('master');
                }
            }
        }

        return $result;
}

CakePHP2のプラグイン作成にあたって

FriendsOfCake/travis が便利で、活用させていただきました。

github.com 一部、(Cake3がメインストリームということで・・)工夫する必要がある箇所もありますが、それでも.travis.ymlをかなり簡潔に保てて利便性が高いと思います。

詳しい内容は 実際のYAMLファイルを御覧ください。

おわり!

かなり足早にではありますが、簡単なご紹介をさせていただきました。
PRやIssueをお待ちしております!!!!💪💪💪 簡単な質問等であれば、お気軽にTwitterでリプ飛ばしてください。

コネヒトではCakePHP3も同様の機構を利用しており、こちらも現在OSS化作業を進めているところです。
近日中に披露できれば、と思います。

*1:databaseのconnection定義