[{"data":1,"prerenderedAt":2647},["ShallowReactive",2],{"category-devstack-5":3},{"count":4,"content":5},43,[6,1071,1943],{"id":7,"title":8,"body":9,"category":1058,"createdAt":1060,"description":8,"extension":1061,"index":1062,"meta":1063,"navigation":144,"path":1064,"publish":144,"seo":1065,"series":1062,"seriesTitle":1062,"stem":1066,"tag":1067,"thumbnail":1069,"updatedAt":1062,"__hash__":1070},"articles\u002Farticles\u002Ftweet-check-laravel-jpb-scheduler.md","Larave 7.0でキュー・ジョブ・スケジューラー触ってみる。取得したツイートが存在するか毎夜毎夜チェックするジョブとスケジューラー。",{"type":10,"value":11,"toc":1045},"minimark",[12,16,19,23,26,29,32,42,45,48,55,68,71,74,78,81,92,95,99,102,108,111,115,118,261,271,592,595,606,609,612,742,745,752,766,769,775,778,781,788,804,811,960,970,982,988,991,998,1004,1007,1013,1019,1026,1032,1035,1038,1041],[13,14,15],"p",{},"こんにちはjunです。仕事でLaravelを触ることになり必死にドキュメントを読んだりして対策をしています。フルスタックフレームワークと言われているほど、Laravelは機能が豊富なので想像していた物が作れて感動しています。",[13,17,18],{},"そこで今回はドキュメントをみてもイマイチパッとしない、ジョブ(Job)、キュー(Queue)、スケジューラー(Scheduler)を触りながら機能を確かめていきたいと思います。",[20,21,22],"h2",{"id":22},"作りたい物",[13,24,25],{},"Laravelを用いて外部APIと通信して、取得したデータをDBに入れたりブラウザ上で操作するアプリケーションを作成していたときにある問題にぶつかりました。TwitterAPIを用いてツイートの情報を保存していたのですが、非公開もしくは削除されたため表示できないツイートがありました。",[13,27,28],{},"削除されたツイートを破棄するシステムを構築する必要があります。システム自体は簡単でDBから保存されたツイート一つ一つのIDを取得してTwitterAPIをを投げて、ツイートが存在するかの結果を確認すればいいだけです。スクレイピングするような感じです。しかしこの方法には課題があります。",[13,30,31],{},"保存されたツイートが大量にある場合、Twitterのサーバーに大量のリクエストを短時間に送信してしまい、Dosとなってしまいます。そのため各リクエスト毎に最低1秒程でも緩急を置く必要があります。その場合、その処理が終了する時間は保存されたツイート量によってはかなり長くなります。",[13,33,34,35,41],{},"大体、このような長くなりがちな処理はキューとして、サーバー上で非同期に行わせるのがベストです。そしてLarvelではジョブを簡単に実装できる",[36,37,40],"a",{"href":38,"target":39},"https:\u002F\u002Freadouble.com\u002Flaravel\u002F7.x\u002Fja\u002Fscheduling.html","_blank","「タスクスケジュール」","という物があります。",[13,43,44],{},"今回は24時になると保存したツイートの存在を確認してくれる自動実行ジョブを構築したいと思います。",[20,46,47],{"id":47},"タスクスケージューラーを理解する",[13,49,50,54],{},[36,51,53],{"href":52,"target":39},"https:\u002F\u002Flaravel.com\u002Fdocs\u002F7.x\u002Fscheduling","公式のドキュメント","を見てみましょう。おおよそですが実装の手順としては",[56,57,58,62,65],"ol",{},[59,60,61],"li",{},"ジョブの定義（実際の処理ロジック）をapp\u002Fjob\u002F 配下に作成する。",[59,63,64],{},"行うジョブやその実行時間は app\u002Fconsole\u002Fkernel.php に記述。",[59,66,67],{},"cronに毎分Larvelスケジューラーを実行させるコードを記述。",[13,69,70],{},"こんな感じです。細かく解説していきます。",[20,72,73],{"id":73},"ジョブを作成する",[75,76,77],"h3",{"id":77},"キューを使えるようにする",[13,79,80],{},"まずはLaracelでジョブを実行できる環境を整えます。「キュー」に関するドキュメントを見ると以下のコマンドを唱えてジョブを記録するテーブルを作成します。",[82,83,88],"pre",{"className":84,"code":86,"language":87},[85],"language-text","php artisan queue:table\n\nphp artisan migrate\n","text",[89,90,86],"code",{"__ignoreMap":91},"",[13,93,94],{},"このコマンドを唱えると「jobs」「failed_jobs」というテーブルが作成されます。ジョブとキューはセットで使われることが多いので上記のコマンドを唱えておきましょう",[96,97,98],"h4",{"id":98},"ジョブファイルを作成する",[13,100,101],{},"ジョブファイルは app\u002Fjob 配下に作成します。しかし最初はこのjobディレクトリは存在しないので以下のコマンドを唱えて、ジョブファイルのテンプレートとディレクトリを作ってもらいます。",[82,103,106],{"className":104,"code":105,"language":87},[85],"php artisan make:job YOUR_JOB_NAME\n",[89,107,105],{"__ignoreMap":91},[13,109,110],{},"「YOUR_JOB_NAME」の部分にジョブの名前を入力してください。他のmakeコマンドと同じ感じですね。このコマンドを唱えるとjobディレクトリとjobクラスが書かれたファイルが配下に作成されます。そのファイルにジョブの実行処理を書いていきます。",[75,112,114],{"id":113},"twitterにapiを送るジョブを記述","TwitterにAPIを送るジョブを記述",[13,116,117],{},"今回はジョブ名を「CheckNotFoundUrls」として作成します。ジョブファイルは作成するとまず下記のように書かれています。",[82,119,123],{"className":120,"code":121,"language":122,"meta":91,"style":91},"language-php shiki shiki-themes material-theme-ocean","\u003C?php\nnamespace App\\Jobs;\n\nuse Illuminate\\Bus\\Queueable;\nuse Illuminate\\Contracts\\Queue\\ShouldQueue;\nuse Illuminate\\Foundation\\Bus\\Dispatchable;\nuse Illuminate\\Queue\\InteractsWithQueue;\nuse Illuminate\\Queue\\SerializesModels;\n\nclass CheckNotFoundUrls implements ShouldQueue\n{\n    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;\n\n    public function __construct()\n    {\n        \u002F\u002F\n    }\n\n    public function handle()\n    {\n        \u002F\u002Fここに処理を記述\n    }\n}\n","php",[89,124,125,133,139,146,152,158,164,170,176,181,187,193,199,204,210,216,222,228,233,239,244,250,255],{"__ignoreMap":91},[126,127,130],"span",{"class":128,"line":129},"line",1,[126,131,132],{},"\u003C?php\n",[126,134,136],{"class":128,"line":135},2,[126,137,138],{},"namespace App\\Jobs;\n",[126,140,142],{"class":128,"line":141},3,[126,143,145],{"emptyLinePlaceholder":144},true,"\n",[126,147,149],{"class":128,"line":148},4,[126,150,151],{},"use Illuminate\\Bus\\Queueable;\n",[126,153,155],{"class":128,"line":154},5,[126,156,157],{},"use Illuminate\\Contracts\\Queue\\ShouldQueue;\n",[126,159,161],{"class":128,"line":160},6,[126,162,163],{},"use Illuminate\\Foundation\\Bus\\Dispatchable;\n",[126,165,167],{"class":128,"line":166},7,[126,168,169],{},"use Illuminate\\Queue\\InteractsWithQueue;\n",[126,171,173],{"class":128,"line":172},8,[126,174,175],{},"use Illuminate\\Queue\\SerializesModels;\n",[126,177,179],{"class":128,"line":178},9,[126,180,145],{"emptyLinePlaceholder":144},[126,182,184],{"class":128,"line":183},10,[126,185,186],{},"class CheckNotFoundUrls implements ShouldQueue\n",[126,188,190],{"class":128,"line":189},11,[126,191,192],{},"{\n",[126,194,196],{"class":128,"line":195},12,[126,197,198],{},"    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;\n",[126,200,202],{"class":128,"line":201},13,[126,203,145],{"emptyLinePlaceholder":144},[126,205,207],{"class":128,"line":206},14,[126,208,209],{},"    public function __construct()\n",[126,211,213],{"class":128,"line":212},15,[126,214,215],{},"    {\n",[126,217,219],{"class":128,"line":218},16,[126,220,221],{},"        \u002F\u002F\n",[126,223,225],{"class":128,"line":224},17,[126,226,227],{},"    }\n",[126,229,231],{"class":128,"line":230},18,[126,232,145],{"emptyLinePlaceholder":144},[126,234,236],{"class":128,"line":235},19,[126,237,238],{},"    public function handle()\n",[126,240,242],{"class":128,"line":241},20,[126,243,215],{},[126,245,247],{"class":128,"line":246},21,[126,248,249],{},"        \u002F\u002Fここに処理を記述\n",[126,251,253],{"class":128,"line":252},22,[126,254,227],{},[126,256,258],{"class":128,"line":257},23,[126,259,260],{},"}\n",[13,262,263,266,267,270],{},[89,264,265],{},"handle()","に処理内容を記述します。ジョブを実行するときはこのクラスを読み込み、ジョブインスタンスを作成し、",[89,268,269],{},"dispatch()","するとジョブが実行されます。まあ、とりあえずTwitterにAPIを飛ばすコードを書きます。コードは載せますが詳細の説明は省きます。",[82,272,274],{"className":120,"code":273,"language":122,"meta":91,"style":91},"\u003C?php\nnamespace App\\Jobs;\n\n\u002F\u002FTwitter API ライブラリ\nuse Abraham\\TwitterOAuth\\TwitterOAuth;\n\nuse App\\Models\\Tweets; \u002F\u002F ツイート情報が格納されたテーブルに接続するモデル\nuse App\\Models\\JobReport;　\u002F\u002F 自作ジョブの結果を記録しておくテーブル\n\nuse Illuminate\\Bus\\Queueable;\nuse Illuminate\\Contracts\\Queue\\ShouldQueue;\nuse Illuminate\\Foundation\\Bus\\Dispatchable;\nuse Illuminate\\Queue\\InteractsWithQueue;\nuse Illuminate\\Queue\\SerializesModels;\n\nclass CheckNotFoundUrls implements ShouldQueue\n{\n    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;\n\n    public function __construct()\n    {\n        \u002F\u002F\n    }\n\n    public function handle()\n    {\n        try{\n            \u002F\u002F ツイート情報があるテーブルからツイート情報（JSON）と必要なレコードを取得\n            $datas = Tweets::select('tweet_rawdata','id','user_id')->get();\n            $deletedTweetOwner = array();\n    \n            foreach($datas as $val){\n                \u002F\u002F'tweet_rawdata'カラムはjsonなので一旦decode。\n                \u002F\u002Fjsonからid_strというツイートIDを取得\n                $decodeTweetData = json_decode($val->tweet_rawdata);\n                $tweet_id = $decodeTweetData->id_str;\n\n                \u002F\u002FAPIに接続するための準備（ライブラリ）をして、APIを送信\n                $connection = new TwitterOAuth(env('TW_CONSUMER_KEY'), env('TW_CONSUMER_KEY_SECRET'), env('TW_ACCESS_TOKEN'), env('TW_ACCESS_TOKEN_SECRET'));\n                $connection->setTimeouts(10, 15);\n                $content = $connection->get(\"statuses\u002Fshow\",['id'=>intval($tweet_id)]);\n    \n                \u002F\u002F請求したツイートIDのツイートが存在しないという結果ならばレコードのIDを $deletedTweetOwnerにユーザーIDを記録\n                if(property_exists($content,'errors')){\n                    switch($content->errors[0]->code){\n                        case 144:\n                            Tweets::destroy($val->id);\n                            array_push($deletedTweetOwner,$val->id);\n                        break;\n                    }\n                }\n                \u002F\u002FDosにならないように1秒止める\n                sleep(1.0);\n            }\n  \n        }catch(\\Exception $e){\n            \u002F\u002F処理が失敗したら管理者用のレポートにエラ〜メッセージを記録する。\n            JobReport::reportException(0,$e->getMessage());\n        }\n    }\n}\n",[89,275,276,280,284,288,293,298,302,307,312,316,320,324,328,332,336,340,344,348,352,356,360,364,368,372,377,382,387,393,399,405,411,417,423,429,435,441,447,452,458,464,470,476,481,486,492,498,504,510,516,522,528,534,540,546,552,558,564,570,576,582,587],{"__ignoreMap":91},[126,277,278],{"class":128,"line":129},[126,279,132],{},[126,281,282],{"class":128,"line":135},[126,283,138],{},[126,285,286],{"class":128,"line":141},[126,287,145],{"emptyLinePlaceholder":144},[126,289,290],{"class":128,"line":148},[126,291,292],{},"\u002F\u002FTwitter API ライブラリ\n",[126,294,295],{"class":128,"line":154},[126,296,297],{},"use Abraham\\TwitterOAuth\\TwitterOAuth;\n",[126,299,300],{"class":128,"line":160},[126,301,145],{"emptyLinePlaceholder":144},[126,303,304],{"class":128,"line":166},[126,305,306],{},"use App\\Models\\Tweets; \u002F\u002F ツイート情報が格納されたテーブルに接続するモデル\n",[126,308,309],{"class":128,"line":172},[126,310,311],{},"use App\\Models\\JobReport;　\u002F\u002F 自作ジョブの結果を記録しておくテーブル\n",[126,313,314],{"class":128,"line":178},[126,315,145],{"emptyLinePlaceholder":144},[126,317,318],{"class":128,"line":183},[126,319,151],{},[126,321,322],{"class":128,"line":189},[126,323,157],{},[126,325,326],{"class":128,"line":195},[126,327,163],{},[126,329,330],{"class":128,"line":201},[126,331,169],{},[126,333,334],{"class":128,"line":206},[126,335,175],{},[126,337,338],{"class":128,"line":212},[126,339,145],{"emptyLinePlaceholder":144},[126,341,342],{"class":128,"line":218},[126,343,186],{},[126,345,346],{"class":128,"line":224},[126,347,192],{},[126,349,350],{"class":128,"line":230},[126,351,198],{},[126,353,354],{"class":128,"line":235},[126,355,145],{"emptyLinePlaceholder":144},[126,357,358],{"class":128,"line":241},[126,359,209],{},[126,361,362],{"class":128,"line":246},[126,363,215],{},[126,365,366],{"class":128,"line":252},[126,367,221],{},[126,369,370],{"class":128,"line":257},[126,371,227],{},[126,373,375],{"class":128,"line":374},24,[126,376,145],{"emptyLinePlaceholder":144},[126,378,380],{"class":128,"line":379},25,[126,381,238],{},[126,383,385],{"class":128,"line":384},26,[126,386,215],{},[126,388,390],{"class":128,"line":389},27,[126,391,392],{},"        try{\n",[126,394,396],{"class":128,"line":395},28,[126,397,398],{},"            \u002F\u002F ツイート情報があるテーブルからツイート情報（JSON）と必要なレコードを取得\n",[126,400,402],{"class":128,"line":401},29,[126,403,404],{},"            $datas = Tweets::select('tweet_rawdata','id','user_id')->get();\n",[126,406,408],{"class":128,"line":407},30,[126,409,410],{},"            $deletedTweetOwner = array();\n",[126,412,414],{"class":128,"line":413},31,[126,415,416],{},"    \n",[126,418,420],{"class":128,"line":419},32,[126,421,422],{},"            foreach($datas as $val){\n",[126,424,426],{"class":128,"line":425},33,[126,427,428],{},"                \u002F\u002F'tweet_rawdata'カラムはjsonなので一旦decode。\n",[126,430,432],{"class":128,"line":431},34,[126,433,434],{},"                \u002F\u002Fjsonからid_strというツイートIDを取得\n",[126,436,438],{"class":128,"line":437},35,[126,439,440],{},"                $decodeTweetData = json_decode($val->tweet_rawdata);\n",[126,442,444],{"class":128,"line":443},36,[126,445,446],{},"                $tweet_id = $decodeTweetData->id_str;\n",[126,448,450],{"class":128,"line":449},37,[126,451,145],{"emptyLinePlaceholder":144},[126,453,455],{"class":128,"line":454},38,[126,456,457],{},"                \u002F\u002FAPIに接続するための準備（ライブラリ）をして、APIを送信\n",[126,459,461],{"class":128,"line":460},39,[126,462,463],{},"                $connection = new TwitterOAuth(env('TW_CONSUMER_KEY'), env('TW_CONSUMER_KEY_SECRET'), env('TW_ACCESS_TOKEN'), env('TW_ACCESS_TOKEN_SECRET'));\n",[126,465,467],{"class":128,"line":466},40,[126,468,469],{},"                $connection->setTimeouts(10, 15);\n",[126,471,473],{"class":128,"line":472},41,[126,474,475],{},"                $content = $connection->get(\"statuses\u002Fshow\",['id'=>intval($tweet_id)]);\n",[126,477,479],{"class":128,"line":478},42,[126,480,416],{},[126,482,483],{"class":128,"line":4},[126,484,485],{},"                \u002F\u002F請求したツイートIDのツイートが存在しないという結果ならばレコードのIDを $deletedTweetOwnerにユーザーIDを記録\n",[126,487,489],{"class":128,"line":488},44,[126,490,491],{},"                if(property_exists($content,'errors')){\n",[126,493,495],{"class":128,"line":494},45,[126,496,497],{},"                    switch($content->errors[0]->code){\n",[126,499,501],{"class":128,"line":500},46,[126,502,503],{},"                        case 144:\n",[126,505,507],{"class":128,"line":506},47,[126,508,509],{},"                            Tweets::destroy($val->id);\n",[126,511,513],{"class":128,"line":512},48,[126,514,515],{},"                            array_push($deletedTweetOwner,$val->id);\n",[126,517,519],{"class":128,"line":518},49,[126,520,521],{},"                        break;\n",[126,523,525],{"class":128,"line":524},50,[126,526,527],{},"                    }\n",[126,529,531],{"class":128,"line":530},51,[126,532,533],{},"                }\n",[126,535,537],{"class":128,"line":536},52,[126,538,539],{},"                \u002F\u002FDosにならないように1秒止める\n",[126,541,543],{"class":128,"line":542},53,[126,544,545],{},"                sleep(1.0);\n",[126,547,549],{"class":128,"line":548},54,[126,550,551],{},"            }\n",[126,553,555],{"class":128,"line":554},55,[126,556,557],{},"  \n",[126,559,561],{"class":128,"line":560},56,[126,562,563],{},"        }catch(\\Exception $e){\n",[126,565,567],{"class":128,"line":566},57,[126,568,569],{},"            \u002F\u002F処理が失敗したら管理者用のレポートにエラ〜メッセージを記録する。\n",[126,571,573],{"class":128,"line":572},58,[126,574,575],{},"            JobReport::reportException(0,$e->getMessage());\n",[126,577,579],{"class":128,"line":578},59,[126,580,581],{},"        }\n",[126,583,585],{"class":128,"line":584},60,[126,586,227],{},[126,588,590],{"class":128,"line":589},61,[126,591,260],{},[13,593,594],{},"ちなみにTwitterへのAPIアクセスには Abraham\\TwitterOAuth というTwttiter公式が紹介するサードパーティライブラリを使用しています。composerで読み込んでuseすればすぐに使え、上記のようにouthインスタンスを作成するだけで使えます。簡単なのでLarvelでTwitterAPIを使う時などにおすすめです。",[596,597,601,602],"div",{"className":598},[599,600],"alert","alert-info","\n    ",[36,603,605],{"href":604,"target":39},"https:\u002F\u002Fgithub.com\u002Fabraham\u002Ftwitteroauth","GITHUB：abraham\u002Ftwitteroauth\n    ",[13,607,608],{},"とりあえずこれでジョブは完成しました。試しにチェックをしてみたいですが、なんとかしてこのジョブを実行する必要があります。しかしLaravelには定義したジョブを実行するコマンドが見当たりません。（もしかしたら見落としてるだけかも）",[13,610,611],{},"コマンドでジョブを叩いて実行したいので、以下のカスタムコマンドを自作します。",[82,613,615],{"className":120,"code":614,"language":122,"meta":91,"style":91},"\u003C?php\n\nnamespace App\\Console\\Commands;\n\nuse Illuminate\\Console\\Command;\n\nclass DispatchJob extends Command\n{\n    protected $signature = 'job:dispatch {job}';\n\n    \u002F**\n     * The console command description.\n     *\n     * @var string\n     *\u002F\n    protected $description = 'Dispatch job';\n\n    public function __construct()\n    {\n        parent::__construct();\n    }\n\n    public function handle()\n    {\n        $class = '\\\\App\\\\Jobs\\\\' . $this->argument('job');\n        dispatch(new $class());\n    }\n}\n",[89,616,617,621,625,630,634,639,643,648,652,657,661,666,671,676,681,686,691,695,699,703,708,712,716,720,724,729,734,738],{"__ignoreMap":91},[126,618,619],{"class":128,"line":129},[126,620,132],{},[126,622,623],{"class":128,"line":135},[126,624,145],{"emptyLinePlaceholder":144},[126,626,627],{"class":128,"line":141},[126,628,629],{},"namespace App\\Console\\Commands;\n",[126,631,632],{"class":128,"line":148},[126,633,145],{"emptyLinePlaceholder":144},[126,635,636],{"class":128,"line":154},[126,637,638],{},"use Illuminate\\Console\\Command;\n",[126,640,641],{"class":128,"line":160},[126,642,145],{"emptyLinePlaceholder":144},[126,644,645],{"class":128,"line":166},[126,646,647],{},"class DispatchJob extends Command\n",[126,649,650],{"class":128,"line":172},[126,651,192],{},[126,653,654],{"class":128,"line":178},[126,655,656],{},"    protected $signature = 'job:dispatch {job}';\n",[126,658,659],{"class":128,"line":183},[126,660,145],{"emptyLinePlaceholder":144},[126,662,663],{"class":128,"line":189},[126,664,665],{},"    \u002F**\n",[126,667,668],{"class":128,"line":195},[126,669,670],{},"     * The console command description.\n",[126,672,673],{"class":128,"line":201},[126,674,675],{},"     *\n",[126,677,678],{"class":128,"line":206},[126,679,680],{},"     * @var string\n",[126,682,683],{"class":128,"line":212},[126,684,685],{},"     *\u002F\n",[126,687,688],{"class":128,"line":218},[126,689,690],{},"    protected $description = 'Dispatch job';\n",[126,692,693],{"class":128,"line":224},[126,694,145],{"emptyLinePlaceholder":144},[126,696,697],{"class":128,"line":230},[126,698,209],{},[126,700,701],{"class":128,"line":235},[126,702,215],{},[126,704,705],{"class":128,"line":241},[126,706,707],{},"        parent::__construct();\n",[126,709,710],{"class":128,"line":246},[126,711,227],{},[126,713,714],{"class":128,"line":252},[126,715,145],{"emptyLinePlaceholder":144},[126,717,718],{"class":128,"line":257},[126,719,238],{},[126,721,722],{"class":128,"line":374},[126,723,215],{},[126,725,726],{"class":128,"line":379},[126,727,728],{},"        $class = '\\\\App\\\\Jobs\\\\' . $this->argument('job');\n",[126,730,731],{"class":128,"line":384},[126,732,733],{},"        dispatch(new $class());\n",[126,735,736],{"class":128,"line":389},[126,737,227],{},[126,739,740],{"class":128,"line":395},[126,741,260],{},[13,743,744],{},"このstack overflowの記事を引用しています。ありがとうございます。",[596,746,601,748],{"className":747},[599,600],[36,749,751],{"href":750,"target":39},"https:\u002F\u002Fstackoverflow.com\u002Fquestions\u002F43357472\u002Fhow-to-manually-run-a-laravel-lumen-job-using-command-line\u002F51045851","StackOverflow How to manually run a laravel\u002Flumen job using command line\n    ",[13,753,754,755,758,759,762,763,765],{},"このファイルをapp\u002FConsole\u002FCommands 配下に作成します。Console\u002FCommands 配下では",[89,756,757],{},"php artisan"," を用いたコマンドを自分で作成することができます。ここでは ",[89,760,761],{},"php artisan job:dispatch JOB_NAME"," と唱えればJOB_NAMEで書かれたジョブクラスの",[89,764,265],{},"を実行してくれます。",[13,767,768],{},"これを用いて以下のように唱えます。",[82,770,773],{"className":771,"code":772,"language":87},[85],"php artisan job:dispatch CheckNotFoundUrls\n",[89,774,772],{"__ignoreMap":91},[13,776,777],{},"するとprintなどを一時的につけて、結果を見ると無事に404のツイートの数が検知され、削除を実行してくれました。",[75,779,780],{"id":780},"ジョブが実行される時間を定める",[13,782,783,784,787],{},"それでは次に上で作成したジョブをスケジューラーに登録します。スケジューラーで実行させるジョブは ",[89,785,786],{},"app\u002FConsole\u002FKernel.php"," にジョブインスタンスを作って実行します。",[13,789,790,792,793,796,797,800,801,803],{},[89,791,786],{}," には ",[89,794,795],{},"schedule()"," というメソッドがあり ",[89,798,799],{},"php artisan run:schedule"," を実行すると ",[89,802,795],{}," が実行されます。",[13,805,806,807,810],{},"先ほどのジョブクラスをこの",[89,808,809],{},"Kernel.php","で使えるようにして、ジョブインスタンスを作成します。",[82,812,814],{"className":120,"code":813,"language":122,"meta":91,"style":91},"\u003C?php\n\nnamespace App\\Console;\n\nuse Illuminate\\Console\\Scheduling\\Schedule;\nuse Illuminate\\Foundation\\Console\\Kernel as ConsoleKernel;\nuse App\\Jobs\\CheckNotFoundUrls; \u002F\u002Fここで先ほど作成したジョブクラス\nuse DateTime;\n\nclass Kernel extends ConsoleKernel\n{\n    \u002F**\n     * The Artisan commands provided by your application.\n     *\n     * @var array\n     *\u002F\n    protected $commands = [\n        \u002F\u002F\n    ];\n\n    \u002F**\n     * Define the application's command schedule.\n     *\n     * @param  \\Illuminate\\Console\\Scheduling\\Schedule  $schedule\n     * @return void\n     *\u002F\n    protected function schedule(Schedule $schedule)\n    {\n        \u002F\u002F実行するjobをキューに投入する。そして実行時間を設定。\n        $schedule->job(new CheckNotFoundUrls(),'checkUrl')->diary()\n    }\n}\n",[89,815,816,820,824,829,833,838,843,848,853,857,862,866,870,875,879,884,888,893,897,902,906,910,915,919,924,929,933,938,942,947,952,956],{"__ignoreMap":91},[126,817,818],{"class":128,"line":129},[126,819,132],{},[126,821,822],{"class":128,"line":135},[126,823,145],{"emptyLinePlaceholder":144},[126,825,826],{"class":128,"line":141},[126,827,828],{},"namespace App\\Console;\n",[126,830,831],{"class":128,"line":148},[126,832,145],{"emptyLinePlaceholder":144},[126,834,835],{"class":128,"line":154},[126,836,837],{},"use Illuminate\\Console\\Scheduling\\Schedule;\n",[126,839,840],{"class":128,"line":160},[126,841,842],{},"use Illuminate\\Foundation\\Console\\Kernel as ConsoleKernel;\n",[126,844,845],{"class":128,"line":166},[126,846,847],{},"use App\\Jobs\\CheckNotFoundUrls; \u002F\u002Fここで先ほど作成したジョブクラス\n",[126,849,850],{"class":128,"line":172},[126,851,852],{},"use DateTime;\n",[126,854,855],{"class":128,"line":178},[126,856,145],{"emptyLinePlaceholder":144},[126,858,859],{"class":128,"line":183},[126,860,861],{},"class Kernel extends ConsoleKernel\n",[126,863,864],{"class":128,"line":189},[126,865,192],{},[126,867,868],{"class":128,"line":195},[126,869,665],{},[126,871,872],{"class":128,"line":201},[126,873,874],{},"     * The Artisan commands provided by your application.\n",[126,876,877],{"class":128,"line":206},[126,878,675],{},[126,880,881],{"class":128,"line":212},[126,882,883],{},"     * @var array\n",[126,885,886],{"class":128,"line":218},[126,887,685],{},[126,889,890],{"class":128,"line":224},[126,891,892],{},"    protected $commands = [\n",[126,894,895],{"class":128,"line":230},[126,896,221],{},[126,898,899],{"class":128,"line":235},[126,900,901],{},"    ];\n",[126,903,904],{"class":128,"line":241},[126,905,145],{"emptyLinePlaceholder":144},[126,907,908],{"class":128,"line":246},[126,909,665],{},[126,911,912],{"class":128,"line":252},[126,913,914],{},"     * Define the application's command schedule.\n",[126,916,917],{"class":128,"line":257},[126,918,675],{},[126,920,921],{"class":128,"line":374},[126,922,923],{},"     * @param  \\Illuminate\\Console\\Scheduling\\Schedule  $schedule\n",[126,925,926],{"class":128,"line":379},[126,927,928],{},"     * @return void\n",[126,930,931],{"class":128,"line":384},[126,932,685],{},[126,934,935],{"class":128,"line":389},[126,936,937],{},"    protected function schedule(Schedule $schedule)\n",[126,939,940],{"class":128,"line":395},[126,941,215],{},[126,943,944],{"class":128,"line":401},[126,945,946],{},"        \u002F\u002F実行するjobをキューに投入する。そして実行時間を設定。\n",[126,948,949],{"class":128,"line":407},[126,950,951],{},"        $schedule->job(new CheckNotFoundUrls(),'checkUrl')->diary()\n",[126,953,954],{"class":128,"line":413},[126,955,227],{},[126,957,958],{"class":128,"line":419},[126,959,260],{},[13,961,962,965,966,969],{},[89,963,964],{},"$schedule->()","の第一引数にジョブインスタンス、第二引数にキュー名を入れます。キューというのは実行登録したジョブを一時的にためておく場所のことです。私もよくわかっていませんが、ジョブの実行数を制限したり、どの順に実行をしていくかなども決められるそうです。とりあえず今回は404チェック用のジョブキューとして ",[89,967,968],{},"checkUrl"," としておきます。",[13,971,972,973,976,977,981],{},"そして行末に",[89,974,975],{},"diary()","などがありますが、",[36,978,980],{"href":979,"target":39},"https:\u002F\u002Flaravel.com\u002Fdocs\u002F7.x\u002Fscheduling#schedule-frequency-options","公式ドキュメント","の通り実行する時間を指定できます。cron通りの時刻設定もできますし、1日ごと、週末ごと、月初に１回などよくある時間設定はメソッドを指定すれば設定できます。",[13,983,984,985,987],{},"上記のコードでは",[89,986,975],{},"なので毎日0時になるとジョブが実行されます。",[75,989,990],{"id":990},"cronを設定する",[13,992,993,994,997],{},"ジョブを定義し、その時間も定めたのでもう自動で実行されそうな気もしますが、これだけでは動きません。指定の時間になったら特定のコマンドつまり",[89,995,996],{},"php artisan schedule:run"," を唱えてphpを動かさないといけません。",[13,999,1000,1001,1003],{},"ここはサーバー(Linux)のcronというものを用いて、常に",[89,1002,996],{}," を唱えるように設定します。cronとは時間設定基づいてコマンドを定期的に唱えてくれるUNIX系のOSに入っているプログラムです。",[13,1005,1006],{},"cronに以下の設定を記述します。",[82,1008,1011],{"className":1009,"code":1010,"language":87},[85],"~ $ crontab -e #これでcronの設定ファイルを編集できます。\n",[89,1012,1010],{"__ignoreMap":91},[82,1014,1017],{"className":1015,"code":1016,"language":87},[85],"* * * * * cd \u002Fpath-to-your-project && php artisan schedule:run >> \u002Fdev\u002Fnull 2>&1\n",[89,1018,1016],{"__ignoreMap":91},[13,1020,1021,1022,1025],{},"行の最初にある",[89,1023,1024],{},"(* * * * *)","は時間の設定をしています。この書き方の場合は毎分実行するようになります。つまり毎分Laravelのスケジューラーを叩いているだけなんです。",[13,1027,1028,1031],{},[89,1029,1030],{},"cd \u002Fpath-to-your-project"," はLaravelプロジェクトのルートへ移動するという意味です。そしてプロジェクトルートでスケジュールを実行します。",[20,1033,1034],{"id":1034},"以上でスケジューラー実装完了",[13,1036,1037],{},"以上でスケジューラーの実装が完了しました。今回のような404を毎日チェックしたり、定期的に時間を設定して行う処理などをアプリ内で行う機能を実装する時に便利です。",[13,1039,1040],{},"ユーザーごとに実行するかどうか、いつ実行するかの情報を格納しておくことで一般ユーザーが定期的な処理を行う処理を設定することもできます。",[1042,1043,1044],"style",{},"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":91,"searchDepth":141,"depth":141,"links":1046},[1047,1048,1049,1057],{"id":22,"depth":135,"text":22},{"id":47,"depth":135,"text":47},{"id":73,"depth":135,"text":73,"children":1050},[1051,1054,1055,1056],{"id":77,"depth":141,"text":77,"children":1052},[1053],{"id":98,"depth":148,"text":98},{"id":113,"depth":141,"text":114},{"id":780,"depth":141,"text":780},{"id":990,"depth":141,"text":990},{"id":1034,"depth":135,"text":1034},[1059],"devstack","2020-07-11","md",null,{},"\u002Farticles\u002Ftweet-check-laravel-jpb-scheduler",{"title":8,"description":8},"articles\u002Ftweet-check-laravel-jpb-scheduler",[1068],"laravel","_mix\u002Flaravel-schedular-768x768.jpg","4qvDlezjHnhP7U6K9l6HlE2ilA1kcrRrD19TfDyuMNc",{"id":1072,"title":1073,"body":1074,"category":1933,"createdAt":1934,"description":1073,"extension":1061,"index":1062,"meta":1935,"navigation":144,"path":1936,"publish":144,"seo":1937,"series":1062,"seriesTitle":1062,"stem":1938,"tag":1939,"thumbnail":1941,"updatedAt":1062,"__hash__":1942},"articles\u002Farticles\u002Flalavel7-deplay-on-heroku.md","HerokuにLaravel 7.0で作成したアプリケーションをデプロイする。",{"type":10,"value":1075,"toc":1902},[1076,1079,1087,1091,1094,1105,1112,1116,1119,1134,1137,1140,1144,1150,1153,1167,1170,1173,1177,1180,1185,1192,1195,1227,1231,1235,1246,1249,1252,1259,1262,1266,1273,1276,1280,1286,1289,1292,1295,1298,1301,1308,1311,1314,1318,1321,1324,1331,1334,1339,1342,1346,1352,1355,1358,1361,1364,1372,1377,1380,1383,1386,1389,1393,1404,1408,1411,1415,1418,1596,1599,1613,1616,1623,1626,1634,1637,1644,1652,1655,1659,1665,1668,1671,1675,1678,1681,1684,1689,1692,1695,1699,1702,1708,1715,1718,1721,1725,1733,1736,1739,1745,1748,1754,1760,1766,1772,1775,1781,1784,1788,1794,1797,1814,1817,1821,1824,1838,1841,1845,1848,1851,1855,1858,1864,1867,1870,1873,1876,1879,1886,1889,1892,1896,1899],[13,1077,1078],{},"こんにちはじゅんです。会社で試験的に作成したLaravelアプリケーションをHerokuにデプロイする機会があり、いくらかハマりポイントなどがあったのでメモがてら残そうと思います。どんなLaravel アプリなのかなど、ある程度具体的な要件の定義から書くので、",[13,1080,1081,1082,1086],{},"「さっさとデプロイの説明しろや！」という人は目次から ",[1083,1084,1085],"strong",{},"「Herokuへのデプロイ作業を開始」"," をクリックして飛んでください。",[20,1088,1090],{"id":1089},"デプロイの前に作成したlaravel-アプリについて","デプロイの前に、作成したLaravel アプリについて",[13,1092,1093],{},"会社でRestfulなWebAPIとそのコンテンツを提供するサービスを開発しようという動きがあり、Laravelやvue、webAPIは少しわかる私に試験的な開発をまかされLaravelで作りました。",[13,1095,1096,1097,1100,1101,1104],{},"最近、巷で有名な ",[1083,1098,1099],{},"「Headless CMS」と似たような動きをします。"," 細かい要件は書くことはできないですが、 ",[1083,1102,1103],{},"LaravelでAPIサーバー兼コンテンツ管理画面を作成して置きます。"," お客さんはアプリケーションにログインしてコンテンツの編集などを行えます。そしてフロントエンドは別のサーバーに配置するか、既存のアプリを使用します。",[13,1106,1107,1108,1111],{},"Ajaxなどを通して認証付きAPIをLaravelが置いてあるサーバーに投げれば、 ",[1083,1109,1110],{},"データ（掲載コンテンツの内容）をJSON・XMLで手に入れることができます。"," 欲しい情報は適宜、非同期でAPIで呼び出して取得するという形式を取っています。モダンなやり方ですね。",[75,1113,1115],{"id":1114},"laravel-でapiサーバーを構築した理由","Laravel でAPIサーバーを構築した理由",[13,1117,1118],{},"APIサーバーを構築するフレームワークは他にもありますが以下のような理由により、Laravelを選びました。",[1120,1121,1122,1125,1128,1131],"ul",{},[59,1123,1124],{},"私が扱いやすい。学習しやすい。",[59,1126,1127],{},"Laravel Passport でOAuth2.0 に準拠したAPI認証機能をさくっと構築可能",[59,1129,1130],{},"Laravel Mixのおかげで管理画面（お客さんが触るGUI）の作成がとても簡単。",[59,1132,1133],{},"LaravelにはAPIに関する認証や制限を設置できるので、",[13,1135,1136],{},"決められたお客さんのコンテンツ（サーバー）からのAPIアクセスを許可する。\nお試しユーザー、プレミアムユーザーと言ったユーザーロールごとに使用できるAPIの制限が簡単。\nと言ったことも可能です。「やっぱりLaravelはフルスタックフレームワークだけあるな」と思えるほどの物です。とにかく上記のようなLaravel アプリを今回はHerokuにデプロイします。",[13,1138,1139],{},"作成したLaravelアプリについての説明・構築は今回の記事ではしません。Herokuへのデプロイがメインになります。",[75,1141,1143],{"id":1142},"なぜheroku","なぜHeroku??",[13,1145,1146,1147],{},"今回のプロジェクト自体がまだ試験的であることも理由の一つですが、単にVPSとかで構築するのが面倒だったからです。 ",[1083,1148,1149],{},"各種ミドルウェア やソフトが最新版の物であっても、マニュアルがあってもセキュリティや権限の設定とかを考えて構築すると日が暮れます。",[13,1151,1152],{},"Herokuの理念が",[596,1154,1157,1158],{"className":1155},[599,1156],"alert-success","\nHeroku はアプリの構築、提供、監視、スケールに役立つクラウドプラットフォームで、アイデアを出してから運用を開始するまでのプロセスを迅速に進めることが可能です。また、インフラストラクチャの管理の問題からも解放されます。\n",[1159,1160,1161],"small",{},[1162,1163,1164],"i",{},[36,1165,1166],{"href":1166,"target":39},"https:\u002F\u002Fdeveloper.mozilla.org\u002Fja\u002Fdocs\u002FWeb\u002FCSS\u002Ffont-feature-settings",[13,1168,1169],{},"とあるようにインフラの構築や管理は面倒です。そのため予めインフラがすぐに作られ、しかしある程度カスタマイズが可能なサービスを利用すると開発者としてはアプリ（今回でいうLaravelアプリ）の開発に注力できます。",[13,1171,1172],{},"またVPSでも可能だと思いますが、GitHubとかと連携して本番環境・開発環境を構築できるのも魅力の一つです。まだ試験的な段階なので、AWSやVPSほどの自由度は今回のアプリではいらないかな？と思いHerokuにしました。",[20,1174,1176],{"id":1175},"herokuで構築した開発運用フロー","Herokuで構築した開発・運用フロー",[13,1178,1179],{},"下図のような感じです。バージョン管理などはGitHubを用いています。適宜、リモートリポジトリにpushしたらその変更が自動でHerokuのアプリ（個々のサーバーみたいなものだと思ってください。）に反映されます。",[1181,1182],"image-render",{":src":1183,":width":1184},"'_mix\u002Fheroku_archtecture-768x446.jpg'","'100%'",[13,1186,1187,1188,1191],{},"プルリクエストを送ると、 ",[1083,1189,1190],{},"そのリクエストに応じたアプリが自動的に構築されるように設定もできます。"," Stagingで最終確認をしてOKであれば公開環境にデプロイさせます。",[13,1193,1194],{},"そのため今回の記事では以下のような流れで構築してます。",[56,1196,1197,1200,1203,1206,1209,1212,1215,1218,1221,1224],{},[59,1198,1199],{},"Herokuのアカウント作成、アプリの作成",[59,1201,1202],{},"Herokuパイプラインの作成",[59,1204,1205],{},"Herokuアプリの設定",[59,1207,1208],{},"HerokuとGithubの連携",[59,1210,1211],{},"DBとの連携",[59,1213,1214],{},"環境変数の設定",[59,1216,1217],{},"Laravelアプリでのmigrationなど",[59,1219,1220],{},"本番環境との連携",[59,1222,1223],{},"SSL化する",[59,1225,1226],{},"API呼び出し確認",[20,1228,1230],{"id":1229},"herokuへのデプロイ作業を開始","Herokuへのデプロイ作業を開始",[75,1232,1234],{"id":1233},"herokuのアカウントを作成","Herokuのアカウントを作成",[13,1236,1237,1238,1241,1245],{},"当たり前ですがHerokuでのアカウントを作成します。これがないと何も始まりません。 ",[36,1239],{"href":1240,"target":39},"https:\u002F\u002Fjp.heroku.com\u002F",[36,1242,1240],{"href":1240,"rel":1243},[1244],"nofollow","にアクセスして「新規登録」に進みます。",[1181,1247],{":src":1248,":width":1184},"'_mix\u002Fheroku-1.png'",[13,1250,1251],{},"名前やアドレスなど適宜入力します。アカウントを作成する段階では日本語でも大丈夫です。選択するプランですがとりあえず無料の物にしておきましょう。",[13,1253,1254,1258],{},[36,1255,1256],{"href":1256,"rel":1257},"https:\u002F\u002Fjp.heroku.com\u002Fpricing",[1244]," にてプランの内容や価格を知ることができます。完全フリーの場合はSSL(httpsにするやつ)の設定が行えない、最低限のスペックしかないなど本当に試験的にサーバー上にアップする程度に使用します。",[13,1260,1261],{},"商用で運用する場合は有料のStandardなどにアップグレードしましょう。ここでは開発サーバーをフリー版、本番環境をhobbyにしておきます。",[75,1263,1265],{"id":1264},"herokuアプリを作成する","Herokuアプリを作成する。",[13,1267,1268,1269,1272],{},"今回デプロイするLaravelのソース達を入れる、Herokuアプリを作成します。 ",[1083,1270,1271],{},"Herokuアプリというのは簡単にいうとサーバーそのものだと思ってください。"," Herokuアプリに、使用するソフトやドキュメントルートの設定、環境変数の設定、連携するGithubのレポジトリなどの設定を行うことで、Laravelが正常に動いてアプリケーションサーバー（サービスを提供するサーバー）として機能するようになります。",[13,1274,1275],{},"ここでは実務の開発体制をとるのでpipelineを構築します。",[96,1277,1279],{"id":1278},"pipe-linesを構築する","pipe linesを構築する",[13,1281,1282,1285],{},[1083,1283,1284],{},"pipe linesというのは「開発、レビュー、ステージング、本番」など複数の段階に分けてアプリを動かすことができるHerokuアプリのグループのことをいいます。"," もう少し簡単にいうと、",[13,1287,1288],{},"修正した物開発中のものをテストする「開発サーバー」、お客さんが触り実際にサービスが提供される「本番サーバー」を同時に構築して管理しやすくしたアプリ達です。今回は開発用・確認用、本番の２つの構成にします。",[13,1290,1291],{},"アカウントを作ると個人のダッシュボード画面に移動します。そこの「New」をクリックし、[Create New pipeline」を選択します。",[1181,1293],{":src":1294,":width":1184},"'_mix\u002Fheroku_createnew-1024x306.png'",[13,1296,1297],{},"pipelineを作成すると、pipelineの名前と連携するリポジトリを選ぶことができます。ここではまだ連携しないので、名前だけ入力して「create pipeline」で作成。すると二つのアプリを加えることができるpipelineが作成されました。",[1181,1299],{":src":1300,":width":1184},"'_mix\u002Fdashnoard-1024x262.png'",[13,1302,1303,1304,1307],{},"そしてたらアプリの構築をしていきます。二つ作るのは少し面倒ですが仕方ないです。では ",[1083,1305,1306],{},"開発の方（staging）"," から作成していきます。「Add app」をクリックすると、既存のアプリを追加するか、新しく作るかを聞いてきます。ここでは「create new app」を選択。すると右側にアプリの基本情報を入力する欄が出てきます。",[13,1309,1310],{},"そしたらアプリの名前とリージョンを選択して「create app」します。フリープランの場合はアメリカかヨーロッパリージョンしか選べませんが、特に問題ないです。（その地域に隕石が落るとかしてサーバーが物理的にやられない限り）",[13,1312,1313],{},"アプリ名を英語で入力して「create」をクリックして作成されます。名前はとりあえず「staging-app-laravel」としておきます。",[75,1315,1317],{"id":1316},"build-packを設定する","Build packを設定する",[13,1319,1320],{},"開発用アプリの設定が可能になったのでダッシュボードから、アプリ名をクリックして移動します。すると下のようなアプリに関する設定ができる画面に移ります。設定画面でできることは後で使用するheroku CLIでも行うことができます。今回はGUIの方をつかっていきます。",[1181,1322],{":src":1323,":width":1184},"'_mix\u002Fapp_setting_dev-1024x497.png'",[13,1325,1326,1327,1330],{},"まずは ",[1083,1328,1329],{},"一番最初にbuild packというものを設定"," します。build packではnode.js,PHPなどを選択します。まあ使うサーバーサイドの言語を選ぶ物だと思ってください。上記の画面から「Settings」を選択します。",[13,1332,1333],{},"「Buildpacks」という項目があるので「Add builpack」でパックを追加します。",[1181,1335],{":src":1336,":width":1337,":center":1338},"'_mix\u002Fbuldpack-300x222.png'","'500px'","true",[13,1340,1341],{},"LaravelなのでPHPは必須です。そしてnode modulesもあるのでnode.jsも入れます。ここでサーバーサイドの言語を複数追加すれば各言語でアプリを動かすことができます。",[75,1343,1345],{"id":1344},"githubと連携してソースを入れる","Githubと連携してソースを入れる",[13,1347,1326,1348,1351],{},[1083,1349,1350],{},"Laravelアプリケーションのソースをこのアプリにぶち込む必要があります。"," herokuではGithubとの連携がオススメされているので、今回はその方法で行きます。上記画面から「Deploy」をクリックして移動します。",[1181,1353],{":src":1354,":width":1184},"'_mix\u002Fdeply_set-1024x257.png'",[13,1356,1357],{},"deployment method でGithubを選びます。すると連携するリポジトリ名を入力する欄が出現します。もし初めてherokuを触ってgithubと連携していない場合は「githubと連携する？」みたいな物が表示されたはずなので、言われる通りに連携します。",[13,1359,1360],{},"githubのアカウントを持っていてログイン状態が保持されていれば、githubの連携許可画面が表示されます。それで簡単に連携ができます。",[13,1362,1363],{},"連携が完了すると下のようにデプロイに関する設定が出てきます。githubを用いる場合は２通りのデプロイがあります。",[1120,1365,1366,1369],{},[59,1367,1368],{},"ブランチを指定して手動でデプロイ",[59,1370,1371],{},"ブランチに変化があったら自動でデプロイ",[13,1373,1374],{},[1083,1375,1376],{},"毎回herokuにログインするのも面倒なので「Enable Automatic Deploys」を選択して自動デプロイができるようにしておきます。",[1181,1378],{":src":1379,":width":1184},"'_mix\u002Fautodeploy-1024x421.png'",[13,1381,1382],{},"リポジトリにソースをプッシュすると以下のようにbuildしているのがダッシュボードで見ることができます。（たまに更新ボタンを押さないと表示されない時がある。）",[1181,1384],{":src":1385,":width":1337,":center":1338},"'_mix\u002Fbuildings-300x166.png'",[13,1387,1388],{},"しかしまだDBとの連携や環境変数の設定が済んでいないので、アプリをみたところで500エラーしか表示されません。次はDBの設定に移ります。",[75,1390,1392],{"id":1391},"jawsdbを設定する","JawsDBを設定する",[13,1394,1395,1396,1399,1400,1403],{},"herokuでは",[89,1397,1398],{},"Procfile"," を用いてドキュメントルート やwebサーバー&phpの設定ができますが、DBに関してはアドオンを利用する必要があります。herokuではpostgreSQLの使用を進めていますが、私が作成したLaravelアプリはmysql仕様で作っていたので ",[1083,1401,1402],{},"今回はmysqlをDBとして使用します。","（まあpostgreSQLに変更もできましたけど…）",[75,1405,1407],{"id":1406},"使用するdbのアドオンについて","使用するDBのアドオンについて",[13,1409,1410],{},"ちなみに、「heroku mysql DB」と検索すると **「ClearDB」というmysqlベースのDBアドオンを入れるように書かれた記事が多いですが、私はそこでハマった物がありました。**以下に記述しておきます。",[96,1412,1414],{"id":1413},"laravel-55以上の場合cleardbの使用はおすすめしない理由","Laravel 5.5以上の場合、ClearDBの使用はおすすめしない理由",[13,1416,1417],{},"ClearDBを用いててデータベースへマイグレーションをおこなった際に。こんなエラーが発生。",[82,1419,1423],{"className":1420,"code":1421,"language":1422,"meta":91,"style":91},"language-bash shiki shiki-themes material-theme-ocean","php artisan migrate\n...\n...\nSQLSTATE[42000]: Syntax error or access violation: 1071 Specified key was too long; max key length is 767 bytes (SQL: alter table `users` add unique `users_email_unique`(`email`))\nSQLSTATE[42000]: Syntax error or access violation: 1071 Specified key was too long; max key length is 767 bytes\n","bash",[89,1424,1425,1437,1443,1447,1555],{"__ignoreMap":91},[126,1426,1427,1430,1434],{"class":128,"line":129},[126,1428,122],{"class":1429},"s5Dmg",[126,1431,1433],{"class":1432},"sfyAc"," artisan",[126,1435,1436],{"class":1432}," migrate\n",[126,1438,1439],{"class":128,"line":135},[126,1440,1442],{"class":1441},"sdLwU","...\n",[126,1444,1445],{"class":128,"line":141},[126,1446,1442],{"class":1441},[126,1448,1449,1452,1455,1458,1461,1464,1467,1471,1474,1477,1480,1483,1486,1490,1493,1495,1498,1501,1504,1507,1511,1514,1517,1520,1523,1526,1529,1532,1534,1537,1539,1542,1544,1547,1549,1552],{"class":128,"line":148},[126,1450,1451],{"class":1429},"SQLSTATE[42000]:",[126,1453,1454],{"class":1432}," Syntax",[126,1456,1457],{"class":1432}," error",[126,1459,1460],{"class":1432}," or",[126,1462,1463],{"class":1432}," access",[126,1465,1466],{"class":1432}," violation:",[126,1468,1470],{"class":1469},"sx098"," 1071",[126,1472,1473],{"class":1432}," Specified",[126,1475,1476],{"class":1432}," key",[126,1478,1479],{"class":1432}," was",[126,1481,1482],{"class":1432}," too",[126,1484,1485],{"class":1432}," long",[126,1487,1489],{"class":1488},"sAklC",";",[126,1491,1492],{"class":1429}," max",[126,1494,1476],{"class":1432},[126,1496,1497],{"class":1432}," length",[126,1499,1500],{"class":1432}," is",[126,1502,1503],{"class":1469}," 767",[126,1505,1506],{"class":1432}," bytes",[126,1508,1510],{"class":1509},"s0W1g"," (SQL: ",[126,1512,1513],{"class":1432},"alter",[126,1515,1516],{"class":1432}," table",[126,1518,1519],{"class":1488}," `",[126,1521,1522],{"class":1429},"users",[126,1524,1525],{"class":1488},"`",[126,1527,1528],{"class":1429}," add",[126,1530,1531],{"class":1432}," unique",[126,1533,1519],{"class":1488},[126,1535,1536],{"class":1429},"users_email_unique",[126,1538,1525],{"class":1488},[126,1540,1541],{"class":1488},"(",[126,1543,1525],{"class":1488},[126,1545,1546],{"class":1429},"email",[126,1548,1525],{"class":1488},[126,1550,1551],{"class":1488},")",[126,1553,1554],{"class":1509},")\n",[126,1556,1557,1559,1561,1563,1565,1567,1569,1571,1573,1575,1577,1579,1581,1583,1585,1587,1589,1591,1593],{"class":128,"line":154},[126,1558,1451],{"class":1429},[126,1560,1454],{"class":1432},[126,1562,1457],{"class":1432},[126,1564,1460],{"class":1432},[126,1566,1463],{"class":1432},[126,1568,1466],{"class":1432},[126,1570,1470],{"class":1469},[126,1572,1473],{"class":1432},[126,1574,1476],{"class":1432},[126,1576,1479],{"class":1432},[126,1578,1482],{"class":1432},[126,1580,1485],{"class":1432},[126,1582,1489],{"class":1488},[126,1584,1492],{"class":1429},[126,1586,1476],{"class":1432},[126,1588,1497],{"class":1432},[126,1590,1500],{"class":1432},[126,1592,1503],{"class":1469},[126,1594,1595],{"class":1432}," bytes\n",[13,1597,1598],{},"ローカルでは起きなかった謎のエラー。「上限は767bytesだけど、1071bytesのキーが設定されている」と怒っている。とりあえずmigrationが中途半端にUserテーブルだけ作って止まってしまったので、migrationをリフレッシュする。",[82,1600,1602],{"className":1420,"code":1601,"language":1422,"meta":91,"style":91},"php artisan migrate:fresh\n",[89,1603,1604],{"__ignoreMap":91},[126,1605,1606,1608,1610],{"class":128,"line":129},[126,1607,122],{"class":1429},[126,1609,1433],{"class":1432},[126,1611,1612],{"class":1432}," migrate:fresh\n",[13,1614,1615],{},"このエラーを調べたところどうやら使用した「ClearDB」のmysqlが5.5と古い上に、Laravelで設定していたmysqlで用いる文字コードとの関係で起きていた。以下のQiita記事が非常に参考になった。",[596,1617,601,1619],{"className":1618},[599,600],[36,1620,1622],{"href":1621,"target":39},"https:\u002F\u002Fqiita.com\u002Fbeer_geek\u002Fitems\u002F6e4264db142745ea666f","Laravel5.4以上、MySQL5.7.7未満 でusersテーブルのマイグレーションを実行すると Syntax error が発生する",[13,1624,1625],{},"対処は２つ。",[1120,1627,1628,1631],{},[59,1629,1630],{},"mysqlのバージョンを上げて5.7にする。",[59,1632,1633],{},"文字コードを上限を超えないものに変更する。",[13,1635,1636],{},"今回の場合はmysqlのバージョンを上げる方にした。しかしこちらのStackOver Flow にもあるようにClear DBで用いられているmysqlのバージョンを上げるのは無理らしい。。",[596,1638,601,1640],{"className":1639},[599,600],[36,1641,1643],{"href":1642,"target":39},"https:\u002F\u002Fstackoverflow.com\u002Fquestions\u002F56253354\u002Fhow-to-update-the-version-of-the-mysql-engine-in-cleardb","How to update the version of the MySQL engine in ClearDB?\n    ",[13,1645,1646,1647,1651],{},"代わりに",[36,1648,1650],{"href":1649,"target":39},"https:\u002F\u002Felements.heroku.com\u002Faddons\u002Fjawsdb","「JawsDB」","というのを使うとmysqlのバージョンが5.7のものが入ってくれる。",[13,1653,1654],{},"herokuでのmysql導入を調べると「ClearDBを入れろ」という記事が多く、言われたままにやったら起きたのでハマりポイントでした。",[75,1656,1658],{"id":1657},"jawsdbアドオンを入れる","jawsDBアドオンを入れる",[13,1660,1661,1662],{},"アドオンはherokuの追加機能のような物です。「Resources」を選ぶと「Add-ons」という項目がありそこでアドオンを導入できます。jawsDBと入力するとすぐに出てきます。 ",[1083,1663,1664],{},"「jawsDB MySQL」を選択して追加。",[1181,1666],{":src":1667,":width":1337,":center":1338},"'_mix\u002Fjawsdb-300x248.png'",[13,1669,1670],{},"データ容量などに応じたプランを選ぶことができます。ここではFreeでおk。「Provision」をクリックしてアプリに追加します。",[75,1672,1674],{"id":1673},"dbの接続情報を手に入れる","DBの接続情報を手に入れる",[13,1676,1677],{},"このjawsDBはherokuのアプリ内（というかサーバー）に組み込まれている訳ではないので、ホストやポートなどを指定する必要があります。jawsDBを導入するとそのherokuアプリを結びついたデータベースが作成されます。",[13,1679,1680],{},"その情報はアドオンのページで、jawsDBの箇所がリンクになっているのでそこをクリックします。",[1181,1682],{":src":1683,":width":1337,":center":1338},"'_mix\u002Faddons-768x194.png'",[13,1685,1686],{},[1083,1687,1688],{},"するとDBへの接続情報が書かれた画面が表示されます。ここに接続先ホスト、ユーザー名、パスワードなどが書いてあるので控えておきます。",[1181,1690],{":src":1691,":width":1184},"'_mix\u002Fjawsdashboard-768x360.png'",[13,1693,1694],{},"自前のmysqlコマンドやクラアントから普通にアクセスすることができます。では次にこれらの情報を元に環境変数を設定します。後少しでアプリがデプロイされます！頑張ってください！",[75,1696,1698],{"id":1697},"config-bars環境変数を設定","config bars(環境変数)を設定",[13,1700,1701],{},"DBの接続情報を手に入れたのでLaravel内で使用される環境変数を定義していきます。Laravelのファイル群に「.env」というのがあったと思います。そのファイルは環境変数を定義しており、DBのパスワード、APIキーなど重要な値や環境によって変化する値が格納されています。",[13,1703,1704,1705],{},"基本的には.envはgitでは管理せず、githubにはプッシュされないように除外してあります。",[1083,1706,1707],{},"ローカルとherokuではDBの接続情報も違うので異なる環境変数を設定する必要がります。",[13,1709,1710,1711,1714],{},"そのために ",[1083,1712,1713],{},"「Settings」の「config bars」にて環境変数を設定","できます。Laravelの.envを開いて必要な項目をコピーして適宜適切な値を入れておきましょう。",[1181,1716],{":src":1717,":width":1184},"'_mix\u002Fconfigure-768x115.png'",[75,1719,1720],{"id":1720},"マイグレーションを行う",[96,1722,1724],{"id":1723},"heroku-cliを入れる","heroku CLIを入れる",[13,1726,1727,1728,1732],{},"環境変数の設定を終えればLaravelのマイグレーションが使えるようになります。heroku CLIを用いてアプリをBashで操作します。ローカルに",[36,1729,1731],{"href":1730,"target":39},"https:\u002F\u002Fdevcenter.heroku.com\u002Farticles\u002Fheroku-cli","heroku CLI","をダウンロードします。",[96,1734,1735],{"id":1735},"アプリをbashから操作する",[13,1737,1738],{},"macならターミナルを開いてログインをします。",[82,1740,1743],{"className":1741,"code":1742,"language":87},[85],"~ % heroku login\nheroku: Press any key to open up the browser to login or q to exit:[Enter]\nOpening browser to https:\u002F\u002Fcli-auth.heroku.com\u002Fauth\u002Fcli\u002Fbrowser\u002F********\nLogging in... done\nLogged in as your@email.com\n",[89,1744,1742],{"__ignoreMap":91},[13,1746,1747],{},"ブラウザがいったん開いてログインが完了です。そしたらアプリをbashで操作します。以下のコマンドを唱えます。",[82,1749,1752],{"className":1750,"code":1751,"language":87},[85],"~ % heroku run bash -a staging-app-laravel\nRunning bash on ⬢ staging-app-laravel... done\n",[89,1753,1751],{"__ignoreMap":91},[13,1755,1756,1759],{},[1083,1757,1758],{},"「-a」でアプリ名を指定できます。バッシュの時はアプリ名を指定しないと怒られます。"," すると表示が少し変わりバッシュ操作が可能になります。",[13,1761,1762,1763,1765],{},"ホームディレクトリ 内にLaravelのソース達が配置されているのが確認できます。このLaravelアプリのルートで ",[89,1764,757],{},"を唱えることができます。それ以外のディレクトリなどで行うと「そんなコマンド知らんよ」と怒られます。ではマイグレーション。",[82,1767,1770],{"className":1768,"code":1769,"language":87},[85],"~ $ php artisan migrate\n**************************************\n*     Application In Production!     *\n**************************************\n\n Do you really wish to run this command? (yes\u002Fno) [no]:\n > yes\n\nMigration table created successfully.\nMigrating: 2014_10_12_000000_create_users_table\nMigrated:  2014_10_12_000000_create_users_table (0.06 seconds)\nMigrating: 2014_10_12_100000_create_password_resets_table\nMigrated:  2014_10_12_100000_create_password_resets_table (0.05 seconds)\nMigrating: 2016_06_01_000001_create_oauth_auth_codes_table\nMigrated:  2016_06_01_000001_create_oauth_auth_codes_table (0.09 seconds)\nMigrating: 2016_06_01_000002_create_oauth_access_tokens_table\nMigrated:  2016_06_01_000002_create_oauth_access_tokens_table (0.09 seconds)\nMigrating: 2016_06_01_000003_create_oauth_refresh_tokens_table\nMigrated:  2016_06_01_000003_create_oauth_refresh_tokens_table (0.06 seconds)\nMigrating: 2016_06_01_000004_create_oauth_clients_table\nMigrated:  2016_06_01_000004_create_oauth_clients_table (0.05 seconds)\nMigrating: 2016_06_01_000005_create_oauth_personal_access_clients_table\nMigrated:  2016_06_01_000005_create_oauth_personal_access_clients_table (0.02 seconds)\nMigrating: 2019_08_19_000000_create_failed_jobs_table\nMigrated:  2019_08_19_000000_create_failed_jobs_table (0.03 seconds)\n...#自分で設定した他のテーブルも無事マイグレーションされた。\n",[89,1771,1769],{"__ignoreMap":91},[13,1773,1774],{},"これでDBに必要なテーブルが設定されました。ユーザーテーブルに私たちが管理するための管理ユーザーを作成するシーダーを予め作成したおいたのでそれも実行。",[82,1776,1779],{"className":1777,"code":1778,"language":87},[85],"~ $ php artisan db:seed\n",[89,1780,1778],{"__ignoreMap":91},[13,1782,1783],{},"これでDBにもテーブルが挿入されたのでアプリが動くようになりました。",[75,1785,1787],{"id":1786},"herokuアプリをブラウザで開く","Herokuアプリをブラウザで開く",[13,1789,1790,1791],{},"ブラウザのHerokuの画面に戻り、pipe lineの一覧を開きます。 ",[1083,1792,1793],{},"開発アプリの「Open app」をクリックすると構築したLaravelアプリの画面が表示されました。",[13,1795,1796],{},"「500 Internal ServerError」などが表示されている場合は以下の４つを確かめましょう。",[56,1798,1799,1802,1805,1808,1811],{},[59,1800,1801],{},"$ heroku logs --tail -a your_app_name でログを確かめる。",[59,1803,1804],{},"Laravelアプリそのものにミスはないか？",[59,1806,1807],{},"環境変数の設定はあっているか？",[59,1809,1810],{},"DBに接続されているか？DBのテーブルの設定は正しいか？",[59,1812,1813],{},"ビルドのログを確かめてビルドが失敗していないか？",[13,1815,1816],{},"アプリのビルドが成功している場合はサーバー自体は構築できています。Laravelアプリや環境変数などに問題ある可能性が高いので確認してみましょう。",[75,1818,1820],{"id":1819},"次は本番のアプリ","次は本番のアプリ！！",[13,1822,1823],{},"ここまでもかなりボリューミーですが、今回は２つアプリを入れたので２つ目も設定します。ですがやることは開発の方でやったように",[1120,1825,1826,1829,1832,1835],{},[59,1827,1828],{},"ビルドパックを入れる",[59,1830,1831],{},"アドオンを入れる",[59,1833,1834],{},"環境変数を設定する",[59,1836,1837],{},"DBと接続してマイグレーションする",[13,1839,1840],{},"ぐらいです。そして私の運用ではgithubと本番のアプリは連携させず、stagingのアプリを本番に移行するので少し手間が省けます。stagingのものを本番に移行する場合はpipelineの画面で「promote to production」を選択すればすぐに反映されます。",[75,1842,1844],{"id":1843},"本番サーバーのssl化","本番サーバーのSSL化",[13,1846,1847],{},"最後に本番サーバーのSSL化です。無料プランのは自動でSSLは付与されません。証明書があれば付与することができますが無料のdynoはスペックが最低限な上、証明書取得でもお金がかかるので大人しくhobbyプラン以上の有料版にアップグレードしましょう。",[13,1849,1850],{},"本番アプリをhobbyプランに上げるとACM（自動証明書管理）が有効になり、証明書が発行できて有効になってます。SSL化はこれだけです。込み入った証明書や独自ドメイン の付与の際には少し設定が必要です。",[75,1852,1854],{"id":1853},"apiサーバーとして機能しているかを確かめる","APIサーバーとして機能しているかを確かめる",[13,1856,1857],{},"とりあえず本番環境までのデプロイが完了しました。Laravel Passport を用いて認証付きAPIを投げられるのでそのチェックをしてみます。とその前にlaravel passportの設定上必要なものがあるのでアプリ内にbashで入って以下を実行。",[82,1859,1862],{"className":1860,"code":1861,"language":87},[85],"~ $ php artisan passport:install\n",[89,1863,1861],{"__ignoreMap":91},[13,1865,1866],{},"Laravel Passport を用いて認証付きAPIを投げられるのでそのチェックをしてみます。シーダーで作成した管理ユーザーでログインします。Laravel Passport ではユーザーごとのアクセストークンが発行できるので、それを作成。（トークン取得画面のUIなどは適宜作成してください。Laravel側でも用意されています。）",[1181,1868],{":src":1869,":width":1337,":center":1338},"'_mix\u002Fapis-768x841.png'",[13,1871,1872],{},"アクセストークンを発行して手元にメモしておきます。「Talend API Tester」というChrome拡張機能を使ってこのHerokuアプリ宛にAPIを投げてみます。",[13,1874,1875],{},"アプリのドメイン名が表示されているのでコピー。外部からのAPIアクセスの際にはAPIキーをリクエストヘッダーに仕込んで送ると受け取る設定になっています。まずはAPIキーなして投げると、",[1181,1877],{":src":1878,":width":1184},"'_mix\u002Fapi_test_01-1-768x330.png'",[13,1880,1881,1882,1885],{},"おっ！401 Unauthorized が戻ってきました。 ",[1083,1883,1884],{},"とりあえず指定のAPIルーティングは存在して、リクエストが受け取れています。つまりAPIサーバーとして機能していることがわかります。"," ではヘッダに先ほどのキーを仕込んで、また諸所の設定値も入れて再度投げると、",[1181,1887],{":src":1888,":width":1184},"'_mix\u002Fapitest_02-768x438.png'",[13,1890,1891],{},"200 OK が返りました。プレビューを見るとJSONでのデータが戻ってきているので成功です。これでHerokuにLaravelで開発したAPIサーバー及び管理アプリを構築することができました。このサーバーに認証キー付きAPIを投げることでデータを取得できます。",[75,1893,1895],{"id":1894},"終了","終了〜〜〜",[13,1897,1898],{},"以上がherokuにlaravel で作成したAPIサービスを構築する手順でした。めちゃくちゃ長い記事になりましたが、VPSならもっと長くなっています笑。githubやSSL化そして他の管理を気にせず簡単にデプロイできるのでherokuが人気なのもわかります。Xserverとかは異なってスケーラブルでnodeとかpythonとかも入れられ、githubで管理できるのは大きいです。",[1042,1900,1901],{},"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 .sdLwU, html code.shiki .sdLwU{--shiki-default:#82AAFF}html pre.shiki code .sx098, html code.shiki .sx098{--shiki-default:#F78C6C}html pre.shiki code .sAklC, html code.shiki .sAklC{--shiki-default:#89DDFF}html pre.shiki code .s0W1g, html code.shiki .s0W1g{--shiki-default:#BABED8}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":91,"searchDepth":141,"depth":141,"links":1903},[1904,1908,1909],{"id":1089,"depth":135,"text":1090,"children":1905},[1906,1907],{"id":1114,"depth":141,"text":1115},{"id":1142,"depth":141,"text":1143},{"id":1175,"depth":135,"text":1176},{"id":1229,"depth":135,"text":1230,"children":1910},[1911,1912,1915,1916,1917,1918,1921,1922,1923,1924,1928,1929,1930,1931,1932],{"id":1233,"depth":141,"text":1234},{"id":1264,"depth":141,"text":1265,"children":1913},[1914],{"id":1278,"depth":148,"text":1279},{"id":1316,"depth":141,"text":1317},{"id":1344,"depth":141,"text":1345},{"id":1391,"depth":141,"text":1392},{"id":1406,"depth":141,"text":1407,"children":1919},[1920],{"id":1413,"depth":148,"text":1414},{"id":1657,"depth":141,"text":1658},{"id":1673,"depth":141,"text":1674},{"id":1697,"depth":141,"text":1698},{"id":1720,"depth":141,"text":1720,"children":1925},[1926,1927],{"id":1723,"depth":148,"text":1724},{"id":1735,"depth":148,"text":1735},{"id":1786,"depth":141,"text":1787},{"id":1819,"depth":141,"text":1820},{"id":1843,"depth":141,"text":1844},{"id":1853,"depth":141,"text":1854},{"id":1894,"depth":141,"text":1895},[1059],"2020-07-07",{},"\u002Farticles\u002Flalavel7-deplay-on-heroku",{"title":1073,"description":1073},"articles\u002Flalavel7-deplay-on-heroku",[122,1068,1940],"infrastructure","_mix\u002Fheroku_archtecture-768x446.jpg","i3Z8K7cn-JUTupKxFl5lgfnHLhl9EAJPK97f9yd7CX0",{"id":1944,"title":1945,"body":1946,"category":2638,"createdAt":2639,"description":1945,"extension":1061,"index":1062,"meta":2640,"navigation":144,"path":2641,"publish":144,"seo":2642,"series":1062,"seriesTitle":1062,"stem":2643,"tag":2644,"thumbnail":2645,"updatedAt":1062,"__hash__":2646},"articles\u002Farticles\u002Fpseudo-element-icon.md","動的な変更に強い！擬似要素でWebアイコンを表示させる。(IE対策も)",{"type":10,"value":1947,"toc":2629},[1948,1951,1954,1992,1995,1998,2001,2004,2008,2011,2015,2018,2026,2029,2035,2038,2042,2048,2051,2221,2414,2417,2420,2426,2433,2436,2439,2445,2448,2452,2458,2519,2522,2599,2602,2614,2617,2620,2623,2626],[13,1949,1950],{},"こんにちはjunです。Webアイコンっていいですよね。サイトはお洒落になるし、各ボタンの意図やUX\u002FUIの観点では重要な物です。フォントを実装するといば、FontAwsome、GoogleIconを使うのがメインだと思います。中には画像でやる人もいるけどね。",[13,1952,1953],{},"ただアイコンというのは単独ではなくて、文字と併用したり、要素の一部としてひっそりと使うことが多いです。これらのアイコンを入れるときは",[82,1955,1959],{"className":1956,"code":1957,"language":1958,"meta":91,"style":91},"language-HTML shiki shiki-themes material-theme-ocean","\u003Ci class=\"far fa-smile-wink\">\u003C\u002Fi>\n","HTML",[89,1960,1961],{"__ignoreMap":91},[126,1962,1963,1966,1969,1973,1976,1979,1982,1984,1987,1989],{"class":128,"line":129},[126,1964,1965],{"class":1488},"\u003C",[126,1967,1162],{"class":1968},"s-wAU",[126,1970,1972],{"class":1971},"sJ14y"," class",[126,1974,1975],{"class":1488},"=",[126,1977,1978],{"class":1488},"\"",[126,1980,1981],{"class":1432},"far fa-smile-wink",[126,1983,1978],{"class":1488},[126,1985,1986],{"class":1488},">\u003C\u002F",[126,1988,1162],{"class":1968},[126,1990,1991],{"class":1488},">\n",[13,1993,1994],{},"と言ったiタグを入れるのが簡単ですが実務で扱うようなサイトの場合、この方法では面倒になることがあります。",[20,1996,1997],{"id":1997},"タグの操作が必要",[13,1999,2000],{},"iタグと対応するアイコンのクラス名を入れることで、アイコンは表示することはできます。しかしタグでアイコンを打ち込むと、対応するアイコンごとのHTMLタグを埋め込む、必要性が出てきてしまいます。",[13,2002,2003],{},"特に動的に内容を出力したり変更したりする場合は特に大変です。例えば下図のようなリストタグの要件があるとします。",[1181,2005],{":src":2006,":width":2007},"'_mix\u002Fpseudo-element-icon.png'","'300px'",[13,2009,2010],{},"胡散臭い内容ですが、とりあえずUX的にもこんなリストデザインが必要になったとしましょう。",[75,2012,2014],{"id":2013},"iタグでやってみると","iタグでやってみると...",[13,2016,2017],{},"今回の要件ではページの至る箇所で使われることとします。するとリストタグとして使う性質上、",[1120,2019,2020,2023],{},[59,2021,2022],{},"リストの数が決まっているわけではない。（可変的）",[59,2024,2025],{},"内容によって数が変わる。",[13,2027,2028],{},"という内容が可変的であるという考慮が必要です。そのためiタグというHTML自体を埋め込んで対処すると、上記のデザインが必要な箇所全てにiタグを入れる必要が出てきます。",[13,2030,2031,2032],{},"リストの数が少なければなんとか対処できますが、大量にあったり、リリース前段階に内容が変わるなんて普通に起きます。差し替えというのは間違えが起きやすいです。そのためiタグでアイコンを埋め込んで表示する方法は、",[1083,2033,2034],{},"このような動的な変更に弱いです。",[13,2036,2037],{},"もしiタグ埋め込み方式をするとなれば予め、アイコンのiタグを入れてdisplayで操作したりとか、jsで気合でタグを埋め込むとかになりますが、どちらもいい手段とは言えません。",[20,2039,2041],{"id":2040},"擬似要素でアイコンをつけるクラス名で操作可能","擬似要素でアイコンをつける＝クラス名で操作可能",[13,2043,2044,2045],{},"iタグでアイコンを変える場合、要素であるiタグ、そしてクラス名が必要です。",[1083,2046,2047],{},"ただし擬似要素を扱うことでクラス名を付け加える、書き換えるだけでアイコンの変更と、付与・削除ができるようになります。",[13,2049,2050],{},"擬似要素は特に「付与・削除できる」という点がiタグ埋め込みより優れています。上のリストタグのHTML\u002FCSSは以下のように書かれています。",[82,2052,2056],{"className":2053,"code":2054,"language":2055,"meta":91,"style":91},"language-html shiki shiki-themes material-theme-ocean","\u003Cul class=\"p-deco_list u-list_01\">\n    \u003Cli>最近よく眠れない..\u003C\u002Fli>\n    \u003Cli>なかなか疲れが取れない..\u003C\u002Fli>\n    \u003Cli>朝起きるのが辛い..\u003C\u002Fli>\n\u003C\u002Ful>\n\n\u003Cul class=\"p-deco_list u-list_02\">\n    \u003Cli>使い始めてから、寝つきがよくなった！\u003C\u002Fli>\n    \u003Cli>疲れが嘘のように取れた！\u003C\u002Fli>\n    \u003Cli>朝の目覚めが変わった！\u003C\u002Fli>\n\u003C\u002Ful>\n","html",[89,2057,2058,2077,2097,2114,2131,2139,2143,2162,2179,2196,2213],{"__ignoreMap":91},[126,2059,2060,2062,2064,2066,2068,2070,2073,2075],{"class":128,"line":129},[126,2061,1965],{"class":1488},[126,2063,1120],{"class":1968},[126,2065,1972],{"class":1971},[126,2067,1975],{"class":1488},[126,2069,1978],{"class":1488},[126,2071,2072],{"class":1432},"p-deco_list u-list_01",[126,2074,1978],{"class":1488},[126,2076,1991],{"class":1488},[126,2078,2079,2082,2084,2087,2090,2093,2095],{"class":128,"line":135},[126,2080,2081],{"class":1488},"    \u003C",[126,2083,59],{"class":1968},[126,2085,2086],{"class":1488},">",[126,2088,2089],{"class":1509},"最近よく眠れない..",[126,2091,2092],{"class":1488},"\u003C\u002F",[126,2094,59],{"class":1968},[126,2096,1991],{"class":1488},[126,2098,2099,2101,2103,2105,2108,2110,2112],{"class":128,"line":141},[126,2100,2081],{"class":1488},[126,2102,59],{"class":1968},[126,2104,2086],{"class":1488},[126,2106,2107],{"class":1509},"なかなか疲れが取れない..",[126,2109,2092],{"class":1488},[126,2111,59],{"class":1968},[126,2113,1991],{"class":1488},[126,2115,2116,2118,2120,2122,2125,2127,2129],{"class":128,"line":148},[126,2117,2081],{"class":1488},[126,2119,59],{"class":1968},[126,2121,2086],{"class":1488},[126,2123,2124],{"class":1509},"朝起きるのが辛い..",[126,2126,2092],{"class":1488},[126,2128,59],{"class":1968},[126,2130,1991],{"class":1488},[126,2132,2133,2135,2137],{"class":128,"line":154},[126,2134,2092],{"class":1488},[126,2136,1120],{"class":1968},[126,2138,1991],{"class":1488},[126,2140,2141],{"class":128,"line":160},[126,2142,145],{"emptyLinePlaceholder":144},[126,2144,2145,2147,2149,2151,2153,2155,2158,2160],{"class":128,"line":166},[126,2146,1965],{"class":1488},[126,2148,1120],{"class":1968},[126,2150,1972],{"class":1971},[126,2152,1975],{"class":1488},[126,2154,1978],{"class":1488},[126,2156,2157],{"class":1432},"p-deco_list u-list_02",[126,2159,1978],{"class":1488},[126,2161,1991],{"class":1488},[126,2163,2164,2166,2168,2170,2173,2175,2177],{"class":128,"line":172},[126,2165,2081],{"class":1488},[126,2167,59],{"class":1968},[126,2169,2086],{"class":1488},[126,2171,2172],{"class":1509},"使い始めてから、寝つきがよくなった！",[126,2174,2092],{"class":1488},[126,2176,59],{"class":1968},[126,2178,1991],{"class":1488},[126,2180,2181,2183,2185,2187,2190,2192,2194],{"class":128,"line":178},[126,2182,2081],{"class":1488},[126,2184,59],{"class":1968},[126,2186,2086],{"class":1488},[126,2188,2189],{"class":1509},"疲れが嘘のように取れた！",[126,2191,2092],{"class":1488},[126,2193,59],{"class":1968},[126,2195,1991],{"class":1488},[126,2197,2198,2200,2202,2204,2207,2209,2211],{"class":128,"line":183},[126,2199,2081],{"class":1488},[126,2201,59],{"class":1968},[126,2203,2086],{"class":1488},[126,2205,2206],{"class":1509},"朝の目覚めが変わった！",[126,2208,2092],{"class":1488},[126,2210,59],{"class":1968},[126,2212,1991],{"class":1488},[126,2214,2215,2217,2219],{"class":128,"line":189},[126,2216,2092],{"class":1488},[126,2218,1120],{"class":1968},[126,2220,1991],{"class":1488},[82,2222,2226],{"className":2223,"code":2224,"language":2225,"meta":91,"style":91},"language-css shiki shiki-themes material-theme-ocean",".p-deco_list li{\n    list-style:none;\n}\n\n \u002F*　擬似要素アイコンの共通設定*\u002F\n.p-deco_list li::before{\n    content:'';\n    font-family: 'FontAwesome';\n    padding:5px 10px; \u002F* 文字間隔調整 *\u002F\n}\n\n.u-list_01 li::before{\n    content:'\\f119';\n}\n\n.u-list_02 li::before{\n    content:'\\f4da';\n}\n","css",[89,2227,2228,2241,2256,2260,2264,2270,2286,2298,2316,2334,2338,2342,2357,2372,2376,2380,2395,2410],{"__ignoreMap":91},[126,2229,2230,2233,2236,2239],{"class":128,"line":129},[126,2231,2232],{"class":1488},".",[126,2234,2235],{"class":1429},"p-deco_list",[126,2237,2238],{"class":1429}," li",[126,2240,192],{"class":1488},[126,2242,2243,2247,2250,2253],{"class":128,"line":135},[126,2244,2246],{"class":2245},"s6YsC","    list-style",[126,2248,2249],{"class":1488},":",[126,2251,2252],{"class":1509},"none",[126,2254,2255],{"class":1488},";\n",[126,2257,2258],{"class":128,"line":141},[126,2259,260],{"class":1488},[126,2261,2262],{"class":128,"line":148},[126,2263,145],{"emptyLinePlaceholder":144},[126,2265,2266],{"class":128,"line":154},[126,2267,2269],{"class":2268},"sC9rS"," \u002F*　擬似要素アイコンの共通設定*\u002F\n",[126,2271,2272,2274,2276,2278,2281,2284],{"class":128,"line":160},[126,2273,2232],{"class":1488},[126,2275,2235],{"class":1429},[126,2277,2238],{"class":1429},[126,2279,2280],{"class":1488},"::",[126,2282,2283],{"class":1971},"before",[126,2285,192],{"class":1488},[126,2287,2288,2291,2293,2296],{"class":128,"line":166},[126,2289,2290],{"class":2245},"    content",[126,2292,2249],{"class":1488},[126,2294,2295],{"class":1488},"''",[126,2297,2255],{"class":1488},[126,2299,2300,2303,2305,2308,2311,2314],{"class":128,"line":172},[126,2301,2302],{"class":2245},"    font-family",[126,2304,2249],{"class":1488},[126,2306,2307],{"class":1488}," '",[126,2309,2310],{"class":1432},"FontAwesome",[126,2312,2313],{"class":1488},"'",[126,2315,2255],{"class":1488},[126,2317,2318,2321,2323,2326,2329,2331],{"class":128,"line":178},[126,2319,2320],{"class":2245},"    padding",[126,2322,2249],{"class":1488},[126,2324,2325],{"class":1469},"5px",[126,2327,2328],{"class":1469}," 10px",[126,2330,1489],{"class":1488},[126,2332,2333],{"class":2268}," \u002F* 文字間隔調整 *\u002F\n",[126,2335,2336],{"class":128,"line":183},[126,2337,260],{"class":1488},[126,2339,2340],{"class":128,"line":189},[126,2341,145],{"emptyLinePlaceholder":144},[126,2343,2344,2346,2349,2351,2353,2355],{"class":128,"line":195},[126,2345,2232],{"class":1488},[126,2347,2348],{"class":1429},"u-list_01",[126,2350,2238],{"class":1429},[126,2352,2280],{"class":1488},[126,2354,2283],{"class":1971},[126,2356,192],{"class":1488},[126,2358,2359,2361,2363,2365,2368,2370],{"class":128,"line":201},[126,2360,2290],{"class":2245},[126,2362,2249],{"class":1488},[126,2364,2313],{"class":1488},[126,2366,2367],{"class":1509},"\\f119",[126,2369,2313],{"class":1488},[126,2371,2255],{"class":1488},[126,2373,2374],{"class":128,"line":206},[126,2375,260],{"class":1488},[126,2377,2378],{"class":128,"line":212},[126,2379,145],{"emptyLinePlaceholder":144},[126,2381,2382,2384,2387,2389,2391,2393],{"class":128,"line":218},[126,2383,2232],{"class":1488},[126,2385,2386],{"class":1429},"u-list_02",[126,2388,2238],{"class":1429},[126,2390,2280],{"class":1488},[126,2392,2283],{"class":1971},[126,2394,192],{"class":1488},[126,2396,2397,2399,2401,2403,2406,2408],{"class":128,"line":224},[126,2398,2290],{"class":2245},[126,2400,2249],{"class":1488},[126,2402,2313],{"class":1488},[126,2404,2405],{"class":1509},"\\f4da",[126,2407,2313],{"class":1488},[126,2409,2255],{"class":1488},[126,2411,2412],{"class":128,"line":230},[126,2413,260],{"class":1488},[13,2415,2416],{},"iタグを入れない代わりに、ulタグに擬似要素アイコンを付けるための設定（p-deco_list）とアイコンの種類を指定（u-list_*）しています。そしてCSSでクラス名配下のliに対して擬似要素を付けるようにしています。",[20,2418,2419],{"id":2419},"擬似要素でアイコンを表示するのは意外と簡単",[13,2421,2422,2423],{},"CSSを見ればわかりますが、意外と簡単です。fontawesome、GoogleIconは「フォントアイコン」とも呼ばれているように、",[1083,2424,2425],{},"文字のように扱うことができます。",[13,2427,2428,2429,2432],{},"fontawesomeを使うならば、",[1083,2430,2431],{},"font-familyに’FontAwesome’と入れればいいのです。"," するとCDNとかがちゃんと読み込まれていれば、fontawesomeのアイコンが使えるようになります。",[13,2434,2435],{},"そしてアイコンの種類はcontentで指定します。「\\f4da」みたいなのは文字コードです。この文字コードはfontawesomeで簡単に見つけられます。下図のfontawesomeのアイコン詳細画面を開くと、iタグの横に「f4da」とあります。",[1181,2437],{":src":2438,":width":1184},"'_mix\u002Ffontawesome-1024x296.png'",[13,2440,2441,2442],{},"この英数字とバックスラッシュ（\\）をCSSのcontentに入れればアイコンの指定が完了です。",[1083,2443,2444],{},"バックスラッシュを入れないと、文字コードでなく「f4da」という文字列として認識されてしまいます。",[13,2446,2447],{},"「擬似要素！？iタグより難しいじゃん..」と思う方は、iタグで表示されたアイコンを開発者ツールでみてみましょう。実はiタグもこの方法と同じように、クラス名で文字コードを指定して、擬似要素内にアイコンを表示しているだけなんですよ。その方法をiタグ使わないでやっているだけです。",[20,2449,2451],{"id":2450},"ie野郎対策","IE野郎対策",[13,2453,2454,2455],{},"私もiタグがいらなくて、クラスを指定すればいいことにウキウキしながら実装を進めていくと問題にぶち当たりました。IEです。",[1083,2456,2457],{},"実は下のコードだけではIEは表示されないという事件が発覚して、頭を悩ませていました。",[82,2459,2461],{"className":2223,"code":2460,"language":2225,"meta":91,"style":91},".p-deco_list li::before{\n    content:'';\n    font-family: 'FontAwesome';\n    padding:5px 10px; \u002F* 文字間隔調整 *\u002F\n}\n",[89,2462,2463,2477,2487,2501,2515],{"__ignoreMap":91},[126,2464,2465,2467,2469,2471,2473,2475],{"class":128,"line":129},[126,2466,2232],{"class":1488},[126,2468,2235],{"class":1429},[126,2470,2238],{"class":1429},[126,2472,2280],{"class":1488},[126,2474,2283],{"class":1971},[126,2476,192],{"class":1488},[126,2478,2479,2481,2483,2485],{"class":128,"line":135},[126,2480,2290],{"class":2245},[126,2482,2249],{"class":1488},[126,2484,2295],{"class":1488},[126,2486,2255],{"class":1488},[126,2488,2489,2491,2493,2495,2497,2499],{"class":128,"line":141},[126,2490,2302],{"class":2245},[126,2492,2249],{"class":1488},[126,2494,2307],{"class":1488},[126,2496,2310],{"class":1432},[126,2498,2313],{"class":1488},[126,2500,2255],{"class":1488},[126,2502,2503,2505,2507,2509,2511,2513],{"class":128,"line":148},[126,2504,2320],{"class":2245},[126,2506,2249],{"class":1488},[126,2508,2325],{"class":1469},[126,2510,2328],{"class":1469},[126,2512,1489],{"class":1488},[126,2514,2333],{"class":2268},[126,2516,2517],{"class":128,"line":154},[126,2518,260],{"class":1488},[13,2520,2521],{},"chromeやFireFox（一応Edge）は表示されるんですが、IE11は表示されてませんでした。要件にはIE11が入っていたので実装は必須です。頭を悩ませてググっていると方法がありました。",[82,2523,2525],{"className":2223,"code":2524,"language":2225,"meta":91,"style":91},".p-deco_list li::before{\n    content:'';\n    font-family: 'FontAwesome';\n    font-feature-settings: 'liga';\n    padding:5px 10px; \u002F* 文字間隔調整 *\u002F\n}\n",[89,2526,2527,2541,2551,2565,2581,2595],{"__ignoreMap":91},[126,2528,2529,2531,2533,2535,2537,2539],{"class":128,"line":129},[126,2530,2232],{"class":1488},[126,2532,2235],{"class":1429},[126,2534,2238],{"class":1429},[126,2536,2280],{"class":1488},[126,2538,2283],{"class":1971},[126,2540,192],{"class":1488},[126,2542,2543,2545,2547,2549],{"class":128,"line":135},[126,2544,2290],{"class":2245},[126,2546,2249],{"class":1488},[126,2548,2295],{"class":1488},[126,2550,2255],{"class":1488},[126,2552,2553,2555,2557,2559,2561,2563],{"class":128,"line":141},[126,2554,2302],{"class":2245},[126,2556,2249],{"class":1488},[126,2558,2307],{"class":1488},[126,2560,2310],{"class":1432},[126,2562,2313],{"class":1488},[126,2564,2255],{"class":1488},[126,2566,2567,2570,2572,2574,2577,2579],{"class":128,"line":148},[126,2568,2569],{"class":2245},"    font-feature-settings",[126,2571,2249],{"class":1488},[126,2573,2307],{"class":1488},[126,2575,2576],{"class":1432},"liga",[126,2578,2313],{"class":1488},[126,2580,2255],{"class":1488},[126,2582,2583,2585,2587,2589,2591,2593],{"class":128,"line":154},[126,2584,2320],{"class":2245},[126,2586,2249],{"class":1488},[126,2588,2325],{"class":1469},[126,2590,2328],{"class":1469},[126,2592,1489],{"class":1488},[126,2594,2333],{"class":2268},[126,2596,2597],{"class":128,"line":160},[126,2598,260],{"class":1488},[13,2600,2601],{},"「font-feature-settings: ‘liga’;」というのを設定したらなんと！表示されました。このプロパティですがmdnでは",[596,2603,2605,2606,2609,2610],{"className":2604},[599,600],"\nCSS の ",[89,2607,2608],{},"font-feature-settings"," プロパティは、 OpenType フォントの拡張タイポグラフィの特性を制御します。\n",[1162,2611,2612],{},[36,2613,1166],{"href":1166,"target":39},[13,2615,2616],{},"(；´Д`A？",[13,2618,2619],{},"全然分からん。とりあえずフォントアイコン＝フォントなので、そのアイコンフォントを表示させるための設定がIEではないのか？と結論づけました。フォントについて詳しい人教えてください。",[20,2621,2622],{"id":2622},"アイコンは基本的には擬似要素がいいかも",[13,2624,2625],{},"そんな感じで、擬似要素を用いてアイコンを付ける方法は以上です。今回の例で出したような、動的な内容にアイコンが伴う場合はクラス名で指定できる、擬似要素アイコンがiタグ埋め込みより優れています。意外とやっている原理は簡単なのでぜひ使えると便利ですよ。",[1042,2627,2628],{},"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 pre.shiki code .sJ14y, html code.shiki .sJ14y{--shiki-default:#C792EA}html pre.shiki code .sfyAc, html code.shiki .sfyAc{--shiki-default:#C3E88D}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 .s0W1g, html code.shiki .s0W1g{--shiki-default:#BABED8}html pre.shiki code .s5Dmg, html code.shiki .s5Dmg{--shiki-default:#FFCB6B}html pre.shiki code .s6YsC, html code.shiki .s6YsC{--shiki-default:#B2CCD6}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":91,"searchDepth":141,"depth":141,"links":2630},[2631,2634,2635,2636,2637],{"id":1997,"depth":135,"text":1997,"children":2632},[2633],{"id":2013,"depth":141,"text":2014},{"id":2040,"depth":135,"text":2041},{"id":2419,"depth":135,"text":2419},{"id":2450,"depth":135,"text":2451},{"id":2622,"depth":135,"text":2622},[1059],"2020-04-26",{},"\u002Farticles\u002Fpseudo-element-icon",{"title":1945,"description":1945},"articles\u002Fpseudo-element-icon",[2055,2225],"_mix\u002Fpseudo-element-icon.png","vqsAO5STJjp70MfR8z6uFmS_BPvxDfVteLbqAiJvnO0",1780987140594]