[{"data":1,"prerenderedAt":6146},["ShallowReactive",2],{"articles-page-3":3},{"count":4,"content":5},63,[6,1159,1273,1422,1580,3292,4378,4695,5176,5649],{"id":7,"title":8,"body":9,"category":1144,"createdAt":1146,"description":1147,"extension":1148,"index":1149,"meta":1150,"navigation":1151,"path":1152,"publish":1151,"seo":1153,"series":1149,"seriesTitle":1149,"stem":1154,"tag":1155,"thumbnail":1157,"updatedAt":1149,"__hash__":1158},"articles\u002Farticles\u002Fprivate-develop-released.md","リリースまで進み続けるんだ！これはお前が始めた個人開発だろ？",{"type":10,"value":11,"toc":1099},"minimark",[12,24,38,44,47,50,54,57,60,79,82,93,96,99,103,107,115,118,121,138,141,144,147,151,165,173,176,179,190,193,196,213,216,219,222,225,228,314,318,324,329,332,335,338,349,352,361,372,375,379,382,385,388,392,395,398,401,415,418,421,424,438,441,455,458,472,475,478,481,484,507,515,538,541,544],[13,14,15,16,23],"p",{},"この記事は",[17,18,22],"a",{"href":19,"rel":20},"https:\u002F\u002Fqiita.com\u002Fadvent-calendar\u002F2022\u002Findividual-developers",[21],"nofollow","Qiita 個人開発 Advent Calendar 2022の12\u002F11の記事","になります。",[13,25,26,27,32,33],{},"こんにちはjunです。2022年の11月に",[17,28,31],{"href":29,"rel":30},"https:\u002F\u002Froute-share.net",[21],"RouteShare","というユーザー投稿型webサービスをリリースしました。地図に情報を入力し、ワードプレスの様に記事を作成し、いろんな道や場所の魅力を投稿できます。ユーザーが自由に地図に情報を入力して地図を見ながら記事を読めること、地図（地理情報）を中心としたコンテンツです。ツーリスト、サイクリスト、ライダーなど、いろんな場所に旅行したり地理情報をまとめる人向けになっています。ぜひご興味ありましたらご利用ください。メールアドレスかGoogleアカウントで登録が可能です。",[17,34,37],{"href":35,"rel":36},"https:\u002F\u002Froute-share.net\u002Fabout",[21],"RouteShareについてより知りたい方はこちら",[39,40],"image-render",{":src":41,":width":42,":center":43,":current":43},"'large_logo.png'","'300px'","true",[13,45,46],{},"さて、宣伝はここまでにしておきます。このサービスは企画、設計、デザイン、実装、デプロイ、支払い全てに至って自分で行った、個人開発プロダクトです。途中、友人にコンセプトや使い心地のレビューはもらいましたが基本的には一人で開発しました。開発開始から９ヶ月で作り上げたサービスで、これからは運営や改善を繋げながら収益化を目指そうと思っています。",[13,48,49],{},"今回の記事ではリリースに至るまでの、企画、設計、デザイン、実装、デプロイ、支払いなど実務的な内容を書こうと思います。結構長くなると思いますが、個人開発をしてみたい人の参考になればと思います。適宜、好きなセクションに飛ばしてみてみてください。",[51,52,53],"h2",{"id":53},"ここでいいう個人開発とサービス種別について",[13,55,56],{},"人によっては「個人開発」の意味合いやどこまでやるかという感覚が異なると思いますので、あらかじめここで解説する「個人開発」というものを定義しておきます。",[13,58,59],{},"ここでいう個人開発は",[61,62,63,67,70,73,76],"ul",{},[64,65,66],"li",{},"公開され、第三者が利用することができる。",[64,68,69],{},"誰かの役に立つ、得になるもの。",[64,71,72],{},"最終的に事業として収益化を目指すもの。",[64,74,75],{},"一人または少人数で法人格がない人がつくるもの、開発に関して給与が発生しない。",[64,77,78],{},"会社に勤務していたり、学生だったり何かしらの別の所属を持っている中、個人的に開発するもの",[13,80,81],{},"としておきます。そのため",[61,83,84,87,90],{},[64,85,86],{},"収益化を目的としない、趣味的・実験的な内容",[64,88,89],{},"オープンソース開発、活動",[64,91,92],{},"法人格を持って運営する。大人数で金の力でなんとかする",[13,94,95],{},"といったことではないとしておきます。まあ「法人格を持って運営する」あたりは「事業として収益化を目指すもの」とぶつかったりするので、あくまで「こんぐらいのレベル感なんだなー」と思ってください。",[13,97,98],{},"また、作成するサービス種別はwebサービスとしておきます。ただしある程度のセクションはネイティブ開発など他種別の個人開発にも応用できると思います。",[51,100,102],{"id":101},"作成した個人開発の開発技術的概要","作成した個人開発の開発・技術的概要",[104,105,106],"h3",{"id":106},"開発概要",[61,108,109,112],{},[64,110,111],{},"作成期間:9ヶ月",[64,113,114],{},"実作成人月:3人月ぐらい？",[13,116,117],{},"2022年の2月から同年11月にリリースしました。退勤後や土日を使用して作成したので、期間は9ヶ月かかりました。アプリケーションの構成自体はそれほど複雑でないので、実際は3人月ぐらいでできそうな規模です。",[13,119,120],{},"内訳としては",[61,122,123,126,129,132,135],{},[64,124,125],{},"2~3月: 設計と企画、技術検証",[64,127,128],{},"3~4.5月: ボイラーテンプレート的な開発",[64,130,131],{},"4.5~8月: 基幹機能開発",[64,133,134],{},"9~10月: レスポンシブや細かい機能、調整",[64,136,137],{},"11月: 最終確認、デプロイ作業",[13,139,140],{},"という感じです。構想自体は結構前から練っていたのでそれほど時間はとりませんでした。",[104,142,143],{"id":143},"技術概要",[13,145,146],{},"公開サービスなのでセキュリティ上可能限りで紹介します。",[148,149,150],"h4",{"id":150},"フロントエンド",[61,152,153,156,159,162],{},[64,154,155],{},"Nuxt.js（SSR）",[64,157,158],{},"Google Map API",[64,160,161],{},"Editor.js",[64,163,164],{},"Bootstrap-vue",[13,166,167,168,172],{},"フロントエンドは得意なNuxt.jsを使用して構築しました。基本的なUIはNuxt.jsにて記述し、Google Map を用いた地図操作・レンダリングは別途、JSでクラスを作成しNuxtと連携しました。また、記事の作成の際には",[17,169,161],{"href":170,"rel":171},"https:\u002F\u002Feditorjs.io\u002F",[21],"というブロックベースのエディタを採用しました。",[13,174,175],{},"デザインは正直得意ではないので、デザインの４原則とブランドカラーを決めてBootstrapをカスタマイズして実装しました。またモーダルなどUIを作るのが面倒だったという理由もあります。",[148,177,178],{"id":178},"バックエンド",[61,180,181,184,187],{},[64,182,183],{},"Laravel（フレームワーク）",[64,185,186],{},"mysql（DB）",[64,188,189],{},"apache（Webサーバ）",[13,191,192],{},"バックエンドは得意なLaravelを使用しました。",[148,194,195],{"id":195},"その他",[61,197,198,201,204,207,210],{},[64,199,200],{},"Git",[64,202,203],{},"Google App engine (フロントエンドサーバ)",[64,205,206],{},"Google Domains",[64,208,209],{},"Github actions",[64,211,212],{},"Docker （仮想化）",[13,214,215],{},"フロントではNode.jsを動かせるGoogle App engineを使用しています。理由は後述します。",[13,217,218],{},"以降のセッションでは開発に関する細かい内容を解説していきます。",[51,220,221],{"id":221},"詳細なリリースまでの流れ",[104,223,224],{"id":224},"とりあえずこの記事で伝えたいこと",[13,226,227],{},"結構長くなるので、重要なことを箇条書きしました。それぞれアンカーを張っていますので気になるとこを見てみてください。",[61,229,230,236,242,248,254,260,266,272,278,284,290,296,302,308],{},[64,231,232],{},[17,233,235],{"href":234},"#%E3%81%BE%E3%81%9A%E3%81%AF%E8%87%AA%E5%88%86%E3%81%8C%E3%81%82%E3%82%8B%E3%81%A8%E3%81%84%E3%81%84%E3%81%AA%E3%81%A8%E6%80%9D%E3%81%A3%E3%81%9F%E3%81%93%E3%81%A8%E3%82%92%E5%A4%A7%E5%88%87%E3%81%AB","まずは自分があるといいなと思ったことを大切に",[64,237,238],{},[17,239,241],{"href":240},"#%E4%BC%81%E7%94%BB%E6%99%82%E3%81%AF%E3%82%A2%E3%82%B8%E3%83%A3%E3%82%A4%E3%83%AB%E3%82%B5%E3%83%A0%E3%83%A9%E3%82%A4%E3%82%92%E8%AA%AD%E3%82%82%E3%81%86","企画時はアジャイルサムライを読もう",[64,243,244],{},[17,245,247],{"href":246},"#%E7%AB%B6%E5%90%88%E3%82%92%E8%AA%BF%E3%81%B9%E3%81%A6%E9%9C%80%E8%A6%81%E3%82%92%E8%AA%BF%E3%81%B9%E3%82%8B","競合を調べて需要を調べる",[64,249,250],{},[17,251,253],{"href":252},"#%E3%81%97%E3%81%A3%E3%81%8B%E3%82%8A%E5%85%B7%E7%8F%BE%E5%8C%96%E3%81%99%E3%82%8B","しっかり具現化する",[64,255,256],{},[17,257,259],{"href":258},"#%E3%82%84%E3%82%89%E3%81%AA%E3%81%84%E3%81%93%E3%81%A8%E3%82%92%E6%B1%BA%E3%82%81%E3%82%8B%E3%81%AE%E3%81%AF%E9%87%8D%E8%A6%81","やらないことを決めるのは重要",[64,261,262],{},[17,263,265],{"href":264},"#%E5%8F%8E%E7%9B%8A%E5%8C%96%E3%81%AF%E8%A6%96%E9%87%8E%E3%81%AB%E5%85%A5%E3%82%8D%E3%81%86","収益化は視野に入ろう",[64,267,268],{},[17,269,271],{"href":270},"#%E6%94%AF%E5%87%BA%E3%81%AF%E3%81%A8%E3%81%AB%E3%81%8B%E3%81%8F%E3%82%B1%E3%83%81%E3%82%8C","支出はとにかくケチれ",[64,273,274],{},[17,275,277],{"href":276},"#%E3%83%87%E3%82%B6%E3%82%A4%E3%83%B3%E3%81%AF%E3%82%B7%E3%83%B3%E3%83%97%E3%83%AB%E3%81%AB%E8%80%83%E3%81%88%E3%82%8B","デザインはシンプルに考える",[64,279,280],{},[17,281,283],{"href":282},"#%E6%8A%80%E8%A1%93%E3%81%AF%E8%87%AA%E5%88%86%E3%81%8C%E5%BE%97%E6%84%8F%E3%81%AA%E3%82%82%E3%81%AE%E3%82%92","技術は自分が得意なものを",[64,285,286],{},[17,287,289],{"href":288},"#%E5%AE%8C%E7%92%A7%E3%82%92%E7%9B%AE%E6%8C%87%E3%81%95%E3%81%AA%E3%81%84","完璧を目指さない",[64,291,292],{},[17,293,295],{"href":294},"#%E3%83%86%E3%82%B9%E3%83%88%E3%80%81Git%E3%81%AF%E3%81%A7%E3%81%8D%E3%82%8B%E3%82%88%E3%81%86%E3%81%AB","テスト、Gitはできるように",[64,297,298],{},[17,299,301],{"href":300},"#%E7%9B%AE%E6%A8%99%E6%97%A5%E3%82%92%E6%B1%BA%E3%82%81%E3%82%8B","目標日を決める",[64,303,304],{},[17,305,307],{"href":306},"#%E7%B6%99%E7%B6%9A%E7%9A%84%E3%81%AA%E9%81%8B%E5%96%B6%E3%81%A8%E9%96%8B%E7%99%BA%E3%82%92%E3%82%81%E3%81%96%E3%81%99","継続的な運営と開発をめざす",[64,309,310],{},[17,311,313],{"href":312},"#%E5%A4%96%E9%83%A8%E3%82%B5%E3%83%BC%E3%83%93%E3%82%B9%E5%88%A9%E7%94%A8%E6%99%82%E3%81%AF%E8%A6%8F%E7%B4%84%E3%82%92%E3%81%97%E3%81%A3%E3%81%8B%E3%82%8A%E8%AA%AD%E3%82%80","外部サービス利用時は規約をしっかり読む",[104,315,317],{"id":316},"企画構想","企画、構想",[13,319,320,323],{},[321,322,317],"em",{},"では、プロジェクトの基礎となる部分です。頭の中にある曖昧なアイデアや目的を言語化し、はっきりさせることで実際に作りたいサービスがわかってきます。設計を行うことでアイデアを実現させる、ソフトウェア構成を考えようやく開発に至ります。",[13,325,326],{},[327,328],"span",{"id":235},[148,330,331],{"id":331},"開発に至る経緯",[39,333],{":src":334,":width":42,":center":43,":current":43},"'process.svg'",[13,336,337],{},"RouteShareを作成する経緯としては私がサイクリストで、色んなとこにツーリングを行く経験が元になっています。その際、ルートを作るときにGoogle My Mapを使用していました。どんなルートにしようかを考えるとき他人のブログやストリートビューを見たりしていました。しかし、",[61,339,340,343,346],{},[64,341,342],{},"もっと総合的にまとめられているサイトがないこと",[64,344,345],{},"住所名や写真だと「実際にそこはどのへんなの？」と地図を見ながら情報を掲載したい",[64,347,348],{},"地図の埋め込みや表示を行っているサイトがない",[13,350,351],{},"とルートの共有できるサービスってあんまないのでは？と思う様になりました。",[13,353,354,355,360],{},"またツーリング友達が",[17,356,359],{"href":357,"rel":358},"https:\u002F\u002Fwww.itmedia.co.jp\u002Fnews\u002Farticles\u002F1907\u002F03\u002Fnews096.html",[21],"ルートラボ","というサービスがなくなり、ルートの共有に困っているということも聞きました。",[61,362,363,366,369],{},[64,364,365],{},"ユーザーが手軽に地図に情報を掲載できる",[64,367,368],{},"地図や文章を共有できる",[64,370,371],{},"公開されて無料でできるサービス",[13,373,374],{},"というサービスが無いかを考えました。ともあれ最初の構想においてはまず自分の体験や、必要だなと思うこと、あるといいなと思うサービスを思うかべたり、既存アプリの欠点を探すといいです。",[13,376,377],{},[327,378],{"id":241},[148,380,381],{"id":381},"企画の時に役に立つアジャイルサムライ",[13,383,384],{},"ちょっと脱線しますが、企画の段階には「アジャイルサムライ」という名著が役に立ちます。チームでのアジャイル開発に関する内容がメインですが、プロジェクトの立案や管理に関する具体的な方法に関してのTipsが多いです。チームだけでなく個人開発でも十分利用できることが書いてあります。ぼっち開発なので第４章の「全体像を捉える」以降が特に役に立ちます。以降の内容でもアジャイルサムライで出てくる内容がちょくちょく出てきます。",[13,386,387],{},"個人開発を行うときはアジャイルサムライを購入してみたり、他にも企画やプロジェクト管理に関する著書を買ってみてもいいでしょう。",[13,389,390],{},[327,391],{"id":247},[148,393,394],{"id":394},"競合調査",[39,396],{":src":397,":width":42,":center":43,":current":43},"'search.svg'",[13,399,400],{},"競合調査をする理由は、",[61,402,403,406,409,412],{},[64,404,405],{},"機能がダダ被りであった場合、例えリリースしても「ユーザーがこのアプリを使うメリット」が見つからない状態となるのを防ぐ。",[64,407,408],{},"自分が知らない機能を探し出す。",[64,410,411],{},"なぜ収益化できているのか、人気なのかの理由を知る。",[64,413,414],{},"差別化できることを探す。",[13,416,417],{},"単純なコピーアプリでなく「ユーザーがこのアプリを使うメリット」を出せる様にします。上記で「案外無いんじゃないのかな？」と書きましたが、案外あったりします笑。車輪の再発明にならない様にまず落ち着いて、ググってみます。",[13,419,420],{},"RouteShareの場合、まずGoogle Map、Strava、Ride with GPSを調査しました。いくつかのアプリは有料機能の無料期間があったので、サブスク購入して調査しました。また、自分が知らないアプリがあるかもしれないため、「ルート　共有　アプリ」とか「ツーリング　おすすめ　アプリ」と素人気分になって調べたり、友人に聞き取りをおこないました。",[13,422,423],{},"競合調査の結果、一部のアプリには",[61,425,426,429,432,435],{},[64,427,428],{},"地図に情報入力ができる",[64,430,431],{},"記事を作成できる",[64,433,434],{},"ナビゲーション機能がある",[64,436,437],{},"自動トラッキング記録機能がある",[13,439,440],{},"といった考えていた機能とかぶるとこ、思いつかなかった機能がありました。各種サービスごとに",[61,442,443,446,449,452],{},[64,444,445],{},"特徴的な機能",[64,447,448],{},"ターゲット",[64,450,451],{},"何ができて、どんなユーザーのニーズを満たすか",[64,453,454],{},"何が足りないか",[13,456,457],{},"をリストアップしていきます。「何が足りないか」は「開発に至る経緯」で思った欲しい機能で比較すると良いです。そうすると",[61,459,460,463,466,469],{},[64,461,462],{},"Google Map は地図入力のUIと情報量は群を抜いているが、StravaやRide with GPSのような他のユーザーと情報を共有しにくい。コミュニティー機能がない。",[64,464,465],{},"Stravaはアスリート向けで、どちらかというとトレーニング記録サービスという感じ。有料機能でルート作成機能はあるが、一つのラインしか描画できない。",[64,467,468],{},"Ride with GPSは作りたい内容と似ていて、地図機能や記事作成機能がそれなりにある。しかしサイクリスト向けであり、一つのラインしか描画できない（無料の場合）。記事のUIが微妙。",[64,470,471],{},"YAMAPは登山者向け。トラッキング記録機能やオフライン地図保存がある。情報の共有機能もあるがリッチな入力は難しい。",[13,473,474],{},"などアプリごとの欠点・あった方がいいなと思うとこもありました。既存のサービスを調査したら次は、開発するプロジェクトをはっきりさせていきます。",[148,476,477],{"id":477},"エレベータピッチ",[39,479],{":src":480,":width":42,":center":43,":current":43},"'undraw_presentation_re_sxof.svg'",[13,482,483],{},"競合調査を行い意外と被っていたり、何を作ればいいかなどアイデアが曖昧な状態であることが多いです。アジャイルサムライである「全体像を捉える」を行います。特にエレベーターピッチと呼ばれる",[61,485,486,489,492,495,498,501,504],{},[64,487,488],{},"「潜在的なニーズを満たしたり、抱えている課題を解決したり」したい",[64,490,491],{},"「対象顧客」向けの",[64,493,494],{},"「プロダクト名」というプロダクトは",[64,496,497],{},"「プロダクトのカテゴリー」である。",[64,499,500],{},"これは「重要な利点、対価に見合う説得力のある理由」ができ、",[64,502,503],{},"「代替手段の最右翼」とは違って、",[64,505,506],{},"「差別化の決定的な特徴」が備わっている。",[13,508,509,510,514],{},"以上のテンプレートを使って個人開発で",[511,512,513],"strong",{},"作るプロダクトが具体的になんであり、誰のものなのかということ","をはっきりさせます。RouteShareでは以下の様になりました。",[61,516,517,520,523,526,529,532,535],{},[64,518,519],{},"「自身の旅程や訪れた道、場所の感想・写真を共有」したい",[64,521,522],{},"「旅行者、ドライバー、ライダー、サイクリスト、街歩き」向けの",[64,524,525],{},"「Route Share」というプロダクトは",[64,527,528],{},"「地図と記事を用いる地図型SNSサービス」である。",[64,530,531],{},"これは「旅程や地理情報の共有し、地域や場所、道の魅力を発信する」ことができ",[64,533,534],{},"「StravaとGoogle Map」とは違って",[64,536,537],{},"「地図上の描画とレポート機能による共有とコミュニティ機能」が備わっている",[13,539,540],{},"となりました。自分がサイクリストだったこともあり、ターゲットを限定的にしていましたが「旅行者、ドライバー、ライダー、サイクリスト、街歩き」など「ツーリスト」全体にターゲットを広げました。",[13,542,543],{},"他に自分がなぜ旅行が好きなのかを深掘りして、「地域や場所、道の魅力を発信する」という「個人の記録」にとどまらない使い方ができるサービスを作りたいという方向性が定まりました。",[545,546,547,550,553,556,559,570,573,579,587,592,597,602,622,627,632,637,642,645,648,652,656,659,662,666,669,672,675,678,690,693,696,699,702,704,707,710,714,717,720,723,726,729,732,735,738,764,767,770,773,776,779,782,785,788,791,794,797,800,803,806,809,812,815,818,822,825,828,832,835,838,841,855,858,860,863,865,868,882,885,888,891,905,908,910,919,922,925,928,932,935,938,941,944,947,950,954,957,960,963,966,969,972,975,978,982,985,988,996,999,1002,1005,1009,1012,1015,1022,1025,1036,1039,1043,1046,1049,1052,1055,1058,1062,1065,1068,1080,1083,1086,1089,1096],"sapn",{"id":253},[148,548,549],{"id":549},"パッケージリスト",[13,551,552],{},"大まかなプロダクトの方向性が定まり、次はパッケージリストというものを作ります。これは本の表紙の様なもので、１枚の紙にプロダクトのイメージ図、特徴機能、キャッチコピーを掲載します。ただこの中では特徴機能が重要な気がします。",[13,554,555],{},"この「特徴機能」は最初にいくつか候補を挙げておきます。それらの候補から最終的に３つに絞ってパッケージに載せます。しかしここで重要なのは「このプロダクトで何ができるのか」をはっきりさせることです。",[13,557,558],{},"この際、アジャイルサムライでは「フィーチャーでなく効能をつたえる」ことを重視しています。フィーチャーとは一言ですむ特徴や機能名のことです。RouteShareの場合、地図機能、記事機能、SNSサービスといった用語が当てはまります。しかしこれらの「特徴」や「機能名」を言ったとこでユーザーからしてみれば、「で、それらは一体何がいいの？」となっていまいます。これらの「特徴」や「機能名」でユーザーの",[61,560,561,564,567],{},[64,562,563],{},"どんなニーズを満たすのか",[64,565,566],{},"どんな役に立つことやメリットがあるのか",[64,568,569],{},"何ができるようになるのか",[13,571,572],{},"を具体的に言える様にしましょう。アジャイルサムライでは上記３つのことを「効能」と言っています。RouteShareでは以下の様になりました。",[574,575,576],"blockquote",{},[13,577,578],{},"地図にルートの描画、スポットの設置ができる。",[61,580,581,584],{},[64,582,583],{},"わかりやすいルートを作ることができる。どこの情報かわかりやすい。",[64,585,586],{},"地図でどこに何があるかを直ぐに把握できる。",[574,588,589],{},[13,590,591],{},"地図のルートスポットから記事にリンクをはれる。",[61,593,594],{},[64,595,596],{},"地図と記事の関連性がわかりやすいので、楽に内容を共有できる。",[574,598,599],{},[13,600,601],{},"地図＋記事の投稿を作成できる。",[61,603,604,607,610,613,616,619],{},[64,605,606],{},"自分が行った旅先の情報を他人に共有できる。",[64,608,609],{},"プランナーとして利用でき、仲間とルートや旅程を共有しやすい。",[64,611,612],{},"道路情報や気をつけることなど、実際に行かないとわからない様な詳細な情報を共有でき、取得できる。",[64,614,615],{},"自分たちの町などのおすすめの回り方を発信できる。",[64,617,618],{},"写真も投稿できるのでダイアリーとして、旅の思い出を残すことができる。",[64,620,621],{},"直感的に場所を把握することができる。",[574,623,624],{},[13,625,626],{},"設定したパスやスポットからどこに行ったかのデータを取得される",[61,628,629],{},[64,630,631],{},"検索で場所、ルートから投稿を検索できる様になる。自分がいきたいと思っている、知りたいと思っている場所の情報を地図から探すことができる。",[574,633,634],{},[13,635,636],{},"投稿にタグを貼ることができる。",[61,638,639],{},[64,640,641],{},"都道府県、地名、やりたいことから目的のルートを探ることができる。",[13,643,644],{},"まずは「〜ができる」「〜する」という特徴を書いたら、「それによってユーザーにとってどう役に立つのか」を箇条書きで書きます。",[13,646,647],{},"パッケージは作っても作らなくてもいいですが、作った方がやる気は上がる気がします笑。",[13,649,650],{},[327,651],{"id":259},[148,653,655],{"id":654},"やることやらないこと","やること、やらないこと",[13,657,658],{},"方向性が決まり、実装したい機能が定まってきたら選定を行いましょう。RouteShareも開発していくとわかってきた技術的難点やコストの問題で端折った機能がたくさんあります。個人開発ではスコープ管理が自由な分、「こーした方がユーザーにとっていいのでは..」と考えることが多くなります。やること、やらないことを決めないと実装したい機能が後からどんどん湧いて、リリースが先延ばしになってしまい、最終的に途中で頓挫することが多いです。",[13,660,661],{},"正直言うとそのサービスが需要があるか、ニーズにマッチしているかは市場に出して使ってみてもらわないと効果測定ができません。後々の設計や技術選定、開発を行いながら「やらないこと（あとでやる）」と「やること」を選定していくといいです。経験的に「やらないこと」に振れないかと第一に考えるとスコープが無限増殖しません。",[13,663,664],{},[327,665],{"id":265},[148,667,668],{"id":668},"収益化構想",[39,670],{":src":671,":width":42,":center":43,":current":43},"'undraw_investing_re_bov7.svg'",[13,673,674],{},"このサービスでは収益化を考えています。私自身は無料のサービスには限界があり、最終的には何らかの形で有料化・収益化することでより良いサービスになると考えています。使用ユーザーが増えることはその分、サーバスペックも要求されるようになりますし、当初のニーズに対する答え合わせもできるようになってきます。より安定的に、素早くビジネスとサービス品質を加速させるためにはマネタイズは重要になります。",[13,676,677],{},"RouteShareも一大プラットフォームになるべく、収益化の構想は考えています。ただし、正直言うとここは自分でも曖昧で「取らぬ狸の皮算用」をしているのでは？と感じているとこがあります。RouteShareでは以下の方法での収益化パターンを考えています。",[679,680,681,684,687],"ol",{},[64,682,683],{},"広告添付",[64,685,686],{},"有料機能、サブスク化",[64,688,689],{},"法人掲載枠を設ける",[691,692,683],"h5",{"id":683},[13,694,695],{},"Google Adsenseなどのサービスを利用して広告を添付します。シンプルで導入はしやすいです。しかし広告添付は収益化のためには非常に多くのユーザーが必要であり、また不安定です。",[691,697,686],{"id":698},"有料機能サブスク化",[13,700,701],{},"一部機能に制限をつけ利用したい場合は有料会員としてサブスクを求める方法です。使用しているユーザーや料金、収益状況が把握しやすく収益が安定しやすいです。しかし有料で買いたいほどのニーズの解決や無料枠との差別化が必要であり、収益は単価を多く取るか少なくするかによって必要なユーザー数に依存するようになります。",[691,703,689],{"id":689},[13,705,706],{},"私は最終的にはこの方法での収益化を目指しています。法人と契約して優先的に掲載できる枠を設けたり、法人のみが使用できる機能を提供、企画を立案してプロモーションを行うことです。サービスにそれなりにユーザーがいて、法人に対してそれなりの利益を提供するまでサービスの成熟と営業が必要なりますが、toCな収益形態よりも安定的で発展性のある収益化形態です。",[13,708,709],{},"収益化方法は色々とありますが、少なくともどのように収益を確保するかの構想は初期の設計段階でも考えておくといいです。",[13,711,712],{},[327,713],{"id":271},[148,715,716],{"id":716},"支出はシビアに感覚で判断しない",[13,718,719],{},"いきなり企画の段階でいうのも何ですが、個人の広報力やブランドがない状態でデプロイしても正直ユーザーは来てくれません。個人開発のツイートがバズってたくさん！というのは偶に見ますが非常に稀です。",[13,721,722],{},"正直大方のサービスは運営していき、少しずつユーザーを増やしていきます。サービスがいつバズって収益化まで繋がるかは「サービスがどれほどの期間運営しているか」に依存します。下手すると年単位になることもあります。",[13,724,725],{},"その時、支出の存在はリリース後の悩みの種になりますのでサーバ代は必ず計算して、いかにケチれるようにすることが重要です。",[13,727,728],{},"私の場合Laravelを使用するAPIサーバのためにVPSを新規に借りようとしましたが、計算しても月2000円は追加の出になるため、実は別のブログで運用しているレンタルサーバに導入しています笑",[13,730,731],{},"フロントに関してはGoogle App Engineを使用して必要な時に稼働するようにしました。とにかく支出はケチれるようにして、かかる費用は毎月監視するようにしましょう。",[148,733,734],{"id":734},"リスク",[13,736,737],{},"ここまではどんな機能を実装しようか、サービスがどうなるのかワクワクしながら進めたと思います。しかし機能を実装するで上にどんなリスクが生じるのかを考えましょう。実際に企画初期段階では以下のように考えていました。",[61,739,740,743,746,749,752,755,758,761],{},[64,741,742],{},"ユーザーが本当に何を望んでいるかわからない",[64,744,745],{},"Stravaなどの強い競合からどうユーザーを引き込むか",[64,747,748],{},"最初の広報とユーザーに広めるか",[64,750,751],{},"ルートパスからの検索の実装方法",[64,753,754],{},"検索コスト",[64,756,757],{},"リッチテキストの実装",[64,759,760],{},"Google API のリクエストコスト",[64,762,763],{},"本当に開発しきれるか",[13,765,766],{},"この時さまざまなリスクが思い浮かびますが、そのリスクが「自分が制御できるか」で対応すべきかを決めましょう。例えば「このサービスは受けるだろうか」「ユーザーのニーズをあてているか」は正直考えるだけ無駄なリスクです。なぜなら「その答えはユーザーのみが知っていて、そもそも市場に出さないとわからない」からです。もちろん競合調査などをして可能な限りリスクを下げることが必要ですが、自分がどれほどそのリスクに対して対策を取れるか、制御できるかを念頭にしておくことが大切です。",[13,768,769],{},"リスクをよく考えることで早い段階で課題を明らかにでき、プロジェクトが進んでから爆弾が爆発するのを防ぐことができます。あと気持ちがすっきりします笑",[148,771,772],{"id":772},"プロジェクトの管理",[13,774,775],{},"プロジェクトの管理はGoogle ドキュメントとスプレッドシートで行いました。企画書などはドキュメントにまとめ、スコープ、実装機能表などをスプレッドシートで管理しました。",[13,777,778],{},"個人開発であればJiraやBacklogよりもスプレッドシートぐらいで十分です。チームでも2,3人ぐらいまでなら大丈夫そうです。これらの記録は後で見返したりする時に便利ですし、課題管理はプロジェクトがどれほど進んでいるかの指標になるので作成しておくといいです。",[13,780,781],{},"また設計に関しては基本的にスプレッドシート、図などはDiagrams.netを使用しました。",[104,783,784],{"id":784},"設計",[39,786],{":src":787,":width":42,":center":43,":current":43},"'undraw_dev_productivity_re_fylf.svg'",[13,789,790],{},"企画・構想が決まったら設計を行います。ここからようやくエンジニアっぽいことをやっていきます。",[148,792,793],{"id":793},"サイトマップと画面イメージを作成する",[13,795,796],{},"機能については企画の段階でまとめました。サービスではユーザーが画面上のUIを通じてデータの更新・閲覧を行います。すなわち「どんな画面が表示され、そこで何ができて、どう遷移するのか」を把握します。",[13,798,799],{},"いきなり画面図を書くのは大変なので最初はサイトマップというものを作成して「どんなページ（画面）」があれば良いのか？を決定します。webサービスの場合はURLのパスも決めてしましましょう。",[39,801],{":src":802,":center":43,":current":43},"'sitemap.png'",[13,804,805],{},"サイトマップは名称、パス、タイトル、用途を記述し、認証や特定のミドルウェア的な制御を行う場合は追加で記入しておきます。",[13,807,808],{},"サイトマップを作成したらワイヤーフレームを作成します。ワイヤーフレームはページにどんなパーツを表示させ、どんな要素（データなど）を表示するのかを整理することができます。フロント部分で表示する要素を整理することで、実際にどんなテーブル構成にすればいいかを知ることができるようになります。",[148,810,811],{"id":811},"ワークフロー図を作る",[13,813,814],{},"次はワークフロー図を作成します。ここでいうワークフロー図はプログラム的な処理のフローです。例えばGoogle連携についてのフローは以下のような画像で表現しました。",[39,816],{":src":817,":center":43,":current":43},"'wf.png'",[148,819,821],{"id":820},"er図を作る","ER図を作る",[13,823,824],{},"ER図にてテーブル設計を行います。フロント・バック内でのフローや要素を整理するとER図はすんなりとできることが多いです。",[39,826],{":src":827,":center":43,":current":43},"'er.png'",[13,829,830],{},[327,831],{"id":277},[148,833,834],{"id":834},"デザインを作る",[13,836,837],{},"エンジニアで一番大変なのはこのデザインの部分です。ワイヤーフレームをもとにして実際のデザインを作成します。私はAdobe XDを使用して画面ごとのデザイン、UI、パーツを作成しました。ここはFigmaでもXDでも大丈夫ですが、コンポーネントやレイヤー分けできるツールを使用した方がいいです。",[13,839,840],{},"デザインセンスがないと感じる場合",[61,842,843,846,849,852],{},[64,844,845],{},"他のサイトを参考にする",[64,847,848],{},"デザインの4原則を守る",[64,850,851],{},"ブランドカラーを決める",[64,853,854],{},"CSSフレームワークを使用する",[13,856,857],{},"以上４点を意識します。",[691,859,845],{"id":845},[13,861,862],{},"RouteShareはTwitter、Qiita、Stravaを参考にしてそれらを混ぜ込んだ形にしました。よく見ると似ている箇所があります。もちろん完全にパクるのは良くないですが、デザイン・UIの参考として作成した方が、少なくともダサいデザインにはならないと思います。",[691,864,848],{"id":848},[13,866,867],{},"これを守るだけでなんかいい感じになります。デザインの4原則とは",[679,869,870,873,876,879],{},[64,871,872],{},"近接",[64,874,875],{},"整列",[64,877,878],{},"強弱",[64,880,881],{},"反復",[13,883,884],{},"の4点です。",[39,886],{":src":887,":center":43,":current":43},"'list.png'",[13,889,890],{},"例えば上記は登録された投稿一覧ですが、",[61,892,893,896,899,902],{},[64,894,895],{},"タイトルと投稿場所、サマリーとユーザー情報はある程度マージンをとる。",[64,897,898],{},"それぞれの要素のパディングを同じにして開始位置を揃える。",[64,900,901],{},"背景色を利用して強弱、領域の区別をできるようにする",[64,903,904],{},"下のボタンなど似ている要素は似たレイアウトと感じで表示する",[13,906,907],{},"この4点を意識していくと結構いい感じになります。",[691,909,851],{"id":851},[13,911,912,913,918],{},"ブランドを決めます。RouteShareは道路の新型青看板の青色をもとにしています。その色を起点にして",[17,914,917],{"href":915,"rel":916},"https:\u002F\u002Fwww.colorhexa.com\u002F",[21],"ColorHexa","などで輝度方向のパターンの色を使用するといいです。基本的にはブランドカラーはこのようにし決定して、色々とカラフルにしない方がいいです。しかしUIではキャンセル、確認といった箇所では赤・緑・黄色などを使用してしても問題ありません。",[39,920],{":src":921,":center":43,":current":43},"'bar.png'",[691,923,854],{"id":924},"cssフレームワークを使用する",[13,926,927],{},"デザインとCSS・UI系の工数を省きたい場合はCSSフレームワークを使用した方がいいです。RouteShareはBootstrap-vueを使用しています。フレームワークは好きなものを使用して大丈夫ですが、上記のブランドカラーなどを適用したりプロパティーを変更できるフレームワークを使用した方がいいです。",[13,929,930],{},[327,931],{"id":283},[148,933,934],{"id":934},"技術選定",[13,936,937],{},"設計やデザインを作成して技術選定を行います。リリースを重視し、個人開発であればあなたの得意なスタックで大丈夫です。正直いうと、ユーザーにとってみればバックエンドに何を使っていようが、動いていればまずいいのです。あとはあなたの開発効率の問題になります。個人開発ではモチベーションや初版リリースまで辿り着けるかが第一の関門となっています。",[13,939,940],{},"そのため技術的な勉強という要素を入れてしまうと、途中で挫折しかねません。もちろん選定したスタックによってはコストがかかってしまうものもあります。しかしその中でもコストと開発難易度がちょうど良くなるスタックを選択した方がいいです。",[13,942,943],{},"一番最初のことろはサーバーレス環境で全部おさめてやりたいとも思いましたが、デプロイの設定やFireBaseなどは初めてだったこともありやめました。LaravelとフロントエンドはVue.jsという構成も考えましたが、フロントの構築は１つのフレームワークで行った方が自分的にはかなり楽だったので、LaravelをバックにフロントはNuxt.jsという構成を取りました。",[13,945,946],{},"APIサーバとフロントサーバを用意する必要がありましたが、Google App engineの無料枠を利用できる様にしたり、APIサーバーも構成を工夫することで費用を抑えることができました。",[13,948,949],{},"１番の悩みの種はGoogle Map APIの課金ですが解決方法は見つけ、まずはリリースを優先するために地図表示はGoogle Mapにまずは任せることにしました。",[13,951,952],{},[327,953],{"id":313},[148,955,956],{"id":956},"外部サービスの利用時は規約をしっかり理解する",[13,958,959],{},"もし外部サービスのAPIなどを使用する時は規約を読んでおきましょう。公開して、収益を求めるサービスなので規約違反はNGです。",[13,961,962],{},"RouteShareの場合はGoogle Map APIで規約違反をしてしまう機能を開発しそうになった時がありました。RouteShareは地図にセットしたラインやスポットを計算して、周辺地図の画像をサムネイルとして設定できる機能があります。↓",[39,964],{":src":965,":width":42,":center":43,":current":43},"'sample_card.png'",[13,967,968],{},"このとき当初はGoogle Map static image apiを使用しており、バックエンドで画像のURLにリクエストしてレスポンスの画像をこちらのサーバに置いておくという方法をとっていました。しかしこの方法は規約違反であり、必ずブラウザからstatic image apiのURLを記載することとの規約がありました。",[13,970,971],{},"現在は地図サービスのAPIを別途契約して、タイル画像を取得して生成するようにしました。知らぬ間に規約違反することもあるので、規約は難しいですが一読しておくことをお勧めします。",[104,973,974],{"id":974},"開発",[13,976,977],{},"設計、技術選定、デザインがある程度完成したら早速開発を始めましょう。正直のこの開発時期が一番、挫折しやすいと思います。ここでは開発時に大切なことを書いておく程度にします。",[13,979,980],{},[327,981],{"id":289},[148,983,984],{"id":984},"とにかく作り上げること",[13,986,987],{},"個人開発の完成は自身のモチベに依存するため",[61,989,990,993],{},[64,991,992],{},"Better than Nothing",[64,994,995],{},"Done is better than perfect",[13,997,998],{},"の気持ちを常に持っていた方がいいです。開発をしている際に一部の設計を見直したり、機能を追加・変更することがあると思いますがスケジュールとスコープがオーバーしないようにしましょう。",[13,1000,1001],{},"私も開発をしている時に「こうした方がいいのでは？」「もっと最適な方法があるのでは？」と思って思うように開発が進まないことがありました。",[13,1003,1004],{},"もちろん期限内に最善を尽くすの良いですが、完璧を求めようとするとズルズルと起源と対応すべきコストが増大します。途中で色々面倒になったり、仕事が忙しくなり放置..ということになります。今の自分が持っている技術スタックの最善を尽くして開発を進めてください。",[13,1006,1007],{},[327,1008],{"id":295},[148,1010,1011],{"id":1011},"保守性とテスト性は重要",[13,1013,1014],{},"作り上げることは重要ですが、品質を犠牲にするわけではありません。リリースはゴールではなく、マイルストーンであり経過点です。むしろサービスはリリースしてから始まりますので、リリース後の保守や改善のことも考えなければなりません。",[13,1016,1017,1018,1021],{},"その際に",[321,1019,1020],{},"作り上げることを言い訳に","自分ですらコードの内容を把握できないものや、保守性の低いものはリリース後のモチベを下げる要員になります。",[13,1023,1024],{},"継続的な開発のために",[61,1026,1027,1030,1033],{},[64,1028,1029],{},"保守性を高める記述を心がける",[64,1031,1032],{},"可能な限りテストコードを書いて、改善コードをデプロイしやすくする",[64,1034,1035],{},"バージョン管理は必ず導入する",[13,1037,1038],{},"以上のことはやっておいて損はありません。",[13,1040,1041],{},[327,1042],{"id":301},[148,1044,1045],{"id":1045},"毎日コツコツやること",[13,1047,1048],{},"あとはもう時間をかけるのみです。個人開発は睡眠時間を削ったり土日を使うことで捻出しました。仕事が終わったら直ぐに個人開発の環境を立ち上げておきます。",[13,1050,1051],{},"まとまった休みにやるよりも、毎日ちょっとずつの方が結果的にリリース達成の確率は上がります。",[13,1053,1054],{},"ここはもう、何とか時間を作ってください。心の中に「リリースまで進み続けるんだ！これはお前が始めた個人開発だろ？」と問いかけるようにしてください。",[13,1056,1057],{},"そしてリリース日やマイルストーンなど明確な目標期限を定めてください。個人開発は管理が自由な分、開発時間などを伸ばしがちです。期限を定めることでダラダラと続けることを防げますし、スコープが増大することも防げます。",[13,1059,1060],{},[327,1061],{"id":307},[104,1063,1064],{"id":1064},"リリース構成",[13,1066,1067],{},"開発進み完成してきたらリリースの準備を行います。リリース方法は使用している開発構成によってまちまちですが、基本的にはgitを用いてリリースを管理できるようにしたほうがいです。可能な限り、手動での反映を少なくできるようにした方がいいです。",[13,1069,1070,1071,1075,1076,1079],{},"RouteShareではgithub上で",[1072,1073,1074],"code",{},"release",",",[1072,1077,1078],{},"test","といったブランチを作成して、本番環境もそれらのブランチからプルするようにしています。フロントエンドはGoogle App engineを使用しており、ビルドからサーバへのデプロイはgithub actionsで自動的に行えるようにしています。",[13,1081,1082],{},"ドメインの設定やSSLなどやることが多いですが、この辺はリリース時に1回で済むので頑張って調べてください。（私もそのうち書きます..）",[13,1084,1085],{},"今後の機能リリースではできる限り本番へのデプロイが楽になるような構成を最初に心掛けた方がいいです。",[51,1087,1088],{"id":1088},"個人開発のススメ",[13,1090,1091,1092,1095],{},"色々端折ってしまった所がありますが、内容は以上となります。本記事では開発したRouteShareの詳細はお伝えしませんでしたが、また違う記事で機能について開発面で細かく解説した記事でも書こうと思います。ぜひ旅行好きな方は",[17,1093,31],{"href":29,"rel":1094},[21],"を使ってみてください！",[13,1097,1098],{},"デプロイから1ヶ月たち、まだユーザー数やトラフィックも少ないですが今後は運用に注力したり、リリースまでに間に合わなかった機能を実装しようと思います。エンジニアであればこのように製品を自ら開発してビジネスを始めることができます。ディベロッパー的な視点や技術だけでなく、経営や運営、企画の力も付きます。総合的な力をつけたいエンジニアはサービスの大小あれど、色々な人に使ってもらえるアプリケーションを開発してみるといいと思います！",{"title":1100,"searchDepth":1101,"depth":1101,"links":1102},"",3,[1103,1105,1114,1143],{"id":53,"depth":1104,"text":53},2,{"id":101,"depth":1104,"text":102,"children":1106},[1107,1108],{"id":106,"depth":1101,"text":106},{"id":143,"depth":1101,"text":143,"children":1109},[1110,1112,1113],{"id":150,"depth":1111,"text":150},4,{"id":178,"depth":1111,"text":178},{"id":195,"depth":1111,"text":195},{"id":221,"depth":1104,"text":221,"children":1115},[1116,1117,1129,1137,1142],{"id":224,"depth":1101,"text":224},{"id":316,"depth":1101,"text":317,"children":1118},[1119,1120,1121,1122,1123,1124,1125,1126,1127,1128],{"id":331,"depth":1111,"text":331},{"id":381,"depth":1111,"text":381},{"id":394,"depth":1111,"text":394},{"id":477,"depth":1111,"text":477},{"id":549,"depth":1111,"text":549},{"id":654,"depth":1111,"text":655},{"id":668,"depth":1111,"text":668},{"id":716,"depth":1111,"text":716},{"id":734,"depth":1111,"text":734},{"id":772,"depth":1111,"text":772},{"id":784,"depth":1101,"text":784,"children":1130},[1131,1132,1133,1134,1135,1136],{"id":793,"depth":1111,"text":793},{"id":811,"depth":1111,"text":811},{"id":820,"depth":1111,"text":821},{"id":834,"depth":1111,"text":834},{"id":934,"depth":1111,"text":934},{"id":956,"depth":1111,"text":956},{"id":974,"depth":1101,"text":974,"children":1138},[1139,1140,1141],{"id":984,"depth":1111,"text":984},{"id":1011,"depth":1111,"text":1011},{"id":1045,"depth":1111,"text":1045},{"id":1064,"depth":1101,"text":1064},{"id":1088,"depth":1104,"text":1088},[1145],"learning","2022-12-11","ユーザー投稿型webサービスの個人開発をリリースするまでの道のり","md",null,{},true,"\u002Farticles\u002Fprivate-develop-released",{"title":8,"description":1147},"articles\u002Fprivate-develop-released",[1156],"dev_exp","private-develop-released\u002Fthumbnail.jpeg","nztFX4zrPEZhku2rJ4fzSsXPLzjLH78Rqjadot2TAvk",{"id":1160,"title":1161,"body":1162,"category":1261,"createdAt":1263,"description":1264,"extension":1148,"index":1149,"meta":1265,"navigation":1151,"path":1266,"publish":1151,"seo":1267,"series":1149,"seriesTitle":1149,"stem":1268,"tag":1269,"thumbnail":1271,"updatedAt":1149,"__hash__":1272},"articles\u002Farticles\u002Fhandle-cloudfront-cache.md","cloudfrontでクエリパラメータを使ってコンテンツの更新におけるキャッシュを制御する",{"type":10,"value":1163,"toc":1259},[1164,1176,1179,1186,1194,1201,1204,1207,1210,1213,1216,1219,1222,1225,1228,1231,1234,1245,1248],[13,1165,1166,1167,1171,1172,1175],{},"こんにちはjunです。",[17,1168,1170],{"href":29,"rel":1169},[21],"RouteShareというユーザー投稿型の個人開発","でユーザーがアップロードしたファイルをS3に配置し、cloudfrontでキャッシュを効かせて取得するように設定しました。このときユーザーアバターや投稿コンテンツの自動生成サムネイルなど特定のファイルは",[1072,1173,1174],{},"{ID}.png","のようにDB上のIDと拡張子で表現していました。IDベースのファイル名にすることでIDさえわかればユーザーや投稿のサムネイルが表示できるというメリットがありました。",[13,1177,1178],{},"しかし上記の方法ではファイル名が変わらず、更新した際のcloudfrontのキャッシュで画像が切り替わらないという事態がありました。今回はこのような「cloudfront上で同じ名前で管理しているが、都度更新があった際に確実に更新したファイルを表示させる方法」についての内容です。",[13,1180,1181],{},[17,1182,1185],{"href":1183,"rel":1184},"https:\u002F\u002Faws.amazon.com\u002Fjp\u002Fpremiumsupport\u002Fknowledge-center\u002Fcloudfront-serving-outdated-content-s3\u002F",[21],"参考記事はこちら",[13,1187,1188,1189],{},"まず最初にcloudfrontにはサービス上でキャッシュを削除する機能はあります。指定のパスを設定することで、キャッシュさせたURL（ここではファイルURL）をcloudfront上から削除することができます。キャッシュがなくなるのでオリジンに取得しにいき、更新したコンテンツを取得できます。しかしそれをコンテンツごとに行うのは大変です。単純に数も多いですし更新があったコンテンツを検知して、APIでcloudfrontの操作を行う必要があるからです。",[17,1190,1193],{"href":1191,"rel":1192},"https:\u002F\u002Fdocs.aws.amazon.com\u002Fja_jp\u002FAmazonCloudFront\u002Flatest\u002FDeveloperGuide\u002FInvalidation.html#PayingForInvalidation",[21],"また無料枠分はありますが、キャッシュの削除にはお金がかかります。",[13,1195,1196,1197,1200],{},"cloudfrontは実はキャッシュは効かさないこともできますが、折角のCDNが勿体無いです。更新を効かしつつも普段はキャッシュさせてパフォーマンスを上げる方法としては、ファイル名に",[1072,1198,1199],{},"?ver=xxxx","のようなクエリパラメータをつけて、別のURLとして認識させる方法があります。これはcloudfrontのみならず、他のキャッシュ対策でもよく行われます。バージョンパラメータはクライアント側で制御します。例えば、ユーザーアバターの際はアップロード更新時にユーザーレコードのupdated_atを更新し、ユーザーレコードを参照する箇所ではそのupdated_atをクエリパラメータに入れます。ビルドしたjsやcssなどにはビルド日時とかを入れられるようにするといいかもしれません。",[13,1202,1203],{},"ただし、cloudfrontのデフォルトのキャッシュポリシーであるCachingOptimizedはこのクエリパラメータを考慮しません。どんなクエリパラメータをつけようが、同じファイル名であればキャッシュしてしまいます。そのため、新しいキャッシュポリシーを加えます。",[13,1205,1206],{},"最初にcloudfrontの画面を開き、ポリシーを選択します。",[39,1208],{":src":1209,":width":42,":center":43,":current":43},"'menu.png'",[13,1211,1212],{},"カスタムポリシーからキャッシュポリシーを作成を選択",[39,1214],{":src":1215,":center":43,":current":43},"'create.png'",[13,1217,1218],{},"クエリパラメータを有効にしたい既存のキャッシュポリシーと同じにします。今回はデフォルトのCachingOptimizedとTTLや圧縮サポートを選択。そして 「キャッシュキー設定」にてクエリ文字列を追加し、扱うパラメータを入力します。ポリシー名を設定し、問題なければ作成をクリックしてポリシーを登録します。",[39,1220],{":src":1221,":center":43,":current":43},"'cache_key.png'",[13,1223,1224],{},"次にポリシーを当てはめたいディストリビューションのビヘイビアを編集します。",[39,1226],{":src":1227,":center":43,":current":43},"'behavior.png'",[13,1229,1230],{},"キャッシュポリシーを先ほどの名前のものに設定します。",[39,1232],{":src":1233,":center":43,":current":43},"'set_policy.png'",[13,1235,1236,1237,1240,1241,1244],{},"変更を保存をクリックして完了です。こうすれば",[1072,1238,1239],{},"?ver=2022-12-01-00-00-01","から",[1072,1242,1243],{},"?ver=2022-12-01-00-00-02","に変更した際に別のリソースとして認識され、オリジンへの取得が行われ再度そのクエリパラメータと共にキャッシュされます。",[13,1246,1247],{},"これの良い点は更新日時に応じてほぼ確実にキャッシュを殺すことができると同時に、次の更新まではキャッシュが効くようになることです。ヘッダー・クッキーでは容易にバージョンパラメータを追加するのが難しいので、ファイルであればクエリパラメータがおすすめです。",[13,1249,1250,1251,1240,1253,1255,1256,1258],{},"ちなみにパラメーターパターンに対するキャッシュは効いているので、",[1072,1252,1239],{},[1072,1254,1243],{},"に変わったとして",[1072,1257,1239],{},"でアクセスするとキャッシュ期限まで古いものが表示されます。そのため「更新されたら必ず過去のキャッシュは消えるようにしないといけない」という場合は上記のcloudfrontでのキャッシュ削除が必要です。",{"title":1100,"searchDepth":1101,"depth":1101,"links":1260},[],[1262],"ministack","2022-12-01","クエリパラメータをもちいてコンテンツキャッシュによって更新が効かない事態を防ぐ",{},"\u002Farticles\u002Fhandle-cloudfront-cache",{"title":1161,"description":1264},"articles\u002Fhandle-cloudfront-cache",[1270],"aws","util\u002FArch_Amazon-CloudFront_64@5x.png","-ACQ7N5nl6EKYNvLu0Wqud1YIRKtjV-LoxVZjBGLnKw",{"id":1274,"title":1275,"body":1276,"category":1413,"createdAt":1414,"description":1275,"extension":1148,"index":1149,"meta":1415,"navigation":1151,"path":1416,"publish":1151,"seo":1417,"series":1149,"seriesTitle":1149,"stem":1418,"tag":1419,"thumbnail":1149,"updatedAt":1149,"__hash__":1421},"articles\u002Farticles\u002Fqueryselector-error-with-numeric.md","Document.querySelector() で先頭が数字のIDを指定するとエラーが起きる。",{"type":10,"value":1277,"toc":1411},[1278,1297,1303,1351,1357,1364,1372,1377,1394,1407],[13,1279,1280,1281,1284,1285,1288,1289,1292,1293,1296],{},"こんにちはjunです。最近、editor.jsだったり色々バニラJSを触る機会がありました。特定のNodeを取得する時に",[1072,1282,1283],{},"document.querySelector()","を使用することでCSS・jqueryライクに要素を取得できます。今回はIDを持つ要素を",[1072,1286,1287],{},"getByElementId()","を使わず、",[1072,1290,1291],{},"querySelector()","を用いて",[1072,1294,1295],{},"querySelector(\"#~~~\")","と取得した時に遭遇したエラーについてです。",[13,1298,1299,1300,1302],{},"タイトルの通りなのですが、",[1072,1301,1291],{},"で先頭が数字のIDを指定するとエラーが起きます。",[1304,1305,1309],"pre",{"className":1306,"code":1307,"language":1308,"meta":1100,"style":1100},"language-js shiki shiki-themes material-theme-ocean","document.querySelector(\"#16test\");\n\u002F\u002F VM490:1 Uncaught DOMException: Failed to execute 'querySelector' on 'Document': '#16test' is not a valid selector.\n","js",[1072,1310,1311,1345],{"__ignoreMap":1100},[327,1312,1315,1319,1323,1327,1330,1333,1337,1339,1342],{"class":1313,"line":1314},"line",1,[327,1316,1318],{"class":1317},"s0W1g","document",[327,1320,1322],{"class":1321},"sAklC",".",[327,1324,1326],{"class":1325},"sdLwU","querySelector",[327,1328,1329],{"class":1317},"(",[327,1331,1332],{"class":1321},"\"",[327,1334,1336],{"class":1335},"sfyAc","#16test",[327,1338,1332],{"class":1321},[327,1340,1341],{"class":1317},")",[327,1343,1344],{"class":1321},";\n",[327,1346,1347],{"class":1313,"line":1104},[327,1348,1350],{"class":1349},"sC9rS","\u002F\u002F VM490:1 Uncaught DOMException: Failed to execute 'querySelector' on 'Document': '#16test' is not a valid selector.\n",[13,1352,1353,1354,1356],{},"簡単にコンソールでチェックできます。「not a valid selector」の通り、有効なセレクタじゃないよと怒っています。なぜこんなことが起きるのかを調べたところ、",[1072,1355,1291],{},"はCSSセレクタの仕様を使っており、CSSのIDセレクタは「#の後に数字をつけてはいけない」という仕様があるからです。",[13,1358,1359,1360],{},"Stackoverflow\n",[17,1361,1362],{"href":1362,"rel":1363},"https:\u002F\u002Fstackoverflow.com\u002Fquestions\u002F37270787\u002Funcaught-syntaxerror-failed-to-execute-queryselector-on-document",[21],[13,1365,1366,1367],{},"上記のStackoverflowの回答が役に立ちました。HTML5の仕様としてはIDの最初の文字に数字を入れることは問題ありません。しかしCSS3のIDセレクタの仕様ではなんと、#のあとに数字を使用してはいけないと確かに書かれています。",[17,1368,1371],{"href":1369,"rel":1370},"https:\u002F\u002Fwww.w3.org\u002FTR\u002FCSS2\u002Fsyndata.html#characters",[21],"詳細はW3Cのこちらのページにあります。",[574,1373,1374],{},[13,1375,1376],{},"they cannot start with a digit, two hyphens, or a hyphen followed by a digit.",[13,1378,1379,1380,1383,1384,1386,1387,1389,1390,1393],{},"第一の解決策は",[1072,1381,1382],{},"getElementById()","を使うことです。そう思うと、本来DOMにIDを持つ要素は必ず一つなので",[1072,1385,1382],{},"を使えばいいのに、なんで",[1072,1388,1291],{},"を使っていたんだろうか？と思っていたら、ランダムに生成したIDをもつ要素配下のある子要素を取得する処理を実装する際、",[1072,1391,1392],{},"querySelector(`#{id} .item`)","みたいな感じで実装してた時でした。",[13,1395,1396,1397,1400,1401,1403,1404,1406],{},"この場合ランダムに生成されるIDに数字が含まれないようにするか、",[1072,1398,1399],{},"getElementById(id)?.querySelector(\".item\")","として一旦",[1072,1402,1382],{},"を使ってから",[1072,1405,1291],{},"を使うといいかなと思います。",[1408,1409,1410],"style",{},"html pre.shiki code .s0W1g, html code.shiki .s0W1g{--shiki-default:#BABED8}html pre.shiki code .sAklC, html code.shiki .sAklC{--shiki-default:#89DDFF}html pre.shiki code .sdLwU, html code.shiki .sdLwU{--shiki-default:#82AAFF}html pre.shiki code .sfyAc, html code.shiki .sfyAc{--shiki-default:#C3E88D}html pre.shiki code .sC9rS, html code.shiki .sC9rS{--shiki-default:#464B5D;--shiki-default-font-style:italic}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}",{"title":1100,"searchDepth":1101,"depth":1101,"links":1412},[],[1262],"2022-11-23",{},"\u002Farticles\u002Fqueryselector-error-with-numeric",{"title":1275,"description":1275},"articles\u002Fqueryselector-error-with-numeric",[1420,1308],"html","-tKJHB-rOhRzOw2x_oCwOadM10j-DEzx-pvkwNQqzxA",{"id":1423,"title":1424,"body":1425,"category":1570,"createdAt":1571,"description":1424,"extension":1148,"index":1149,"meta":1572,"navigation":1151,"path":1573,"publish":1151,"seo":1574,"series":1149,"seriesTitle":1149,"stem":1575,"tag":1576,"thumbnail":1578,"updatedAt":1149,"__hash__":1579},"articles\u002Farticles\u002Flaravel-socialite-scope-careless.md","LaravelのSociateでscopeで間違えてちょっと詰まった話",{"type":10,"value":1426,"toc":1568},[1427,1430,1433,1436,1475,1478,1485,1503,1512,1519,1524,1529,1532,1535,1565],[13,1428,1429],{},"こんにちはjunです。LaravelでGoogleとのログイン機能、連携機能を作っていた時にscopeメソッドの使い方でちょっと詰まりました。というよりか自分がよくドキュメントを読まなかったケアレスミスでしたが、どこかの開発者が詰まってこの記事を見つけられるようにインターネットの海にこの情報を書いておきます。",[13,1431,1432],{},"Laravelは本当に素晴らしいライブラリで、Laravel socialiteというライブラリを使用すれば外部サービスでのログイン機能があっという間に使用できます\n一通りログイン周りの機能を整えて、Google Driveとの連携の実装を行おうとして諸所の設定を行いました。",[13,1434,1435],{},"GCPの管理画面でGoogle Driveを追加してリファランスにしたがって以下のようにログイン時の処理にDriveへのアクセスを求めるようにスコープを追加しました。",[1304,1437,1441],{"className":1438,"code":1439,"language":1440,"meta":1100,"style":1100},"language-php shiki shiki-themes material-theme-ocean","public function redirectToGoogle(Request $request){\n    return Socialite::driver('google')\n    ->with([\"access_type\" => \"offline\", \"prompt\" => \"consent select_account\"])\n    ->setScopes([\"https:\u002F\u002Fwww.googleapis.com\u002Fauth\u002Fdrive\"])\n    ->redirect();\n}\n","php",[1072,1442,1443,1448,1453,1458,1463,1469],{"__ignoreMap":1100},[327,1444,1445],{"class":1313,"line":1314},[327,1446,1447],{},"public function redirectToGoogle(Request $request){\n",[327,1449,1450],{"class":1313,"line":1104},[327,1451,1452],{},"    return Socialite::driver('google')\n",[327,1454,1455],{"class":1313,"line":1101},[327,1456,1457],{},"    ->with([\"access_type\" => \"offline\", \"prompt\" => \"consent select_account\"])\n",[327,1459,1460],{"class":1313,"line":1111},[327,1461,1462],{},"    ->setScopes([\"https:\u002F\u002Fwww.googleapis.com\u002Fauth\u002Fdrive\"])\n",[327,1464,1466],{"class":1313,"line":1465},5,[327,1467,1468],{},"    ->redirect();\n",[327,1470,1472],{"class":1313,"line":1471},6,[327,1473,1474],{},"}\n",[13,1476,1477],{},"必要なAPIへのスコープを入力することで、取得できるアクセストークンでそのサービスにアクセスできます。しかし上記の実装でテストをしてみると、連携画面からローカルにリダイレクトする際に401が発生してしまい、連携が失敗しました。",[13,1479,1480,1481,1484],{},"なんでだろうとAPIの設定などをよく確認しましたが、原因は",[1072,1482,1483],{},"setScopes()","というメソッドでした。",[13,1486,1487,1488,1075,1491,1493,1494,1496,1497,1502],{},"Laravel socialiteはOauthのスコープを設定する際に２つのメソッドをしようできます。",[1072,1489,1490],{},"scopes()",[1072,1492,1483],{},"です。メソッドの名前的に",[1072,1495,1483],{},"を使っていたのですが、これは",[17,1498,1501],{"href":1499,"rel":1500},"https:\u002F\u002Flaravel.com\u002Fdocs\u002F9.x\u002Fsocialite#access-scopes",[21],"ドキュメントにもある通り"," に",[574,1504,1505],{},[13,1506,1507,1508,1511],{},"You can ",[511,1509,1510],{},"overwrite all existing scopes"," on the authentication request using the setScopes method:",[13,1513,1514,1515,1518],{},"と",[511,1516,1517],{},"すべての存在するスコープを上書きする","と書いてあります。",[13,1520,1521,1523],{},[1072,1522,1490],{},"はドキュメントでは",[574,1525,1526],{},[13,1527,1528],{},"This method will merge all previously specified scopes with the scopes that you specify:",[13,1530,1531],{},"と前々にあったスコープにマージすると書かれています。",[13,1533,1534],{},"細かい理由は分かりませんが、多分socialiteが提供する標準のスコープが消されてしまったのが原因かもしれません。とりあえず以下のように修正したら直りました。",[1304,1536,1538],{"className":1438,"code":1537,"language":1440,"meta":1100,"style":1100},"public function redirectToGoogle(Request $request){\n    return Socialite::driver('google')\n    ->with([\"access_type\" => \"offline\", \"prompt\" => \"consent select_account\"])\n    ->scopes([\"https:\u002F\u002Fwww.googleapis.com\u002Fauth\u002Fdrive\"]) \u002F\u002F scopesに変更\n    ->redirect();\n}\n",[1072,1539,1540,1544,1548,1552,1557,1561],{"__ignoreMap":1100},[327,1541,1542],{"class":1313,"line":1314},[327,1543,1447],{},[327,1545,1546],{"class":1313,"line":1104},[327,1547,1452],{},[327,1549,1550],{"class":1313,"line":1101},[327,1551,1457],{},[327,1553,1554],{"class":1313,"line":1111},[327,1555,1556],{},"    ->scopes([\"https:\u002F\u002Fwww.googleapis.com\u002Fauth\u002Fdrive\"]) \u002F\u002F scopesに変更\n",[327,1558,1559],{"class":1313,"line":1465},[327,1560,1468],{},[327,1562,1563],{"class":1313,"line":1471},[327,1564,1474],{},[1408,1566,1567],{},"html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}",{"title":1100,"searchDepth":1101,"depth":1101,"links":1569},[],[1262],"2022-05-23",{},"\u002Farticles\u002Flaravel-socialite-scope-careless",{"title":1424,"description":1424},"articles\u002Flaravel-socialite-scope-careless",[1577],"laravel","_common\u002Flaravel.png","tdenFwXM57aAqHk4siNzWCo8FXhh1YpEFCNAWNUhrnw",{"id":1581,"title":1582,"body":1583,"category":3284,"createdAt":3285,"description":1582,"extension":1148,"index":1149,"meta":3286,"navigation":1151,"path":3287,"publish":1151,"seo":3288,"series":1149,"seriesTitle":1149,"stem":3289,"tag":3290,"thumbnail":1578,"updatedAt":1149,"__hash__":3291},"articles\u002Farticles\u002Flaravel-i18n-json-dump.md","Laravelで多言語用のJSONを出力するコマンドを作る",{"type":10,"value":1584,"toc":3272},[1585,1600,1631,1638,1653,1666,1670,1673,1679,1757,1764,1767,1775,1778,1781,1788,1791,1794,1804,1812,1815,1818,1821,1827,1834,2064,2071,2077,2080,2083,2090,2253,2280,2283,2286,2384,2404,2408,2442,2457,2460,2475,2489,2492,2495,2822,2825,2838,2846,2851,2854,3061,3259,3266,3269],[13,1586,1587,1588,1591,1592,1595,1596,1599],{},"こんにちはjunです。個人開発で多言語対応のLaravelアプリを作っています。多言語では各種言語の単語・文章のマッピング（辞書）をする配列・JSONを作成し、レンダリング時にメソッドを使用して文字を表示します。Laravelでは",[1072,1589,1590],{},"resources\u002Flang"," 配下に",[1072,1593,1594],{},"en","、",[1072,1597,1598],{},"ja","のようなディレクトリを作成し、その中に言語のマッピングをします。大体は以下のような配列を作ります。",[1304,1601,1604],{"className":1438,"code":1602,"filename":1603,"language":1440,"meta":1100,"style":1100},"\u003C?php\nreturn [\n    'login'=>'ログイン',\n    'logout'=>'ログアウト'\n]\n\n","resources\u002Flang\u002Fja\u002Fwords.php",[1072,1605,1606,1611,1616,1621,1626],{"__ignoreMap":1100},[327,1607,1608],{"class":1313,"line":1314},[327,1609,1610],{},"\u003C?php\n",[327,1612,1613],{"class":1313,"line":1104},[327,1614,1615],{},"return [\n",[327,1617,1618],{"class":1313,"line":1101},[327,1619,1620],{},"    'login'=>'ログイン',\n",[327,1622,1623],{"class":1313,"line":1111},[327,1624,1625],{},"    'logout'=>'ログアウト'\n",[327,1627,1628],{"class":1313,"line":1465},[327,1629,1630],{},"]\n",[13,1632,1633,1634,1637],{},"そしてビューファイルなどで",[1072,1635,1636],{},"__('words.login')","のように使用します。",[1639,1640,1644,1645,1648,1649,1652],"div",{"className":1641},[1642,1643],"alert","alert-info","\n多言語のメソッドが",[1072,1646,1647],{},"__","みたいな名称である理由としては、使いまくるので簡単な名称になっています。Vueとかでは",[1072,1650,1651],{},"$t()","みたいなものを使用します。\n",[13,1654,1655,1656,1658,1659,1595,1662,1665],{},"上記のようなPHPファイルを作成してもできますが、",[1072,1657,1590],{}," 直下に",[1072,1660,1661],{},"en.json",[1072,1663,1664],{},"ja.json","のようなJSONファイルを作成しても、多言語メソッドで呼び出せます。",[51,1667,1669],{"id":1668},"jsonの弱点","JSONの弱点",[13,1671,1672],{},"JSONで作るメリットはマッピングのデータを他のアプリでも利用できることです。例えば、Vue・ReactではVuei18n、react-i18nextなどを使用します。同じように多言語メソッドを使用します。その際のマッピングデータとしてJSONを使用します。であればJSONファイルを作っておくことで、Vueや外部へマッピングデータを提供しやすくなります。",[13,1674,1675,1676,1678],{},"ただしJSONで作成するとLaravel側で",[1072,1677,1636],{},"のような呼び出しができません。この時JSONは以下のようになっています。",[1304,1680,1684],{"className":1681,"code":1682,"language":1683,"meta":1100,"style":1100},"language-json shiki shiki-themes material-theme-ocean","{\n    \"words\":{\n        \"login\":\"ログイン\",\n        \"logout\":\"ログアウト\"\n    }\n}\n","json",[1072,1685,1686,1691,1705,1729,1748,1753],{"__ignoreMap":1100},[327,1687,1688],{"class":1313,"line":1314},[327,1689,1690],{"class":1321},"{\n",[327,1692,1693,1696,1700,1702],{"class":1313,"line":1104},[327,1694,1695],{"class":1321},"    \"",[327,1697,1699],{"class":1698},"sJ14y","words",[327,1701,1332],{"class":1321},[327,1703,1704],{"class":1321},":{\n",[327,1706,1707,1710,1714,1716,1719,1721,1724,1726],{"class":1313,"line":1101},[327,1708,1709],{"class":1321},"        \"",[327,1711,1713],{"class":1712},"s5Dmg","login",[327,1715,1332],{"class":1321},[327,1717,1718],{"class":1321},":",[327,1720,1332],{"class":1321},[327,1722,1723],{"class":1335},"ログイン",[327,1725,1332],{"class":1321},[327,1727,1728],{"class":1321},",\n",[327,1730,1731,1733,1736,1738,1740,1742,1745],{"class":1313,"line":1111},[327,1732,1709],{"class":1321},[327,1734,1735],{"class":1712},"logout",[327,1737,1332],{"class":1321},[327,1739,1718],{"class":1321},[327,1741,1332],{"class":1321},[327,1743,1744],{"class":1335},"ログアウト",[327,1746,1747],{"class":1321},"\"\n",[327,1749,1750],{"class":1313,"line":1465},[327,1751,1752],{"class":1321},"    }\n",[327,1754,1755],{"class":1313,"line":1471},[327,1756,1474],{"class":1321},[13,1758,1759,1760,1763],{},"すべて一次元にしてしまうと後で混乱してしまうので、カスケードさせておくと良いです。Vuei18nでは",[1072,1761,1762],{},"$t('words.login')","で呼び出せますが、なぜかLaravelではJSONのカスケード配下のキーを呼び出すことができません。",[13,1765,1766],{},"そのため",[61,1768,1769,1772],{},[64,1770,1771],{},"Laravel以外のアプリへの提供→JSON",[64,1773,1774],{},"Laravelの多言語対応→PHP",[13,1776,1777],{},"という２重管理でそれぞれ設定しないといけなくなり、非効率的です。",[51,1779,1780],{"id":1780},"解決方法",[13,1782,1783,1784,1787],{},"解決方法としてはPHPで作成した配列をJSONにダンプすることです。そうすることでPHPファイルとJSONの言語ファイルを作成することができます。そこで今回の記事ではPHPの言語ファイルをカスケードさせたJSONに出力するような",[1072,1785,1786],{},"artisan","コマンドを作ってみたいと思います。",[51,1789,1790],{"id":1790},"コマンドの実装",[104,1792,1793],{"id":1793},"対応する言語ディレクトリ",[13,1795,1796,1797,1591,1799,1595,1801,1803],{},"まずはLaravelの言語ファイルのドキュメントの通り、",[1072,1798,1590],{},[1072,1800,1594],{},[1072,1802,1598],{},"のようなディレクトリを作成しておきましょう。今回は英語と日本語にしておきます。",[1304,1805,1810],{"className":1806,"code":1808,"language":1809},[1807],"language-text","resources\n├── lang\n│   ├── en\n│   │   ├── auth.php\n│   │   ├── words.php\n│   │   └── exceptions.php\n│   └── ja\n│       ├── auth.php\n│       ├── words.php\n│       └── exceptions.php\n...\n","text",[1072,1811,1808],{"__ignoreMap":1100},[13,1813,1814],{},"そしてそのディレクトリごとにphpファイルを分けます。とりあえずこのようにしておきます。",[104,1816,1817],{"id":1817},"コマンドのファイルを作成",[13,1819,1820],{},"それではカスタムのコマンドを作成しましょう。",[1304,1822,1825],{"className":1823,"code":1824,"language":1809},[1807],"php artisan make:command Dumplang\n",[1072,1826,1824],{"__ignoreMap":1100},[13,1828,1829,1830,1833],{},"コマンドもartisanで作成できます。",[1072,1831,1832],{},"app\u002FConsole\u002FCommands","というディレクトリが作成され、そこにファイルが作成されます。",[1304,1835,1837],{"className":1438,"code":1836,"language":1440,"meta":1100,"style":1100},"\u003C?php\n\nnamespace App\\Console\\Commands;\n\nuse Illuminate\\Console\\Command;\n\nclass Dumblang extends Command\n{\n    \u002F**\n     * The name and signature of the console command.\n     *\n     * @var string\n     *\u002F\n    protected $signature = 'lang:dump';\n\n    \u002F**\n     * The console command description.\n     *\n     * @var string\n     *\u002F\n    protected $description = 'Convert each php lang files to JSON files.';\n\n    \u002F**\n     * Create a new command instance.\n     *\n     * @return void\n     *\u002F\n    public function __construct()\n    {\n        parent::__construct();\n    }\n\n    \u002F**\n     * Execute the console command.\n     *\n     * @return int\n     *\u002F\n    public function handle()\n    {\n        \n    }\n}\n",[1072,1838,1839,1843,1848,1853,1857,1862,1866,1872,1877,1883,1889,1895,1901,1907,1913,1918,1923,1929,1934,1939,1944,1950,1955,1960,1966,1971,1977,1982,1988,1994,2000,2005,2010,2015,2021,2026,2032,2037,2043,2048,2054,2059],{"__ignoreMap":1100},[327,1840,1841],{"class":1313,"line":1314},[327,1842,1610],{},[327,1844,1845],{"class":1313,"line":1104},[327,1846,1847],{"emptyLinePlaceholder":1151},"\n",[327,1849,1850],{"class":1313,"line":1101},[327,1851,1852],{},"namespace App\\Console\\Commands;\n",[327,1854,1855],{"class":1313,"line":1111},[327,1856,1847],{"emptyLinePlaceholder":1151},[327,1858,1859],{"class":1313,"line":1465},[327,1860,1861],{},"use Illuminate\\Console\\Command;\n",[327,1863,1864],{"class":1313,"line":1471},[327,1865,1847],{"emptyLinePlaceholder":1151},[327,1867,1869],{"class":1313,"line":1868},7,[327,1870,1871],{},"class Dumblang extends Command\n",[327,1873,1875],{"class":1313,"line":1874},8,[327,1876,1690],{},[327,1878,1880],{"class":1313,"line":1879},9,[327,1881,1882],{},"    \u002F**\n",[327,1884,1886],{"class":1313,"line":1885},10,[327,1887,1888],{},"     * The name and signature of the console command.\n",[327,1890,1892],{"class":1313,"line":1891},11,[327,1893,1894],{},"     *\n",[327,1896,1898],{"class":1313,"line":1897},12,[327,1899,1900],{},"     * @var string\n",[327,1902,1904],{"class":1313,"line":1903},13,[327,1905,1906],{},"     *\u002F\n",[327,1908,1910],{"class":1313,"line":1909},14,[327,1911,1912],{},"    protected $signature = 'lang:dump';\n",[327,1914,1916],{"class":1313,"line":1915},15,[327,1917,1847],{"emptyLinePlaceholder":1151},[327,1919,1921],{"class":1313,"line":1920},16,[327,1922,1882],{},[327,1924,1926],{"class":1313,"line":1925},17,[327,1927,1928],{},"     * The console command description.\n",[327,1930,1932],{"class":1313,"line":1931},18,[327,1933,1894],{},[327,1935,1937],{"class":1313,"line":1936},19,[327,1938,1900],{},[327,1940,1942],{"class":1313,"line":1941},20,[327,1943,1906],{},[327,1945,1947],{"class":1313,"line":1946},21,[327,1948,1949],{},"    protected $description = 'Convert each php lang files to JSON files.';\n",[327,1951,1953],{"class":1313,"line":1952},22,[327,1954,1847],{"emptyLinePlaceholder":1151},[327,1956,1958],{"class":1313,"line":1957},23,[327,1959,1882],{},[327,1961,1963],{"class":1313,"line":1962},24,[327,1964,1965],{},"     * Create a new command instance.\n",[327,1967,1969],{"class":1313,"line":1968},25,[327,1970,1894],{},[327,1972,1974],{"class":1313,"line":1973},26,[327,1975,1976],{},"     * @return void\n",[327,1978,1980],{"class":1313,"line":1979},27,[327,1981,1906],{},[327,1983,1985],{"class":1313,"line":1984},28,[327,1986,1987],{},"    public function __construct()\n",[327,1989,1991],{"class":1313,"line":1990},29,[327,1992,1993],{},"    {\n",[327,1995,1997],{"class":1313,"line":1996},30,[327,1998,1999],{},"        parent::__construct();\n",[327,2001,2003],{"class":1313,"line":2002},31,[327,2004,1752],{},[327,2006,2008],{"class":1313,"line":2007},32,[327,2009,1847],{"emptyLinePlaceholder":1151},[327,2011,2013],{"class":1313,"line":2012},33,[327,2014,1882],{},[327,2016,2018],{"class":1313,"line":2017},34,[327,2019,2020],{},"     * Execute the console command.\n",[327,2022,2024],{"class":1313,"line":2023},35,[327,2025,1894],{},[327,2027,2029],{"class":1313,"line":2028},36,[327,2030,2031],{},"     * @return int\n",[327,2033,2035],{"class":1313,"line":2034},37,[327,2036,1906],{},[327,2038,2040],{"class":1313,"line":2039},38,[327,2041,2042],{},"    public function handle()\n",[327,2044,2046],{"class":1313,"line":2045},39,[327,2047,1993],{},[327,2049,2051],{"class":1313,"line":2050},40,[327,2052,2053],{},"        \n",[327,2055,2057],{"class":1313,"line":2056},41,[327,2058,1752],{},[327,2060,2062],{"class":1313,"line":2061},42,[327,2063,1474],{},[13,2065,2066,2067,2070],{},"まずは上記のように、シグネチャと説明を記述します。シグネチャでは",[1072,2068,2069],{},"lang:dump"," とすると",[1304,2072,2075],{"className":2073,"code":2074,"language":1809},[1807],"php artisan lang:dump\n",[1072,2076,2074],{"__ignoreMap":1100},[13,2078,2079],{},"と入力するとこのコマンドを実行できます。",[104,2081,2082],{"id":2082},"言語ディレクトリからファイルを読み込む",[13,2084,2085,2086,2089],{},"コマンドの内容は",[1072,2087,2088],{},"handle()","に記述します。全体は以下の通りです。",[1304,2091,2093],{"className":1438,"code":2092,"language":1440,"meta":1100,"style":1100},"use Illuminate\\Support\\Facades\\File;\nuse Illuminate\\Support\\Facades\\Lang;\n\npublic function handle()\n{\n    $suports = [\"ja\",\"en\"];\n    \n    foreach($suports as $lng){\n        $langdir = resource_path('lang\u002F'.$lng);\n        if(is_dir($langdir)){\n            $files = scandir($langdir);\n            if($files === false) continue;\n\n            $files = array_filter($files,function($f){\n                return strpos($f,'.php') !== false;\n            });\n\n            $trans = [];\n            foreach($files as $f){\n                $content_key = str_replace('.php','',$f);\n                $content = include resource_path(\"lang\u002F$lng\u002F$f\");\n                $trans[$content_key] = $content;\n            }\n\n            $json = json_encode($trans,JSON_UNESCAPED_UNICODE);\n            File::put(resource_path('lang\u002F'.$lng.'.json'),$json);\n            File::put(base_path('nuxt\u002Flang\u002F'.$lng.'.json'),$json);\n        }else{\n            print(\"Lang directory for ${$lng} dose not exisits\");\n        }\n    }\n    return Command::SUCCESS;\n}\n",[1072,2094,2095,2100,2105,2109,2114,2118,2123,2128,2133,2138,2143,2148,2153,2157,2162,2167,2172,2176,2181,2186,2191,2196,2201,2206,2210,2215,2220,2225,2230,2235,2240,2244,2249],{"__ignoreMap":1100},[327,2096,2097],{"class":1313,"line":1314},[327,2098,2099],{},"use Illuminate\\Support\\Facades\\File;\n",[327,2101,2102],{"class":1313,"line":1104},[327,2103,2104],{},"use Illuminate\\Support\\Facades\\Lang;\n",[327,2106,2107],{"class":1313,"line":1101},[327,2108,1847],{"emptyLinePlaceholder":1151},[327,2110,2111],{"class":1313,"line":1111},[327,2112,2113],{},"public function handle()\n",[327,2115,2116],{"class":1313,"line":1465},[327,2117,1690],{},[327,2119,2120],{"class":1313,"line":1471},[327,2121,2122],{},"    $suports = [\"ja\",\"en\"];\n",[327,2124,2125],{"class":1313,"line":1868},[327,2126,2127],{},"    \n",[327,2129,2130],{"class":1313,"line":1874},[327,2131,2132],{},"    foreach($suports as $lng){\n",[327,2134,2135],{"class":1313,"line":1879},[327,2136,2137],{},"        $langdir = resource_path('lang\u002F'.$lng);\n",[327,2139,2140],{"class":1313,"line":1885},[327,2141,2142],{},"        if(is_dir($langdir)){\n",[327,2144,2145],{"class":1313,"line":1891},[327,2146,2147],{},"            $files = scandir($langdir);\n",[327,2149,2150],{"class":1313,"line":1897},[327,2151,2152],{},"            if($files === false) continue;\n",[327,2154,2155],{"class":1313,"line":1903},[327,2156,1847],{"emptyLinePlaceholder":1151},[327,2158,2159],{"class":1313,"line":1909},[327,2160,2161],{},"            $files = array_filter($files,function($f){\n",[327,2163,2164],{"class":1313,"line":1915},[327,2165,2166],{},"                return strpos($f,'.php') !== false;\n",[327,2168,2169],{"class":1313,"line":1920},[327,2170,2171],{},"            });\n",[327,2173,2174],{"class":1313,"line":1925},[327,2175,1847],{"emptyLinePlaceholder":1151},[327,2177,2178],{"class":1313,"line":1931},[327,2179,2180],{},"            $trans = [];\n",[327,2182,2183],{"class":1313,"line":1936},[327,2184,2185],{},"            foreach($files as $f){\n",[327,2187,2188],{"class":1313,"line":1941},[327,2189,2190],{},"                $content_key = str_replace('.php','',$f);\n",[327,2192,2193],{"class":1313,"line":1946},[327,2194,2195],{},"                $content = include resource_path(\"lang\u002F$lng\u002F$f\");\n",[327,2197,2198],{"class":1313,"line":1952},[327,2199,2200],{},"                $trans[$content_key] = $content;\n",[327,2202,2203],{"class":1313,"line":1957},[327,2204,2205],{},"            }\n",[327,2207,2208],{"class":1313,"line":1962},[327,2209,1847],{"emptyLinePlaceholder":1151},[327,2211,2212],{"class":1313,"line":1968},[327,2213,2214],{},"            $json = json_encode($trans,JSON_UNESCAPED_UNICODE);\n",[327,2216,2217],{"class":1313,"line":1973},[327,2218,2219],{},"            File::put(resource_path('lang\u002F'.$lng.'.json'),$json);\n",[327,2221,2222],{"class":1313,"line":1979},[327,2223,2224],{},"            File::put(base_path('nuxt\u002Flang\u002F'.$lng.'.json'),$json);\n",[327,2226,2227],{"class":1313,"line":1984},[327,2228,2229],{},"        }else{\n",[327,2231,2232],{"class":1313,"line":1990},[327,2233,2234],{},"            print(\"Lang directory for ${$lng} dose not exisits\");\n",[327,2236,2237],{"class":1313,"line":1996},[327,2238,2239],{},"        }\n",[327,2241,2242],{"class":1313,"line":2002},[327,2243,1752],{},[327,2245,2246],{"class":1313,"line":2007},[327,2247,2248],{},"    return Command::SUCCESS;\n",[327,2250,2251],{"class":1313,"line":2012},[327,2252,1474],{},[1304,2254,2256],{"className":1438,"code":2255,"language":1440,"meta":1100,"style":1100},"$suports = [\"ja\",\"en\"];\n\nforeach($suports as $lng){\n\n}\n",[1072,2257,2258,2263,2267,2272,2276],{"__ignoreMap":1100},[327,2259,2260],{"class":1313,"line":1314},[327,2261,2262],{},"$suports = [\"ja\",\"en\"];\n",[327,2264,2265],{"class":1313,"line":1104},[327,2266,1847],{"emptyLinePlaceholder":1151},[327,2268,2269],{"class":1313,"line":1101},[327,2270,2271],{},"foreach($suports as $lng){\n",[327,2273,2274],{"class":1313,"line":1111},[327,2275,1847],{"emptyLinePlaceholder":1151},[327,2277,2278],{"class":1313,"line":1465},[327,2279,1474],{},[13,2281,2282],{},"まずは取得予定の言語の配列を用意します。それを再帰的に処理します。",[13,2284,2285],{},"元となる言語ディレクトリからファイルの一覧を取得します。",[1304,2287,2289],{"className":1438,"code":2288,"language":1440,"meta":1100,"style":1100},"foreach($suports as $lng){\n    $langdir = resource_path('lang\u002F'.$lng);\n    if(is_dir($langdir)){\n        $files = scandir($langdir);\n        if($files === false) continue;\n\n        $files = array_filter($files,function($f){\n            return strpos($f,'.php') !== false;\n        });\n\n        $trans = [];\n        foreach($files as $f){\n            $content_key = str_replace('.php','',$f);\n            $content = include resource_path(\"lang\u002F$lng\u002F$f\");\n            $trans[$content_key] = $content;\n        }\n\n        \u002F\u002F ....\n    }\n}\n",[1072,2290,2291,2295,2300,2305,2310,2315,2319,2324,2329,2334,2338,2343,2348,2353,2358,2363,2367,2371,2376,2380],{"__ignoreMap":1100},[327,2292,2293],{"class":1313,"line":1314},[327,2294,2271],{},[327,2296,2297],{"class":1313,"line":1104},[327,2298,2299],{},"    $langdir = resource_path('lang\u002F'.$lng);\n",[327,2301,2302],{"class":1313,"line":1101},[327,2303,2304],{},"    if(is_dir($langdir)){\n",[327,2306,2307],{"class":1313,"line":1111},[327,2308,2309],{},"        $files = scandir($langdir);\n",[327,2311,2312],{"class":1313,"line":1465},[327,2313,2314],{},"        if($files === false) continue;\n",[327,2316,2317],{"class":1313,"line":1471},[327,2318,1847],{"emptyLinePlaceholder":1151},[327,2320,2321],{"class":1313,"line":1868},[327,2322,2323],{},"        $files = array_filter($files,function($f){\n",[327,2325,2326],{"class":1313,"line":1874},[327,2327,2328],{},"            return strpos($f,'.php') !== false;\n",[327,2330,2331],{"class":1313,"line":1879},[327,2332,2333],{},"        });\n",[327,2335,2336],{"class":1313,"line":1885},[327,2337,1847],{"emptyLinePlaceholder":1151},[327,2339,2340],{"class":1313,"line":1891},[327,2341,2342],{},"        $trans = [];\n",[327,2344,2345],{"class":1313,"line":1897},[327,2346,2347],{},"        foreach($files as $f){\n",[327,2349,2350],{"class":1313,"line":1903},[327,2351,2352],{},"            $content_key = str_replace('.php','',$f);\n",[327,2354,2355],{"class":1313,"line":1909},[327,2356,2357],{},"            $content = include resource_path(\"lang\u002F$lng\u002F$f\");\n",[327,2359,2360],{"class":1313,"line":1915},[327,2361,2362],{},"            $trans[$content_key] = $content;\n",[327,2364,2365],{"class":1313,"line":1920},[327,2366,2239],{},[327,2368,2369],{"class":1313,"line":1925},[327,2370,1847],{"emptyLinePlaceholder":1151},[327,2372,2373],{"class":1313,"line":1931},[327,2374,2375],{},"        \u002F\u002F ....\n",[327,2377,2378],{"class":1313,"line":1936},[327,2379,1752],{},[327,2381,2382],{"class":1313,"line":1941},[327,2383,1474],{},[13,2385,2386,2389,2390,2393,2394,2396,2397,2399,2400,2403],{},[1072,2387,2388],{},"resource_path('lang\u002F'.$lng)","で先程の言語ディレクトリのパスを取得し、",[1072,2391,2392],{},"scandir()","を用いて内部のファイルを配列で取得します。",[1072,2395,2392],{},"はphp以外のファイルや",[1072,2398,1322],{},"みたいなSELinuxが勝手に作る謎ファイルも読み取ってしまうので、",[1072,2401,2402],{},"array_filter()","を用いてフィルターします。",[104,2405,2407],{"id":2406},"一つの配列に打ち込んでjson化する","一つの配列に打ち込んでJSON化する",[1304,2409,2411],{"className":1438,"code":2410,"language":1440,"meta":1100,"style":1100},"$trans = [];\nforeach($files as $f){\n    $content_key = str_replace('.php','',$f);\n    $content = include resource_path(\"lang\u002F$lng\u002F$f\");\n    $trans[$content_key] = $content;\n}\n",[1072,2412,2413,2418,2423,2428,2433,2438],{"__ignoreMap":1100},[327,2414,2415],{"class":1313,"line":1314},[327,2416,2417],{},"$trans = [];\n",[327,2419,2420],{"class":1313,"line":1104},[327,2421,2422],{},"foreach($files as $f){\n",[327,2424,2425],{"class":1313,"line":1101},[327,2426,2427],{},"    $content_key = str_replace('.php','',$f);\n",[327,2429,2430],{"class":1313,"line":1111},[327,2431,2432],{},"    $content = include resource_path(\"lang\u002F$lng\u002F$f\");\n",[327,2434,2435],{"class":1313,"line":1465},[327,2436,2437],{},"    $trans[$content_key] = $content;\n",[327,2439,2440],{"class":1313,"line":1471},[327,2441,1474],{},[13,2443,2444,2445,2448,2449,2452,2453,2456],{},"JSONではphpファイル名を一次キーとして利用したいので ",[1072,2446,2447],{},"str_replace('.php','',$f)","でファイル名を取得します。",[1072,2450,2451],{},"include resource_path(\"lang\u002F$lng\u002F$f\")","でphpファイルの記述を取得ます。そしてJSONにする配列に、ファイル名をキーとして打ち込みます。",[1072,2454,2455],{},"$trans[$content_key] = $content;"," それをスキャンした言語PHPファイル全てに行います。",[104,2458,2459],{"id":2459},"ファイルを出力",[1304,2461,2463],{"className":1438,"code":2462,"language":1440,"meta":1100,"style":1100},"$json = json_encode($trans,JSON_UNESCAPED_UNICODE);\nFile::put(resource_path('lang\u002F'.$lng.'.json'),$json);\n",[1072,2464,2465,2470],{"__ignoreMap":1100},[327,2466,2467],{"class":1313,"line":1314},[327,2468,2469],{},"$json = json_encode($trans,JSON_UNESCAPED_UNICODE);\n",[327,2471,2472],{"class":1313,"line":1104},[327,2473,2474],{},"File::put(resource_path('lang\u002F'.$lng.'.json'),$json);\n",[13,2476,2477,2478,2481,2482,2484,2485,2488],{},"そして１つにまとめた配列を",[1072,2479,2480],{},"json_encode","をしておき、それを",[1072,2483,1590],{}," 配下します。その際には",[1072,2486,2487],{},"言語名.json","となるようにしておきます。",[51,2490,2491],{"id":2491},"全容と実行",[13,2493,2494],{},"コードは以下の通りです。",[1304,2496,2498],{"className":1438,"code":2497,"language":1440,"meta":1100,"style":1100},"\u003C?php\n\nnamespace App\\Console\\Commands;\n\nuse Illuminate\\Console\\Command;\nuse Illuminate\\Support\\Facades\\File;\nuse Illuminate\\Support\\Facades\\Lang;\n\nclass Dumblang extends Command\n{\n    \u002F**\n     * The name and signature of the console command.\n     *\n     * @var string\n     *\u002F\n    protected $signature = 'lang:dump';\n\n    \u002F**\n     * The console command description.\n     *\n     * @var string\n     *\u002F\n    protected $description = 'Convert each php lang files to JSON files.';\n\n    \u002F**\n     * Create a new command instance.\n     *\n     * @return void\n     *\u002F\n    public function __construct()\n    {\n        parent::__construct();\n    }\n\n    \u002F**\n     * Execute the console command.\n     *\n     * @return int\n     *\u002F\n    public function handle()\n    {\n        $suports = config('app.support_langs');\n        \n        foreach($suports as $lng){\n            $langdir = resource_path('lang\u002F'.$lng);\n            if(is_dir($langdir)){\n                $files = scandir($langdir);\n                if($files === false) continue;\n\n                $files = array_filter($files,function($f){\n                    return strpos($f,'.php') !== false;\n                });\n\n                $trans = [];\n                foreach($files as $f){\n                    $content_key = str_replace('.php','',$f);\n                    $content = include resource_path(\"lang\u002F$lng\u002F$f\");\n                    $trans[$content_key] = $content;\n                }\n\n                $json = json_encode($trans,JSON_UNESCAPED_UNICODE);\n                File::put(resource_path('lang\u002F'.$lng.'.json'),$json);\n            }else{\n                print(\"Lang directory for ${$lng} dose not exisits\");\n            }\n        }\n        return Command::SUCCESS;\n    }\n}\n",[1072,2499,2500,2504,2508,2512,2516,2520,2524,2528,2532,2536,2540,2544,2548,2552,2556,2560,2564,2568,2572,2576,2580,2584,2588,2592,2596,2600,2604,2608,2612,2616,2620,2624,2628,2632,2636,2640,2644,2648,2652,2656,2660,2664,2669,2674,2680,2686,2692,2698,2704,2709,2715,2721,2727,2732,2738,2744,2750,2756,2762,2768,2773,2779,2785,2790,2796,2801,2806,2812,2817],{"__ignoreMap":1100},[327,2501,2502],{"class":1313,"line":1314},[327,2503,1610],{},[327,2505,2506],{"class":1313,"line":1104},[327,2507,1847],{"emptyLinePlaceholder":1151},[327,2509,2510],{"class":1313,"line":1101},[327,2511,1852],{},[327,2513,2514],{"class":1313,"line":1111},[327,2515,1847],{"emptyLinePlaceholder":1151},[327,2517,2518],{"class":1313,"line":1465},[327,2519,1861],{},[327,2521,2522],{"class":1313,"line":1471},[327,2523,2099],{},[327,2525,2526],{"class":1313,"line":1868},[327,2527,2104],{},[327,2529,2530],{"class":1313,"line":1874},[327,2531,1847],{"emptyLinePlaceholder":1151},[327,2533,2534],{"class":1313,"line":1879},[327,2535,1871],{},[327,2537,2538],{"class":1313,"line":1885},[327,2539,1690],{},[327,2541,2542],{"class":1313,"line":1891},[327,2543,1882],{},[327,2545,2546],{"class":1313,"line":1897},[327,2547,1888],{},[327,2549,2550],{"class":1313,"line":1903},[327,2551,1894],{},[327,2553,2554],{"class":1313,"line":1909},[327,2555,1900],{},[327,2557,2558],{"class":1313,"line":1915},[327,2559,1906],{},[327,2561,2562],{"class":1313,"line":1920},[327,2563,1912],{},[327,2565,2566],{"class":1313,"line":1925},[327,2567,1847],{"emptyLinePlaceholder":1151},[327,2569,2570],{"class":1313,"line":1931},[327,2571,1882],{},[327,2573,2574],{"class":1313,"line":1936},[327,2575,1928],{},[327,2577,2578],{"class":1313,"line":1941},[327,2579,1894],{},[327,2581,2582],{"class":1313,"line":1946},[327,2583,1900],{},[327,2585,2586],{"class":1313,"line":1952},[327,2587,1906],{},[327,2589,2590],{"class":1313,"line":1957},[327,2591,1949],{},[327,2593,2594],{"class":1313,"line":1962},[327,2595,1847],{"emptyLinePlaceholder":1151},[327,2597,2598],{"class":1313,"line":1968},[327,2599,1882],{},[327,2601,2602],{"class":1313,"line":1973},[327,2603,1965],{},[327,2605,2606],{"class":1313,"line":1979},[327,2607,1894],{},[327,2609,2610],{"class":1313,"line":1984},[327,2611,1976],{},[327,2613,2614],{"class":1313,"line":1990},[327,2615,1906],{},[327,2617,2618],{"class":1313,"line":1996},[327,2619,1987],{},[327,2621,2622],{"class":1313,"line":2002},[327,2623,1993],{},[327,2625,2626],{"class":1313,"line":2007},[327,2627,1999],{},[327,2629,2630],{"class":1313,"line":2012},[327,2631,1752],{},[327,2633,2634],{"class":1313,"line":2017},[327,2635,1847],{"emptyLinePlaceholder":1151},[327,2637,2638],{"class":1313,"line":2023},[327,2639,1882],{},[327,2641,2642],{"class":1313,"line":2028},[327,2643,2020],{},[327,2645,2646],{"class":1313,"line":2034},[327,2647,1894],{},[327,2649,2650],{"class":1313,"line":2039},[327,2651,2031],{},[327,2653,2654],{"class":1313,"line":2045},[327,2655,1906],{},[327,2657,2658],{"class":1313,"line":2050},[327,2659,2042],{},[327,2661,2662],{"class":1313,"line":2056},[327,2663,1993],{},[327,2665,2666],{"class":1313,"line":2061},[327,2667,2668],{},"        $suports = config('app.support_langs');\n",[327,2670,2672],{"class":1313,"line":2671},43,[327,2673,2053],{},[327,2675,2677],{"class":1313,"line":2676},44,[327,2678,2679],{},"        foreach($suports as $lng){\n",[327,2681,2683],{"class":1313,"line":2682},45,[327,2684,2685],{},"            $langdir = resource_path('lang\u002F'.$lng);\n",[327,2687,2689],{"class":1313,"line":2688},46,[327,2690,2691],{},"            if(is_dir($langdir)){\n",[327,2693,2695],{"class":1313,"line":2694},47,[327,2696,2697],{},"                $files = scandir($langdir);\n",[327,2699,2701],{"class":1313,"line":2700},48,[327,2702,2703],{},"                if($files === false) continue;\n",[327,2705,2707],{"class":1313,"line":2706},49,[327,2708,1847],{"emptyLinePlaceholder":1151},[327,2710,2712],{"class":1313,"line":2711},50,[327,2713,2714],{},"                $files = array_filter($files,function($f){\n",[327,2716,2718],{"class":1313,"line":2717},51,[327,2719,2720],{},"                    return strpos($f,'.php') !== false;\n",[327,2722,2724],{"class":1313,"line":2723},52,[327,2725,2726],{},"                });\n",[327,2728,2730],{"class":1313,"line":2729},53,[327,2731,1847],{"emptyLinePlaceholder":1151},[327,2733,2735],{"class":1313,"line":2734},54,[327,2736,2737],{},"                $trans = [];\n",[327,2739,2741],{"class":1313,"line":2740},55,[327,2742,2743],{},"                foreach($files as $f){\n",[327,2745,2747],{"class":1313,"line":2746},56,[327,2748,2749],{},"                    $content_key = str_replace('.php','',$f);\n",[327,2751,2753],{"class":1313,"line":2752},57,[327,2754,2755],{},"                    $content = include resource_path(\"lang\u002F$lng\u002F$f\");\n",[327,2757,2759],{"class":1313,"line":2758},58,[327,2760,2761],{},"                    $trans[$content_key] = $content;\n",[327,2763,2765],{"class":1313,"line":2764},59,[327,2766,2767],{},"                }\n",[327,2769,2771],{"class":1313,"line":2770},60,[327,2772,1847],{"emptyLinePlaceholder":1151},[327,2774,2776],{"class":1313,"line":2775},61,[327,2777,2778],{},"                $json = json_encode($trans,JSON_UNESCAPED_UNICODE);\n",[327,2780,2782],{"class":1313,"line":2781},62,[327,2783,2784],{},"                File::put(resource_path('lang\u002F'.$lng.'.json'),$json);\n",[327,2786,2787],{"class":1313,"line":4},[327,2788,2789],{},"            }else{\n",[327,2791,2793],{"class":1313,"line":2792},64,[327,2794,2795],{},"                print(\"Lang directory for ${$lng} dose not exisits\");\n",[327,2797,2799],{"class":1313,"line":2798},65,[327,2800,2205],{},[327,2802,2804],{"class":1313,"line":2803},66,[327,2805,2239],{},[327,2807,2809],{"class":1313,"line":2808},67,[327,2810,2811],{},"        return Command::SUCCESS;\n",[327,2813,2815],{"class":1313,"line":2814},68,[327,2816,1752],{},[327,2818,2820],{"class":1313,"line":2819},69,[327,2821,1474],{},[13,2823,2824],{},"ちょっと気をつける点としては",[61,2826,2827,2833],{},[64,2828,2829,2832],{},[1072,2830,2831],{},"is_dir($langdir)"," で言語ディレクトリの存在チェック",[64,2834,2835,2837],{},[1072,2836,2392],{}," で取得したファイルをフィルタする",[13,2839,2840,2841,1595,2843,2845],{},"実行してみると",[1072,2842,1664],{},[1072,2844,1661],{},"というのが作成され、みてみると",[1304,2847,2849],{"className":2848,"code":1808,"language":1809},[1807],[1072,2850,1808],{"__ignoreMap":1100},[13,2852,2853],{},"↓",[1304,2855,2857],{"className":1681,"code":2856,"filename":1664,"language":1683,"meta":1100,"style":1100},"{\n    \"auth\":{\n        \"login\":\"ログイン\",\n        \"logout\":\"ログアウト\",\n    },\n    \"words\":{\n        \"save\":\"保存\",\n        \"update\":\"更新\"\n    },\n    \"exceptions\":{\n        \"401\":\"ログインしてください。\",\n        \"model\":{\n            \"403\":\"このデータにアクセスできません。\",\n            \"404\":\"対応するデータが見つかりません。\"\n        }\n    }\n}\n",[1072,2858,2859,2863,2874,2892,2910,2915,2925,2945,2963,2967,2978,2998,3009,3031,3049,3053,3057],{"__ignoreMap":1100},[327,2860,2861],{"class":1313,"line":1314},[327,2862,1690],{"class":1321},[327,2864,2865,2867,2870,2872],{"class":1313,"line":1104},[327,2866,1695],{"class":1321},[327,2868,2869],{"class":1698},"auth",[327,2871,1332],{"class":1321},[327,2873,1704],{"class":1321},[327,2875,2876,2878,2880,2882,2884,2886,2888,2890],{"class":1313,"line":1101},[327,2877,1709],{"class":1321},[327,2879,1713],{"class":1712},[327,2881,1332],{"class":1321},[327,2883,1718],{"class":1321},[327,2885,1332],{"class":1321},[327,2887,1723],{"class":1335},[327,2889,1332],{"class":1321},[327,2891,1728],{"class":1321},[327,2893,2894,2896,2898,2900,2902,2904,2906,2908],{"class":1313,"line":1111},[327,2895,1709],{"class":1321},[327,2897,1735],{"class":1712},[327,2899,1332],{"class":1321},[327,2901,1718],{"class":1321},[327,2903,1332],{"class":1321},[327,2905,1744],{"class":1335},[327,2907,1332],{"class":1321},[327,2909,1728],{"class":1321},[327,2911,2912],{"class":1313,"line":1465},[327,2913,2914],{"class":1321},"    },\n",[327,2916,2917,2919,2921,2923],{"class":1313,"line":1471},[327,2918,1695],{"class":1321},[327,2920,1699],{"class":1698},[327,2922,1332],{"class":1321},[327,2924,1704],{"class":1321},[327,2926,2927,2929,2932,2934,2936,2938,2941,2943],{"class":1313,"line":1868},[327,2928,1709],{"class":1321},[327,2930,2931],{"class":1712},"save",[327,2933,1332],{"class":1321},[327,2935,1718],{"class":1321},[327,2937,1332],{"class":1321},[327,2939,2940],{"class":1335},"保存",[327,2942,1332],{"class":1321},[327,2944,1728],{"class":1321},[327,2946,2947,2949,2952,2954,2956,2958,2961],{"class":1313,"line":1874},[327,2948,1709],{"class":1321},[327,2950,2951],{"class":1712},"update",[327,2953,1332],{"class":1321},[327,2955,1718],{"class":1321},[327,2957,1332],{"class":1321},[327,2959,2960],{"class":1335},"更新",[327,2962,1747],{"class":1321},[327,2964,2965],{"class":1313,"line":1879},[327,2966,2914],{"class":1321},[327,2968,2969,2971,2974,2976],{"class":1313,"line":1885},[327,2970,1695],{"class":1321},[327,2972,2973],{"class":1698},"exceptions",[327,2975,1332],{"class":1321},[327,2977,1704],{"class":1321},[327,2979,2980,2982,2985,2987,2989,2991,2994,2996],{"class":1313,"line":1891},[327,2981,1709],{"class":1321},[327,2983,2984],{"class":1712},"401",[327,2986,1332],{"class":1321},[327,2988,1718],{"class":1321},[327,2990,1332],{"class":1321},[327,2992,2993],{"class":1335},"ログインしてください。",[327,2995,1332],{"class":1321},[327,2997,1728],{"class":1321},[327,2999,3000,3002,3005,3007],{"class":1313,"line":1897},[327,3001,1709],{"class":1321},[327,3003,3004],{"class":1712},"model",[327,3006,1332],{"class":1321},[327,3008,1704],{"class":1321},[327,3010,3011,3014,3018,3020,3022,3024,3027,3029],{"class":1313,"line":1903},[327,3012,3013],{"class":1321},"            \"",[327,3015,3017],{"class":3016},"sx098","403",[327,3019,1332],{"class":1321},[327,3021,1718],{"class":1321},[327,3023,1332],{"class":1321},[327,3025,3026],{"class":1335},"このデータにアクセスできません。",[327,3028,1332],{"class":1321},[327,3030,1728],{"class":1321},[327,3032,3033,3035,3038,3040,3042,3044,3047],{"class":1313,"line":1909},[327,3034,3013],{"class":1321},[327,3036,3037],{"class":3016},"404",[327,3039,1332],{"class":1321},[327,3041,1718],{"class":1321},[327,3043,1332],{"class":1321},[327,3045,3046],{"class":1335},"対応するデータが見つかりません。",[327,3048,1747],{"class":1321},[327,3050,3051],{"class":1313,"line":1915},[327,3052,2239],{"class":1321},[327,3054,3055],{"class":1313,"line":1920},[327,3056,1752],{"class":1321},[327,3058,3059],{"class":1313,"line":1925},[327,3060,1474],{"class":1321},[1304,3062,3064],{"className":1681,"code":3063,"filename":1661,"language":1683,"meta":1100,"style":1100},"{\n    \"auth\":{\n        \"login\":\"Login\",\n        \"logout\":\"Logout\",\n    },\n    \"words\":{\n        \"save\":\"Saving\",\n        \"update\":\"Updating\"\n    },\n    \"exceptions\":{\n        \"401\":\"Please login.\",\n        \"model\":{\n            \"403\":\"You can not access to this data.\",\n            \"404\":\"The data you request is not found.\"\n        }\n    }\n}\n",[1072,3065,3066,3070,3080,3099,3118,3122,3132,3151,3168,3172,3182,3201,3211,3230,3247,3251,3255],{"__ignoreMap":1100},[327,3067,3068],{"class":1313,"line":1314},[327,3069,1690],{"class":1321},[327,3071,3072,3074,3076,3078],{"class":1313,"line":1104},[327,3073,1695],{"class":1321},[327,3075,2869],{"class":1698},[327,3077,1332],{"class":1321},[327,3079,1704],{"class":1321},[327,3081,3082,3084,3086,3088,3090,3092,3095,3097],{"class":1313,"line":1101},[327,3083,1709],{"class":1321},[327,3085,1713],{"class":1712},[327,3087,1332],{"class":1321},[327,3089,1718],{"class":1321},[327,3091,1332],{"class":1321},[327,3093,3094],{"class":1335},"Login",[327,3096,1332],{"class":1321},[327,3098,1728],{"class":1321},[327,3100,3101,3103,3105,3107,3109,3111,3114,3116],{"class":1313,"line":1111},[327,3102,1709],{"class":1321},[327,3104,1735],{"class":1712},[327,3106,1332],{"class":1321},[327,3108,1718],{"class":1321},[327,3110,1332],{"class":1321},[327,3112,3113],{"class":1335},"Logout",[327,3115,1332],{"class":1321},[327,3117,1728],{"class":1321},[327,3119,3120],{"class":1313,"line":1465},[327,3121,2914],{"class":1321},[327,3123,3124,3126,3128,3130],{"class":1313,"line":1471},[327,3125,1695],{"class":1321},[327,3127,1699],{"class":1698},[327,3129,1332],{"class":1321},[327,3131,1704],{"class":1321},[327,3133,3134,3136,3138,3140,3142,3144,3147,3149],{"class":1313,"line":1868},[327,3135,1709],{"class":1321},[327,3137,2931],{"class":1712},[327,3139,1332],{"class":1321},[327,3141,1718],{"class":1321},[327,3143,1332],{"class":1321},[327,3145,3146],{"class":1335},"Saving",[327,3148,1332],{"class":1321},[327,3150,1728],{"class":1321},[327,3152,3153,3155,3157,3159,3161,3163,3166],{"class":1313,"line":1874},[327,3154,1709],{"class":1321},[327,3156,2951],{"class":1712},[327,3158,1332],{"class":1321},[327,3160,1718],{"class":1321},[327,3162,1332],{"class":1321},[327,3164,3165],{"class":1335},"Updating",[327,3167,1747],{"class":1321},[327,3169,3170],{"class":1313,"line":1879},[327,3171,2914],{"class":1321},[327,3173,3174,3176,3178,3180],{"class":1313,"line":1885},[327,3175,1695],{"class":1321},[327,3177,2973],{"class":1698},[327,3179,1332],{"class":1321},[327,3181,1704],{"class":1321},[327,3183,3184,3186,3188,3190,3192,3194,3197,3199],{"class":1313,"line":1891},[327,3185,1709],{"class":1321},[327,3187,2984],{"class":1712},[327,3189,1332],{"class":1321},[327,3191,1718],{"class":1321},[327,3193,1332],{"class":1321},[327,3195,3196],{"class":1335},"Please login.",[327,3198,1332],{"class":1321},[327,3200,1728],{"class":1321},[327,3202,3203,3205,3207,3209],{"class":1313,"line":1897},[327,3204,1709],{"class":1321},[327,3206,3004],{"class":1712},[327,3208,1332],{"class":1321},[327,3210,1704],{"class":1321},[327,3212,3213,3215,3217,3219,3221,3223,3226,3228],{"class":1313,"line":1903},[327,3214,3013],{"class":1321},[327,3216,3017],{"class":3016},[327,3218,1332],{"class":1321},[327,3220,1718],{"class":1321},[327,3222,1332],{"class":1321},[327,3224,3225],{"class":1335},"You can not access to this data.",[327,3227,1332],{"class":1321},[327,3229,1728],{"class":1321},[327,3231,3232,3234,3236,3238,3240,3242,3245],{"class":1313,"line":1909},[327,3233,3013],{"class":1321},[327,3235,3037],{"class":3016},[327,3237,1332],{"class":1321},[327,3239,1718],{"class":1321},[327,3241,1332],{"class":1321},[327,3243,3244],{"class":1335},"The data you request is not found.",[327,3246,1747],{"class":1321},[327,3248,3249],{"class":1313,"line":1915},[327,3250,2239],{"class":1321},[327,3252,3253],{"class":1313,"line":1920},[327,3254,1752],{"class":1321},[327,3256,3257],{"class":1313,"line":1925},[327,3258,1474],{"class":1321},[13,3260,3261,3262,3265],{},"これでPHPファイルだけでJSONの言語ファイルも作成して、フロントでのVueの言語ファイルなどに提供することができるようになります。動的にこのJSONファイルは生成するので、バージョン管理をするときは",[1072,3263,3264],{},".gitignore","で指定しておくといいです。そしてデプロイや更新時にはこのコマンドを打ち忘れないようにしましょう。",[13,3267,3268],{},"ちなみにですが、実際にやっていることは特定のディレクトリのPHPファイルの内容を取得して、それをJSONにして生成したファイルを置いているだけなので素のPHP、pythonやrubyとか他の言語でも行けると思いますよ。",[1408,3270,3271],{},"html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .sAklC, html code.shiki .sAklC{--shiki-default:#89DDFF}html pre.shiki code .sJ14y, html code.shiki .sJ14y{--shiki-default:#C792EA}html pre.shiki code .s5Dmg, html code.shiki .s5Dmg{--shiki-default:#FFCB6B}html pre.shiki code .sfyAc, html code.shiki .sfyAc{--shiki-default:#C3E88D}html pre.shiki code .sx098, html code.shiki .sx098{--shiki-default:#F78C6C}",{"title":1100,"searchDepth":1101,"depth":1101,"links":3273},[3274,3275,3276,3283],{"id":1668,"depth":1104,"text":1669},{"id":1780,"depth":1104,"text":1780},{"id":1790,"depth":1104,"text":1790,"children":3277},[3278,3279,3280,3281,3282],{"id":1793,"depth":1101,"text":1793},{"id":1817,"depth":1101,"text":1817},{"id":2082,"depth":1101,"text":2082},{"id":2406,"depth":1101,"text":2407},{"id":2459,"depth":1101,"text":2459},{"id":2491,"depth":1104,"text":2491},[1262],"2022-05-22",{},"\u002Farticles\u002Flaravel-i18n-json-dump",{"title":1582,"description":1582},"articles\u002Flaravel-i18n-json-dump",[1440,1577,1308],"tdUMQGHoIH7SI9TXJlID3MGccNOOjq6udWMjq_HTqC0",{"id":3293,"title":3294,"body":3295,"category":4366,"createdAt":4368,"description":4369,"extension":1148,"index":1149,"meta":4370,"navigation":1151,"path":4371,"publish":1151,"seo":4372,"series":1149,"seriesTitle":1149,"stem":4373,"tag":4374,"thumbnail":4376,"updatedAt":1149,"__hash__":4377},"articles\u002Farticles\u002Fnuxt-auth-middleware.md","Nuxt.jsのSSR・SPA時のフロント側の認証ビューを自前で実装する",{"type":10,"value":3296,"toc":4354},[3297,3300,3303,3306,3317,3320,3323,3326,3329,3332,3335,3338,3349,3353,3356,3408,3411,3475,3478,3485,3569,3572,3575,3686,3693,3727,3731,3736,3743,3839,3948,3952,3959,3963,3972,3975,4157,4160,4164,4171,4331,4334,4337,4340,4351],[13,3298,3299],{},"こんにちはjunです。webアプリを作成する時は大体、認証機能をつけることが多いです。LaravelやDjangoなどでは一発で行けますが、Nuxt.js、Vue.jsを使用したSPA、Node.jsでのSSRコンテンツを作成する場合は一捻り必要です。",[13,3301,3302],{},"Nuxt.jsなどで作成するアプリはバックエンドと独立し、都度バックエンドへのAPI通信の際にトークンを渡したりすることで認証が必要なAPIへアクセスしています。",[13,3304,3305],{},"ビュー側の処理でも",[61,3307,3308,3311,3314],{},[64,3309,3310],{},"ログインしているユーザーはこのように表示",[64,3312,3313],{},"このルートはログインしているユーザーのみアクセス可能",[64,3315,3316],{},"未ログインユーザーはログイン画面移動",[13,3318,3319],{},"といった処理が行われることが多いです。この記事では上記のような認証を用いたビューの表現、ルーターの設定をNuxt.jsでどう行うかの解説をしたいと思います。SPA（クライアントサイドレンダリング）とSSR（サーバーサイドレンダリング）の２パターンを実装します。ちなみにnuxt-authなどのライブラリは使用しません。",[13,3321,3322],{},"また、ログインメソッドの処理やリクエストヘッダにどうこうするとか、バックエンド側の処理については今回は解説しません。あくまでフロント側（Nuxt.js側）の表示やロジックに認証が必要な場合にどう実装するかについて解説するのみです。",[51,3324,3325],{"id":3325},"大まかな処理",[13,3327,3328],{},"まずこの実装を行うにあたりJWTやクッキーなどなんらかの認証トークンが取得できていること、またそれらの値を使用できることを前提として進めます。",[13,3330,3331],{},"LaravelやDjangoなどではリクエストを送る際にヘッダーにセッションIDを含むクッキーを送信することで、サーバー側でログインによるビューやロジックの分岐を行っています。",[13,3333,3334],{},"しかしNuxt.jsでSPAの場合はコンテンツをクライアント側で生成し、またSSRの場合はnode.jsのサーバーで行われます。すなわちセッションやユーザー情報を保存しているAPIサーバーから「どうにかしてNuxt.js側にユーザー情報を渡す」必要があります。",[13,3336,3337],{},"これから実装する内容はSSR、SPAどちらも以下の通りです。",[679,3339,3340,3343,3346],{},[64,3341,3342],{},"Nuxt.js側でユーザーの情報を保存する。",[64,3344,3345],{},"ログインの是非はユーザー情報の有無で判断する。",[64,3347,3348],{},"何かしらのタイミングでユーザー情報を取得するAPIを都度発行する。",[51,3350,3352],{"id":3351},"storeの調整","Storeの調整",[13,3354,3355],{},"ではまずStoreにて以下のようにuserステートを作成します。",[1304,3357,3362],{"className":3358,"code":3359,"filename":3360,"language":3361,"meta":1100,"style":1100},"language-javascript shiki shiki-themes material-theme-ocean","export const state = () => ({\n    user:null,\n});\n","store\u002Findex.js","javascript",[1072,3363,3364,3390,3399],{"__ignoreMap":1100},[327,3365,3366,3370,3373,3376,3379,3382,3385,3388],{"class":1313,"line":1314},[327,3367,3369],{"class":3368},"s6cf3","export",[327,3371,3372],{"class":1698}," const",[327,3374,3375],{"class":1317}," state ",[327,3377,3378],{"class":1321},"=",[327,3380,3381],{"class":1321}," ()",[327,3383,3384],{"class":1698}," =>",[327,3386,3387],{"class":1317}," (",[327,3389,1690],{"class":1321},[327,3391,3392,3396],{"class":1313,"line":1104},[327,3393,3395],{"class":3394},"s-wAU","    user",[327,3397,3398],{"class":1321},":null,\n",[327,3400,3401,3404,3406],{"class":1313,"line":1101},[327,3402,3403],{"class":1321},"}",[327,3405,1341],{"class":1317},[327,3407,1344],{"class":1321},[13,3409,3410],{},"デフォルトではnullにしておきます。このuserステートがnullでなく、ユーザー情報のオブジェクトである場合をログイン状態とします。mutationなどでこのステートに値をセットできるように作っておきます。",[1304,3412,3414],{"className":3358,"code":3413,"filename":3360,"language":3361,"meta":1100,"style":1100},"export const mutations = {\n    setUser(state,{user}){\n            state.user = user;\n    }\n}\n",[1072,3415,3416,3430,3450,3467,3471],{"__ignoreMap":1100},[327,3417,3418,3420,3422,3425,3427],{"class":1313,"line":1314},[327,3419,3369],{"class":3368},[327,3421,3372],{"class":1698},[327,3423,3424],{"class":1317}," mutations ",[327,3426,3378],{"class":1321},[327,3428,3429],{"class":1321}," {\n",[327,3431,3432,3435,3437,3441,3444,3447],{"class":1313,"line":1104},[327,3433,3434],{"class":3394},"    setUser",[327,3436,1329],{"class":1321},[327,3438,3440],{"class":3439},"s7ZW3","state",[327,3442,3443],{"class":1321},",{",[327,3445,3446],{"class":3439},"user",[327,3448,3449],{"class":1321},"}){\n",[327,3451,3452,3455,3457,3459,3462,3465],{"class":1313,"line":1101},[327,3453,3454],{"class":1317},"            state",[327,3456,1322],{"class":1321},[327,3458,3446],{"class":1317},[327,3460,3461],{"class":1321}," =",[327,3463,3464],{"class":1317}," user",[327,3466,1344],{"class":1321},[327,3468,3469],{"class":1313,"line":1111},[327,3470,1752],{"class":1321},[327,3472,3473],{"class":1313,"line":1465},[327,3474,1474],{"class":1321},[51,3476,3477],{"id":3477},"ミドルェアの作成",[13,3479,3480,3481,3484],{},"次に「ログインしたユーザーのみがアクセス可能なページ」を実装できるようにミドルウェアを実装します。",[1072,3482,3483],{},"middleware\u002Fauth.js","を作成します。",[1304,3486,3488],{"className":3358,"code":3487,"filename":3483,"language":3361,"meta":1100,"style":1100},"export default function ({ store, redirect }) {\n    if (!store.state.user) {\n        redirect('\u002Flogin');\n    }\n}\n",[1072,3489,3490,3516,3542,3561,3565],{"__ignoreMap":1100},[327,3491,3492,3494,3497,3500,3503,3506,3508,3511,3514],{"class":1313,"line":1314},[327,3493,3369],{"class":3368},[327,3495,3496],{"class":3368}," default",[327,3498,3499],{"class":1698}," function",[327,3501,3502],{"class":1321}," ({",[327,3504,3505],{"class":3439}," store",[327,3507,1075],{"class":1321},[327,3509,3510],{"class":3439}," redirect",[327,3512,3513],{"class":1321}," })",[327,3515,3429],{"class":1321},[327,3517,3518,3521,3523,3526,3529,3531,3533,3535,3537,3540],{"class":1313,"line":1104},[327,3519,3520],{"class":3368},"    if",[327,3522,3387],{"class":3394},[327,3524,3525],{"class":1321},"!",[327,3527,3528],{"class":1317},"store",[327,3530,1322],{"class":1321},[327,3532,3440],{"class":1317},[327,3534,1322],{"class":1321},[327,3536,3446],{"class":1317},[327,3538,3539],{"class":3394},") ",[327,3541,1690],{"class":1321},[327,3543,3544,3547,3549,3552,3555,3557,3559],{"class":1313,"line":1101},[327,3545,3546],{"class":1325},"        redirect",[327,3548,1329],{"class":3394},[327,3550,3551],{"class":1321},"'",[327,3553,3554],{"class":1335},"\u002Flogin",[327,3556,3551],{"class":1321},[327,3558,1341],{"class":3394},[327,3560,1344],{"class":1321},[327,3562,3563],{"class":1313,"line":1111},[327,3564,1752],{"class":1321},[327,3566,3567],{"class":1313,"line":1465},[327,3568,1474],{"class":1321},[13,3570,3571],{},"単純にStoreのUserステートがnullかどうかでログインページに飛ばすようにしています。",[13,3573,3574],{},"ページコンポーネントでは以下のようにしてミドルウェアを有効にします。",[1304,3576,3581],{"className":3577,"code":3578,"filename":3579,"language":3580,"meta":1100,"style":1100},"language-vue shiki shiki-themes material-theme-ocean","\u003Ctemplate>\n    \u003Cdiv>\n        auth\n    \u003C\u002Fdiv>\n\u003C\u002Ftemplate>\n\u003Cscript>\nexport default {\n    name:\"home\",\n    middleware:\"auth\",\n}\n\u003C\u002Fscript>\n","pages\u002Fauth\u002Findex.vue","vue",[1072,3582,3583,3594,3603,3608,3617,3626,3635,3643,3659,3674,3678],{"__ignoreMap":1100},[327,3584,3585,3588,3591],{"class":1313,"line":1314},[327,3586,3587],{"class":1321},"\u003C",[327,3589,3590],{"class":3394},"template",[327,3592,3593],{"class":1321},">\n",[327,3595,3596,3599,3601],{"class":1313,"line":1104},[327,3597,3598],{"class":1321},"    \u003C",[327,3600,1639],{"class":3394},[327,3602,3593],{"class":1321},[327,3604,3605],{"class":1313,"line":1101},[327,3606,3607],{"class":1317},"        auth\n",[327,3609,3610,3613,3615],{"class":1313,"line":1111},[327,3611,3612],{"class":1321},"    \u003C\u002F",[327,3614,1639],{"class":3394},[327,3616,3593],{"class":1321},[327,3618,3619,3622,3624],{"class":1313,"line":1465},[327,3620,3621],{"class":1321},"\u003C\u002F",[327,3623,3590],{"class":3394},[327,3625,3593],{"class":1321},[327,3627,3628,3630,3633],{"class":1313,"line":1471},[327,3629,3587],{"class":1321},[327,3631,3632],{"class":3394},"script",[327,3634,3593],{"class":1321},[327,3636,3637,3639,3641],{"class":1313,"line":1868},[327,3638,3369],{"class":3368},[327,3640,3496],{"class":3368},[327,3642,3429],{"class":1321},[327,3644,3645,3648,3650,3652,3655,3657],{"class":1313,"line":1874},[327,3646,3647],{"class":3394},"    name",[327,3649,1718],{"class":1321},[327,3651,1332],{"class":1321},[327,3653,3654],{"class":1335},"home",[327,3656,1332],{"class":1321},[327,3658,1728],{"class":1321},[327,3660,3661,3664,3666,3668,3670,3672],{"class":1313,"line":1879},[327,3662,3663],{"class":3394},"    middleware",[327,3665,1718],{"class":1321},[327,3667,1332],{"class":1321},[327,3669,2869],{"class":1335},[327,3671,1332],{"class":1321},[327,3673,1728],{"class":1321},[327,3675,3676],{"class":1313,"line":1885},[327,3677,1474],{"class":1321},[327,3679,3680,3682,3684],{"class":1313,"line":1891},[327,3681,3621],{"class":1321},[327,3683,3632],{"class":3394},[327,3685,3593],{"class":1321},[13,3687,3688,3689,3692],{},"こうすることでログインが必要なページを実装することができます。または",[1072,3690,3691],{},"nuxt.config.js"," にて以下のように設定することで全てのページに認証ミドルウェアを適用できます。",[1304,3694,3696],{"className":3358,"code":3695,"filename":3691,"language":3361,"meta":1100,"style":1100},"router: {\n    middleware: 'auth',\n},\n",[1072,3697,3698,3707,3722],{"__ignoreMap":1100},[327,3699,3700,3703,3705],{"class":1313,"line":1314},[327,3701,3702],{"class":1712},"router",[327,3704,1718],{"class":1321},[327,3706,3429],{"class":1321},[327,3708,3709,3711,3713,3716,3718,3720],{"class":1313,"line":1104},[327,3710,3663],{"class":1712},[327,3712,1718],{"class":1321},[327,3714,3715],{"class":1321}," '",[327,3717,2869],{"class":1335},[327,3719,3551],{"class":1321},[327,3721,1728],{"class":1321},[327,3723,3724],{"class":1313,"line":1101},[327,3725,3726],{"class":1321},"},\n",[104,3728,3730],{"id":3729},"特定のページディレクトリを除く場合","特定のページ、ディレクトリを除く場合",[13,3732,3733,3735],{},[1072,3734,3691],{}," にてグローバルな認証ミドルウェアを実装できますが、未ログインでもアクセス可能なページや、未ログインでないと閲覧できないページ（ログインページなど）では不便です。",[13,3737,3738,3739,3742],{},"個別にミドルウェアを作成してページごとに設定してオーバーライドすることも可能ですが、私はよく以下のように",[1072,3740,3741],{},"auth.js","実装しています。",[1304,3744,3746],{"className":3358,"code":3745,"filename":3483,"language":3361,"meta":1100,"style":1100},"\u002F\u002F 特定のページの認証を外す\nexport default function ({ store, redirect }) {\n    if (!store.state.user && route.fullPath !== '\u002Flogin') {\n        redirect('\u002Flogin');\n    }\n}\n",[1072,3747,3748,3753,3773,3815,3831,3835],{"__ignoreMap":1100},[327,3749,3750],{"class":1313,"line":1314},[327,3751,3752],{"class":1349},"\u002F\u002F 特定のページの認証を外す\n",[327,3754,3755,3757,3759,3761,3763,3765,3767,3769,3771],{"class":1313,"line":1104},[327,3756,3369],{"class":3368},[327,3758,3496],{"class":3368},[327,3760,3499],{"class":1698},[327,3762,3502],{"class":1321},[327,3764,3505],{"class":3439},[327,3766,1075],{"class":1321},[327,3768,3510],{"class":3439},[327,3770,3513],{"class":1321},[327,3772,3429],{"class":1321},[327,3774,3775,3777,3779,3781,3783,3785,3787,3789,3791,3794,3797,3799,3802,3805,3807,3809,3811,3813],{"class":1313,"line":1101},[327,3776,3520],{"class":3368},[327,3778,3387],{"class":3394},[327,3780,3525],{"class":1321},[327,3782,3528],{"class":1317},[327,3784,1322],{"class":1321},[327,3786,3440],{"class":1317},[327,3788,1322],{"class":1321},[327,3790,3446],{"class":1317},[327,3792,3793],{"class":1321}," &&",[327,3795,3796],{"class":1317}," route",[327,3798,1322],{"class":1321},[327,3800,3801],{"class":1317},"fullPath",[327,3803,3804],{"class":1321}," !==",[327,3806,3715],{"class":1321},[327,3808,3554],{"class":1335},[327,3810,3551],{"class":1321},[327,3812,3539],{"class":3394},[327,3814,1690],{"class":1321},[327,3816,3817,3819,3821,3823,3825,3827,3829],{"class":1313,"line":1111},[327,3818,3546],{"class":1325},[327,3820,1329],{"class":3394},[327,3822,3551],{"class":1321},[327,3824,3554],{"class":1335},[327,3826,3551],{"class":1321},[327,3828,1341],{"class":3394},[327,3830,1344],{"class":1321},[327,3832,3833],{"class":1313,"line":1465},[327,3834,1752],{"class":1321},[327,3836,3837],{"class":1313,"line":1471},[327,3838,1474],{"class":1321},[1304,3840,3842],{"className":3358,"code":3841,"filename":3483,"language":3361,"meta":1100,"style":1100},"\u002F\u002F 特定のディレクトリ配下を外す\nexport default function ({ store, redirect }) {\n    if (!store.state.user && route.fullPath.indexOf('\u002Fpublic') != -1) {\n        redirect('\u002Flogin');\n    }\n}\n",[1072,3843,3844,3849,3869,3924,3940,3944],{"__ignoreMap":1100},[327,3845,3846],{"class":1313,"line":1314},[327,3847,3848],{"class":1349},"\u002F\u002F 特定のディレクトリ配下を外す\n",[327,3850,3851,3853,3855,3857,3859,3861,3863,3865,3867],{"class":1313,"line":1104},[327,3852,3369],{"class":3368},[327,3854,3496],{"class":3368},[327,3856,3499],{"class":1698},[327,3858,3502],{"class":1321},[327,3860,3505],{"class":3439},[327,3862,1075],{"class":1321},[327,3864,3510],{"class":3439},[327,3866,3513],{"class":1321},[327,3868,3429],{"class":1321},[327,3870,3871,3873,3875,3877,3879,3881,3883,3885,3887,3889,3891,3893,3895,3897,3900,3902,3904,3907,3909,3911,3914,3917,3920,3922],{"class":1313,"line":1101},[327,3872,3520],{"class":3368},[327,3874,3387],{"class":3394},[327,3876,3525],{"class":1321},[327,3878,3528],{"class":1317},[327,3880,1322],{"class":1321},[327,3882,3440],{"class":1317},[327,3884,1322],{"class":1321},[327,3886,3446],{"class":1317},[327,3888,3793],{"class":1321},[327,3890,3796],{"class":1317},[327,3892,1322],{"class":1321},[327,3894,3801],{"class":1317},[327,3896,1322],{"class":1321},[327,3898,3899],{"class":1325},"indexOf",[327,3901,1329],{"class":3394},[327,3903,3551],{"class":1321},[327,3905,3906],{"class":1335},"\u002Fpublic",[327,3908,3551],{"class":1321},[327,3910,3539],{"class":3394},[327,3912,3913],{"class":1321},"!=",[327,3915,3916],{"class":1321}," -",[327,3918,3919],{"class":3016},"1",[327,3921,3539],{"class":3394},[327,3923,1690],{"class":1321},[327,3925,3926,3928,3930,3932,3934,3936,3938],{"class":1313,"line":1111},[327,3927,3546],{"class":1325},[327,3929,1329],{"class":3394},[327,3931,3551],{"class":1321},[327,3933,3554],{"class":1335},[327,3935,3551],{"class":1321},[327,3937,1341],{"class":3394},[327,3939,1344],{"class":1321},[327,3941,3942],{"class":1313,"line":1465},[327,3943,1752],{"class":1321},[327,3945,3946],{"class":1313,"line":1471},[327,3947,1474],{"class":1321},[51,3949,3951],{"id":3950},"meリクエストを初期処理にいれる","meリクエストを初期処理にいれる。",[13,3953,3954,3955,3958],{},"storeとミドルェアの準備ができたので、APIサーバーに通信をしてユーザー情報を取得するメソッドを作成しておきます。ここではmeリクエストと呼ぶことにします。今回はaxiosで",[1072,3956,3957],{},"https:\u002F\u002Fapi.example.com\u002Fauth\u002Fme","へトークンと一緒にリクエストするとユーザー情報のJSONがレスポンスとして得られるとします。期限切れやトークンがない場合や間違っている場合などは401がレスポンスとして戻ります。",[104,3960,3962],{"id":3961},"spa","SPA",[13,3964,3965,3966,3971],{},"SPAの場合は",[17,3967,3970],{"href":3968,"rel":3969},"https:\u002F\u002Fgithub.com\u002Fpotato4d\u002Fnuxt-client-init-module",[21],"nuxt-client-init-moduleというライブラリ"," を使用するとスムーズです。SSRの場合はNuxtServiceInitという初期処理を実装できるフックがあるのですが、SPAの場合はそれがありあません。Pluginでできなくもないのですが、nuxt-client-init-moduleを使用するとうまくいきやすいです。",[13,3973,3974],{},"上記のライブラリをインストールしてstoreにてmeリクエストをします。",[1304,3976,3979],{"className":3358,"code":3977,"filename":3978,"language":3361,"meta":1100,"style":1100},"export const actions = {\n  async nuxtClientInit({ commit }, context) {\n    await this.$axios.get('https:\u002F\u002Fapi.example.com\u002Fauth\u002Fme')\n    .then(async res=>{\n        commit('setUser', {user:res.data});\n    })\n    .catch(err=>{\n        console.error(err)\n    })\n  }\n}\n","store\u002Findex.js]",[1072,3980,3981,3994,4018,4045,4066,4103,4110,4126,4142,4148,4153],{"__ignoreMap":1100},[327,3982,3983,3985,3987,3990,3992],{"class":1313,"line":1314},[327,3984,3369],{"class":3368},[327,3986,3372],{"class":1698},[327,3988,3989],{"class":1317}," actions ",[327,3991,3378],{"class":1321},[327,3993,3429],{"class":1321},[327,3995,3996,3999,4002,4005,4008,4011,4014,4016],{"class":1313,"line":1104},[327,3997,3998],{"class":1698},"  async",[327,4000,4001],{"class":3394}," nuxtClientInit",[327,4003,4004],{"class":1321},"({",[327,4006,4007],{"class":3439}," commit",[327,4009,4010],{"class":1321}," },",[327,4012,4013],{"class":3439}," context",[327,4015,1341],{"class":1321},[327,4017,3429],{"class":1321},[327,4019,4020,4023,4026,4029,4031,4034,4036,4038,4040,4042],{"class":1313,"line":1101},[327,4021,4022],{"class":3368},"    await",[327,4024,4025],{"class":1321}," this.",[327,4027,4028],{"class":1317},"$axios",[327,4030,1322],{"class":1321},[327,4032,4033],{"class":1325},"get",[327,4035,1329],{"class":3394},[327,4037,3551],{"class":1321},[327,4039,3957],{"class":1335},[327,4041,3551],{"class":1321},[327,4043,4044],{"class":3394},")\n",[327,4046,4047,4050,4053,4055,4058,4061,4064],{"class":1313,"line":1111},[327,4048,4049],{"class":1321},"    .",[327,4051,4052],{"class":1325},"then",[327,4054,1329],{"class":3394},[327,4056,4057],{"class":1698},"async",[327,4059,4060],{"class":3439}," res",[327,4062,4063],{"class":1698},"=>",[327,4065,1690],{"class":1321},[327,4067,4068,4071,4073,4075,4078,4080,4082,4085,4087,4089,4092,4094,4097,4099,4101],{"class":1313,"line":1465},[327,4069,4070],{"class":1325},"        commit",[327,4072,1329],{"class":3394},[327,4074,3551],{"class":1321},[327,4076,4077],{"class":1335},"setUser",[327,4079,3551],{"class":1321},[327,4081,1075],{"class":1321},[327,4083,4084],{"class":1321}," {",[327,4086,3446],{"class":3394},[327,4088,1718],{"class":1321},[327,4090,4091],{"class":1317},"res",[327,4093,1322],{"class":1321},[327,4095,4096],{"class":1317},"data",[327,4098,3403],{"class":1321},[327,4100,1341],{"class":3394},[327,4102,1344],{"class":1321},[327,4104,4105,4108],{"class":1313,"line":1471},[327,4106,4107],{"class":1321},"    }",[327,4109,4044],{"class":3394},[327,4111,4112,4114,4117,4119,4122,4124],{"class":1313,"line":1868},[327,4113,4049],{"class":1321},[327,4115,4116],{"class":1325},"catch",[327,4118,1329],{"class":3394},[327,4120,4121],{"class":3439},"err",[327,4123,4063],{"class":1698},[327,4125,1690],{"class":1321},[327,4127,4128,4131,4133,4136,4138,4140],{"class":1313,"line":1874},[327,4129,4130],{"class":1317},"        console",[327,4132,1322],{"class":1321},[327,4134,4135],{"class":1325},"error",[327,4137,1329],{"class":3394},[327,4139,4121],{"class":1317},[327,4141,4044],{"class":3394},[327,4143,4144,4146],{"class":1313,"line":1879},[327,4145,4107],{"class":1321},[327,4147,4044],{"class":3394},[327,4149,4150],{"class":1313,"line":1885},[327,4151,4152],{"class":1321},"  }\n",[327,4154,4155],{"class":1313,"line":1891},[327,4156,1474],{"class":1321},[13,4158,4159],{},"nuxtClientInitを使用することでページ側の初期化処理より前にユーザー情報を取得できます。",[104,4161,4163],{"id":4162},"ssr","SSR",[13,4165,4166,4167,4170],{},"SSRの場合は今度は",[1072,4168,4169],{},"NuxtServiceInit","に変更するだけです。これは特にライブラリは必要なく、SSRであれば利用できます。",[1304,4172,4174],{"className":3358,"code":4173,"filename":3978,"language":3361,"meta":1100,"style":1100},"export const actions = {\n  async NuxtServiceInit({ commit }, context) {\n    \u002F\u002F サーバーサイドでトークンを入れるなどが必要な場合は適宜入れください。\n     await this.$axios.get('https:\u002F\u002Fapi.example.com\u002Fauth\u002Fme')\n    .then(async res=>{\n        commit('setUser', {user:res.data});\n    })\n    .catch(err=>{\n        console.error(err)\n    })\n  }\n}\n",[1072,4175,4176,4188,4207,4212,4235,4251,4283,4289,4303,4317,4323,4327],{"__ignoreMap":1100},[327,4177,4178,4180,4182,4184,4186],{"class":1313,"line":1314},[327,4179,3369],{"class":3368},[327,4181,3372],{"class":1698},[327,4183,3989],{"class":1317},[327,4185,3378],{"class":1321},[327,4187,3429],{"class":1321},[327,4189,4190,4192,4195,4197,4199,4201,4203,4205],{"class":1313,"line":1104},[327,4191,3998],{"class":1698},[327,4193,4194],{"class":3394}," NuxtServiceInit",[327,4196,4004],{"class":1321},[327,4198,4007],{"class":3439},[327,4200,4010],{"class":1321},[327,4202,4013],{"class":3439},[327,4204,1341],{"class":1321},[327,4206,3429],{"class":1321},[327,4208,4209],{"class":1313,"line":1101},[327,4210,4211],{"class":1349},"    \u002F\u002F サーバーサイドでトークンを入れるなどが必要な場合は適宜入れください。\n",[327,4213,4214,4217,4219,4221,4223,4225,4227,4229,4231,4233],{"class":1313,"line":1111},[327,4215,4216],{"class":3368},"     await",[327,4218,4025],{"class":1321},[327,4220,4028],{"class":1317},[327,4222,1322],{"class":1321},[327,4224,4033],{"class":1325},[327,4226,1329],{"class":3394},[327,4228,3551],{"class":1321},[327,4230,3957],{"class":1335},[327,4232,3551],{"class":1321},[327,4234,4044],{"class":3394},[327,4236,4237,4239,4241,4243,4245,4247,4249],{"class":1313,"line":1465},[327,4238,4049],{"class":1321},[327,4240,4052],{"class":1325},[327,4242,1329],{"class":3394},[327,4244,4057],{"class":1698},[327,4246,4060],{"class":3439},[327,4248,4063],{"class":1698},[327,4250,1690],{"class":1321},[327,4252,4253,4255,4257,4259,4261,4263,4265,4267,4269,4271,4273,4275,4277,4279,4281],{"class":1313,"line":1471},[327,4254,4070],{"class":1325},[327,4256,1329],{"class":3394},[327,4258,3551],{"class":1321},[327,4260,4077],{"class":1335},[327,4262,3551],{"class":1321},[327,4264,1075],{"class":1321},[327,4266,4084],{"class":1321},[327,4268,3446],{"class":3394},[327,4270,1718],{"class":1321},[327,4272,4091],{"class":1317},[327,4274,1322],{"class":1321},[327,4276,4096],{"class":1317},[327,4278,3403],{"class":1321},[327,4280,1341],{"class":3394},[327,4282,1344],{"class":1321},[327,4284,4285,4287],{"class":1313,"line":1868},[327,4286,4107],{"class":1321},[327,4288,4044],{"class":3394},[327,4290,4291,4293,4295,4297,4299,4301],{"class":1313,"line":1874},[327,4292,4049],{"class":1321},[327,4294,4116],{"class":1325},[327,4296,1329],{"class":3394},[327,4298,4121],{"class":3439},[327,4300,4063],{"class":1698},[327,4302,1690],{"class":1321},[327,4304,4305,4307,4309,4311,4313,4315],{"class":1313,"line":1879},[327,4306,4130],{"class":1317},[327,4308,1322],{"class":1321},[327,4310,4135],{"class":1325},[327,4312,1329],{"class":3394},[327,4314,4121],{"class":1317},[327,4316,4044],{"class":3394},[327,4318,4319,4321],{"class":1313,"line":1885},[327,4320,4107],{"class":1321},[327,4322,4044],{"class":3394},[327,4324,4325],{"class":1313,"line":1891},[327,4326,4152],{"class":1321},[327,4328,4329],{"class":1313,"line":1897},[327,4330,1474],{"class":1321},[13,4332,4333],{},"NuxtServiceInitというサーバーサイドで上記のリクエストを行って、storeにユーザー情報をいれてくれます。401が来てもcatchしてくれ、ユーザー情報はnullのままになります。",[51,4335,4336],{"id":4336},"完了",[13,4338,4339],{},"エッセンスは以下の通りです。",[679,4341,4342,4345,4348],{},[64,4343,4344],{},"初期処理にユーザー情報を取得するAPIをリクエスト",[64,4346,4347],{},"認証済みであればstoreのuserにユーザー情報のオブジェクトが入る",[64,4349,4350],{},"ミドルウェアやstoreの情報を使用して認証による分岐を行う",[1408,4352,4353],{},"html pre.shiki code .s6cf3, html code.shiki .s6cf3{--shiki-default:#89DDFF;--shiki-default-font-style:italic}html pre.shiki code .sJ14y, html code.shiki .sJ14y{--shiki-default:#C792EA}html pre.shiki code .s0W1g, html code.shiki .s0W1g{--shiki-default:#BABED8}html pre.shiki code .sAklC, html code.shiki .sAklC{--shiki-default:#89DDFF}html pre.shiki code .s-wAU, html code.shiki .s-wAU{--shiki-default:#F07178}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .s7ZW3, html code.shiki .s7ZW3{--shiki-default:#BABED8;--shiki-default-font-style:italic}html pre.shiki code .sdLwU, html code.shiki .sdLwU{--shiki-default:#82AAFF}html pre.shiki code .sfyAc, html code.shiki .sfyAc{--shiki-default:#C3E88D}html pre.shiki code .s5Dmg, html code.shiki .s5Dmg{--shiki-default:#FFCB6B}html pre.shiki code .sC9rS, html code.shiki .sC9rS{--shiki-default:#464B5D;--shiki-default-font-style:italic}html pre.shiki code .sx098, html code.shiki .sx098{--shiki-default:#F78C6C}",{"title":1100,"searchDepth":1101,"depth":1101,"links":4355},[4356,4357,4358,4361,4365],{"id":3325,"depth":1104,"text":3325},{"id":3351,"depth":1104,"text":3352},{"id":3477,"depth":1104,"text":3477,"children":4359},[4360],{"id":3729,"depth":1101,"text":3730},{"id":3950,"depth":1104,"text":3951,"children":4362},[4363,4364],{"id":3961,"depth":1101,"text":3962},{"id":4162,"depth":1101,"text":4163},{"id":4336,"depth":1104,"text":4336},[4367],"devstack","2022-05-19","フロント側の認証機能を実装します",{},"\u002Farticles\u002Fnuxt-auth-middleware",{"title":3294,"description":4369},"articles\u002Fnuxt-auth-middleware",[1308,4375],"nuxt","_common\u002Fnuxt.jpg","MWiO6wSm6JBqm1_bIaWiFdYFIqDsWKtpAvMLHvfsPGg",{"id":4379,"title":4380,"body":4381,"category":4682,"createdAt":4683,"description":4684,"extension":1148,"index":1149,"meta":4685,"navigation":1151,"path":4686,"publish":1151,"seo":4687,"series":1149,"seriesTitle":1149,"stem":4688,"tag":4689,"thumbnail":4693,"updatedAt":1149,"__hash__":4694},"articles\u002Farticles\u002Fsecurity-incident-by-git-deplay.md","ドキュメントルート配下のコンテンツをGitを用いてデプロイする時のセキュリティ上の注意点",{"type":10,"value":4382,"toc":4668},[4383,4390,4393,4396,4413,4417,4420,4424,4432,4435,4438,4441,4444,4463,4469,4473,4483,4498,4501,4505,4508,4511,4514,4517,4521,4530,4536,4543,4546,4565,4580,4586,4590,4601,4604,4613,4616,4619,4625,4629,4635,4641,4644,4650,4653,4659,4662],[13,4384,4385,4386,4389],{},"こんにちはjunです。最近関わっているプロジェクトではGitを用いた本番環境での運用を行っています。本番環境にgitを置くことによって",[1072,4387,4388],{},"git pull","するだけで改修したコードをすぐに反映できます。さらにブランチを分ければABテストや選択的なデプロイなども行えます。そのためシステム開発においてはGitによる本番環境での運用は欠かせません。",[13,4391,4392],{},"Laravelの様なシステム以外にも最近はwordpressのカスタムテーマやカスタムプラグインをgit管理し、本番環境にpullすることがあります。しかしその際の設定によってはプライベートのソースコードが流失したり、書き換えられたりなどセキュリティインシデントを犯しかねない設定になることがあります。私が公開前に止められたヒヤリハットとしてぜひ共有したいと思います。",[13,4394,4395],{},"今回解説するGitの管理と運用は以下のとおりです。",[61,4397,4398,4401,4404,4407,4410],{},[64,4399,4400],{},"プライベートリポジトリ",[64,4402,4403],{},"リモートはgithub",[64,4405,4406],{},"管理しているソースはwordpressのカスタムテーマ",[64,4408,4409],{},".gitがドキュメントルート配下にある",[64,4411,4412],{},"サーバはxserver（法人用レンタルサーバ）",[51,4414,4416],{"id":4415},"今回何が危なかったのか","今回何が危なかったのか？",[13,4418,4419],{},"まず結論から先に述べますと「パーソナルアクセストークンを用いたgit設定がドキュメントルート配下にあった」ことです。わかる人は結構やべーとわかるかもしれません。取り合えず解説していきます。",[104,4421,4423],{"id":4422},"パーソナルアクセストークンpatとは","パーソナルアクセストークン（PAT）とは",[13,4425,4426,4427],{},"パーソナルアクセストークンとはgithubにて使用されるパスワードの代わりとなるアクセストークンです。",[17,4428,4431],{"href":4429,"rel":4430},"https:\u002F\u002Fdocs.github.com\u002Fja\u002Fauthentication\u002Fkeeping-your-account-and-data-secure\u002Fcreating-a-personal-access-token",[21],"参考",[13,4433,4434],{},"一昔前はプライベートリポジトリにURL（HTTP）でアクセスする場合はドメインの部分にGithubアカウントのIDとパスワードを合わせる方法がとられていました（Basic認証）。ただしその方法はセキュリティ上危ないので、廃止され現在はパーソナルアクセストークン という予測がしづらく、行える操作スコープと使用期限が設定されたトークンを発行してパスワードの代わりに使用する様になりました。",[13,4436,4437],{},"そしてgitはpull,pushなどを行う際にそのorigin（リモートリポジトリ）のURL（HTTP）かSSHを使用します。SSHは環境によって難しかったり設定が大変なこともあり、PAT付きのURLを用いて接続すると簡単にプライベートリポジトリに接続ができます。基本的にパーソナルアクセストークン はこの様にgithubのアカウントが必要なプライベートリポジトリの読み取り、リポジトリ操作を行う際に使用します。",[13,4439,4440],{},"今回のテーマファイルのリポジトリはもちろんプライベートなので、トークン付きのURLかSSHで接続する必要があります。そして今回はPATをメインに使用して、gitの操作を行っていました。",[104,4442,4443],{"id":4443},"git設定ファイルとは",[13,4445,4446,4447,4450,4451,4454,4455,4458,4459,4462],{},"git設定ファイルとはリモートリポジトリ の接続先、ブランチ構成、ユーザーなどgit管理に必要な設定ファイルのことです。",[1072,4448,4449],{},"git init","してローカルリポジトリ を作成した際に",[1072,4452,4453],{},"ls -la","と打つと、",[1072,4456,4457],{},".git","という隠しディレクトリが表示されます。それが設定ディレクトリであり、中に移動すると",[1072,4460,4461],{},".git\u002Fconfig","という設定ファイルがあります。",[13,4464,4465,4466,4468],{},"先ほどのパーソナルアクセストークンを用いたURLなどはその",[1072,4467,4461],{},"に書かれます。実際にlessやcatを使用してみてみると、リモートのURLなどが記載されています。",[104,4470,4472],{"id":4471},"なぜ読み取れた","なぜ読み取れた？",[13,4474,4475,4476,4479,4480,4482],{},"今回gitで運用しようとしていたwordpressのテーマファイルはドキュメントルート配下に設定します。ルートからみて　",[1072,4477,4478],{},"\u002Fwp-content\u002Fthemes\u002Fcustom","というリポジトリ名にもなるテーマディレクトリを作成してそこに",[1072,4481,4449],{},"をして接続先の情報を記載しました。上記のとおりPATを用いています。",[13,4484,4485,4486,4489,4490,4493,4494,4497],{},"インフラに詳しい人ならわかると思いますが、基本的にドキュメントルート配下のファイルは",[1072,4487,4488],{},".htaccess","や",[1072,4491,4492],{},"Apache","で何かしら設定されていない場合、自由にみることができます。本来アクセスされない様なファイルにも例えば、",[1072,4495,4496],{},"https:\u002F\u002Fexample.comm\u002Fwp-content\u002Fthemes\u002Fcustom\u002F.git\u002Fconfig","とURLでアクセスすると読み取れることがあります。",[13,4499,4500],{},"xserverの場合はconfigファイルがダウンロードされ、もちろん接続先情報は記載されていました。そのためパーソナルアクセストークン が記載されたgitの設定ファイルが第三者にダウンロード可能な状態で公開しうるとこでした。",[104,4502,4504],{"id":4503},"どうゆうことが起きかねる","どうゆうことが起きかねる？",[13,4506,4507],{},"仮に公開し、攻撃者がこの存在に気づくと何が起こり得るでしょうか？まず、パーソナルアクセストークン が盗まれリポジトリに対して不正アクセスされます。まだwebサーバを通じて設定ファイルを読み取っただけなので、gitコマンドを打たれて本番環境を破壊されることはないと思いますが、プライベートリポジトリに何かしらの攻撃をされるでしょう。本来プライベートなリポジトリ の情報を取得されてしまうのです。",[13,4509,4510],{},"またパーソナルアクセストークン には操作範囲を設定できます。今回のはリポジトリに対する読み書き操作でしたが、他にも管理者権限レベルの操作を付与できるスコープもあります。つまり、権限の強いトークンの場合はリポジトリ の内容が盗まれたり改変されるだけでなく、アカウント全体に影響が出かねないものになります。",[51,4512,4513],{"id":4513},"対策",[13,4515,4516],{},"ドキュメントルート配下にリポジトリを設定する場合は設定ファイルにアクセスできない様にする必要があります。またはPATを記載しないことです。",[104,4518,4520],{"id":4519},"gitディレクトリへのアクセスを404にする",".gitディレクトリへのアクセスを404にする",[13,4522,4523,4524,4526,4527,4529],{},"まず手取り早くできるのは",[1072,4525,4488],{},"にて",[1072,4528,4457],{},"を含むURLがあったら404にしてしまうことです。",[1304,4531,4534],{"className":4532,"code":4533,"language":1809},[1807],"RedirectMatch 404 \u002F\\.git\n",[1072,4535,4533],{"__ignoreMap":1100},[13,4537,4538],{},[17,4539,4542],{"href":4540,"rel":4541},"https:\u002F\u002Fstackoverflow.com\u002Fquestions\u002F6142437\u002Fmake-git-directory-web-inaccessible",[21],"こちらのStacoverflowが役に立ちました。",[13,4544,4545],{},"解説によると",[61,4547,4548,4551,4559,4562],{},[64,4549,4550],{},"ドキュメントルート配下すべての.gitディレクトリ （設定ファイル）へのアクセスを禁止できる。つまり複数のリポジトリがあっても一気に対応可能。",[64,4552,4553,4489,4555,4558],{},[1072,4554,3264],{},[1072,4556,4557],{},".gitmodules","にも対応できる。",[64,4560,4561],{},"これから新しく追加される.gitにも対応できる。",[64,4563,4564],{},"404にすることでリポジトリ の存在を悟らせない。",[13,4566,4567,4568,4570,4571,4573,4574,4576,4577,4579],{},"実際に",[1072,4569,4488],{},"で設定すると、",[1072,4572,4496],{},"へのアクセスは404になりました。上記の設定の場合はURLに",[1072,4575,4457],{},"が含まれた瞬間、404になるのがコツです。さすがにないと思いますがURLに",[1072,4578,4457],{},"を使用するコンテンツがある場合も404になるのでそこは注意が必要です。",[13,4581,4582,4583,4585],{},"webサーバによる読み取りとgitコマンドは関係ないので、上記の設定をしたとしても",[1072,4584,4388],{},"など操作は引き続き使えます。",[104,4587,4589],{"id":4588},"sshに切り替える","SSHに切り替える",[13,4591,4592,4593,4596,4597,4600],{},"また可能であればSSHによる接続に切り替えることです。SSHによる接続のURLは",[1072,4594,4595],{},"git@github.com:example:repository.git","となります。前半の",[1072,4598,4599],{},"git@github.com","はsshのgitユーザーでgitub.comに接続するという意味です。そのユーザーで接続する際の秘密鍵やそのパスの設定はwebサーバーからは読み取ることができません。（上記のURLに直に書いてればべつだけど）",[13,4602,4603],{},"そのためパーソナルアクセストークンよりかはSSHでGitに接続するほうが安全性的にかなりベストです。PATより堅牢な方法です。",[13,4605,4606,4607,4612],{},"ちなみに本番環境からリポジトリへSSHで接続する際は",[17,4608,4611],{"href":4609,"rel":4610},"https:\u002F\u002Fdocs.github.com\u002Fja\u002Fauthentication\u002Fkeeping-your-account-and-data-secure\u002Freviewing-your-deploy-keys",[21],"デプロイキー","を使用するべきです。デプロイキーは特定のリポジトリのみのアクセスに限定するとともに、読み込み専用にすることができます。",[13,4614,4615],{},"PATでは読みに加えて書き込み権限と付与すればその他の管理者権限が使用できます。つまり盗まれた際の被害が甚大に対して、デプロイキーは特定のリポジトリ の読み込み権限のみを許可出るので被害が小さく済みます。",[104,4617,4618],{"id":4618},"対策まとめ",[13,4620,4621,4622,4624],{},"一番はwebサーバーで",[1072,4623,4457],{},"に対するアクセス権限をなくすことです。これでパーソナルアクセストークンであっても外部から見られる心配はありません。ただし可能な限りまずはより安全なデプロイキーによるSSHで接続できる様にし、かつwebサーバの設定を行うことがベストです。",[51,4626,4628],{"id":4627},"うちは大丈夫チェック方法linux","うちは大丈夫？チェック方法(linux)",[13,4630,4631,4632,4634],{},"まずはドキュメントルート配下に",[1072,4633,4457],{},"がいるかをチェックしましょう。Linuxの場合はドキュメントルート配下に以下のコマンドで探せます。",[1304,4636,4639],{"className":4637,"code":4638,"language":1809},[1807],"find \u002FDOCUMENT\u002FROOT\u002F -name .git -type d\n",[1072,4640,4638],{"__ignoreMap":1100},[13,4642,4643],{},"もしあった場合",[1304,4645,4648],{"className":4646,"code":4647,"language":1809},[1807],"\u002FDOCUMENT\u002FROOT\u002Fwp-content\u002Fthemes\u002Fexample\u002F.git\n",[1072,4649,4647],{"__ignoreMap":1100},[13,4651,4652],{},"このように結果が出てきますので、URLに載せて",[1304,4654,4657],{"className":4655,"code":4656,"language":1809},[1807],"https:\u002F\u002Fexample.com\u002Fwp-content\u002Fthemes\u002Fexample\u002F.git\nhttps:\u002F\u002Fexample.com\u002Fwp-content\u002Fthemes\u002Fexample\u002F.git\u002Fconfig\n",[1072,4658,4656],{"__ignoreMap":1100},[13,4660,4661],{},"の２種類にアクセスしてディレクトリ 、configが閲覧されるかをチェックしましょう。サーバによっては対策済みの場合があります。",[13,4663,4664,4665,4667],{},"見れてしまったらまず",[1072,4666,4488],{},"で閲覧できなくしましょう。中を見てパスワードやパーソナルアクセストークンがあったら今すぐ対処が必要です！！",{"title":1100,"searchDepth":1101,"depth":1101,"links":4669},[4670,4676,4681],{"id":4415,"depth":1104,"text":4416,"children":4671},[4672,4673,4674,4675],{"id":4422,"depth":1101,"text":4423},{"id":4443,"depth":1101,"text":4443},{"id":4471,"depth":1101,"text":4472},{"id":4503,"depth":1101,"text":4504},{"id":4513,"depth":1104,"text":4513,"children":4677},[4678,4679,4680],{"id":4519,"depth":1101,"text":4520},{"id":4588,"depth":1101,"text":4589},{"id":4618,"depth":1101,"text":4618},{"id":4627,"depth":1104,"text":4628},[4367],"2022-04-21","wordpressテーマやドキュメントルート配下にGitを置く際の注意点",{},"\u002Farticles\u002Fsecurity-incident-by-git-deplay",{"title":4380,"description":4684},"articles\u002Fsecurity-incident-by-git-deplay",[4690,4691,4692],"security","infrastructure","git","_common\u002Fsecurity.jpg","zHB7HHf-61vEYna3ziFZoIxx8rS0KxIKcw1QiPSv20k",{"id":4696,"title":4697,"body":4698,"category":5167,"createdAt":5168,"description":5169,"extension":1148,"index":1149,"meta":5170,"navigation":1151,"path":5171,"publish":1151,"seo":5172,"series":1149,"seriesTitle":1149,"stem":5173,"tag":5174,"thumbnail":1578,"updatedAt":1149,"__hash__":5175},"articles\u002Farticles\u002Flaravel-custom-faker.md","Laravelでカスタムなフェイカーを作成する。",{"type":10,"value":4699,"toc":5158},[4700,4703,4712,4715,4735,4738,4741,4745,4759,4765,4797,4804,4817,4820,4827,4880,4883,4886,4889,4892,4898,5045,5063,5066,5075,5137,5144,5147,5150,5156],[13,4701,4702],{},"こんにちはjunです。Laravelはフルスタックフレームワークと言われるほど開発者に嬉しい機能が揃っています。その中でFakerと呼ばれるダミーデータを挿入する機能はよく使用します。ページングやいろんな文字を入れてみて、ビュー側やロジックなどが問題ないかを確かめることができるので、効率的な開発には必要不可欠です。",[13,4704,4705,4706,4711],{},"Fakerは標準で英語ですが、設定によって日本語にすることができます。FakerはPHPfakerというDevライブラリを使用しており、",[17,4707,4710],{"href":4708,"rel":4709},"https:\u002F\u002Ffakerphp.github.io\u002Fformatters\u002Fnumbers-and-strings\u002F",[21],"PHP Faker Formatters","で使用できるFakerの一覧を見れます。これらのFakerはおもにFactoryで使用します。",[13,4713,4714],{},"例えば氏名、メールアドレス、文章などは以下の通りです。",[1304,4716,4718],{"className":1438,"code":4717,"language":1440,"meta":1100,"style":1100},"$this->faker->name();\n$this->faker->email();\n$this->faker->realText();\n",[1072,4719,4720,4725,4730],{"__ignoreMap":1100},[327,4721,4722],{"class":1313,"line":1314},[327,4723,4724],{},"$this->faker->name();\n",[327,4726,4727],{"class":1313,"line":1104},[327,4728,4729],{},"$this->faker->email();\n",[327,4731,4732],{"class":1313,"line":1101},[327,4733,4734],{},"$this->faker->realText();\n",[13,4736,4737],{},"上記の通りそれっぽいダミーデータを作れるのですが、時たまにアプリで必要なデータ形式のFakerがほしかったり、ランダム・特定条件のマスターのIDを出してほしい、もう少し実装するサービスに即した内容を出してほしいと言った要望がある場合はFakerを自作する必要があります。",[13,4739,4740],{},"今回はそのカスタムFakerの実装を解説したいと思います。",[51,4742,4744],{"id":4743},"カスタムfakerのクラスを作成","カスタムFakerのクラスを作成",[13,4746,4747,4748,4751,4752,4755,4756,4758],{},"最初にカスタムFakerのクラスを作成します。",[1072,4749,4750],{},"App\u002FFaker\u002FCustome.php","を作成します。今回は",[1072,4753,4754],{},"Custome.php","に実装したいFakerを作成しますが、もしクラスごとにわたい場合は",[1072,4757,4754],{},"をFakerごとのクラスに分けてください。",[13,4760,4761,4762,4764],{},"そして",[1072,4763,4754],{},"は以下のように記述します。",[1304,4766,4768],{"className":1438,"code":4767,"filename":4750,"language":1440,"meta":1100,"style":1100},"namespace App\\Faker;\nuse Faker\\Provider\\Base;\n\nclass Custome extends Base{\n\n}\n\n",[1072,4769,4770,4775,4780,4784,4789,4793],{"__ignoreMap":1100},[327,4771,4772],{"class":1313,"line":1314},[327,4773,4774],{},"namespace App\\Faker;\n",[327,4776,4777],{"class":1313,"line":1104},[327,4778,4779],{},"use Faker\\Provider\\Base;\n",[327,4781,4782],{"class":1313,"line":1101},[327,4783,1847],{"emptyLinePlaceholder":1151},[327,4785,4786],{"class":1313,"line":1111},[327,4787,4788],{},"class Custome extends Base{\n",[327,4790,4791],{"class":1313,"line":1465},[327,4792,1847],{"emptyLinePlaceholder":1151},[327,4794,4795],{"class":1313,"line":1471},[327,4796,1474],{},[13,4798,4799,4800,4803],{},"ここでは",[1072,4801,4802],{},"Faker\\Provider\\Base","を継承させてください。Fakerの元ファイルもFakerメソッドを定義しているクラスで継承しています。",[13,4805,4806,4808,4809,4812,4813,4816],{},[1072,4807,4802],{},"からは",[1072,4810,4811],{},"randomElements()","といったランダムな配列を取得する、任意範囲の数字を取得する",[1072,4814,4815],{},"numberBetween()","といった便利なメソッドを使用できます。",[104,4818,4819],{"id":4819},"フェイカーの書き方",[13,4821,4822,4823,4826],{},"例えば、食品の名前を出してくれる",[1072,4824,4825],{},"foodname()","というFakerを作ってみるとします。",[1304,4828,4830],{"className":1438,"code":4829,"filename":4750,"language":1440,"meta":1100,"style":1100},"namespace App\\Faker;\nuse Faker\\Provider\\Base;\n\nclass Custome extends Base{\n\n     protected static $foodName = ['ラーメン','パスタ','おにぎり','パン','炒飯']\n\n     public function foodname(){\n        return static::randomElement(static::$foodName);\n     }\n}\n\n",[1072,4831,4832,4836,4840,4844,4848,4852,4857,4861,4866,4871,4876],{"__ignoreMap":1100},[327,4833,4834],{"class":1313,"line":1314},[327,4835,4774],{},[327,4837,4838],{"class":1313,"line":1104},[327,4839,4779],{},[327,4841,4842],{"class":1313,"line":1101},[327,4843,1847],{"emptyLinePlaceholder":1151},[327,4845,4846],{"class":1313,"line":1111},[327,4847,4788],{},[327,4849,4850],{"class":1313,"line":1465},[327,4851,1847],{"emptyLinePlaceholder":1151},[327,4853,4854],{"class":1313,"line":1471},[327,4855,4856],{},"     protected static $foodName = ['ラーメン','パスタ','おにぎり','パン','炒飯']\n",[327,4858,4859],{"class":1313,"line":1868},[327,4860,1847],{"emptyLinePlaceholder":1151},[327,4862,4863],{"class":1313,"line":1874},[327,4864,4865],{},"     public function foodname(){\n",[327,4867,4868],{"class":1313,"line":1879},[327,4869,4870],{},"        return static::randomElement(static::$foodName);\n",[327,4872,4873],{"class":1313,"line":1885},[327,4874,4875],{},"     }\n",[327,4877,4878],{"class":1313,"line":1891},[327,4879,1474],{},[13,4881,4882],{},"このような感じで、配列に静的なプロパティを作成します。そしてメソッドでそのリストからランダムで呼び出す処理を実装すれば大丈夫です。DB上にあるマスターなどを使用したい場合、DBと接続して取得してもいいと思います。",[13,4884,4885],{},"引数を設定できるので、特定の食品だけ取り出すみたいな処理を加えていいでしょう。まずカスタムFakerメソッドを実装できたので、実際に使えるようにします。",[51,4887,4888],{"id":4888},"サービスプロバイダを作成",[13,4890,4891],{},"Laravelの標準のFakerはサービスプロバイダでシングルトンとして登録されています。それをオーバーライドするような処理を行っています。",[13,4893,4894,4897],{},[1072,4895,4896],{},"App\u002FProviders\u002FFakerServiceProvider.php","配下にサービスプロバイダを作成します。",[1304,4899,4901],{"className":1438,"code":4900,"filename":4896,"language":1440,"meta":1100,"style":1100},"\nnamespace App\\Providers;\nuse Faker\\{Factory, Generator};\nuse Illuminate\\Support\\ServiceProvider;\nuse App\\Faker\\Custome;\n\nclass FakerServiceProvider extends ServiceProvider\n{\n    \u002F**\n     * Register services.\n     *\n     * @return void\n     *\u002F\n    public function register()\n    {\n        $this->app->singleton(Generator::class, function () {\n            $faker = Factory::create(config('app.faker_locale'));\n            $faker->addProvider(new Custome($faker));\n            return $faker;\n        });\n    }\n\n    \u002F**\n     * Bootstrap services.\n     *\n     * @return void\n     *\u002F\n    public function boot()\n    {\n        \u002F\u002F\n    }\n}\n",[1072,4902,4903,4907,4912,4917,4922,4927,4931,4936,4940,4944,4949,4953,4957,4961,4966,4970,4975,4980,4985,4990,4994,4998,5002,5006,5011,5015,5019,5023,5028,5032,5037,5041],{"__ignoreMap":1100},[327,4904,4905],{"class":1313,"line":1314},[327,4906,1847],{"emptyLinePlaceholder":1151},[327,4908,4909],{"class":1313,"line":1104},[327,4910,4911],{},"namespace App\\Providers;\n",[327,4913,4914],{"class":1313,"line":1101},[327,4915,4916],{},"use Faker\\{Factory, Generator};\n",[327,4918,4919],{"class":1313,"line":1111},[327,4920,4921],{},"use Illuminate\\Support\\ServiceProvider;\n",[327,4923,4924],{"class":1313,"line":1465},[327,4925,4926],{},"use App\\Faker\\Custome;\n",[327,4928,4929],{"class":1313,"line":1471},[327,4930,1847],{"emptyLinePlaceholder":1151},[327,4932,4933],{"class":1313,"line":1868},[327,4934,4935],{},"class FakerServiceProvider extends ServiceProvider\n",[327,4937,4938],{"class":1313,"line":1874},[327,4939,1690],{},[327,4941,4942],{"class":1313,"line":1879},[327,4943,1882],{},[327,4945,4946],{"class":1313,"line":1885},[327,4947,4948],{},"     * Register services.\n",[327,4950,4951],{"class":1313,"line":1891},[327,4952,1894],{},[327,4954,4955],{"class":1313,"line":1897},[327,4956,1976],{},[327,4958,4959],{"class":1313,"line":1903},[327,4960,1906],{},[327,4962,4963],{"class":1313,"line":1909},[327,4964,4965],{},"    public function register()\n",[327,4967,4968],{"class":1313,"line":1915},[327,4969,1993],{},[327,4971,4972],{"class":1313,"line":1920},[327,4973,4974],{},"        $this->app->singleton(Generator::class, function () {\n",[327,4976,4977],{"class":1313,"line":1925},[327,4978,4979],{},"            $faker = Factory::create(config('app.faker_locale'));\n",[327,4981,4982],{"class":1313,"line":1931},[327,4983,4984],{},"            $faker->addProvider(new Custome($faker));\n",[327,4986,4987],{"class":1313,"line":1936},[327,4988,4989],{},"            return $faker;\n",[327,4991,4992],{"class":1313,"line":1941},[327,4993,2333],{},[327,4995,4996],{"class":1313,"line":1946},[327,4997,1752],{},[327,4999,5000],{"class":1313,"line":1952},[327,5001,1847],{"emptyLinePlaceholder":1151},[327,5003,5004],{"class":1313,"line":1957},[327,5005,1882],{},[327,5007,5008],{"class":1313,"line":1962},[327,5009,5010],{},"     * Bootstrap services.\n",[327,5012,5013],{"class":1313,"line":1968},[327,5014,1894],{},[327,5016,5017],{"class":1313,"line":1973},[327,5018,1976],{},[327,5020,5021],{"class":1313,"line":1979},[327,5022,1906],{},[327,5024,5025],{"class":1313,"line":1984},[327,5026,5027],{},"    public function boot()\n",[327,5029,5030],{"class":1313,"line":1990},[327,5031,1993],{},[327,5033,5034],{"class":1313,"line":1996},[327,5035,5036],{},"        \u002F\u002F\n",[327,5038,5039],{"class":1313,"line":2002},[327,5040,1752],{},[327,5042,5043],{"class":1313,"line":2007},[327,5044,1474],{},[13,5046,5047,5050,5051,5054,5055,5058,5059,5062],{},[1072,5048,5049],{},"Factory::create(config('app.faker_locale'))","としておくと、",[1072,5052,5053],{},"config\u002Fapp.php","の",[1072,5056,5057],{},"faker_locale","を用いて日本語化できます。そしてFakerのインスタンスに",[1072,5060,5061],{},"addProvider()","を使用して、作成したカスタムフェイカーを追加します。",[104,5064,5065],{"id":5065},"サービスプロバイダの登録",[13,5067,5068,5069,5054,5071,5074],{},"このサービスプロバイダを",[1072,5070,5053],{},[1072,5072,5073],{},"providers"," に追加します。",[1304,5076,5078],{"className":1438,"code":5077,"filename":5053,"language":1440,"meta":1100,"style":1100},"'providers' => [\n\u002F*\n* Laravel Framework Service Providers...\n*\u002F\n\u002F\u002F 省略\n\n\u002F*\n* Application Service Providers...\n　追加\n*\u002F\n    App\\Providers\\FakerServiceProvider::class,\n],\n",[1072,5079,5080,5085,5090,5095,5100,5105,5109,5113,5118,5123,5127,5132],{"__ignoreMap":1100},[327,5081,5082],{"class":1313,"line":1314},[327,5083,5084],{},"'providers' => [\n",[327,5086,5087],{"class":1313,"line":1104},[327,5088,5089],{},"\u002F*\n",[327,5091,5092],{"class":1313,"line":1101},[327,5093,5094],{},"* Laravel Framework Service Providers...\n",[327,5096,5097],{"class":1313,"line":1111},[327,5098,5099],{},"*\u002F\n",[327,5101,5102],{"class":1313,"line":1465},[327,5103,5104],{},"\u002F\u002F 省略\n",[327,5106,5107],{"class":1313,"line":1471},[327,5108,1847],{"emptyLinePlaceholder":1151},[327,5110,5111],{"class":1313,"line":1868},[327,5112,5089],{},[327,5114,5115],{"class":1313,"line":1874},[327,5116,5117],{},"* Application Service Providers...\n",[327,5119,5120],{"class":1313,"line":1879},[327,5121,5122],{},"　追加\n",[327,5124,5125],{"class":1313,"line":1885},[327,5126,5099],{},[327,5128,5129],{"class":1313,"line":1891},[327,5130,5131],{},"    App\\Providers\\FakerServiceProvider::class,\n",[327,5133,5134],{"class":1313,"line":1897},[327,5135,5136],{},"],\n",[13,5138,5139,5140,5143],{},"そうするとfakerにて",[1072,5141,5142],{},"$this->faker->foodname()","でランダムな食品名が出てきます。",[51,5145,5146],{"id":5146},"すぐに検証したい場合の方法",[13,5148,5149],{},"上記のFakerはFactoryなどで使用できますが、Tinkerなどですぐに確かめたいということがあると思います。そんな時はTinkerで以下のようにFakerのインスタンスを生成して、チェックできます。なお上記のサービスプロバイダーを登録している必要があります。",[1304,5151,5154],{"className":5152,"code":5153,"language":1809},[1807],"php artian tinker\n>> $faker = app()->make(Faker\\Generator::class)\n>> $faker->foodname()\n=>'パスタ'\n",[1072,5155,5153],{"__ignoreMap":1100},[1408,5157,1567],{},{"title":1100,"searchDepth":1101,"depth":1101,"links":5159},[5160,5163,5166],{"id":4743,"depth":1104,"text":4744,"children":5161},[5162],{"id":4819,"depth":1101,"text":4819},{"id":4888,"depth":1104,"text":4888,"children":5164},[5165],{"id":5065,"depth":1101,"text":5065},{"id":5146,"depth":1104,"text":5146},[1262],"2022-04-02","Laravelでのカスタムフェイカーの作り方",{},"\u002Farticles\u002Flaravel-custom-faker",{"title":4697,"description":5169},"articles\u002Flaravel-custom-faker",[1440,1577],"J5_Trsafdn5yWViWjr3IsUw74QmcNr3WsF_8_DUEepU",{"id":5177,"title":5178,"body":5179,"category":5640,"createdAt":5641,"description":5642,"extension":1148,"index":1149,"meta":5643,"navigation":1151,"path":5644,"publish":1151,"seo":5645,"series":1149,"seriesTitle":1149,"stem":5646,"tag":5647,"thumbnail":1578,"updatedAt":1149,"__hash__":5648},"articles\u002Farticles\u002Flaravel-protect-resource.md","Laravelでログインしたユーザーのみ読み取れる画像・アセットを設定する方法",{"type":10,"value":5180,"toc":5633},[5181,5184,5195,5197,5205,5211,5214,5217,5225,5228,5249,5276,5279,5285,5288,5291,5320,5323,5337,5344,5347,5354,5357,5364,5393,5400,5427,5430,5433,5499,5511,5537,5552,5569,5572,5575,5578,5584,5618,5628,5631],[13,5182,5183],{},"こんにちはjunです。Laravel製のシステムにてWebマニュアルを作成していた時、「あれ？マニュルはログインユーザーのみ見れる様にするけど、画像などはどうすればいいんだ？」という事態がありました。",[13,5185,5186,5187,5190,5191,5194],{},"Laravelはルートを定義し、その際に認証を設けることができます。ただし静的な画像（今回の様なあらかじめセットしておくマニュアル画像など）を配置する場合は",[1072,5188,5189],{},"public","配下または、",[1072,5192,5193],{},"storage\u002Fpublic","配下に置くことが多いと思います。しかしそれらのディレクトリは名の通りいかなるアクセスに対してリクエストを許可しています。",[13,5196,1766],{},[61,5198,5199,5202],{},[64,5200,5201],{},"ログインをしないと見れない画像やアセット",[64,5203,5204],{},"アクセスを制限したい画像やアセット",[13,5206,5207,5208,5210],{},"を実装したい時は単純に",[1072,5209,5189],{},"配下に置くことはできません。この場合Laravelではコントローラーを使用して、アセットのリクエストに対して一度認証のロジックをかける必要があります。普段Laravelを使用していると、特定のURLとそのビューに対する認証はルートを定義するだけで簡単に設定できます。しかしビュー以外のアセットファイルの場合はWebサーバーとLaravelの仕組みを少し理解している必要ががります。今回はその様な保護したアセットルートの設定方法を解説しようと思います。",[51,5212,5213],{"id":5213},"概要",[13,5215,5216],{},"Laravelで構築されたURLで指定のビューやファイルをレスポンスとして返す時２通りの処理方法があります。",[679,5218,5219,5222],{},[64,5220,5221],{},"ドキュメントルート配下にリクエストで示されたファイルがある場合、それを返す。（webサーバー）",[64,5223,5224],{},"ドキュメントルートにない場合、Route.phpで定義したルートを照らし合わせ、設定したビューやファイルを返す。（webサーバー+PHP）",[13,5226,5227],{},"「２」の方はよくわかると思います。例えば以下の様なルートを定義した時",[1304,5229,5232],{"className":1438,"code":5230,"filename":5231,"language":1440,"meta":1100,"style":1100},"Route::get('\u002Ftest', function () {\n    return view('welcome');\n});\n","route.php",[1072,5233,5234,5239,5244],{"__ignoreMap":1100},[327,5235,5236],{"class":1313,"line":1314},[327,5237,5238],{},"Route::get('\u002Ftest', function () {\n",[327,5240,5241],{"class":1313,"line":1104},[327,5242,5243],{},"    return view('welcome');\n",[327,5245,5246],{"class":1313,"line":1101},[327,5247,5248],{},"});\n",[13,5250,5251,5254,5255,5258,5259,5261,5262,1075,5265,5267,5268,5271,5272,5275],{},[1072,5252,5253],{},"https:\u002F\u002Fexample.com\u002Ftest","というURLにアクセスすると",[1072,5256,5257],{},"view('welcome')","で定義したHTML（画面）がレスポンスとして表示されます。一方「１」の方はというと例えば",[1072,5260,5189],{},"配下に置いた",[1072,5263,5264],{},"css",[1072,5266,1308],{},"など静的なファイルがあげられます。例えば",[1072,5269,5270],{},"https:\u002F\u002Fexample.com\u002Fcss\u002Fstyle.css","の場合、webサーバーは",[1072,5273,5274],{},"public\u002Fcss\u002Fstyle.css","があればそれをレスポンスとして返します。",[13,5277,5278],{},"両者の違いはwebサーバーだけで完結しているか、PHP（Laravel）も動かしているかです。これはpublic配下の.htaccessを見ると理解できます。",[1304,5280,5283],{"className":5281,"code":5282,"filename":4488,"language":1809,"meta":1100},[1807],"\u003CIfModule mod_rewrite.c>\n    # Send Requests To Front Controller...\n    RewriteCond %{REQUEST_FILENAME} !-d\n    RewriteCond %{REQUEST_FILENAME} !-f\n    RewriteRule ^ index.php [L]\n\u003C\u002FIfModule>\n",[1072,5284,5282],{"__ignoreMap":1100},[13,5286,5287],{},"一部省略していますが、重要なのはこの箇所です。これは「もし、リクエストしたディレクトリおよびファイルがない場合、index.phpを実行する」という意味です。つまりLaravelが置かれたwebサーバーでは、まず「リクエストされたファイルが静的に置かれているかをチェック」そしてもしない場合は「index.phpを実行してLaravelが動的にルートに対するレスポンスを作成する」ということが行われています。",[13,5289,5290],{},"Apache側で静的ファイルに対するアクセスを設定していない場合、基本的にリクエストに合致するファイルがある場合はレスポンスしてしまいます。今回の様な保護したファイル、つまりLaravelの認証などを通してファイルを返すためには、独自のルートを定義してレスポンスする必要があります。",[1639,5292,5294,5295,5298,5299,5302,5303,5305,5306,5309,5310],{"className":5293},[1642,1643],"\nLaravelではアップロードされたファイルは",[1072,5296,5297],{},"storage\u002Fapp\u002Fpublic"," 配下に配置し、そのstorageファイルへのリクエストは上記のwebサーバーのみで処理できます。storageディレクトリがpublicとは別なのになぜできるのか？それはシンボリックリンクを張っているからです。構築時に",[1072,5300,5301],{},"php artisan storage:link","というおまじないを唱えたと思います。これはpublic配下に",[1072,5304,5297],{},"に連絡する",[1072,5307,5308],{},"storage","というシンボリックリンクを配置する処理を行っています。\n",[13,5311,5312,5313,5316,5317,5319],{},"実際にpublic配下にstorageというものがあり、Vscodeでは矢印マークが加わっているのが分かります。ls -lを実行してみると",[1072,5314,5315],{},"storage -> \u002Fvar\u002Fwww\u002Fhtml\u002Fstorage\u002Fapp\u002Fpublic","という風に表示されます（私の環境の場合）。ディレクトリとして離れていても、シンボリックリンクを貼ることで",[1072,5318,5297],{},"配下をwebサーバーが走査することができる様にしています。",[13,5321,5322],{},"今回のような保護したアセットファイルルートを設定するために",[679,5324,5325,5328,5331,5334],{},[64,5326,5327],{},"専用のディレクトリを作成",[64,5329,5330],{},"読み取りルートの定義",[64,5332,5333],{},"ファイルの取得処理",[64,5335,5336],{},"レスポンス処理",[13,5338,5339,5340,5343],{},"上記のプログラムを作成します。今回は「ログインしたユーザーが見れるwebマニュアルの画像」ということなので、",[1072,5341,5342],{},"resources","配下にファイルを置いておくことにします。一応後でstorageディレクトリに保護ファイルを配置・取得する方法も記述します。",[51,5345,5346],{"id":5346},"ディレクトリの作成",[13,5348,5349,5350,5353],{},"まずは専用のディレクトリを作成します。今回は静的に置いておくので",[1072,5351,5352],{},"resources\u002Fprotected","という保護アセットファイルディレクトリを作っておきます。リクエストがあった場合はこのディレクトリからファイルを取得します。",[51,5355,5356],{"id":5356},"ルートを定義する",[13,5358,5359,5360,5363],{},"それではルートを定義します。",[1072,5361,5362],{},"routes\u002Fweb.php","にて以下の様なルートを設定。",[1304,5365,5367],{"className":1438,"code":5366,"filename":5362,"language":1440,"meta":1100,"style":1100},"Route::group(['middleware' => 'auth'], function () {\n    Route::get('\u002Fprotected\u002F{path?}', function (Request $request,$path='') {\n        \u002F\u002F 後で書きます..\n    })->where('path', '.*');\n});\n",[1072,5368,5369,5374,5379,5384,5389],{"__ignoreMap":1100},[327,5370,5371],{"class":1313,"line":1314},[327,5372,5373],{},"Route::group(['middleware' => 'auth'], function () {\n",[327,5375,5376],{"class":1313,"line":1104},[327,5377,5378],{},"    Route::get('\u002Fprotected\u002F{path?}', function (Request $request,$path='') {\n",[327,5380,5381],{"class":1313,"line":1101},[327,5382,5383],{},"        \u002F\u002F 後で書きます..\n",[327,5385,5386],{"class":1313,"line":1111},[327,5387,5388],{},"    })->where('path', '.*');\n",[327,5390,5391],{"class":1313,"line":1465},[327,5392,5248],{},[13,5394,5395,5396,5399],{},"authミドルウェアでグルーピングをして",[1072,5397,5398],{},"protected","配下のルートを保護します。",[13,5401,5402,5405,5406,1075,5409,5412,5413,5415,5416,5419,5420,1075,5423,5426],{},[1072,5403,5404],{},"{path?}","は任意の記述を意味します。つまり",[1072,5407,5408],{},"\u002Fprotected\u002Fsecret.jpg",[1072,5410,5411],{},"\u002Fprotected\u002Fmanual\u002Fprivate.png","などのルートにをキャッチすることができます。そして",[1072,5414,5404],{},"パラメータはコールバック（コントローラー）に第二引数として使用できます。先程の例のパスの場合、",[1072,5417,5418],{},"$path","は",[1072,5421,5422],{},"secret.jpg",[1072,5424,5425],{},"manual\u002Fprivate.png","となります。この値は後でファイルの取得に使用します。ちなみに今回はルートに処理内容を記述しますが、プロジェクトによっては複雑な認証処理を実装する場合はコントローラーに記述しても大丈夫です。",[51,5428,5429],{"id":5429},"ファイルの取得とレスポンスを行う",[13,5431,5432],{},"ではファイルの取得の処理を記述します。",[1304,5434,5436],{"className":1438,"code":5435,"filename":5362,"language":1440,"meta":1100,"style":1100},"use Illuminate\\Support\\Facades\\File;\n\nRoute::group(['middleware' => 'auth'], function () {\n    Route::get('\u002Fprotected\u002F{path?}', function (Request $request,$path='') {\n        if($path==='') abort(404);\n\n        $rp = resource_path('protected\u002F'.$path);\n        if(File::exists($rp)){\n            return response()->file($rp);\n        }else{\n            abort(404);\n        }\n    })->where('path', '.*');\n});\n",[1072,5437,5438,5442,5446,5450,5454,5459,5463,5468,5473,5478,5482,5487,5491,5495],{"__ignoreMap":1100},[327,5439,5440],{"class":1313,"line":1314},[327,5441,2099],{},[327,5443,5444],{"class":1313,"line":1104},[327,5445,1847],{"emptyLinePlaceholder":1151},[327,5447,5448],{"class":1313,"line":1101},[327,5449,5373],{},[327,5451,5452],{"class":1313,"line":1111},[327,5453,5378],{},[327,5455,5456],{"class":1313,"line":1465},[327,5457,5458],{},"        if($path==='') abort(404);\n",[327,5460,5461],{"class":1313,"line":1471},[327,5462,1847],{"emptyLinePlaceholder":1151},[327,5464,5465],{"class":1313,"line":1868},[327,5466,5467],{},"        $rp = resource_path('protected\u002F'.$path);\n",[327,5469,5470],{"class":1313,"line":1874},[327,5471,5472],{},"        if(File::exists($rp)){\n",[327,5474,5475],{"class":1313,"line":1879},[327,5476,5477],{},"            return response()->file($rp);\n",[327,5479,5480],{"class":1313,"line":1885},[327,5481,2229],{},[327,5483,5484],{"class":1313,"line":1891},[327,5485,5486],{},"            abort(404);\n",[327,5488,5489],{"class":1313,"line":1897},[327,5490,2239],{},[327,5492,5493],{"class":1313,"line":1903},[327,5494,5388],{},[327,5496,5497],{"class":1313,"line":1909},[327,5498,5248],{},[13,5500,5501,5502,5504,5505,4526,5508,5510],{},"最初に",[1072,5503,5418],{},"がない場合は404にアボートします。そして何かしらファイルが指定された場合は",[1072,5506,5507],{},"resource_path('protected\u002F'.$path)",[1072,5509,5352],{},"配下のファイルパスを取得します。",[1639,5512,5515,5516,5519,5520,4489,5523,5526,5527,5530,5531,5419,5533,5536],{"className":5513},[1642,5514],"alert-danger","\nwebサーバーでなくPHP（Laravel）にてユーザーからの入力値（リクエストパス）を用いてファイルの取得をする場合、PHPの",[1072,5517,5518],{},"file_get_contents()","は使用せずLaravelの",[1072,5521,5522],{},"resource_path()",[1072,5524,5525],{},"storage_path","を使用し、さらにFileファサード、",[1072,5528,5529],{},"file()","メソッドを使用しましょう。",[1072,5532,5518],{},[1072,5534,5535],{},"..\u002F","といった記述は文字列でなく、パスとして認識してしまい想定しないディレクトリのファイルにがブラウザを通じて取得される可能性があります。このような脆弱性をディレクトリトラバーサルといいます。Laravelのファイル取得系のメソッドはその辺は対策済みなので、基本的にはLaravelのメソッドを使用しましょう。\n",[13,5538,5539,5540,5543,5544,5547,5548,5551],{},"ファイルパスを作成したら",[1072,5541,5542],{},"File::exists()"," を使用してファイルが存在するかをチェックします。存在しないファイルをfile()パスで使用すると",[1072,5545,5546],{},"FileNotFoundException","が発生してしまいます。例外処理でやってもいいのですが、",[1072,5549,5550],{},"response","メソッドを呼んでいるので念のためあらかじめチェックしておきます。",[13,5553,5554,5555,5558,5559,5561,5562,5564,5565,5568],{},"ファイルがある場合は ",[1072,5556,5557],{},"response()->file();","を使用して対象の",[1072,5560,5352],{},"配下のファイルをレスポンスとして返します。ない場合は404へアボートします。",[1072,5563,5529],{},"メソッドを使用することで拡張子から",[1072,5566,5567],{},"content-type","を設定してくれるそうでCSVだろうがHTMLでもMP4でも問題なくレスポンスしてくれます。",[13,5570,5571],{},"ルート自体はLaravelのミドルウェアを使用することでファイルを保護し、任意のファイルパスを使用して認証が通ればファイルを取得することができる様になります。",[51,5573,5574],{"id":5574},"storageでやる方法",[13,5576,5577],{},"resourcesは基本的に開発者が静的にファイルを置く場合に使用します。ユーザーが自由にアップロードして、保護しながら呼び出したい時はstorageディレクトリを使用します。ファイルの取得と保護は上記とほぼ同じですが、storageの場合は少しcconfigの設定を行います。",[13,5579,5580,5583],{},[1072,5581,5582],{},"config\u002Ffilesystem.php","にて以下の様に保護storageディレクトリを定義します。",[1304,5585,5587],{"className":1438,"code":5586,"filename":5582,"language":1440,"meta":1100,"style":1100},"'protected' => [\n    'driver' => 'local',\n    'root' => storage_path('app\u002Fprotected'),\n    'url' => env('APP_URL') . '\u002Fstorage',\n    'visibility' => 'private',\n],\n\n",[1072,5588,5589,5594,5599,5604,5609,5614],{"__ignoreMap":1100},[327,5590,5591],{"class":1313,"line":1314},[327,5592,5593],{},"'protected' => [\n",[327,5595,5596],{"class":1313,"line":1104},[327,5597,5598],{},"    'driver' => 'local',\n",[327,5600,5601],{"class":1313,"line":1101},[327,5602,5603],{},"    'root' => storage_path('app\u002Fprotected'),\n",[327,5605,5606],{"class":1313,"line":1111},[327,5607,5608],{},"    'url' => env('APP_URL') . '\u002Fstorage',\n",[327,5610,5611],{"class":1313,"line":1465},[327,5612,5613],{},"    'visibility' => 'private',\n",[327,5615,5616],{"class":1313,"line":1471},[327,5617,5136],{},[13,5619,5620,5621,5624,5625,5627],{},"こうすることで",[1072,5622,5623],{},"Storage::disk('protected')->path()","を用いて対象ファイルパスを取得することができる様になります。ファイルストレージはローカルでなくS3など外部のものを使用することもあるので、この様に設定ファイルで定義しておくといいです。storageにprotectedディレクトリを作成した後、あとはルートを定義して",[1072,5626,5623],{},"を用いてリクエストされたファイルパスを取得し、存在チェックをしてレスポンスで返せばOKです。",[13,5629,5630],{},"今回は簡単なauthミドルウェアですが、権限のロジックを組み込むことで所有者のリクエストのみに見せたり、特定の人のみに見せるといった芸当ができそうです。ただしwebサーバーの静的な配信でなく、ファイルの取得にPHPを動かすことになるので大量配信の場合はパフォーマンスはちょっと心配かもしません。",[1408,5632,1567],{},{"title":1100,"searchDepth":1101,"depth":1101,"links":5634},[5635,5636,5637,5638,5639],{"id":5213,"depth":1104,"text":5213},{"id":5346,"depth":1104,"text":5346},{"id":5356,"depth":1104,"text":5356},{"id":5429,"depth":1104,"text":5429},{"id":5574,"depth":1104,"text":5574},[4367],"2022-03-26","画像、アセットにログインしたユーザーのみリクエストを制限する",{},"\u002Farticles\u002Flaravel-protect-resource",{"title":5178,"description":5642},"articles\u002Flaravel-protect-resource",[1577,1440],"f9NQ8_uhPD5HoW5tFBCWKT-Mw4ExKqCgzTqezzlXg_w",{"id":5650,"title":5651,"body":5652,"category":6138,"createdAt":6139,"description":5651,"extension":1148,"index":1149,"meta":6140,"navigation":1151,"path":6141,"publish":1151,"seo":6142,"series":1149,"seriesTitle":1149,"stem":6143,"tag":6144,"thumbnail":1578,"updatedAt":1149,"__hash__":6145},"articles\u002Farticles\u002Flaravel-validation-unit-test.md","Laravelでカスタムバリデーションのユニットテストをする方法",{"type":10,"value":5653,"toc":6131},[5654,5661,5664,5667,5670,5738,5741,5748,5917,5920,5923,5926,5929,5949,5959,5962,5965,6020,6023,6076,6097,6103,6106,6126,6129],[13,5655,5656,5657,5660],{},"Laravelでは",[1072,5658,5659],{},"Illuminate\\Contracts\\Validation\\Rule","を継承したクラスを用いてカスタムなバリデーションを作成することができます。そしてサービスプロバイダに登録することで、FormRequestで文字列で指定することでリクエストのバリデーションを拡張できます。",[13,5662,5663],{},"ただしこの様な独自コードはきちんとユニットテストをすることが大切です。Laravelではこのカスタムバリデーションを簡単にユニットテストをすることができます。",[51,5665,5666],{"id":5666},"サンプルのバリデーション",[13,5668,5669],{},"とりあえず以下の様な郵便番号のバリデーションを作成したとします。７桁のハイフンなしの数字が郵便番号の形式とします。",[1304,5671,5673],{"className":1438,"code":5672,"language":1440,"meta":1100,"style":1100},"namespace App\\Rules;\nuse Illuminate\\Contracts\\Validation\\Rule;\n\nclass Zipcode implements Rule{\n\n    public function passes($attribute, $value)\n    {\n        return preg_match('\u002F^[0-9]{3}-?[0-9]{4}$\u002F', $value);\n    }\n\n    public function message(){\n        return '郵便番号は7桁の半角数字で入力してください。';\n    }\n}\n",[1072,5674,5675,5680,5685,5689,5694,5698,5703,5707,5712,5716,5720,5725,5730,5734],{"__ignoreMap":1100},[327,5676,5677],{"class":1313,"line":1314},[327,5678,5679],{},"namespace App\\Rules;\n",[327,5681,5682],{"class":1313,"line":1104},[327,5683,5684],{},"use Illuminate\\Contracts\\Validation\\Rule;\n",[327,5686,5687],{"class":1313,"line":1101},[327,5688,1847],{"emptyLinePlaceholder":1151},[327,5690,5691],{"class":1313,"line":1111},[327,5692,5693],{},"class Zipcode implements Rule{\n",[327,5695,5696],{"class":1313,"line":1465},[327,5697,1847],{"emptyLinePlaceholder":1151},[327,5699,5700],{"class":1313,"line":1471},[327,5701,5702],{},"    public function passes($attribute, $value)\n",[327,5704,5705],{"class":1313,"line":1868},[327,5706,1993],{},[327,5708,5709],{"class":1313,"line":1874},[327,5710,5711],{},"        return preg_match('\u002F^[0-9]{3}-?[0-9]{4}$\u002F', $value);\n",[327,5713,5714],{"class":1313,"line":1879},[327,5715,1752],{},[327,5717,5718],{"class":1313,"line":1885},[327,5719,1847],{"emptyLinePlaceholder":1151},[327,5721,5722],{"class":1313,"line":1891},[327,5723,5724],{},"    public function message(){\n",[327,5726,5727],{"class":1313,"line":1897},[327,5728,5729],{},"        return '郵便番号は7桁の半角数字で入力してください。';\n",[327,5731,5732],{"class":1313,"line":1903},[327,5733,1752],{},[327,5735,5736],{"class":1313,"line":1909},[327,5737,1474],{},[51,5739,5740],{"id":5740},"ユニットテストファイルを作成",[13,5742,5743,5744,5747],{},"ひとまず ",[1072,5745,5746],{},"tests\u002FUnit\u002FRules.php"," というものを作成します。",[1304,5749,5752],{"className":1438,"code":5750,"filename":5751,"language":1440,"meta":1100,"style":1100},"namespace Tests\\Unit;\n\nuse App\\Rules\\Zipcode;\nuse Illuminate\\Foundation\\Testing\\TestCase;\nuse Tests\\CreatesApplication;\nuse Illuminate\\Support\\Facades\\Validator;\nuse \\Illuminate\\Validation\\ValidationException;\n\nclass Rules extends TestCase{\n    use CreatesApplication;\n\n    public function test_zipcode_validation(){\n        $tests = [\n            '1234567'=>true,\n            '0012344'=>true,\n            '0012340'=>true,\n            '12345678'=>false,\n            '123456'=>false,\n            '1234 56'=>false,\n            '1234a56'=>false,\n            '1234_56'=>false,\n        ];\n        foreach($tests as $key => $condition){\n            try{\n                Validator::make(['test'=>$key],[\n                    'test'=> new Zipcode()\n                ])->validate();\n                $this->assertTrue($condition===true);\n            }catch(ValidationException $e){\n                $this->assertFalse($condition);\n            }\n        }\n    }\n}\n","Rules.php",[1072,5753,5754,5759,5763,5768,5773,5778,5783,5788,5792,5797,5802,5806,5811,5816,5821,5826,5831,5836,5841,5846,5851,5856,5861,5866,5871,5876,5881,5886,5891,5896,5901,5905,5909,5913],{"__ignoreMap":1100},[327,5755,5756],{"class":1313,"line":1314},[327,5757,5758],{},"namespace Tests\\Unit;\n",[327,5760,5761],{"class":1313,"line":1104},[327,5762,1847],{"emptyLinePlaceholder":1151},[327,5764,5765],{"class":1313,"line":1101},[327,5766,5767],{},"use App\\Rules\\Zipcode;\n",[327,5769,5770],{"class":1313,"line":1111},[327,5771,5772],{},"use Illuminate\\Foundation\\Testing\\TestCase;\n",[327,5774,5775],{"class":1313,"line":1465},[327,5776,5777],{},"use Tests\\CreatesApplication;\n",[327,5779,5780],{"class":1313,"line":1471},[327,5781,5782],{},"use Illuminate\\Support\\Facades\\Validator;\n",[327,5784,5785],{"class":1313,"line":1868},[327,5786,5787],{},"use \\Illuminate\\Validation\\ValidationException;\n",[327,5789,5790],{"class":1313,"line":1874},[327,5791,1847],{"emptyLinePlaceholder":1151},[327,5793,5794],{"class":1313,"line":1879},[327,5795,5796],{},"class Rules extends TestCase{\n",[327,5798,5799],{"class":1313,"line":1885},[327,5800,5801],{},"    use CreatesApplication;\n",[327,5803,5804],{"class":1313,"line":1891},[327,5805,1847],{"emptyLinePlaceholder":1151},[327,5807,5808],{"class":1313,"line":1897},[327,5809,5810],{},"    public function test_zipcode_validation(){\n",[327,5812,5813],{"class":1313,"line":1903},[327,5814,5815],{},"        $tests = [\n",[327,5817,5818],{"class":1313,"line":1909},[327,5819,5820],{},"            '1234567'=>true,\n",[327,5822,5823],{"class":1313,"line":1915},[327,5824,5825],{},"            '0012344'=>true,\n",[327,5827,5828],{"class":1313,"line":1920},[327,5829,5830],{},"            '0012340'=>true,\n",[327,5832,5833],{"class":1313,"line":1925},[327,5834,5835],{},"            '12345678'=>false,\n",[327,5837,5838],{"class":1313,"line":1931},[327,5839,5840],{},"            '123456'=>false,\n",[327,5842,5843],{"class":1313,"line":1936},[327,5844,5845],{},"            '1234 56'=>false,\n",[327,5847,5848],{"class":1313,"line":1941},[327,5849,5850],{},"            '1234a56'=>false,\n",[327,5852,5853],{"class":1313,"line":1946},[327,5854,5855],{},"            '1234_56'=>false,\n",[327,5857,5858],{"class":1313,"line":1952},[327,5859,5860],{},"        ];\n",[327,5862,5863],{"class":1313,"line":1957},[327,5864,5865],{},"        foreach($tests as $key => $condition){\n",[327,5867,5868],{"class":1313,"line":1962},[327,5869,5870],{},"            try{\n",[327,5872,5873],{"class":1313,"line":1968},[327,5874,5875],{},"                Validator::make(['test'=>$key],[\n",[327,5877,5878],{"class":1313,"line":1973},[327,5879,5880],{},"                    'test'=> new Zipcode()\n",[327,5882,5883],{"class":1313,"line":1979},[327,5884,5885],{},"                ])->validate();\n",[327,5887,5888],{"class":1313,"line":1984},[327,5889,5890],{},"                $this->assertTrue($condition===true);\n",[327,5892,5893],{"class":1313,"line":1990},[327,5894,5895],{},"            }catch(ValidationException $e){\n",[327,5897,5898],{"class":1313,"line":1996},[327,5899,5900],{},"                $this->assertFalse($condition);\n",[327,5902,5903],{"class":1313,"line":2002},[327,5904,2205],{},[327,5906,5907],{"class":1313,"line":2007},[327,5908,2239],{},[327,5910,5911],{"class":1313,"line":2012},[327,5912,1752],{},[327,5914,5915],{"class":1313,"line":2017},[327,5916,1474],{},[13,5918,5919],{},"バリデーションテストでは正しい形式は正しいと判断（ポジティブテスト）し、間違っているものは間違っていると判断（ネガティブテスト）できているかをテストします。\nもしも正しいのに間違っていると判断したり、間違っているのに正しいとなったらテストが失敗する様になっています。",[13,5921,5922],{},"ここでバリデーションテストの詳細を解説します。",[104,5924,5925],{"id":5925},"バリデーションのインスタンスを作成",[13,5927,5928],{},"最初にテストしたいバリデーションのインスタンス、そしてバリデーターインスタンスを作成します。",[1304,5930,5932],{"className":1438,"code":5931,"language":1440,"meta":1100,"style":1100},"Validator::make(['test'=>$key],[\n    'test'=> new Zipcode()\n])->validate();\n",[1072,5933,5934,5939,5944],{"__ignoreMap":1100},[327,5935,5936],{"class":1313,"line":1314},[327,5937,5938],{},"Validator::make(['test'=>$key],[\n",[327,5940,5941],{"class":1313,"line":1104},[327,5942,5943],{},"    'test'=> new Zipcode()\n",[327,5945,5946],{"class":1313,"line":1101},[327,5947,5948],{},"])->validate();\n",[13,5950,5951,5954,5955,5958],{},[1072,5952,5953],{},"Validator","では",[1072,5956,5957],{},"make()","を使用して第一引数に、バリデーションをする値とキーを設定します。第二引数にはバリデーションのキーとバリデーションインスタンスを指定します。",[104,5960,5961],{"id":5961},"いろんなパターンをテストする",[13,5963,5964],{},"いろいろなパターンをテストするため以下の様に配列でテストパターンと予期する正誤を設定します。Trueはバリデーション通過で、Falseはバリデーション違反（間違った形式）であることを示します。",[1304,5966,5968],{"className":1438,"code":5967,"language":1440,"meta":1100,"style":1100},"$tests = [\n    '1234567'=>true,\n    '0012344'=>true,\n    '0012340'=>true,\n    '12345678'=>false,\n    '123456'=>false,\n    '1234 56'=>false,\n    '1234a56'=>false,\n    '1234_56'=>false,\n];\n",[1072,5969,5970,5975,5980,5985,5990,5995,6000,6005,6010,6015],{"__ignoreMap":1100},[327,5971,5972],{"class":1313,"line":1314},[327,5973,5974],{},"$tests = [\n",[327,5976,5977],{"class":1313,"line":1104},[327,5978,5979],{},"    '1234567'=>true,\n",[327,5981,5982],{"class":1313,"line":1101},[327,5983,5984],{},"    '0012344'=>true,\n",[327,5986,5987],{"class":1313,"line":1111},[327,5988,5989],{},"    '0012340'=>true,\n",[327,5991,5992],{"class":1313,"line":1465},[327,5993,5994],{},"    '12345678'=>false,\n",[327,5996,5997],{"class":1313,"line":1471},[327,5998,5999],{},"    '123456'=>false,\n",[327,6001,6002],{"class":1313,"line":1868},[327,6003,6004],{},"    '1234 56'=>false,\n",[327,6006,6007],{"class":1313,"line":1874},[327,6008,6009],{},"    '1234a56'=>false,\n",[327,6011,6012],{"class":1313,"line":1879},[327,6013,6014],{},"    '1234_56'=>false,\n",[327,6016,6017],{"class":1313,"line":1885},[327,6018,6019],{},"];\n",[13,6021,6022],{},"そしてforeachでそれぞれチェックします。",[1304,6024,6026],{"className":1438,"code":6025,"language":1440,"meta":1100,"style":1100},"foreach($tests as $key => $condition){\n    try{\n        Validator::make(['test'=>$key],[\n            'test'=> new Zipcode()\n        ])->validate();\n        $this->assertTrue($condition===true);\n    }catch(ValidationException $e){\n        $this->assertFalse($condition);\n    }\n}\n",[1072,6027,6028,6033,6038,6043,6048,6053,6058,6063,6068,6072],{"__ignoreMap":1100},[327,6029,6030],{"class":1313,"line":1314},[327,6031,6032],{},"foreach($tests as $key => $condition){\n",[327,6034,6035],{"class":1313,"line":1104},[327,6036,6037],{},"    try{\n",[327,6039,6040],{"class":1313,"line":1101},[327,6041,6042],{},"        Validator::make(['test'=>$key],[\n",[327,6044,6045],{"class":1313,"line":1111},[327,6046,6047],{},"            'test'=> new Zipcode()\n",[327,6049,6050],{"class":1313,"line":1465},[327,6051,6052],{},"        ])->validate();\n",[327,6054,6055],{"class":1313,"line":1471},[327,6056,6057],{},"        $this->assertTrue($condition===true);\n",[327,6059,6060],{"class":1313,"line":1868},[327,6061,6062],{},"    }catch(ValidationException $e){\n",[327,6064,6065],{"class":1313,"line":1874},[327,6066,6067],{},"        $this->assertFalse($condition);\n",[327,6069,6070],{"class":1313,"line":1879},[327,6071,1752],{},[327,6073,6074],{"class":1313,"line":1885},[327,6075,1474],{},[13,6077,6078,6081,6082,6085,6086,6088,6089,6092,6093,6096],{},[1072,6079,6080],{},"validate()","メソッドは失敗すると",[1072,6083,6084],{},"ValidationException"," を投げます。そのため例外処理で",[1072,6087,6084],{}," をキャッチします。正しい形式を正しいと判断できれば",[1072,6090,6091],{},"$this->assertTrue($condition===true);","となり、まちがった形を間違っていると判断できればキャッチして",[1072,6094,6095],{},"$this->assertFalse($condition);","でアサートされます。",[1304,6098,6101],{"className":6099,"code":6100,"language":1809},[1807],"php artisan test \n",[1072,6102,6100],{"__ignoreMap":1100},[13,6104,6105],{},"にてテストを行い問題なければそのまま通過します。テストは以上の方法でいろんなパターンをテストできます。パターンは思いついたものを配列に書いてもいいですし、プログラム的に大量に配列を生成してもいいかもしれません。まとめると",[61,6107,6108,6111,6116],{},[64,6109,6110],{},"バリデーションインスタンスを作成",[64,6112,6113,6115],{},[1072,6114,6084],{},"を用いて間違っている形を識別",[64,6117,6118,6119,1514,6122,6125],{},"バリデーション対象の値が正誤かどうかは",[1072,6120,6121],{},"assertTrue",[1072,6123,6124],{},"assertFalse","で正しく判断できているかをアサート",[13,6127,6128],{},"こんな感じです。私はこのテストで結構救われたので、バリデーションを作った時は必ずユニットテストをしましょう。",[1408,6130,1567],{},{"title":1100,"searchDepth":1101,"depth":1101,"links":6132},[6133,6134],{"id":5666,"depth":1104,"text":5666},{"id":5740,"depth":1104,"text":5740,"children":6135},[6136,6137],{"id":5925,"depth":1101,"text":5925},{"id":5961,"depth":1101,"text":5961},[1262],"2022-03-25",{},"\u002Farticles\u002Flaravel-validation-unit-test",{"title":5651,"description":5651},"articles\u002Flaravel-validation-unit-test",[1440,1577],"_6kCaD6uDnBdQkj-XY_8G39zixCeVekVE-xS127shV4",1780987151801]