[{"data":1,"prerenderedAt":4188},["ShallowReactive",2],{"tag-php-2":3},{"count":4,"content":5},16,[6,964,2663,2804,2895,3332],{"id":7,"title":8,"body":9,"category":951,"createdAt":953,"description":954,"extension":955,"index":956,"meta":957,"navigation":141,"path":958,"publish":141,"seo":959,"series":956,"seriesTitle":956,"stem":960,"tag":961,"thumbnail":956,"updatedAt":956,"__hash__":963},"articles\u002Farticles\u002Fwp-batch-image-compress.md","大量の大ファイルサイズ画像をPHPでリサイズ・圧縮をする方法",{"type":10,"value":11,"toc":939},"minimark",[12,16,19,22,26,29,50,53,56,59,285,295,298,303,309,341,350,401,408,412,417,428,431,502,508,511,514,517,900,907,911,918,926,929,935],[13,14,15],"p",{},"こんにちはjunです。私はこの技術ブログ以外にもブログを運営しているのですが、そちらはwebエンジニアになる前からやっていました。しかし過去の記事を見ているとやたらと重い画像、１枚あたり１MBも使っている記事が何枚もありました。当時は画像の重さなど知らずにデジカメの画像をそのままアップロードして、使用していたことが原因です。",[13,17,18],{},"記事の中には転送量が12MBにもなっているものもあり、原因は画像でした。それらの画像はやたらとサイズ（寸法）が大きいことが原因なので、リサイズして圧縮することで解決できます。しかし100件以上の記事を1つずつ開いて調べたり、もう一回ダウンロードして、圧縮してアップロードなんて何日かかるかわかりません。そのため今回はPHPとコマンドラインを用いて一気に画像を処理することにしました。",[13,20,21],{},"wordpressを使用していますが、画像処理のエッセンスや大方の処理の流れは応用できるとおもいます。では早速やっていきましょう。",[23,24,25],"h2",{"id":25},"大まかな流れ",[13,27,28],{},"まず全体の処理の流れですが以下の通りです。",[30,31,32,41,44,47],"ol",{},[33,34,35,36,40],"li",{},"ターミナルで ",[37,38,39],"code",{},"wp-content\u002Fuploads\u002F"," 配下にある大サイズ（500kb以上とする）をリストアップ。",[33,42,43],{},"バックアップしておき、数ヶ月残しておく。",[33,45,46],{},"PHPのGDモジュールを用いてリサイズ・圧縮を行う。",[33,48,49],{},"処理した画像を上書き。",[13,51,52],{},"以上の通りです。今回の処理を行うにあたりwordpressがあるサーバーに対してCLI\b操作が行える必要があります。SSHができ、必要な権限を付与しておいてください。今回はこの準備はできているものとします。",[23,54,55],{"id":55},"処理対象をリストアップ",[13,57,58],{},"それではまず対象の画像をリストアップします。アップロードディレクトリに移動してfindコマンドで簡単に見つけられます。",[60,61,66],"pre",{"className":62,"code":63,"language":64,"meta":65,"style":65},"language-bash shiki shiki-themes material-theme-ocean","# \u002Fdocumentroot\u002Fwordpress\u002F は仮です。あなたの環境に合わせてください。\n# 画像が格納されているuploadsへ移動\ncd '\u002Fdocumentroot\u002Fwordpress\u002Fwp-content\u002Fuploads'\nls\n2017 2018 2019 2020 2021 aaaa bbbbb cccc\n\n# プラグイン用画像のディレクトリなどもあるので注意！\n# 自分がアップロードした画像があるディレクトリのみを指定、500kb以上のものをリストアップ。リストはテキストファイルとしてホームディレクトリにおいておく。\nfind 2017 2018 2019 2020 2021 -size +500k > ~\u002Flist.txt\n..\n2017\u002F12\u002Fd7d6b39a47612239ce48bf6579be7f83-644x279.png\n2017\u002F12\u002Fd7d6b39a47612239ce48bf6579be7f83-720x340.png\n2017\u002F12\u002Fd7d6b39a47612239ce48bf6579be7f83-768x332.png\n2017\u002F12\u002Fd7d6b39a47612239ce48bf6579be7f83.png\n2017\u002F12\u002FDSC03961-1200x900.jpg\n2017\u002F12\u002FDSC04310-1200x900.jpg\n2017\u002F12\u002FDSC04311-1200x900.jpg\n2017\u002F12\u002FDSC04314-1200x900.jpg\n2017\u002F12\u002FDSC04315-1200x900.jpg\n2017\u002F12\u002FDSC04316-1200x900.jpg\n2017\u002F12\u002FDSC04317-1200x900.jpg\n2017\u002F12\u002FDSC04319-1200x900.jpg\n2017\u002F12\u002FDSC04320-e1512559527929-1200x1600.jpg\n2017\u002F12\u002FDSC04324-1200x900.jpg\n2017\u002F12\u002FDSC04325-1200x900.jpg\n...\n","bash","",[37,67,68,77,83,101,108,136,143,149,155,184,190,196,202,208,214,220,225,231,237,243,249,255,261,267,273,279],{"__ignoreMap":65},[69,70,73],"span",{"class":71,"line":72},"line",1,[69,74,76],{"class":75},"sC9rS","# \u002Fdocumentroot\u002Fwordpress\u002F は仮です。あなたの環境に合わせてください。\n",[69,78,80],{"class":71,"line":79},2,[69,81,82],{"class":75},"# 画像が格納されているuploadsへ移動\n",[69,84,86,90,94,98],{"class":71,"line":85},3,[69,87,89],{"class":88},"sdLwU","cd",[69,91,93],{"class":92},"sAklC"," '",[69,95,97],{"class":96},"sfyAc","\u002Fdocumentroot\u002Fwordpress\u002Fwp-content\u002Fuploads",[69,99,100],{"class":92},"'\n",[69,102,104],{"class":71,"line":103},4,[69,105,107],{"class":106},"s5Dmg","ls\n",[69,109,111,114,118,121,124,127,130,133],{"class":71,"line":110},5,[69,112,113],{"class":106},"2017",[69,115,117],{"class":116},"sx098"," 2018",[69,119,120],{"class":116}," 2019",[69,122,123],{"class":116}," 2020",[69,125,126],{"class":116}," 2021",[69,128,129],{"class":96}," aaaa",[69,131,132],{"class":96}," bbbbb",[69,134,135],{"class":96}," cccc\n",[69,137,139],{"class":71,"line":138},6,[69,140,142],{"emptyLinePlaceholder":141},true,"\n",[69,144,146],{"class":71,"line":145},7,[69,147,148],{"class":75},"# プラグイン用画像のディレクトリなどもあるので注意！\n",[69,150,152],{"class":71,"line":151},8,[69,153,154],{"class":75},"# 自分がアップロードした画像があるディレクトリのみを指定、500kb以上のものをリストアップ。リストはテキストファイルとしてホームディレクトリにおいておく。\n",[69,156,158,161,164,166,168,170,172,175,178,181],{"class":71,"line":157},9,[69,159,160],{"class":106},"find",[69,162,163],{"class":116}," 2017",[69,165,117],{"class":116},[69,167,120],{"class":116},[69,169,123],{"class":116},[69,171,126],{"class":116},[69,173,174],{"class":96}," -size",[69,176,177],{"class":96}," +500k",[69,179,180],{"class":92}," >",[69,182,183],{"class":96}," ~\u002Flist.txt\n",[69,185,187],{"class":71,"line":186},10,[69,188,189],{"class":88},"..\n",[69,191,193],{"class":71,"line":192},11,[69,194,195],{"class":106},"2017\u002F12\u002Fd7d6b39a47612239ce48bf6579be7f83-644x279.png\n",[69,197,199],{"class":71,"line":198},12,[69,200,201],{"class":106},"2017\u002F12\u002Fd7d6b39a47612239ce48bf6579be7f83-720x340.png\n",[69,203,205],{"class":71,"line":204},13,[69,206,207],{"class":106},"2017\u002F12\u002Fd7d6b39a47612239ce48bf6579be7f83-768x332.png\n",[69,209,211],{"class":71,"line":210},14,[69,212,213],{"class":106},"2017\u002F12\u002Fd7d6b39a47612239ce48bf6579be7f83.png\n",[69,215,217],{"class":71,"line":216},15,[69,218,219],{"class":106},"2017\u002F12\u002FDSC03961-1200x900.jpg\n",[69,221,222],{"class":71,"line":4},[69,223,224],{"class":106},"2017\u002F12\u002FDSC04310-1200x900.jpg\n",[69,226,228],{"class":71,"line":227},17,[69,229,230],{"class":106},"2017\u002F12\u002FDSC04311-1200x900.jpg\n",[69,232,234],{"class":71,"line":233},18,[69,235,236],{"class":106},"2017\u002F12\u002FDSC04314-1200x900.jpg\n",[69,238,240],{"class":71,"line":239},19,[69,241,242],{"class":106},"2017\u002F12\u002FDSC04315-1200x900.jpg\n",[69,244,246],{"class":71,"line":245},20,[69,247,248],{"class":106},"2017\u002F12\u002FDSC04316-1200x900.jpg\n",[69,250,252],{"class":71,"line":251},21,[69,253,254],{"class":106},"2017\u002F12\u002FDSC04317-1200x900.jpg\n",[69,256,258],{"class":71,"line":257},22,[69,259,260],{"class":106},"2017\u002F12\u002FDSC04319-1200x900.jpg\n",[69,262,264],{"class":71,"line":263},23,[69,265,266],{"class":106},"2017\u002F12\u002FDSC04320-e1512559527929-1200x1600.jpg\n",[69,268,270],{"class":71,"line":269},24,[69,271,272],{"class":106},"2017\u002F12\u002FDSC04324-1200x900.jpg\n",[69,274,276],{"class":71,"line":275},25,[69,277,278],{"class":106},"2017\u002F12\u002FDSC04325-1200x900.jpg\n",[69,280,282],{"class":71,"line":281},26,[69,283,284],{"class":88},"...\n",[13,286,287,290,291,294],{},[37,288,289],{},"find -size "," でファイルサイズでフィルターできます。1000件以上あったりと、コピーするのは大変なので ",[37,292,293],{},"list.txt"," などで出力しておきます。",[23,296,297],{"id":297},"画像処理スクリプトを作成",[299,300,302],"h3",{"id":301},"リストをphp-arrayにする","リストをPHP Arrayにする",[13,304,305,306,308],{},"サーバーでのリストアップが終わったので、ローカルでスクリプトを作っていきます。",[37,307,293],{}," はローカルに移動しておきます。",[60,310,312],{"className":62,"code":311,"language":64,"meta":65,"style":65},".\n├── list.php\n├── image.php\n├── list.txt\n",[37,313,314,319,327,334],{"__ignoreMap":65},[69,315,316],{"class":71,"line":72},[69,317,318],{"class":88},".\n",[69,320,321,324],{"class":71,"line":79},[69,322,323],{"class":106},"├──",[69,325,326],{"class":96}," list.php\n",[69,328,329,331],{"class":71,"line":85},[69,330,323],{"class":106},[69,332,333],{"class":96}," image.php\n",[69,335,336,338],{"class":71,"line":103},[69,337,323],{"class":106},[69,339,340],{"class":96}," list.txt\n",[13,342,343,345,346,349],{},[37,344,293],{}," を",[37,347,348],{},"list.php"," としてコピーしておき、配列に変換しておきます。テキストエディタの置換機能などを利用してください。",[60,351,355],{"className":352,"code":353,"filename":348,"language":354,"meta":65,"style":65},"language-php shiki shiki-themes material-theme-ocean","\u003C?php \n$list =[\n\"2017\u002F06\u002F23c7f697593e4a4c83e01310ee4b84ec-1200x900.jpg\",\n\"2017\u002F06\u002F23c7f697593e4a4c83e01310ee4b84ec.jpg\",\n\"2017\u002F06\u002Fcropped-DSC03989-1-1024x723.jpg\",\n\"2017\u002F06\u002Fcropped-DSC03989-1-1200x847.jpg\",\n\"2017\u002F06\u002Fcropped-DSC03989-1.jpg\",\n...\n];\n","php",[37,356,357,362,367,372,377,382,387,392,396],{"__ignoreMap":65},[69,358,359],{"class":71,"line":72},[69,360,361],{},"\u003C?php \n",[69,363,364],{"class":71,"line":79},[69,365,366],{},"$list =[\n",[69,368,369],{"class":71,"line":85},[69,370,371],{},"\"2017\u002F06\u002F23c7f697593e4a4c83e01310ee4b84ec-1200x900.jpg\",\n",[69,373,374],{"class":71,"line":103},[69,375,376],{},"\"2017\u002F06\u002F23c7f697593e4a4c83e01310ee4b84ec.jpg\",\n",[69,378,379],{"class":71,"line":110},[69,380,381],{},"\"2017\u002F06\u002Fcropped-DSC03989-1-1024x723.jpg\",\n",[69,383,384],{"class":71,"line":138},[69,385,386],{},"\"2017\u002F06\u002Fcropped-DSC03989-1-1200x847.jpg\",\n",[69,388,389],{"class":71,"line":145},[69,390,391],{},"\"2017\u002F06\u002Fcropped-DSC03989-1.jpg\",\n",[69,393,394],{"class":71,"line":151},[69,395,284],{},[69,397,398],{"class":71,"line":157},[69,399,400],{},"];\n",[13,402,403,404,407],{},"これで対象画像をリストアップして、PHPが扱えるようになったので ",[37,405,406],{},"image.php"," にて処理の方を書いていきましょう。",[299,409,411],{"id":410},"画像圧縮リサイズ処理","画像圧縮・リサイズ処理",[413,414,416],"h4",{"id":415},"そのまえに","そのまえに..",[13,418,419,420,427],{},"PHPでリサイズなどの画像処理を行う場合は",[421,422,426],"a",{"href":423,"rel":424},"https:\u002F\u002Fwww.php.net\u002Fmanual\u002Fja\u002Fintro.image.php",[425],"nofollow","GDというPHPモジュール","を用います。処理を書く前にこのGDモジュールが使用しているPHP環境にあるかを確認しましょう。",[13,429,430],{},"phpinfoのページでも確認できますが、手っ取り早くCLIでやりましょう。以下のコマンドを打ちます。",[60,432,434],{"className":62,"code":433,"language":64,"meta":65,"style":65},"php --info | grep GD\nGD Support => enabled\nGD headers Version => 2.3.2\nGD library Version => 2.3.2\n",[37,435,436,452,470,487],{"__ignoreMap":65},[69,437,438,440,443,446,449],{"class":71,"line":72},[69,439,354],{"class":106},[69,441,442],{"class":96}," --info",[69,444,445],{"class":92}," |",[69,447,448],{"class":106}," grep",[69,450,451],{"class":96}," GD\n",[69,453,454,457,460,464,467],{"class":71,"line":79},[69,455,456],{"class":106},"GD",[69,458,459],{"class":96}," Support",[69,461,463],{"class":462},"s0W1g"," =",[69,465,466],{"class":92},">",[69,468,469],{"class":96}," enabled\n",[69,471,472,474,477,480,482,484],{"class":71,"line":85},[69,473,456],{"class":106},[69,475,476],{"class":96}," headers",[69,478,479],{"class":96}," Version",[69,481,463],{"class":462},[69,483,466],{"class":92},[69,485,486],{"class":116}," 2.3.2\n",[69,488,489,491,494,496,498,500],{"class":71,"line":103},[69,490,456],{"class":106},[69,492,493],{"class":96}," library",[69,495,479],{"class":96},[69,497,463],{"class":462},[69,499,466],{"class":92},[69,501,486],{"class":116},[13,503,504,507],{},[37,505,506],{},"GD Support => enabled"," が存在し、enabledとなっていれば使用可能です。ない場合などを別途インストールが必要です。まあwordpressが使用できる環境ならば大抵入っていると思います。これでGDの確認を行ったら、処理を書きます。",[413,509,510],{"id":510},"処理の記述",[13,512,513],{},"今回のスクリプトはCMSがるサーバー上で実行するものとします。そのためパスなどの構成も予め、サーバー上であることを想定してます。",[13,515,516],{},"全体は以下の通りです。",[60,518,520],{"className":352,"code":519,"language":354,"meta":65,"style":65},"\u003C?php \n    require_once 'list.php';\n\n    $uploadRoot = '\u002Fdocumentroot\u002Fwordpress\u002Fwp-content\u002Fuploads\u002F';\n\n    foreach($list as $file){\n        \u002F** \n         * ファイル名などを分けておく。\n         * \n         * 例\n         * $file            => '2017\u002F12\u002FDSC04325-1200x900.jpg'\n         * $targetFilename  => 'DSC04325-1200x900.jpg'\n         * $fullPath        => '\u002Fdocumentroot\u002Fwordpress\u002Fwp-content\u002Fuploads\u002F2017\u002F12\u002FDSC04325-1200x900.jpg'\n        *\u002F\n        $filenames = explode('\u002F',$file);\n        $targetFilename = $filenames[count($filenames)-1];\n        $fullPath = $uploadRoot.$file;\n\n        \u002F\u002F getimagesize()を用いて寸法、拡張子の情報を取得。\n        list($w, $h,$type) = getimagesize($fullPath);\n        var_dump($fullPath);\n\n        \u002F\u002F imagecreatefromjpeg()・imagecreatefrompng()にてGDImageを取得。画像を取得しているんだなぐらいだと思ってください。\n        \u002F\u002F JPG\u002FPNGによって分ける必要あり！。\n        switch ($type) {\n            case IMAGETYPE_JPEG:\n                $original_image = imagecreatefromjpeg($fullPath);\n                break;\n            case IMAGETYPE_PNG:\n                $original_image = imagecreatefrompng($fullPath);\n                break;\n            default:\n                var_dump('対応していないファイル形式です。: '.$fullPath);\n                continue;\n        }\n\n        \u002F\u002F 寸法が大きすぎるもの(1000px以上)は600pxぐにらいするリサイズ処理を行う。\n        if($w >= 1000){\n\n            \u002F\u002F imagecreatetruecolor() で新しい画像を埋め込むための「枠」を作る。（新しいキャンバス的な？）\n            \u002F\u002F 縦横比を計算させて元画像と同じにさせること。\n            $newW = 600;\n            $newH = $newW * ($h \u002F $w);\n            $newImg = imagecreatetruecolor($newW, $newH);\n\n            \u002F\u002F リサイズ処理。\n            $success = imagecopyresampled($newImg, $original_image, 0, 0, 0, 0, $newW, $newH, $w, $h);\n\n            if($success){\n                $original_image = $newImg;\n            }else{\n                var_dump('リサイズ失敗: '.$fullPath);\n                continue;\n            }\n        }\n\n        \u002F\u002F 画像を圧縮して同じ名前、パスで上書き保存する。ここも拡張子で異なるので注意！\n        \u002F\u002F 数字は圧縮の品質。低いほど軽くなるが、粗くなる。jpgは0~100,pngは0~9なので注意。\n        switch ($type) {\n            case IMAGETYPE_JPEG:\n                imagejpeg($original_image,$fullPath,60);\n                break;\n            case IMAGETYPE_PNG:\n                imagepng($original_image,$fullPath,6);\n                break;\n            default:\n                var_dump('対応していないファイル形式です。: '.$fullPath);\n                continue;\n        }\n    }\n?>\n",[37,521,522,526,531,535,540,544,549,554,559,564,569,574,579,584,589,594,599,604,608,613,618,623,627,632,637,642,647,653,659,665,671,676,682,688,694,700,705,711,717,722,728,734,740,746,752,757,763,769,774,780,786,792,798,803,809,814,819,825,831,836,841,847,852,857,863,868,873,878,883,888,894],{"__ignoreMap":65},[69,523,524],{"class":71,"line":72},[69,525,361],{},[69,527,528],{"class":71,"line":79},[69,529,530],{},"    require_once 'list.php';\n",[69,532,533],{"class":71,"line":85},[69,534,142],{"emptyLinePlaceholder":141},[69,536,537],{"class":71,"line":103},[69,538,539],{},"    $uploadRoot = '\u002Fdocumentroot\u002Fwordpress\u002Fwp-content\u002Fuploads\u002F';\n",[69,541,542],{"class":71,"line":110},[69,543,142],{"emptyLinePlaceholder":141},[69,545,546],{"class":71,"line":138},[69,547,548],{},"    foreach($list as $file){\n",[69,550,551],{"class":71,"line":145},[69,552,553],{},"        \u002F** \n",[69,555,556],{"class":71,"line":151},[69,557,558],{},"         * ファイル名などを分けておく。\n",[69,560,561],{"class":71,"line":157},[69,562,563],{},"         * \n",[69,565,566],{"class":71,"line":186},[69,567,568],{},"         * 例\n",[69,570,571],{"class":71,"line":192},[69,572,573],{},"         * $file            => '2017\u002F12\u002FDSC04325-1200x900.jpg'\n",[69,575,576],{"class":71,"line":198},[69,577,578],{},"         * $targetFilename  => 'DSC04325-1200x900.jpg'\n",[69,580,581],{"class":71,"line":204},[69,582,583],{},"         * $fullPath        => '\u002Fdocumentroot\u002Fwordpress\u002Fwp-content\u002Fuploads\u002F2017\u002F12\u002FDSC04325-1200x900.jpg'\n",[69,585,586],{"class":71,"line":210},[69,587,588],{},"        *\u002F\n",[69,590,591],{"class":71,"line":216},[69,592,593],{},"        $filenames = explode('\u002F',$file);\n",[69,595,596],{"class":71,"line":4},[69,597,598],{},"        $targetFilename = $filenames[count($filenames)-1];\n",[69,600,601],{"class":71,"line":227},[69,602,603],{},"        $fullPath = $uploadRoot.$file;\n",[69,605,606],{"class":71,"line":233},[69,607,142],{"emptyLinePlaceholder":141},[69,609,610],{"class":71,"line":239},[69,611,612],{},"        \u002F\u002F getimagesize()を用いて寸法、拡張子の情報を取得。\n",[69,614,615],{"class":71,"line":245},[69,616,617],{},"        list($w, $h,$type) = getimagesize($fullPath);\n",[69,619,620],{"class":71,"line":251},[69,621,622],{},"        var_dump($fullPath);\n",[69,624,625],{"class":71,"line":257},[69,626,142],{"emptyLinePlaceholder":141},[69,628,629],{"class":71,"line":263},[69,630,631],{},"        \u002F\u002F imagecreatefromjpeg()・imagecreatefrompng()にてGDImageを取得。画像を取得しているんだなぐらいだと思ってください。\n",[69,633,634],{"class":71,"line":269},[69,635,636],{},"        \u002F\u002F JPG\u002FPNGによって分ける必要あり！。\n",[69,638,639],{"class":71,"line":275},[69,640,641],{},"        switch ($type) {\n",[69,643,644],{"class":71,"line":281},[69,645,646],{},"            case IMAGETYPE_JPEG:\n",[69,648,650],{"class":71,"line":649},27,[69,651,652],{},"                $original_image = imagecreatefromjpeg($fullPath);\n",[69,654,656],{"class":71,"line":655},28,[69,657,658],{},"                break;\n",[69,660,662],{"class":71,"line":661},29,[69,663,664],{},"            case IMAGETYPE_PNG:\n",[69,666,668],{"class":71,"line":667},30,[69,669,670],{},"                $original_image = imagecreatefrompng($fullPath);\n",[69,672,674],{"class":71,"line":673},31,[69,675,658],{},[69,677,679],{"class":71,"line":678},32,[69,680,681],{},"            default:\n",[69,683,685],{"class":71,"line":684},33,[69,686,687],{},"                var_dump('対応していないファイル形式です。: '.$fullPath);\n",[69,689,691],{"class":71,"line":690},34,[69,692,693],{},"                continue;\n",[69,695,697],{"class":71,"line":696},35,[69,698,699],{},"        }\n",[69,701,703],{"class":71,"line":702},36,[69,704,142],{"emptyLinePlaceholder":141},[69,706,708],{"class":71,"line":707},37,[69,709,710],{},"        \u002F\u002F 寸法が大きすぎるもの(1000px以上)は600pxぐにらいするリサイズ処理を行う。\n",[69,712,714],{"class":71,"line":713},38,[69,715,716],{},"        if($w >= 1000){\n",[69,718,720],{"class":71,"line":719},39,[69,721,142],{"emptyLinePlaceholder":141},[69,723,725],{"class":71,"line":724},40,[69,726,727],{},"            \u002F\u002F imagecreatetruecolor() で新しい画像を埋め込むための「枠」を作る。（新しいキャンバス的な？）\n",[69,729,731],{"class":71,"line":730},41,[69,732,733],{},"            \u002F\u002F 縦横比を計算させて元画像と同じにさせること。\n",[69,735,737],{"class":71,"line":736},42,[69,738,739],{},"            $newW = 600;\n",[69,741,743],{"class":71,"line":742},43,[69,744,745],{},"            $newH = $newW * ($h \u002F $w);\n",[69,747,749],{"class":71,"line":748},44,[69,750,751],{},"            $newImg = imagecreatetruecolor($newW, $newH);\n",[69,753,755],{"class":71,"line":754},45,[69,756,142],{"emptyLinePlaceholder":141},[69,758,760],{"class":71,"line":759},46,[69,761,762],{},"            \u002F\u002F リサイズ処理。\n",[69,764,766],{"class":71,"line":765},47,[69,767,768],{},"            $success = imagecopyresampled($newImg, $original_image, 0, 0, 0, 0, $newW, $newH, $w, $h);\n",[69,770,772],{"class":71,"line":771},48,[69,773,142],{"emptyLinePlaceholder":141},[69,775,777],{"class":71,"line":776},49,[69,778,779],{},"            if($success){\n",[69,781,783],{"class":71,"line":782},50,[69,784,785],{},"                $original_image = $newImg;\n",[69,787,789],{"class":71,"line":788},51,[69,790,791],{},"            }else{\n",[69,793,795],{"class":71,"line":794},52,[69,796,797],{},"                var_dump('リサイズ失敗: '.$fullPath);\n",[69,799,801],{"class":71,"line":800},53,[69,802,693],{},[69,804,806],{"class":71,"line":805},54,[69,807,808],{},"            }\n",[69,810,812],{"class":71,"line":811},55,[69,813,699],{},[69,815,817],{"class":71,"line":816},56,[69,818,142],{"emptyLinePlaceholder":141},[69,820,822],{"class":71,"line":821},57,[69,823,824],{},"        \u002F\u002F 画像を圧縮して同じ名前、パスで上書き保存する。ここも拡張子で異なるので注意！\n",[69,826,828],{"class":71,"line":827},58,[69,829,830],{},"        \u002F\u002F 数字は圧縮の品質。低いほど軽くなるが、粗くなる。jpgは0~100,pngは0~9なので注意。\n",[69,832,834],{"class":71,"line":833},59,[69,835,641],{},[69,837,839],{"class":71,"line":838},60,[69,840,646],{},[69,842,844],{"class":71,"line":843},61,[69,845,846],{},"                imagejpeg($original_image,$fullPath,60);\n",[69,848,850],{"class":71,"line":849},62,[69,851,658],{},[69,853,855],{"class":71,"line":854},63,[69,856,664],{},[69,858,860],{"class":71,"line":859},64,[69,861,862],{},"                imagepng($original_image,$fullPath,6);\n",[69,864,866],{"class":71,"line":865},65,[69,867,658],{},[69,869,871],{"class":71,"line":870},66,[69,872,681],{},[69,874,876],{"class":71,"line":875},67,[69,877,687],{},[69,879,881],{"class":71,"line":880},68,[69,882,693],{},[69,884,886],{"class":71,"line":885},69,[69,887,699],{},[69,889,891],{"class":71,"line":890},70,[69,892,893],{},"    }\n",[69,895,897],{"class":71,"line":896},71,[69,898,899],{},"?>\n",[13,901,902,903,906],{},"書いてあるコメントの通りです。ただGDは拡張子ごとに使用する関数を分ける必要があるので、そこがめんどいです。listで予めどんな拡張子が使用されているかを確かめておきましょう。ローカルでテストを行ったら、",[37,904,905],{},"image.php, list.php","をサーバーに転送します。",[23,908,910],{"id":909},"実行前の注意","実行前の注意！",[13,912,913,914,917],{},"実行する前に必ず ",[37,915,916],{},"uploads"," のバックアップをしておきましょう。そうすればもしやらかしたり、予想以上に粗くなったとしてもやり直しができます。OKであれば",[60,919,924],{"className":920,"code":922,"language":923},[921],"language-text","php image.php\n","text",[37,925,922],{"__ignoreMap":65},[13,927,928],{},"そして実行しましょう。私の場合1500画像で5分ほどで終わった気がします。",[13,930,931,934],{},[37,932,933],{},"ls -lh"," や上記のfindコマンドで重い画像がなくなったかを確認しましょう。また、画像のパスをブラウザで叩いて確認してもいいかもしれません。キャッシュが効いていることが多いので、シークレットモードで見ることをお勧めします。",[936,937,938],"style",{},"html pre.shiki code .sC9rS, html code.shiki .sC9rS{--shiki-default:#464B5D;--shiki-default-font-style:italic}html pre.shiki code .sdLwU, html code.shiki .sdLwU{--shiki-default:#82AAFF}html pre.shiki code .sAklC, html code.shiki .sAklC{--shiki-default:#89DDFF}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 .sx098, html code.shiki .sx098{--shiki-default:#F78C6C}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}",{"title":65,"searchDepth":85,"depth":85,"links":940},[941,942,943,950],{"id":25,"depth":79,"text":25},{"id":55,"depth":79,"text":55},{"id":297,"depth":79,"text":297,"children":944},[945,946],{"id":301,"depth":85,"text":302},{"id":410,"depth":85,"text":411,"children":947},[948,949],{"id":415,"depth":103,"text":416},{"id":510,"depth":103,"text":510},{"id":909,"depth":79,"text":910},[952],"ministack","2021-08-15","wordpressに保存された1〜4MBの1500件の画像を一括に処理する。","md",null,{},"\u002Farticles\u002Fwp-batch-image-compress",{"title":8,"description":954},"articles\u002Fwp-batch-image-compress",[962,354],"wordpress","XoVocS1wgJ1s3OqIpny9hQofVYNRjvOcumKbgZx2d98",{"id":965,"title":966,"body":967,"category":2649,"createdAt":2651,"description":2652,"extension":955,"index":956,"meta":2653,"navigation":141,"path":2654,"publish":141,"seo":2655,"series":956,"seriesTitle":956,"stem":2656,"tag":2657,"thumbnail":2661,"updatedAt":956,"__hash__":2662},"articles\u002Farticles\u002Fimplement-recaptcha.md","reCAPTCHAのフロントエンド実装とバックエンド実装（PHP・Laravel）をスクラッチで行う方法",{"type":10,"value":968,"toc":2635},[969,972,975,978,981,985,988,991,994,997,1001,1010,1015,1022,1025,1028,1039,1042,1045,1235,1238,1250,1259,1263,1266,1506,1520,1523,1526,2011,2022,2044,2047,2050,2053,2056,2067,2070,2257,2268,2293,2311,2314,2353,2364,2367,2370,2415,2422,2425,2433,2458,2466,2475,2483,2489,2497,2500,2620,2623,2626,2629,2632],[13,970,971],{},"こんにちはjunです。皆さんはフォームを作成する時にBot・スパム対策を行っていますか？フォームというのは少し知識があれば、簡単にbot的に送信することができます。",[13,973,974],{},"curlでPOSTすることもあれば、javascriptを実行して機械的にフォームを送信することがきるので、スパム（嫌がらせ）やサーバーへの過剰負荷の原因となります。",[13,976,977],{},"この様な機械的な操作を防ぐために、よく「ロボットではありません」「画像に表示されている文字を入力してください」みたいなbotでは簡単に処理できないものを用意します。",[13,979,980],{},"しかしこの様な機能は自分で実装するのは大変です。そんな時に便利なのがreCAPTCHAです。",[23,982,984],{"id":983},"recaptchaとは","reCAPTCHAとは？",[13,986,987],{},"reCAPTCHAはGoogleが無料で提供しているBot対策ツールです。現在v3までリリースされており、v2は画像を選択させたりチェックを入れるといったユーザーのアクションでbotかどうかを検証します。v3はその様なアクションを必要とせず、必要なスクリプトを入れるだけで検証ができます。これからの実装の場合はv3を入れることをお勧めします。",[23,989,990],{"id":990},"実装内容",[13,992,993],{},"今回の解説ではv3でのフロントエンドの実装とバックエンドの実装を解説していきます。そして利用するreCAPTCHAはエンタープライズではなく、無料のものを利用します。バックエンドにはLaravel6(php)を用いて説明します。バックエンドは基本的にやることはどの言語・フレームワークでも特に差異はありません。reCAPTCHAを使う時にライブラリを使用することもありますが、いうてそれほど難しくないので今回はライブラリを使用しないスクラッチで実装します。",[13,995,996],{},"それでは解説を始めます。",[23,998,1000],{"id":999},"recaptchaのキーを手に入れる","reCAPTCHAのキーを手に入れる",[13,1002,1003,1004,1009],{},"reCAPTCHAはGooglenのAPIを使用することでbotか検証を行います。reCAPTCHAを利用するためにGoogleアカウントとreCAPTCHAのキーを登録します。",[421,1005,1008],{"href":1006,"rel":1007},"https:\u002F\u002Fwww.google.com\u002Frecaptcha\u002Fadmin\u002Fcreate",[425],"reCAPTCHAの登録ページ","にて保護対象のドメインを設定します。",[1011,1012],"image-render",{":src":1013,":width":1014},"'_mix\u002Fsch-2021-05-21 23.16.06.png'","'100%'",[13,1016,1017,1018,1021],{},"ラベルは管理用の名前、タイプはv3を使用します。保護対象のドメインを登録します。ドメインは複数設定できます。この時、本番のドメインと",[37,1019,1020],{},"localhost","を登録しておくと開発・本番で利用できます。",[13,1023,1024],{},"設定を終わって「保存」しますと、キーが表示されますので保存しておきます。",[1011,1026],{":src":1027,":width":1014},"'_mix\u002Frecaptcha-key.jpeg'",[13,1029,1030,1034,1035,1038],{},[1031,1032,1033],"em",{},"このサイトキーは、ユーザー表示するHTMLコードで利用します。"," という方のキーはタグで利用し、正直みられても問題ありません。フロント側のキーはドメインと合わせて保護対処のサイトであるかのチェックのためにあるだけだからです。逆にバックの方である ",[1031,1036,1037],{},"このサイトキーは、サイトとreCAPTCHA間の通信で利用します。"," は漏れてはいけません。",[23,1040,1041],{"id":1041},"フロントエンドの実装",[13,1043,1044],{},"それではフロントエンドを実装していきます。かなり簡略化して書いています。以下の様なフォームがあったとしましょう。",[60,1046,1050],{"className":1047,"code":1048,"language":1049,"meta":65,"style":65},"language-html shiki shiki-themes material-theme-ocean","\u003C!DOCTYPE html>\n\u003Chtml>\n    \u003Chead>\n      \u003C!---省略-->\n    \u003C\u002Fhead>\n\n    \u003Cbody>\n        \u003Cform method=\"post\">\n            \u003Cinput type=\"text\" name=\"test\" value=\"\">\n            \u003Cinput type=\"submit\" value=\"送信\">\n        \u003C\u002Fform>\n    \u003C\u002Fbody>\n\u003C\u002Fhtml>\n","html",[37,1051,1052,1068,1077,1087,1092,1101,1105,1114,1138,1179,1209,1218,1226],{"__ignoreMap":65},[69,1053,1054,1057,1061,1065],{"class":71,"line":72},[69,1055,1056],{"class":92},"\u003C!",[69,1058,1060],{"class":1059},"s-wAU","DOCTYPE",[69,1062,1064],{"class":1063},"sJ14y"," html",[69,1066,1067],{"class":92},">\n",[69,1069,1070,1073,1075],{"class":71,"line":79},[69,1071,1072],{"class":92},"\u003C",[69,1074,1049],{"class":1059},[69,1076,1067],{"class":92},[69,1078,1079,1082,1085],{"class":71,"line":85},[69,1080,1081],{"class":92},"    \u003C",[69,1083,1084],{"class":1059},"head",[69,1086,1067],{"class":92},[69,1088,1089],{"class":71,"line":103},[69,1090,1091],{"class":75},"      \u003C!---省略-->\n",[69,1093,1094,1097,1099],{"class":71,"line":110},[69,1095,1096],{"class":92},"    \u003C\u002F",[69,1098,1084],{"class":1059},[69,1100,1067],{"class":92},[69,1102,1103],{"class":71,"line":138},[69,1104,142],{"emptyLinePlaceholder":141},[69,1106,1107,1109,1112],{"class":71,"line":145},[69,1108,1081],{"class":92},[69,1110,1111],{"class":1059},"body",[69,1113,1067],{"class":92},[69,1115,1116,1119,1122,1125,1128,1131,1134,1136],{"class":71,"line":151},[69,1117,1118],{"class":92},"        \u003C",[69,1120,1121],{"class":1059},"form",[69,1123,1124],{"class":1063}," method",[69,1126,1127],{"class":92},"=",[69,1129,1130],{"class":92},"\"",[69,1132,1133],{"class":96},"post",[69,1135,1130],{"class":92},[69,1137,1067],{"class":92},[69,1139,1140,1143,1146,1149,1151,1153,1155,1157,1160,1162,1164,1167,1169,1172,1174,1177],{"class":71,"line":157},[69,1141,1142],{"class":92},"            \u003C",[69,1144,1145],{"class":1059},"input",[69,1147,1148],{"class":1063}," type",[69,1150,1127],{"class":92},[69,1152,1130],{"class":92},[69,1154,923],{"class":96},[69,1156,1130],{"class":92},[69,1158,1159],{"class":1063}," name",[69,1161,1127],{"class":92},[69,1163,1130],{"class":92},[69,1165,1166],{"class":96},"test",[69,1168,1130],{"class":92},[69,1170,1171],{"class":1063}," value",[69,1173,1127],{"class":92},[69,1175,1176],{"class":92},"\"\"",[69,1178,1067],{"class":92},[69,1180,1181,1183,1185,1187,1189,1191,1194,1196,1198,1200,1202,1205,1207],{"class":71,"line":186},[69,1182,1142],{"class":92},[69,1184,1145],{"class":1059},[69,1186,1148],{"class":1063},[69,1188,1127],{"class":92},[69,1190,1130],{"class":92},[69,1192,1193],{"class":96},"submit",[69,1195,1130],{"class":92},[69,1197,1171],{"class":1063},[69,1199,1127],{"class":92},[69,1201,1130],{"class":92},[69,1203,1204],{"class":96},"送信",[69,1206,1130],{"class":92},[69,1208,1067],{"class":92},[69,1210,1211,1214,1216],{"class":71,"line":192},[69,1212,1213],{"class":92},"        \u003C\u002F",[69,1215,1121],{"class":1059},[69,1217,1067],{"class":92},[69,1219,1220,1222,1224],{"class":71,"line":198},[69,1221,1096],{"class":92},[69,1223,1111],{"class":1059},[69,1225,1067],{"class":92},[69,1227,1228,1231,1233],{"class":71,"line":204},[69,1229,1230],{"class":92},"\u003C\u002F",[69,1232,1049],{"class":1059},[69,1234,1067],{"class":92},[13,1236,1237],{},"reCAPTCHAのフロントエンド 実装では",[1239,1240,1241,1244,1247],"ul",{},[33,1242,1243],{},"reCAPTCHAのソースを読み込む",[33,1245,1246],{},"送信ボタンを押したらreCAPTCHAと通信してトークンを手に入れるスクリプトを書く",[33,1248,1249],{},"reCAPTCHAのトークンをフォーム内容と一緒にバックエンドに送信する。",[13,1251,1252,1253,1258],{},"以上の実装を必要とします。",[421,1254,1257],{"href":1255,"rel":1256},"https:\u002F\u002Fdevelopers.google.com\u002Frecaptcha\u002Fdocs\u002Fv3#programmatically_invoke_the_challenge",[425],"本家のドキュメント","を参考にして進めていきましょう。",[299,1260,1262],{"id":1261},"recaptchaのスクリプト読み込みとhtml調整","reCAPTCHAのスクリプト読み込みとHTML調整",[13,1264,1265],{},"まずはreCAPTCHAを有効にするためのスクリプトを読み込みます。そして一部フォームを編集します。",[60,1267,1269],{"className":1047,"code":1268,"language":1049,"meta":65,"style":65},"\u003C!DOCTYPE html>\n\u003Chtml>\n    \u003Chead>\n      \u003C!---省略-->\n        \u003Cscript src=\"https:\u002F\u002Fwww.google.com\u002Frecaptcha\u002Fapi.js?render=YOUR_FRONT_KEY\">\u003C\u002Fscript>\u003C!---追加-->\n    \u003C\u002Fhead>\n\n    \u003Cbody>\n        \u003Cform method=\"post\" id=\"test-form\">\u003C!---追加-->\n            \u003Cinput type=\"text\" name=\"test\" value=\"\">\n            \u003Cinput type=\"hidden\" name=\"recaptcha\" value=\"\">\u003C!---追加-->\n            \u003Cinput type=\"submit\" value=\"送信\">\n        \u003C\u002Fform>\n    \u003C\u002Fbody>\n\u003C\u002Fhtml>\n",[37,1270,1271,1281,1289,1297,1301,1330,1338,1342,1350,1382,1416,1454,1482,1490,1498],{"__ignoreMap":65},[69,1272,1273,1275,1277,1279],{"class":71,"line":72},[69,1274,1056],{"class":92},[69,1276,1060],{"class":1059},[69,1278,1064],{"class":1063},[69,1280,1067],{"class":92},[69,1282,1283,1285,1287],{"class":71,"line":79},[69,1284,1072],{"class":92},[69,1286,1049],{"class":1059},[69,1288,1067],{"class":92},[69,1290,1291,1293,1295],{"class":71,"line":85},[69,1292,1081],{"class":92},[69,1294,1084],{"class":1059},[69,1296,1067],{"class":92},[69,1298,1299],{"class":71,"line":103},[69,1300,1091],{"class":75},[69,1302,1303,1305,1308,1311,1313,1315,1318,1320,1323,1325,1327],{"class":71,"line":110},[69,1304,1118],{"class":92},[69,1306,1307],{"class":1059},"script",[69,1309,1310],{"class":1063}," src",[69,1312,1127],{"class":92},[69,1314,1130],{"class":92},[69,1316,1317],{"class":96},"https:\u002F\u002Fwww.google.com\u002Frecaptcha\u002Fapi.js?render=YOUR_FRONT_KEY",[69,1319,1130],{"class":92},[69,1321,1322],{"class":92},">\u003C\u002F",[69,1324,1307],{"class":1059},[69,1326,466],{"class":92},[69,1328,1329],{"class":75},"\u003C!---追加-->\n",[69,1331,1332,1334,1336],{"class":71,"line":138},[69,1333,1096],{"class":92},[69,1335,1084],{"class":1059},[69,1337,1067],{"class":92},[69,1339,1340],{"class":71,"line":145},[69,1341,142],{"emptyLinePlaceholder":141},[69,1343,1344,1346,1348],{"class":71,"line":151},[69,1345,1081],{"class":92},[69,1347,1111],{"class":1059},[69,1349,1067],{"class":92},[69,1351,1352,1354,1356,1358,1360,1362,1364,1366,1369,1371,1373,1376,1378,1380],{"class":71,"line":157},[69,1353,1118],{"class":92},[69,1355,1121],{"class":1059},[69,1357,1124],{"class":1063},[69,1359,1127],{"class":92},[69,1361,1130],{"class":92},[69,1363,1133],{"class":96},[69,1365,1130],{"class":92},[69,1367,1368],{"class":1063}," id",[69,1370,1127],{"class":92},[69,1372,1130],{"class":92},[69,1374,1375],{"class":96},"test-form",[69,1377,1130],{"class":92},[69,1379,466],{"class":92},[69,1381,1329],{"class":75},[69,1383,1384,1386,1388,1390,1392,1394,1396,1398,1400,1402,1404,1406,1408,1410,1412,1414],{"class":71,"line":186},[69,1385,1142],{"class":92},[69,1387,1145],{"class":1059},[69,1389,1148],{"class":1063},[69,1391,1127],{"class":92},[69,1393,1130],{"class":92},[69,1395,923],{"class":96},[69,1397,1130],{"class":92},[69,1399,1159],{"class":1063},[69,1401,1127],{"class":92},[69,1403,1130],{"class":92},[69,1405,1166],{"class":96},[69,1407,1130],{"class":92},[69,1409,1171],{"class":1063},[69,1411,1127],{"class":92},[69,1413,1176],{"class":92},[69,1415,1067],{"class":92},[69,1417,1418,1420,1422,1424,1426,1428,1431,1433,1435,1437,1439,1442,1444,1446,1448,1450,1452],{"class":71,"line":192},[69,1419,1142],{"class":92},[69,1421,1145],{"class":1059},[69,1423,1148],{"class":1063},[69,1425,1127],{"class":92},[69,1427,1130],{"class":92},[69,1429,1430],{"class":96},"hidden",[69,1432,1130],{"class":92},[69,1434,1159],{"class":1063},[69,1436,1127],{"class":92},[69,1438,1130],{"class":92},[69,1440,1441],{"class":96},"recaptcha",[69,1443,1130],{"class":92},[69,1445,1171],{"class":1063},[69,1447,1127],{"class":92},[69,1449,1176],{"class":92},[69,1451,466],{"class":92},[69,1453,1329],{"class":75},[69,1455,1456,1458,1460,1462,1464,1466,1468,1470,1472,1474,1476,1478,1480],{"class":71,"line":198},[69,1457,1142],{"class":92},[69,1459,1145],{"class":1059},[69,1461,1148],{"class":1063},[69,1463,1127],{"class":92},[69,1465,1130],{"class":92},[69,1467,1193],{"class":96},[69,1469,1130],{"class":92},[69,1471,1171],{"class":1063},[69,1473,1127],{"class":92},[69,1475,1130],{"class":92},[69,1477,1204],{"class":96},[69,1479,1130],{"class":92},[69,1481,1067],{"class":92},[69,1483,1484,1486,1488],{"class":71,"line":204},[69,1485,1213],{"class":92},[69,1487,1121],{"class":1059},[69,1489,1067],{"class":92},[69,1491,1492,1494,1496],{"class":71,"line":210},[69,1493,1096],{"class":92},[69,1495,1111],{"class":1059},[69,1497,1067],{"class":92},[69,1499,1500,1502,1504],{"class":71,"line":216},[69,1501,1230],{"class":92},[69,1503,1049],{"class":1059},[69,1505,1067],{"class":92},[13,1507,1508,1511,1512,1515,1516,1519],{},[37,1509,1510],{},"\u003Cscript src=\"https:\u002F\u002Fwww.google.com\u002Frecaptcha\u002Fapi.js?render=YOUR_FRONT_KEY\">\u003C\u002Fscript>","の",[37,1513,1514],{},"YOUR_FRONT_KEY","に先ほど取得したフロント用のキーを入れます。\n",[37,1517,1518],{},"\u003Cinput type=\"hidden\" name=\"recaptcha\" value=\"\">","にはreCAPTCHAのトークンを挿入してバックエンドに送ります。HTMLフォームであればこの様にしますが、axiosなどの場合はトークンの値をjsを用いて送信するので、このHTMLは要りません。",[299,1521,1522],{"id":1522},"トークンを取得するスクリプトを記述",[13,1524,1525],{},"「送信ボタン」を押した時にreCAPTCHAにAPIを飛ばしてbotかどうかの判定用トークンを取得します。以下の様なスクリプトを記述します。",[60,1527,1529],{"className":1047,"code":1528,"language":1049,"meta":65,"style":65},"\u003C!DOCTYPE html>\n\u003Chtml>\n    \u003Chead>\n      \u003C!---省略-->\n      \u003Cscript src=\"https:\u002F\u002Fwww.google.com\u002Frecaptcha\u002Fapi.js?render=YOUR_FRONT_KEY\">\u003C\u002Fscript>\n    \u003C\u002Fhead>\n\n    \u003Cbody>\n        \u003Cform method=\"post\" id=\"test-form\">\u003C!---追加-->\n            \u003Cinput type=\"text\" name=\"test\" value=\"\">\n            \u003Cinput type=\"hidden\" name=\"recaptcha\" value=\"\">\u003C!---追加-->\n            \u003Cinput type=\"submit\" value=\"送信\">\n        \u003C\u002Fform>\n    \u003C\u002Fbody>\n\n    \u003Cscript>\n        function checkCaptcha(e) {\n            e.preventDefault();\n            grecaptcha.ready(function() {\n                grecaptcha.execute('YOUR_FRONT_KEY', {action: 'submit'}).then(function(token) {\n                    document.getElementById(\"recaptcha\").value=token;\n                    document.getElementById(\"test-form\").submit();\n                });\n            });\n        }\n        document.getElementById(\"test-form\").addEventListener('submit', checkCaptcha);\n        \u003C\u002Fscript>\n\u003C\u002Fhtml>\n",[37,1530,1531,1541,1549,1557,1561,1584,1592,1596,1604,1634,1668,1704,1732,1740,1748,1752,1760,1781,1798,1817,1877,1908,1934,1943,1952,1956,1995,2003],{"__ignoreMap":65},[69,1532,1533,1535,1537,1539],{"class":71,"line":72},[69,1534,1056],{"class":92},[69,1536,1060],{"class":1059},[69,1538,1064],{"class":1063},[69,1540,1067],{"class":92},[69,1542,1543,1545,1547],{"class":71,"line":79},[69,1544,1072],{"class":92},[69,1546,1049],{"class":1059},[69,1548,1067],{"class":92},[69,1550,1551,1553,1555],{"class":71,"line":85},[69,1552,1081],{"class":92},[69,1554,1084],{"class":1059},[69,1556,1067],{"class":92},[69,1558,1559],{"class":71,"line":103},[69,1560,1091],{"class":75},[69,1562,1563,1566,1568,1570,1572,1574,1576,1578,1580,1582],{"class":71,"line":110},[69,1564,1565],{"class":92},"      \u003C",[69,1567,1307],{"class":1059},[69,1569,1310],{"class":1063},[69,1571,1127],{"class":92},[69,1573,1130],{"class":92},[69,1575,1317],{"class":96},[69,1577,1130],{"class":92},[69,1579,1322],{"class":92},[69,1581,1307],{"class":1059},[69,1583,1067],{"class":92},[69,1585,1586,1588,1590],{"class":71,"line":138},[69,1587,1096],{"class":92},[69,1589,1084],{"class":1059},[69,1591,1067],{"class":92},[69,1593,1594],{"class":71,"line":145},[69,1595,142],{"emptyLinePlaceholder":141},[69,1597,1598,1600,1602],{"class":71,"line":151},[69,1599,1081],{"class":92},[69,1601,1111],{"class":1059},[69,1603,1067],{"class":92},[69,1605,1606,1608,1610,1612,1614,1616,1618,1620,1622,1624,1626,1628,1630,1632],{"class":71,"line":157},[69,1607,1118],{"class":92},[69,1609,1121],{"class":1059},[69,1611,1124],{"class":1063},[69,1613,1127],{"class":92},[69,1615,1130],{"class":92},[69,1617,1133],{"class":96},[69,1619,1130],{"class":92},[69,1621,1368],{"class":1063},[69,1623,1127],{"class":92},[69,1625,1130],{"class":92},[69,1627,1375],{"class":96},[69,1629,1130],{"class":92},[69,1631,466],{"class":92},[69,1633,1329],{"class":75},[69,1635,1636,1638,1640,1642,1644,1646,1648,1650,1652,1654,1656,1658,1660,1662,1664,1666],{"class":71,"line":186},[69,1637,1142],{"class":92},[69,1639,1145],{"class":1059},[69,1641,1148],{"class":1063},[69,1643,1127],{"class":92},[69,1645,1130],{"class":92},[69,1647,923],{"class":96},[69,1649,1130],{"class":92},[69,1651,1159],{"class":1063},[69,1653,1127],{"class":92},[69,1655,1130],{"class":92},[69,1657,1166],{"class":96},[69,1659,1130],{"class":92},[69,1661,1171],{"class":1063},[69,1663,1127],{"class":92},[69,1665,1176],{"class":92},[69,1667,1067],{"class":92},[69,1669,1670,1672,1674,1676,1678,1680,1682,1684,1686,1688,1690,1692,1694,1696,1698,1700,1702],{"class":71,"line":192},[69,1671,1142],{"class":92},[69,1673,1145],{"class":1059},[69,1675,1148],{"class":1063},[69,1677,1127],{"class":92},[69,1679,1130],{"class":92},[69,1681,1430],{"class":96},[69,1683,1130],{"class":92},[69,1685,1159],{"class":1063},[69,1687,1127],{"class":92},[69,1689,1130],{"class":92},[69,1691,1441],{"class":96},[69,1693,1130],{"class":92},[69,1695,1171],{"class":1063},[69,1697,1127],{"class":92},[69,1699,1176],{"class":92},[69,1701,466],{"class":92},[69,1703,1329],{"class":75},[69,1705,1706,1708,1710,1712,1714,1716,1718,1720,1722,1724,1726,1728,1730],{"class":71,"line":198},[69,1707,1142],{"class":92},[69,1709,1145],{"class":1059},[69,1711,1148],{"class":1063},[69,1713,1127],{"class":92},[69,1715,1130],{"class":92},[69,1717,1193],{"class":96},[69,1719,1130],{"class":92},[69,1721,1171],{"class":1063},[69,1723,1127],{"class":92},[69,1725,1130],{"class":92},[69,1727,1204],{"class":96},[69,1729,1130],{"class":92},[69,1731,1067],{"class":92},[69,1733,1734,1736,1738],{"class":71,"line":204},[69,1735,1213],{"class":92},[69,1737,1121],{"class":1059},[69,1739,1067],{"class":92},[69,1741,1742,1744,1746],{"class":71,"line":210},[69,1743,1096],{"class":92},[69,1745,1111],{"class":1059},[69,1747,1067],{"class":92},[69,1749,1750],{"class":71,"line":216},[69,1751,142],{"emptyLinePlaceholder":141},[69,1753,1754,1756,1758],{"class":71,"line":4},[69,1755,1081],{"class":92},[69,1757,1307],{"class":1059},[69,1759,1067],{"class":92},[69,1761,1762,1765,1768,1771,1775,1778],{"class":71,"line":227},[69,1763,1764],{"class":1063},"        function",[69,1766,1767],{"class":88}," checkCaptcha",[69,1769,1770],{"class":92},"(",[69,1772,1774],{"class":1773},"s7ZW3","e",[69,1776,1777],{"class":92},")",[69,1779,1780],{"class":92}," {\n",[69,1782,1783,1786,1789,1792,1795],{"class":71,"line":233},[69,1784,1785],{"class":462},"            e",[69,1787,1788],{"class":92},".",[69,1790,1791],{"class":88},"preventDefault",[69,1793,1794],{"class":1059},"()",[69,1796,1797],{"class":92},";\n",[69,1799,1800,1803,1805,1808,1810,1813,1815],{"class":71,"line":239},[69,1801,1802],{"class":462},"            grecaptcha",[69,1804,1788],{"class":92},[69,1806,1807],{"class":88},"ready",[69,1809,1770],{"class":1059},[69,1811,1812],{"class":1063},"function",[69,1814,1794],{"class":92},[69,1816,1780],{"class":92},[69,1818,1819,1822,1824,1827,1829,1832,1834,1836,1839,1842,1845,1848,1850,1852,1854,1857,1859,1861,1864,1866,1868,1870,1873,1875],{"class":71,"line":245},[69,1820,1821],{"class":462},"                grecaptcha",[69,1823,1788],{"class":92},[69,1825,1826],{"class":88},"execute",[69,1828,1770],{"class":1059},[69,1830,1831],{"class":92},"'",[69,1833,1514],{"class":96},[69,1835,1831],{"class":92},[69,1837,1838],{"class":92},",",[69,1840,1841],{"class":92}," {",[69,1843,1844],{"class":1059},"action",[69,1846,1847],{"class":92},":",[69,1849,93],{"class":92},[69,1851,1193],{"class":96},[69,1853,1831],{"class":92},[69,1855,1856],{"class":92},"}",[69,1858,1777],{"class":1059},[69,1860,1788],{"class":92},[69,1862,1863],{"class":88},"then",[69,1865,1770],{"class":1059},[69,1867,1812],{"class":1063},[69,1869,1770],{"class":92},[69,1871,1872],{"class":1773},"token",[69,1874,1777],{"class":92},[69,1876,1780],{"class":92},[69,1878,1879,1882,1884,1887,1889,1891,1893,1895,1897,1899,1902,1904,1906],{"class":71,"line":251},[69,1880,1881],{"class":462},"                    document",[69,1883,1788],{"class":92},[69,1885,1886],{"class":88},"getElementById",[69,1888,1770],{"class":1059},[69,1890,1130],{"class":92},[69,1892,1441],{"class":96},[69,1894,1130],{"class":92},[69,1896,1777],{"class":1059},[69,1898,1788],{"class":92},[69,1900,1901],{"class":462},"value",[69,1903,1127],{"class":92},[69,1905,1872],{"class":462},[69,1907,1797],{"class":92},[69,1909,1910,1912,1914,1916,1918,1920,1922,1924,1926,1928,1930,1932],{"class":71,"line":257},[69,1911,1881],{"class":462},[69,1913,1788],{"class":92},[69,1915,1886],{"class":88},[69,1917,1770],{"class":1059},[69,1919,1130],{"class":92},[69,1921,1375],{"class":96},[69,1923,1130],{"class":92},[69,1925,1777],{"class":1059},[69,1927,1788],{"class":92},[69,1929,1193],{"class":88},[69,1931,1794],{"class":1059},[69,1933,1797],{"class":92},[69,1935,1936,1939,1941],{"class":71,"line":263},[69,1937,1938],{"class":92},"                }",[69,1940,1777],{"class":1059},[69,1942,1797],{"class":92},[69,1944,1945,1948,1950],{"class":71,"line":269},[69,1946,1947],{"class":92},"            }",[69,1949,1777],{"class":1059},[69,1951,1797],{"class":92},[69,1953,1954],{"class":71,"line":275},[69,1955,699],{"class":92},[69,1957,1958,1961,1963,1965,1967,1969,1971,1973,1975,1977,1980,1982,1984,1986,1988,1990,1993],{"class":71,"line":281},[69,1959,1960],{"class":462},"        document",[69,1962,1788],{"class":92},[69,1964,1886],{"class":88},[69,1966,1770],{"class":462},[69,1968,1130],{"class":92},[69,1970,1375],{"class":96},[69,1972,1130],{"class":92},[69,1974,1777],{"class":462},[69,1976,1788],{"class":92},[69,1978,1979],{"class":88},"addEventListener",[69,1981,1770],{"class":462},[69,1983,1831],{"class":92},[69,1985,1193],{"class":96},[69,1987,1831],{"class":92},[69,1989,1838],{"class":92},[69,1991,1992],{"class":462}," checkCaptcha)",[69,1994,1797],{"class":92},[69,1996,1997,1999,2001],{"class":71,"line":649},[69,1998,1213],{"class":92},[69,2000,1307],{"class":1059},[69,2002,1067],{"class":92},[69,2004,2005,2007,2009],{"class":71,"line":655},[69,2006,1230],{"class":92},[69,2008,1049],{"class":1059},[69,2010,1067],{"class":92},[13,2012,2013,2014,2017,2018,2021],{},"フォームの送信ボタンが押された時（submitイベント発火時）に",[37,2015,2016],{},"checkCaptcha","の関数が実行される様に設定します。",[37,2019,2020],{},"e.preventDefault();","を使用してそのままフォームが送信されない様にします。",[13,2023,2024,2025,2028,2029,2032,2033,2036,2037,2039,2040,2043],{},"reCAPTCHAのスクリプトによって",[37,2026,2027],{},"grecaptcha","というオブジェクトが使用できる様になり、その中の",[37,2030,2031],{},"grecaptcha.execute()","にてAPIを実行します。第一引数にフロントのキー、第二引数にアクションを入力します。Promiseなので",[37,2034,2035],{},"then(token)","内のコールバックでトークンを",[37,2038,1518],{},"に突っ込みます。そしてフォームを",[37,2041,2042],{},"submit()","にて送信します。",[13,2045,2046],{},"これでフロントの実装は完了です。フロントでの動きをみてreCAPTCHAはbotかどうかを判断し、この送信を一意なトークンで保存しているのです。トークンはバックエンドでの検証で利用します。",[23,2048,2049],{"id":2049},"バックエンドの実装",[13,2051,2052],{},"それではバックエンドの実装をすすめます。Laravelのコントローラーでの記述を想定しています。バリデーションなどは各自設定してください。",[13,2054,2055],{},"バックエンドで行うことは",[1239,2057,2058,2061,2064],{},[33,2059,2060],{},"フロントからきたトークンをreCAPTCHAのAPIに送信",[33,2062,2063],{},"reCAPTCHAの結果を取得する",[33,2065,2066],{},"結果（スコア）を用いてbotかの判断をする",[13,2068,2069],{},"以上となります。コードは以下の通りです。",[60,2071,2073],{"className":352,"code":2072,"language":354,"meta":65,"style":65},"class Controller extends BaseController\n{\n    use AuthorizesRequests, DispatchesJobs, ValidatesRequests;\n\n    public function checkRecaptcha(Request $request){\n        try {\n            $client = new \\GuzzleHttp\\Client([\n                'headers' => [\n                    'Content-Type' => 'application\u002Fjson',\n                ],\n            ]);\n    \n            $promise = $client->postAsync('https:\u002F\u002Fwww.google.com\u002Frecaptcha\u002Fapi\u002Fsiteverify',\n            [\n                'form_params' =>[\n                    'secret'=>env('RECAPTCHA_SERVER_KEY'),\n                    'response'=>$request->recaptcha\n                ]\n            ]);\n    \n            $res = Promise\\Utils::settle($promise)->wait();\n            $isFulfilled = isset($res[0]['value']);\n            if(!$isFulfilled) throw new \\Exception('RECAPTCHA SERVER returns error');\n    \n            $result = json_decode($res[0]['value']->getBody()->getContents(),true);\n            \n            if(isset($result['error-codes'])){\n                if($result['error-codes'][0] === 'timeout-or-duplicate') return false;\n                throw new \\Exception('RECAPTCHA SERVER returns error:'.$result['error-codes'][0]);\n            }\n\n            return $result['score'] > 0.5 && $result['success'];\n        }catch (\\Exception $e) {\n            report($e);\n            return false;\n        }\n    }\n}\n",[37,2074,2075,2080,2085,2090,2094,2099,2104,2109,2114,2119,2124,2129,2134,2139,2144,2149,2154,2159,2164,2168,2172,2177,2182,2187,2191,2196,2201,2206,2211,2216,2220,2224,2229,2234,2239,2244,2248,2252],{"__ignoreMap":65},[69,2076,2077],{"class":71,"line":72},[69,2078,2079],{},"class Controller extends BaseController\n",[69,2081,2082],{"class":71,"line":79},[69,2083,2084],{},"{\n",[69,2086,2087],{"class":71,"line":85},[69,2088,2089],{},"    use AuthorizesRequests, DispatchesJobs, ValidatesRequests;\n",[69,2091,2092],{"class":71,"line":103},[69,2093,142],{"emptyLinePlaceholder":141},[69,2095,2096],{"class":71,"line":110},[69,2097,2098],{},"    public function checkRecaptcha(Request $request){\n",[69,2100,2101],{"class":71,"line":138},[69,2102,2103],{},"        try {\n",[69,2105,2106],{"class":71,"line":145},[69,2107,2108],{},"            $client = new \\GuzzleHttp\\Client([\n",[69,2110,2111],{"class":71,"line":151},[69,2112,2113],{},"                'headers' => [\n",[69,2115,2116],{"class":71,"line":157},[69,2117,2118],{},"                    'Content-Type' => 'application\u002Fjson',\n",[69,2120,2121],{"class":71,"line":186},[69,2122,2123],{},"                ],\n",[69,2125,2126],{"class":71,"line":192},[69,2127,2128],{},"            ]);\n",[69,2130,2131],{"class":71,"line":198},[69,2132,2133],{},"    \n",[69,2135,2136],{"class":71,"line":204},[69,2137,2138],{},"            $promise = $client->postAsync('https:\u002F\u002Fwww.google.com\u002Frecaptcha\u002Fapi\u002Fsiteverify',\n",[69,2140,2141],{"class":71,"line":210},[69,2142,2143],{},"            [\n",[69,2145,2146],{"class":71,"line":216},[69,2147,2148],{},"                'form_params' =>[\n",[69,2150,2151],{"class":71,"line":4},[69,2152,2153],{},"                    'secret'=>env('RECAPTCHA_SERVER_KEY'),\n",[69,2155,2156],{"class":71,"line":227},[69,2157,2158],{},"                    'response'=>$request->recaptcha\n",[69,2160,2161],{"class":71,"line":233},[69,2162,2163],{},"                ]\n",[69,2165,2166],{"class":71,"line":239},[69,2167,2128],{},[69,2169,2170],{"class":71,"line":245},[69,2171,2133],{},[69,2173,2174],{"class":71,"line":251},[69,2175,2176],{},"            $res = Promise\\Utils::settle($promise)->wait();\n",[69,2178,2179],{"class":71,"line":257},[69,2180,2181],{},"            $isFulfilled = isset($res[0]['value']);\n",[69,2183,2184],{"class":71,"line":263},[69,2185,2186],{},"            if(!$isFulfilled) throw new \\Exception('RECAPTCHA SERVER returns error');\n",[69,2188,2189],{"class":71,"line":269},[69,2190,2133],{},[69,2192,2193],{"class":71,"line":275},[69,2194,2195],{},"            $result = json_decode($res[0]['value']->getBody()->getContents(),true);\n",[69,2197,2198],{"class":71,"line":281},[69,2199,2200],{},"            \n",[69,2202,2203],{"class":71,"line":649},[69,2204,2205],{},"            if(isset($result['error-codes'])){\n",[69,2207,2208],{"class":71,"line":655},[69,2209,2210],{},"                if($result['error-codes'][0] === 'timeout-or-duplicate') return false;\n",[69,2212,2213],{"class":71,"line":661},[69,2214,2215],{},"                throw new \\Exception('RECAPTCHA SERVER returns error:'.$result['error-codes'][0]);\n",[69,2217,2218],{"class":71,"line":667},[69,2219,808],{},[69,2221,2222],{"class":71,"line":673},[69,2223,142],{"emptyLinePlaceholder":141},[69,2225,2226],{"class":71,"line":678},[69,2227,2228],{},"            return $result['score'] > 0.5 && $result['success'];\n",[69,2230,2231],{"class":71,"line":684},[69,2232,2233],{},"        }catch (\\Exception $e) {\n",[69,2235,2236],{"class":71,"line":690},[69,2237,2238],{},"            report($e);\n",[69,2240,2241],{"class":71,"line":696},[69,2242,2243],{},"            return false;\n",[69,2245,2246],{"class":71,"line":702},[69,2247,699],{},[69,2249,2250],{"class":71,"line":707},[69,2251,893],{},[69,2253,2254],{"class":71,"line":713},[69,2255,2256],{},"}\n",[13,2258,2259,2260,2263,2264,2267],{},"recaptchaとのAPI通信には",[37,2261,2262],{},"Guzzle","を使用していますが、とにかくAPI通信ができれば大丈夫です。APIは",[37,2265,2266],{},"https:\u002F\u002Fwww.google.com\u002Frecaptcha\u002Fapi\u002Fsiteverify","にPOSTを送信します。POSTには以下の値が必要です、",[60,2269,2271],{"className":352,"code":2270,"language":354,"meta":65,"style":65},"'form_params' =>[\n    'secret'=>env('RECAPTCHA_SERVER_KEY'),\n    'response'=>$request->recaptcha\n]\n",[37,2272,2273,2278,2283,2288],{"__ignoreMap":65},[69,2274,2275],{"class":71,"line":72},[69,2276,2277],{},"'form_params' =>[\n",[69,2279,2280],{"class":71,"line":79},[69,2281,2282],{},"    'secret'=>env('RECAPTCHA_SERVER_KEY'),\n",[69,2284,2285],{"class":71,"line":85},[69,2286,2287],{},"    'response'=>$request->recaptcha\n",[69,2289,2290],{"class":71,"line":103},[69,2291,2292],{},"]\n",[13,2294,2295,2298,2299,2302,2303,2305,2306,2310],{},[37,2296,2297],{},"env('RECAPTCHA_SERVER_KEY')","はバックエンドで使用するrecaptchaキーです。",[37,2300,2301],{},"$request->recaptcha","は",[37,2304,1518],{},"で挿入されたフロントで取得したrecaptchaのトークンです。このトークンとキーを合わせて、 ",[2307,2308,2309],"strong",{},"保護対象のサーバーであり、検証を行うフォーム送信","　を判別しています。",[13,2312,2313],{},"通信が成功すると以下の様なレスポンスが戻ります。",[60,2315,2317],{"className":352,"code":2316,"language":354,"meta":65,"style":65},"[\n  \"success\"=> true, \n  \"score\"=> 0.8,\n  \"action\"=> string,\n  \"challenge_ts\"=> timestamp,\n  \"hostname\"=> string,\n]\n",[37,2318,2319,2324,2329,2334,2339,2344,2349],{"__ignoreMap":65},[69,2320,2321],{"class":71,"line":72},[69,2322,2323],{},"[\n",[69,2325,2326],{"class":71,"line":79},[69,2327,2328],{},"  \"success\"=> true, \n",[69,2330,2331],{"class":71,"line":85},[69,2332,2333],{},"  \"score\"=> 0.8,\n",[69,2335,2336],{"class":71,"line":103},[69,2337,2338],{},"  \"action\"=> string,\n",[69,2340,2341],{"class":71,"line":110},[69,2342,2343],{},"  \"challenge_ts\"=> timestamp,\n",[69,2345,2346],{"class":71,"line":138},[69,2347,2348],{},"  \"hostname\"=> string,\n",[69,2350,2351],{"class":71,"line":145},[69,2352,2292],{},[13,2354,2355,2356,2359,2360,2363],{},"一番重要なのは",[37,2357,2358],{},"\"score\"=> 0.8","です。このスコアは入力したリクエストがbotか人間かのスコアを示しており、1に近いほど人間が入力しています。逆に0.1あたりはbotの入力です。どこまで厳しくするかはお任せしますが、私は0.5以上であれば人間のリクエストであるとしています。",[37,2361,2362],{},"return $result['score'] > 0.5"," としてfalseであればリクエストを拒否したり、エラーを返す様にします。フォーム系で汎用的に使用できる様に私はサービスプロバイダにしています。",[299,2365,2366],{"id":2366},"エラー処理",[13,2368,2369],{},"エラーの場合は以下の様なレスポンスがきます。（例です）",[60,2371,2373],{"className":352,"code":2372,"language":354,"meta":65,"style":65},"[\n  \"success\"=> false, \n  \"action\"=> string,\n  \"challenge_ts\"=> timestamp,\n  \"hostname\"=> string,\n  \"error-codes\": [\n      0=>'timeout-or-duplicate'\n  ] \n]\n",[37,2374,2375,2379,2384,2388,2392,2396,2401,2406,2411],{"__ignoreMap":65},[69,2376,2377],{"class":71,"line":72},[69,2378,2323],{},[69,2380,2381],{"class":71,"line":79},[69,2382,2383],{},"  \"success\"=> false, \n",[69,2385,2386],{"class":71,"line":85},[69,2387,2338],{},[69,2389,2390],{"class":71,"line":103},[69,2391,2343],{},[69,2393,2394],{"class":71,"line":110},[69,2395,2348],{},[69,2397,2398],{"class":71,"line":138},[69,2399,2400],{},"  \"error-codes\": [\n",[69,2402,2403],{"class":71,"line":145},[69,2404,2405],{},"      0=>'timeout-or-duplicate'\n",[69,2407,2408],{"class":71,"line":151},[69,2409,2410],{},"  ] \n",[69,2412,2413],{"class":71,"line":157},[69,2414,2292],{},[13,2416,2417,2418,2421],{},"このエラーはAPIの通信が失敗したり、必要なパラメーターが不足していたりなどのエラーです。 ",[2307,2419,2420],{},"リクエストがBotである"," という意味ではないので注意。Botかの判定はあくまで成功時に取得するscoreで判定します。",[299,2423,2424],{"id":2424},"エラーの説明",[13,2426,2427,2430,2432],{},[2307,2428,2429],{},"missing-input-secret",[37,2431,2297],{},"のようなサーバー側のrecaptchaのキーを忘れている。",[60,2434,2436],{"className":352,"code":2435,"language":354,"meta":65,"style":65},"'form_params' =>[\n    'secret'=>env('RECAPTCHA_SERVER_KEY'), \u002F\u002F このへん\n    'response'=>$request->recaptcha\n]\n",[37,2437,2438,2442,2450,2454],{"__ignoreMap":65},[69,2439,2440],{"class":71,"line":72},[69,2441,2277],{},[69,2443,2444,2447],{"class":71,"line":79},[69,2445,2446],{},"    'secret'=>env('RECAPTCHA_SERVER_KEY'),",[69,2448,2449],{}," \u002F\u002F このへん\n",[69,2451,2452],{"class":71,"line":85},[69,2453,2287],{},[69,2455,2456],{"class":71,"line":103},[69,2457,2292],{},[13,2459,2460,2463,2465],{},[2307,2461,2462],{},"invalid-input-secret",[37,2464,2297],{},"が不正。間違っているキーを使用している。キーが正しいか、保護対象のドメインとして登録しているかを確認。",[13,2467,2468,2471,2474],{},[2307,2469,2470],{},"missing-input-response",[37,2472,2473],{},"'response'=>$request->recaptcha","を忘れている、空文字。",[13,2476,2477,2480,2482],{},[2307,2478,2479],{},"invalid-input-response",[37,2481,2473],{},"の値が不正。型などを確認。",[13,2484,2485,2488],{},[2307,2486,2487],{},"bad-request","\nPOSTで送っているかを確認。",[13,2490,2491,2494,2496],{},[2307,2492,2493],{},"timeout-or-duplicate",[37,2495,2473],{},"の値を二回送っているか、フロントのトークンが２分以上経過した。",[13,2498,2499],{},"フロントで取得したトークンはバックエンドでのこの検証を行うともう一度利用することができません。またこのトークンは",[60,2501,2505],{"className":2502,"code":2503,"language":2504,"meta":65,"style":65},"language-javascript shiki shiki-themes material-theme-ocean","grecaptcha.execute('YOUR_FRONT_KEY', {action: 'submit'}).then(function(token) {\n    document.getElementById(\"recaptcha\").value=token;\n    document.getElementById(\"test-form\").submit();\n});\n","javascript",[37,2506,2507,2557,2586,2612],{"__ignoreMap":65},[69,2508,2509,2511,2513,2515,2517,2519,2521,2523,2525,2527,2529,2531,2533,2535,2537,2539,2541,2543,2545,2547,2549,2551,2553,2555],{"class":71,"line":72},[69,2510,2027],{"class":462},[69,2512,1788],{"class":92},[69,2514,1826],{"class":88},[69,2516,1770],{"class":462},[69,2518,1831],{"class":92},[69,2520,1514],{"class":96},[69,2522,1831],{"class":92},[69,2524,1838],{"class":92},[69,2526,1841],{"class":92},[69,2528,1844],{"class":1059},[69,2530,1847],{"class":92},[69,2532,93],{"class":92},[69,2534,1193],{"class":96},[69,2536,1831],{"class":92},[69,2538,1856],{"class":92},[69,2540,1777],{"class":462},[69,2542,1788],{"class":92},[69,2544,1863],{"class":88},[69,2546,1770],{"class":462},[69,2548,1812],{"class":1063},[69,2550,1770],{"class":92},[69,2552,1872],{"class":1773},[69,2554,1777],{"class":92},[69,2556,1780],{"class":92},[69,2558,2559,2562,2564,2566,2568,2570,2572,2574,2576,2578,2580,2582,2584],{"class":71,"line":79},[69,2560,2561],{"class":462},"    document",[69,2563,1788],{"class":92},[69,2565,1886],{"class":88},[69,2567,1770],{"class":1059},[69,2569,1130],{"class":92},[69,2571,1441],{"class":96},[69,2573,1130],{"class":92},[69,2575,1777],{"class":1059},[69,2577,1788],{"class":92},[69,2579,1901],{"class":462},[69,2581,1127],{"class":92},[69,2583,1872],{"class":462},[69,2585,1797],{"class":92},[69,2587,2588,2590,2592,2594,2596,2598,2600,2602,2604,2606,2608,2610],{"class":71,"line":85},[69,2589,2561],{"class":462},[69,2591,1788],{"class":92},[69,2593,1886],{"class":88},[69,2595,1770],{"class":1059},[69,2597,1130],{"class":92},[69,2599,1375],{"class":96},[69,2601,1130],{"class":92},[69,2603,1777],{"class":1059},[69,2605,1788],{"class":92},[69,2607,1193],{"class":88},[69,2609,1794],{"class":1059},[69,2611,1797],{"class":92},[69,2613,2614,2616,2618],{"class":71,"line":103},[69,2615,1856],{"class":92},[69,2617,1777],{"class":462},[69,2619,1797],{"class":92},[13,2621,2622],{},"の実行から２分以内で利用する必要があります。そのためページがリロードされた瞬間ときに実行していると、フォーム入力中に時間切れになったります。そのためsubmit時に実行することをお勧めします。",[23,2624,2625],{"id":2625},"実装まとめ",[13,2627,2628],{},"以上がrecaptchaの実装方法です。recaptchaはあくまでBotかどうかの判断のみをしているので、実際にリクエストを通すかはアプリケーション側の仕事です。フロントとバックでの実装が少し面倒ですが、recaptchaの機能を自前で実装しようとするとそこそこ、面倒なのと実績のあるGoogle様に検証してもらうのも結構安心です。",[13,2630,2631],{},"バックエンドはLaravelを想定しますが、他のフレームワークや言語でもやることは特に変わりません。上手くご自身の環境に置き換えてください。",[936,2633,2634],{},"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 .sC9rS, html code.shiki .sC9rS{--shiki-default:#464B5D;--shiki-default-font-style:italic}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 .sdLwU, html code.shiki .sdLwU{--shiki-default:#82AAFF}html pre.shiki code .s7ZW3, html code.shiki .s7ZW3{--shiki-default:#BABED8;--shiki-default-font-style:italic}html pre.shiki code .s0W1g, html code.shiki .s0W1g{--shiki-default:#BABED8}",{"title":65,"searchDepth":85,"depth":85,"links":2636},[2637,2638,2639,2640,2644,2648],{"id":983,"depth":79,"text":984},{"id":990,"depth":79,"text":990},{"id":999,"depth":79,"text":1000},{"id":1041,"depth":79,"text":1041,"children":2641},[2642,2643],{"id":1261,"depth":85,"text":1262},{"id":1522,"depth":85,"text":1522},{"id":2049,"depth":79,"text":2049,"children":2645},[2646,2647],{"id":2366,"depth":85,"text":2366},{"id":2424,"depth":85,"text":2424},{"id":2625,"depth":79,"text":2625},[2650],"devstack","2021-05-21","reCAPTCHAのbot検証をフロントエンド とLaravelでのバックエンドの実装を行います。",{},"\u002Farticles\u002Fimplement-recaptcha",{"title":966,"description":2652},"articles\u002Fimplement-recaptcha",[354,2658,2659,2660],"js","laravel","security","_mix\u002Flaravel-recaptcha.jpg","AYarFTQUZE-KIUOU5A3co7uCUDW9hqT091VEnFYUN6Q",{"id":2664,"title":2665,"body":2666,"category":2796,"createdAt":2797,"description":2665,"extension":955,"index":956,"meta":2798,"navigation":141,"path":2799,"publish":141,"seo":2800,"series":956,"seriesTitle":956,"stem":2801,"tag":2802,"thumbnail":956,"updatedAt":956,"__hash__":2803},"articles\u002Farticles\u002Fphp-unserialize-error-reslove.md","PHPのunserializeがError at offsetエラーが起きた際の対処方法",{"type":10,"value":2667,"toc":2794},[2668,2671,2677,2684,2703,2706,2715,2722,2728,2738,2746,2749,2765,2771,2785,2788,2791],[13,2669,2670],{},"とあるデータベースの内容をCSVに出力して欲しいという簡単な仕事がありました。目的のテーブルをみてみると以下のような記録がありました。",[60,2672,2675],{"className":2673,"code":2674,"language":923},[921],"a:2:{i:0;s:5:“hello”;i:1;s:5:“world”;}　\u002F\u002Fサンプルです\n",[37,2676,2674],{"__ignoreMap":65},[13,2678,2679,2680,2683],{},"「なんじゃこれ」と調べていると、これはシリアライズされたデータでした。PHPのオブジェクトのインスタンス、配列を文字列として保存したい時に",[37,2681,2682],{},"serialize()","を使用し、その時に生成される文字でした。",[2685,2686,2690,2691,2694],"div",{"className":2687},[2688,2689],"alert","alert-success","\nserialize ( mixed $value ) : string\n",[13,2692,2693],{},"値の保存可能な表現を生成します。 型や構造を失わずに PHP の値を保存または渡す際に有用です。 シリアル化された文字列を PHP の値に戻すには、 unserialize() を使用してください。",[13,2695,2696],{},[2697,2698,2699],"small",{},[421,2700,2701],{"href":2701,"rel":2702},"https:\u002F\u002Fwww.php.net\u002Fmanual\u002Fja\u002Ffunction.serialize.php",[425],[13,2704,2705],{},"元に戻す際はunserialize()を用いると元の配列やオブジェクトになります。上の例は",[60,2707,2709],{"className":352,"code":2708,"language":354,"meta":65,"style":65},"array(‘hello’, ‘world’)\n",[37,2710,2711],{"__ignoreMap":65},[69,2712,2713],{"class":71,"line":72},[69,2714,2708],{},[13,2716,2717,2718,2721],{},"となります。今回の記事ではそんな",[37,2719,2720],{},"unserialize()","の時に起きたエラーです。その時に以下のようなエラーが発生しました。",[60,2723,2726],{"className":2724,"code":2725,"language":923},[921],"Fatal error: Uncaught ErrorException: unserialize(): Error at offset XX of XX bytes\n",[37,2727,2725],{"__ignoreMap":65},[13,2729,2730,2731,2733,2734,2737],{},"このエラーの概要としては",[37,2732,2720],{},"する際に",[37,2735,2736],{},"s:5:“world”","などの部分を頼りに値を元に戻すのですが、この部分が文字コードや色々な都合で文字列数が合わない時があります。そうすると上記のようなエラーがおきます。",[13,2739,2740,2743],{},[2307,2741,2742],{},"「unserialize(): error at offset」",[2307,2744,2745],{},"「unserialize(): エラー」",[13,2747,2748],{},"と調べました。似たような症状で悩んでいる人が多いためか、すぐに解決策が出ました。最終的には以下の記事が最善の解決策でした。",[1239,2750,2751,2758],{},[33,2752,2753],{},[421,2754,2757],{"href":2755,"rel":2756},"https:\u002F\u002Fkeruuweb.com\u002Fphp-unserialize-error\u002F",[425],"PHP: unserialize実行時にエラーが発生する場合の対処例",[33,2759,2760],{},[421,2761,2764],{"href":2762,"rel":2763},"https:\u002F\u002Fstackoverflow.com\u002Fquestions\u002F10152904\u002Fhow-to-repair-a-serialized-string-which-has-been-corrupted-by-an-incorrect-byte",[425],"How to repair a serialized string which has been corrupted by an incorrect byte count length?",[60,2766,2769],{"className":2767,"code":2768,"language":923},[921],"$data = 's:4:\"abc\"';\n\n$data = preg_replace_callback('!s:(\\d+):\"([\\s\\S]*?)\";!', function($m) {\n  return 's:' . strlen($m[2]) . ':\"' . $m[2] . '\";';\n}, $data);\n\necho $data; \u002F\u002F => 's:3:\"abc\"'\n\n$text = unserialize($data);\n",[37,2770,2768],{"__ignoreMap":65},[13,2772,2773,2774,2777,2778,2780,2781,2784],{},"上記のコードのように",[37,2775,2776],{},"preg_replace_callback","を用いて",[37,2779,2720],{},"できないデータにある、",[37,2782,2783],{},"'s:4:\"abc\"'","などの文字列数などを正規表現を用いて再計算し、正しい文字列数にすることでエラーが解決できます。",[13,2786,2787],{},"そもそもなぜserializeしたデータの文字列数が間違っているのかなど根本的な原因としては、PHPのバージョンの違いや変換する文字コード、保存するDBでの文字コードなど様々な要因があります。私もローカルのPHPは7.4ですが、データを保存している環境はPHP5やmysql6という古い環境で全く異なっていただったりと、エラーになる要素は満載でした。",[13,2789,2790],{},"とにかく原因とその解決方法である正規表現は正直初見殺しなので、解説記事は非常に助かりました。ありがとうございます。",[936,2792,2793],{},"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":65,"searchDepth":85,"depth":85,"links":2795},[],[952],"2020-12-06",{},"\u002Farticles\u002Fphp-unserialize-error-reslove",{"title":2665,"description":2665},"articles\u002Fphp-unserialize-error-reslove",[354],"xT9EEUnxEQAaXvVrykex81iT_-hbhrt3RUP2eorH-IM",{"id":2805,"title":2806,"body":2807,"category":2886,"createdAt":2887,"description":2806,"extension":955,"index":956,"meta":2888,"navigation":141,"path":2889,"publish":141,"seo":2890,"series":956,"seriesTitle":956,"stem":2891,"tag":2892,"thumbnail":2893,"updatedAt":956,"__hash__":2894},"articles\u002Farticles\u002Fcomposer-require-memory-leak.md","composer require で PHPがメモリ不足になるときの対処法",{"type":10,"value":2808,"toc":2884},[2809,2812,2815,2818,2826,2833,2840,2850,2853,2859,2862,2868,2875,2881],[13,2810,2811],{},"PHPのプロジェクトの時はcomposerを用います。しかしなぜかPHPのメモリエラーで必要なパッケージが全てはいらないという事象に出会いました。困ったと思い以下のワードで調べました。",[13,2813,2814],{},"「composer require メモリ不足」",[13,2816,2817],{},"そのまんまですね笑。とりあえず以下の記事が参考になりました。",[2685,2819,2821],{"className":2820},[2688,2689],[421,2822,2825],{"href":2823,"target":2824},"https:\u002F\u002Fqiita.com\u002Fsetsunachan\u002Fitems\u002F48d56782d4166cef507a","_blank","【Laravel】composer require時のメモリ不足エラーの対処法【Docker】\n",[2685,2827,2829],{"className":2828},[2688,2689],[421,2830,2832],{"href":2831,"target":2824},"https:\u002F\u002Fuiuifree.com\u002Fblog\u002Fdevelop\u002Fphp-composer-install-memory-limit\u002F","composer installでメモリ不足を解消するコマンド",[13,2834,2835,2836,2839],{},"方法は簡単。phpコマンドで ",[37,2837,2838],{},"php -d memory_limit=-1"," のオプションを指定してメモリ使用を無制限にします。そして",[13,2841,2842,2845,2846,2849],{},[37,2843,2844],{},"php -d memory_limit=-1 \u002Fusr\u002Fbin\u002Fcomposer require {Library_name} ","とします。phpコマンドで composerコマンドを使用せず直接 ",[37,2847,2848],{},"\u002Fusr\u002Fbin\u002Fcomposer"," とcomposerのパスを指定します。",[13,2851,2852],{},"もしも",[60,2854,2857],{"className":2855,"code":2856,"language":923},[921],"Could not open input file: \u002Fusr\u002Fbin\u002Fcomposer\n",[37,2858,2856],{"__ignoreMap":65},[13,2860,2861],{},"と指定したパスにcomposerがいない時、忘れてしまった時は",[60,2863,2866],{"className":2864,"code":2865,"language":923},[921],"$ which composer\n\u002Fusr\u002Flocal\u002Fbin\u002Fcomposer\n",[37,2867,2865],{"__ignoreMap":65},[13,2869,2870,2871,2874],{},"which コマンドでパスを明らかにしましょう。私の場合は ",[37,2872,2873],{},"\u002Fusr\u002Flocal\u002Fbin\u002Fcomposer"," にいました。なので上記のコードは",[60,2876,2879],{"className":2877,"code":2878,"language":923},[921],"php -d memory_limit=-1 \u002Fusr\u002Flocal\u002Fbin\u002Fcomposer require {Library_name}\n",[37,2880,2878],{"__ignoreMap":65},[13,2882,2883],{},"となります。これで無事にインストールできました。",{"title":65,"searchDepth":85,"depth":85,"links":2885},[],[952],"2020-12-05",{},"\u002Farticles\u002Fcomposer-require-memory-leak",{"title":2806,"description":2806},"articles\u002Fcomposer-require-memory-leak",[354],"_mix\u002Flogo-composer-transparent.png","Aa4jAWQKQAfZs5LUIbWdJrVBOI7x9mPFFgm1NNr4OAg",{"id":2896,"title":2897,"body":2898,"category":3323,"createdAt":3324,"description":2897,"extension":955,"index":956,"meta":3325,"navigation":141,"path":3326,"publish":141,"seo":3327,"series":956,"seriesTitle":956,"stem":3328,"tag":3329,"thumbnail":3330,"updatedAt":956,"__hash__":3331},"articles\u002Farticles\u002Fwordpress-cli-with-cron.md","wordpress CLIとcronを用いてプラグインの関数を定期自動実行させる",{"type":10,"value":2899,"toc":3313},[2900,2908,2915,2918,2925,2929,2937,2941,2947,2950,2956,2963,2967,2974,2980,2987,2993,2996,3000,3003,3009,3012,3015,3018,3021,3025,3035,3048,3128,3131,3137,3140,3212,3215,3218,3221,3224,3230,3236,3247,3253,3267,3270,3284,3287,3295,3302,3308,3311],[13,2901,2902,2903,2907],{},"こんにちはjunです。",[421,2904,2906],{"href":2905},"\u002Farticles\u002Fmuch-post-migration-wp","「別CMSで作成された4万件分の大量投稿をwordpressに引越しする」","の記事であったように４万件のデータをマルチサイト構成で移行したことがありました。そこで新しい課題として、親サイトでは登録した他のサイト全ての投稿をページングで一覧で表示するという仕様にぶち当たりました。",[13,2909,2910,2911,2914],{},"基本的にwordpressは",[37,2912,2913],{},"get_posts()","を使えば簡単にページングも兼ね備えた一覧ページを作れるのですが、マルチサイト構成の場合は上手くいかずDBから上手く引っ張る必要がありました。しかしマルチサイトだけでも75サイトあり、そこから４万件分の投稿をとる必要があります。表示のたびにそんな高コストの処理はできないので、予め必要なデータをまとめたキャッシュテーブルを作ることで解決しました。",[13,2916,2917],{},"ただしキャッシュテーブルの弱点は更新が必要なことです。手動の更新は実装できましたが、1時間に1回自動で実行するなどの実装が必要となります。すぐに思いついた方法としてはcronを用いてキャッシュ作成の関数を叩くことです。",[13,2919,2920,2921,2924],{},"wordpressにcronやCLIでできないかな〜？と調べていたら公式で",[2307,2922,2923],{},"wp CLI","というものが使えると聞き、見様見真似で実装ができました。wordpressで重い処理の定期実行などを考えている方はぜひ参考にしてみてください。",[23,2926,2928],{"id":2927},"wp-cli-をインストール","wp cli をインストール",[13,2930,2931,2936],{},[421,2932,2935],{"href":2933,"rel":2934},"https:\u002F\u002Fmake.wordpress.org\u002Fcli\u002Fhandbook\u002Fguides\u002Finstalling\u002F",[425],"公式サイト","にあるようにcliを使用するにはコマンドラインを通じてインストールする必要があります。",[299,2938,2940],{"id":2939},"curlなどで-pharファイルを読み込む","curlなどで pharファイルを読み込む",[60,2942,2945],{"className":2943,"code":2944,"language":923},[921],"curl -O https:\u002F\u002Fraw.githubusercontent.com\u002Fwp-cli\u002Fbuilds\u002Fgh-pages\u002Fphar\u002Fwp-cli.phar\n",[37,2946,2944],{"__ignoreMap":65},[13,2948,2949],{},"ドキュメントルート あたりで行ってみましょう。wp-cli.pharというファイルが出現します。これだけでも",[60,2951,2954],{"className":2952,"code":2953,"language":923},[921],"php wp-cli.phar --info\n",[37,2955,2953],{"__ignoreMap":65},[13,2957,2958,2959,2962],{},"という風にして実行することができます。",[37,2960,2961],{},"wp ~~"," と実行するためにはもう一工夫必要です。",[299,2964,2966],{"id":2965},"実行権限をあたえ移動","実行権限をあたえ、移動",[13,2968,2969,2970,2973],{},"以下のようにして実行権限を付与し、",[37,2971,2972],{},"wp","というコマンドで打てるようにします。",[60,2975,2978],{"className":2976,"code":2977,"language":923},[921],"chmod +x wp-cli.phar\nsudo mv wp-cli.phar \u002Fusr\u002Flocal\u002Fbin\u002Fwp\n",[37,2979,2977],{"__ignoreMap":65},[13,2981,2982,2983,2986],{},"はい。これでOKです。",[37,2984,2985],{},"wp --info","を打ってみると",[60,2988,2991],{"className":2989,"code":2990,"language":923},[921],"$ wp --info\nOS:     Linux 4.19.76-linuxkit #1 SMP Tue May 26 11:42:35 UTC 2020 x86_64\nShell:  \u002Fbin\u002Fsh\nPHP binary:     \u002Fusr\u002Flocal\u002Fbin\u002Fphp\nPHP version:    7.4.12\nphp.ini used:\nWP-CLI root dir:        phar:\u002F\u002Fwp-cli.phar\u002Fvendor\u002Fwp-cli\u002Fwp-cli\nWP-CLI vendor dir:      phar:\u002F\u002Fwp-cli.phar\u002Fvendor\nWP_CLI phar path:       \u002Fvar\u002Fwww\u002Fhtml\nWP-CLI packages dir:\nWP-CLI global config:\nWP-CLI project config:\nWP-CLI version: 2.4.0\n",[37,2992,2990],{"__ignoreMap":65},[13,2994,2995],{},"こんな感じに情報が出てくればきちんと機能しています。",[413,2997,2999],{"id":2998},"tips-rootでやると怒られる","TIPS rootでやると怒られる",[13,3001,3002],{},"rootユーザーでコマンドを実行しようとすると以下のように怒られます。",[60,3004,3007],{"className":3005,"code":3006,"language":923},[921],"Error: YIKES! It looks like you're running this as root. You probably meant to run this as the user that your WordPress installation exists under.\n\nIf you REALLY mean to run this as root, we won't stop you, but just bear in mind that any code on this site will then have full control of your server, making it quite DANGEROUS.\n\nIf you'd like to continue as root, please run this again, adding this flag:  --allow-root\n\nIf you'd like to run it as the user that this site is under, you can run the following to become the respective user:\n\n    sudo -u USER -i -- wp \u003Ccommand>\n",[37,3008,3006],{"__ignoreMap":65},[13,3010,3011],{},"ざっくり和訳すると",[13,3013,3014],{},"「ええっ！wpコマンドをrootユーザーで実行しようとしてない！？rootユーザーはサーバーもいろいろ弄れるから危ないよ。まあ、あなたがどうしてもrootユーザーで実行したいなら止めないけど。その時は –allow-root オプションを添えてね」",[13,3016,3017],{},"こんな感じです。rootユーザーはサーバーの最高権限を持っているので使うのは危ないです。さらにwpコマンドを通じてwordpressの内容を容易に破壊することも可能なので、rootでやるのはおすすめしないと警告されます。",[13,3019,3020],{},"回避する場合は警告の通り –allow-root オプションを添えるか、ユーザーを変更して実行します。実務サーバーではアプリケーションをrootで運用することはないので特に問題ないとは思います。",[23,3022,3024],{"id":3023},"自作プラグインの関数にフックさせる","（自作）プラグインの関数にフックさせる",[13,3026,3027,3028,3030,3031,3034],{},"それではCLIがインストールされた所でプラグインの関数にフックさせてみましょう。方法は沢山ありますが簡単なのは自作のコマンドを追加してしますことです。CLIをインストールして、",[37,3029,2972],{},"コマンドを実行すると",[37,3032,3033],{},"WP_CLI","クラスが実行時に使用可能になります。",[13,3036,3037,3038,3043,3044,3047],{},"WP_CLIクラスには",[421,3039,3042],{"href":3040,"rel":3041},"https:\u002F\u002Fmake.wordpress.org\u002Fcli\u002Fhandbook\u002Freferences\u002Finternal-api\u002F",[425],"こちらのリファランス","のように静的メソッドがいくつか用意されています。その中に",[37,3045,3046],{},"WP_CLI::add_command()","をコマンドを追加できるメソッドがあります。第一引数にコマンド名、第二引数にコールバック関数名または関数を指定します。",[60,3049,3052],{"className":352,"code":3050,"filename":3051,"language":354,"meta":65,"style":65},"\u003C?php\n\u002F*\nPlugin Name: test\n*\u002F\n\nif(!defined('ABSPATH')) {\n    die('You are not allowed to call this page directly.');\n}\n\nfunction cmm_test(){\n    echo 'test!';\n}\n\nif ( class_exists( 'WP_CLI' ) ) {\n    WP_CLI::add_command( 'foo', 'cmm_test' );\n}\n","plugins\u002Ftest\u002Ftest.php",[37,3053,3054,3059,3064,3069,3074,3078,3083,3088,3092,3096,3101,3106,3110,3114,3119,3124],{"__ignoreMap":65},[69,3055,3056],{"class":71,"line":72},[69,3057,3058],{},"\u003C?php\n",[69,3060,3061],{"class":71,"line":79},[69,3062,3063],{},"\u002F*\n",[69,3065,3066],{"class":71,"line":85},[69,3067,3068],{},"Plugin Name: test\n",[69,3070,3071],{"class":71,"line":103},[69,3072,3073],{},"*\u002F\n",[69,3075,3076],{"class":71,"line":110},[69,3077,142],{"emptyLinePlaceholder":141},[69,3079,3080],{"class":71,"line":138},[69,3081,3082],{},"if(!defined('ABSPATH')) {\n",[69,3084,3085],{"class":71,"line":145},[69,3086,3087],{},"    die('You are not allowed to call this page directly.');\n",[69,3089,3090],{"class":71,"line":151},[69,3091,2256],{},[69,3093,3094],{"class":71,"line":157},[69,3095,142],{"emptyLinePlaceholder":141},[69,3097,3098],{"class":71,"line":186},[69,3099,3100],{},"function cmm_test(){\n",[69,3102,3103],{"class":71,"line":192},[69,3104,3105],{},"    echo 'test!';\n",[69,3107,3108],{"class":71,"line":198},[69,3109,2256],{},[69,3111,3112],{"class":71,"line":204},[69,3113,142],{"emptyLinePlaceholder":141},[69,3115,3116],{"class":71,"line":210},[69,3117,3118],{},"if ( class_exists( 'WP_CLI' ) ) {\n",[69,3120,3121],{"class":71,"line":216},[69,3122,3123],{},"    WP_CLI::add_command( 'foo', 'cmm_test' );\n",[69,3125,3126],{"class":71,"line":4},[69,3127,2256],{},[13,3129,3130],{},"例えば上記のようにした場合は以下のように実行されます。",[60,3132,3135],{"className":3133,"code":3134,"language":923},[921],"$ wp foo\ntest!\n",[37,3136,3134],{"__ignoreMap":65},[13,3138,3139],{},"なので同じようにして関数を定義して、フックさせます。",[60,3141,3143],{"className":352,"code":3142,"language":354,"meta":65,"style":65},"\u003C?php\n\u002F*\nPlugin Name: test\n*\u002F\n\nif(!defined('ABSPATH')) {\n    die('You are not allowed to call this page directly.');\n}\n\nfunction create_cache_table(){\n    .... \u002F\u002F めちゃくちゃ時間のかかる処理など\n}\n\nif ( class_exists( 'WP_CLI' ) ) {\n    WP_CLI::add_command( 'create_cache', 'create_cache_table' );\n}\n",[37,3144,3145,3149,3153,3157,3161,3165,3169,3173,3177,3181,3186,3191,3195,3199,3203,3208],{"__ignoreMap":65},[69,3146,3147],{"class":71,"line":72},[69,3148,3058],{},[69,3150,3151],{"class":71,"line":79},[69,3152,3063],{},[69,3154,3155],{"class":71,"line":85},[69,3156,3068],{},[69,3158,3159],{"class":71,"line":103},[69,3160,3073],{},[69,3162,3163],{"class":71,"line":110},[69,3164,142],{"emptyLinePlaceholder":141},[69,3166,3167],{"class":71,"line":138},[69,3168,3082],{},[69,3170,3171],{"class":71,"line":145},[69,3172,3087],{},[69,3174,3175],{"class":71,"line":151},[69,3176,2256],{},[69,3178,3179],{"class":71,"line":157},[69,3180,142],{"emptyLinePlaceholder":141},[69,3182,3183],{"class":71,"line":186},[69,3184,3185],{},"function create_cache_table(){\n",[69,3187,3188],{"class":71,"line":192},[69,3189,3190],{},"    .... \u002F\u002F めちゃくちゃ時間のかかる処理など\n",[69,3192,3193],{"class":71,"line":198},[69,3194,2256],{},[69,3196,3197],{"class":71,"line":204},[69,3198,142],{"emptyLinePlaceholder":141},[69,3200,3201],{"class":71,"line":210},[69,3202,3118],{},[69,3204,3205],{"class":71,"line":216},[69,3206,3207],{},"    WP_CLI::add_command( 'create_cache', 'create_cache_table' );\n",[69,3209,3210],{"class":71,"line":4},[69,3211,2256],{},[13,3213,3214],{},"ちなみにif ( class_exists( 'WP_CLI' ) ) がないとUndefined type 'WP_CLI'というエラーが発生して怒られます。ドキュメントルート 配下のwordpressフォルダにはWP_CLIクラスのソースがなく、コマンドを実行（wp-cli.pharを実行）するときにクラスが提供されるそうです。一応これが標準らしい。",[13,3216,3217],{},"コマンドが追加できたのであとはcronを設定してwpコマンドを設定してあげます。",[23,3219,3220],{"id":3220},"cronを設定",[13,3222,3223],{},"rootでないユーザーで操作しているものとします。",[60,3225,3228],{"className":3226,"code":3227,"language":923},[921],"$ crontab -e\n",[37,3229,3227],{"__ignoreMap":65},[60,3231,3234],{"className":3232,"code":3233,"language":923},[921],"PATH = \u002Fusr\u002Flocal\u002Fbin:\u002Fusr\u002Flocal\u002Fsbin:\u002Fusr\u002Flocal\u002Fbin:\u002Fusr\u002Fsbin:\u002Fusr\u002Fbin:\u002Fsbin:\u002Fbin;\n0 * * * * cd \u002Fvar\u002Fwww\u002Fhtml && wp create_cache >> \u002Fdev\u002Fnull 2>&1\n",[37,3235,3233],{"__ignoreMap":65},[13,3237,3238,3239,3242,3243,3246],{},"このcronは1時間ごと（00分）に実行されます。",[37,3240,3241],{},"cd \u002Fvar\u002Fwww\u002Fhtm","l でドキュメントルート へ移動して先ほどのコマンドを打って、思い処理の関数を実行しています。ちなみに上の ",[37,3244,3245],{},"PATH = ...."," がないと",[60,3248,3251],{"className":3249,"code":3250,"language":923},[921],"\u002Fusr\u002Fbin\u002Fenv: 'php': No such file or directory\n",[37,3252,3250],{"__ignoreMap":65},[13,3254,3255,3256,1838,3259,3262,3263,3266],{},"というようにエラーが出てしまい、指定のcron処理が行われません。これ結構ハマったのですが、cron実行時のPATHの設定が",[37,3257,3258],{},"\u002Fbin",[37,3260,3261],{},"\u002Fusr\u002Fbin","だけしか通っていないので、PHPやwpが存在する ",[37,3264,3265],{},"\u002Fusr\u002Flocal\u002Fbin","配下を見ておらずPHPが実行できないのが原因です。",[13,3268,3269],{},"なので予め普段使用しているPATHを出しておいて、crontab上で再定義しています。上記のようにすればあとは指定の時間が来れば",[30,3271,3272,3275,3278,3281],{},[33,3273,3274],{},"cronでコマンドを実行",[33,3276,3277],{},"コマンドに結びついた関数を呼び出す",[33,3279,3280],{},"処理が実行",[33,3282,3283],{},"ログがあればそれに吐き出す",[13,3285,3286],{},"と言った自動運用が可能になります。",[2685,3288,3291,3292],{"className":3289},[2688,3290],"alert-info","\n「>> \u002Fdev\u002Fnull 2>&1」 これは「エラー出力を標準出力とし、\u002Fdev\u002Fnullに書き出す」という意味です。\n",[13,3293,3294],{},"「？」となった方にもう簡単説明します。>> で実行したコマンドの結果などを出力できます。エラーであればエラー文が得られます。そして \u002Fdev\u002Fnull はunixのスペシャルファイルで、空のファイルです。空のファイルを指定して結果を捨てることができます。「無に書き出す」みたいなことをしています。2>&1 はエラー出力を標準出力にします。という意味です。",[13,3296,3297,3298,3301],{},"つまり",[37,3299,3300],{},">> \u002Fdev\u002Fnull 2>&1","はエラー出力であっても結果内容を残さず捨てるという意味です。テストぐらいであれば問題ないのですが、実務でやると何か起きたときに原因が分からないので以下のようにするといいです。",[60,3303,3306],{"className":3304,"code":3305,"language":923},[921],"PATH = \u002Fusr\u002Flocal\u002Fbin:\u002Fusr\u002Flocal\u002Fsbin:\u002Fusr\u002Flocal\u002Fbin:\u002Fusr\u002Fsbin:\u002Fusr\u002Fbin:\u002Fsbin:\u002Fbin;\n0 * * * * cd \u002Fvar\u002Fwww\u002Fhtml && wp create_cache >> \u002Fvar\u002Flog\u002Fcron.log 2>&1\n",[37,3307,3305],{"__ignoreMap":65},[13,3309,3310],{},"こうするときちんとログファイルにエラーが残ります。",[936,3312,2793],{},{"title":65,"searchDepth":85,"depth":85,"links":3314},[3315,3321,3322],{"id":2927,"depth":79,"text":2928,"children":3316},[3317,3318],{"id":2939,"depth":85,"text":2940},{"id":2965,"depth":85,"text":2966,"children":3319},[3320],{"id":2998,"depth":103,"text":2999},{"id":3023,"depth":79,"text":3024},{"id":3220,"depth":79,"text":3220},[2650],"2020-11-26",{},"\u002Farticles\u002Fwordpress-cli-with-cron",{"title":2897,"description":2897},"articles\u002Fwordpress-cli-with-cron",[354,962],"_mix\u002FWordPress-logotype-wmark-e1606319357320.png","1F5rQcoO_KWxxu1bCXWSeo1uX58qewfjTBZ8Az0AndI",{"id":3333,"title":3334,"body":3335,"category":4178,"createdAt":4179,"description":3334,"extension":955,"index":956,"meta":4180,"navigation":141,"path":4181,"publish":141,"seo":4182,"series":956,"seriesTitle":956,"stem":4183,"tag":4184,"thumbnail":4186,"updatedAt":956,"__hash__":4187},"articles\u002Farticles\u002Flalavel7-deplay-on-heroku.md","HerokuにLaravel 7.0で作成したアプリケーションをデプロイする。",{"type":10,"value":3336,"toc":4147},[3337,3340,3347,3351,3354,3365,3372,3376,3379,3393,3396,3399,3403,3409,3412,3424,3427,3430,3434,3437,3440,3447,3450,3482,3486,3490,3500,3503,3506,3513,3516,3520,3527,3530,3534,3540,3543,3546,3549,3552,3555,3562,3565,3568,3572,3575,3578,3585,3588,3593,3596,3600,3606,3609,3612,3615,3618,3626,3631,3634,3637,3640,3643,3647,3658,3662,3665,3669,3672,3839,3842,3856,3859,3867,3870,3878,3881,3888,3896,3899,3903,3909,3912,3915,3919,3922,3925,3928,3933,3936,3939,3943,3946,3952,3959,3962,3965,3969,3977,3980,3983,3989,3992,3998,4004,4011,4017,4020,4026,4029,4033,4039,4042,4059,4062,4066,4069,4083,4086,4090,4093,4096,4100,4103,4109,4112,4115,4118,4121,4124,4131,4134,4137,4141,4144],[13,3338,3339],{},"こんにちはじゅんです。会社で試験的に作成したLaravelアプリケーションをHerokuにデプロイする機会があり、いくらかハマりポイントなどがあったのでメモがてら残そうと思います。どんなLaravel アプリなのかなど、ある程度具体的な要件の定義から書くので、",[13,3341,3342,3343,3346],{},"「さっさとデプロイの説明しろや！」という人は目次から ",[2307,3344,3345],{},"「Herokuへのデプロイ作業を開始」"," をクリックして飛んでください。",[23,3348,3350],{"id":3349},"デプロイの前に作成したlaravel-アプリについて","デプロイの前に、作成したLaravel アプリについて",[13,3352,3353],{},"会社でRestfulなWebAPIとそのコンテンツを提供するサービスを開発しようという動きがあり、Laravelやvue、webAPIは少しわかる私に試験的な開発をまかされLaravelで作りました。",[13,3355,3356,3357,3360,3361,3364],{},"最近、巷で有名な ",[2307,3358,3359],{},"「Headless CMS」と似たような動きをします。"," 細かい要件は書くことはできないですが、 ",[2307,3362,3363],{},"LaravelでAPIサーバー兼コンテンツ管理画面を作成して置きます。"," お客さんはアプリケーションにログインしてコンテンツの編集などを行えます。そしてフロントエンドは別のサーバーに配置するか、既存のアプリを使用します。",[13,3366,3367,3368,3371],{},"Ajaxなどを通して認証付きAPIをLaravelが置いてあるサーバーに投げれば、 ",[2307,3369,3370],{},"データ（掲載コンテンツの内容）をJSON・XMLで手に入れることができます。"," 欲しい情報は適宜、非同期でAPIで呼び出して取得するという形式を取っています。モダンなやり方ですね。",[299,3373,3375],{"id":3374},"laravel-でapiサーバーを構築した理由","Laravel でAPIサーバーを構築した理由",[13,3377,3378],{},"APIサーバーを構築するフレームワークは他にもありますが以下のような理由により、Laravelを選びました。",[1239,3380,3381,3384,3387,3390],{},[33,3382,3383],{},"私が扱いやすい。学習しやすい。",[33,3385,3386],{},"Laravel Passport でOAuth2.0 に準拠したAPI認証機能をさくっと構築可能",[33,3388,3389],{},"Laravel Mixのおかげで管理画面（お客さんが触るGUI）の作成がとても簡単。",[33,3391,3392],{},"LaravelにはAPIに関する認証や制限を設置できるので、",[13,3394,3395],{},"決められたお客さんのコンテンツ（サーバー）からのAPIアクセスを許可する。\nお試しユーザー、プレミアムユーザーと言ったユーザーロールごとに使用できるAPIの制限が簡単。\nと言ったことも可能です。「やっぱりLaravelはフルスタックフレームワークだけあるな」と思えるほどの物です。とにかく上記のようなLaravel アプリを今回はHerokuにデプロイします。",[13,3397,3398],{},"作成したLaravelアプリについての説明・構築は今回の記事ではしません。Herokuへのデプロイがメインになります。",[299,3400,3402],{"id":3401},"なぜheroku","なぜHeroku??",[13,3404,3405,3406],{},"今回のプロジェクト自体がまだ試験的であることも理由の一つですが、単にVPSとかで構築するのが面倒だったからです。 ",[2307,3407,3408],{},"各種ミドルウェア やソフトが最新版の物であっても、マニュアルがあってもセキュリティや権限の設定とかを考えて構築すると日が暮れます。",[13,3410,3411],{},"Herokuの理念が",[2685,3413,3415,3416],{"className":3414},[2688,2689],"\nHeroku はアプリの構築、提供、監視、スケールに役立つクラウドプラットフォームで、アイデアを出してから運用を開始するまでのプロセスを迅速に進めることが可能です。また、インフラストラクチャの管理の問題からも解放されます。\n",[2697,3417,3418],{},[3419,3420,3421],"i",{},[421,3422,3423],{"href":3423,"target":2824},"https:\u002F\u002Fdeveloper.mozilla.org\u002Fja\u002Fdocs\u002FWeb\u002FCSS\u002Ffont-feature-settings",[13,3425,3426],{},"とあるようにインフラの構築や管理は面倒です。そのため予めインフラがすぐに作られ、しかしある程度カスタマイズが可能なサービスを利用すると開発者としてはアプリ（今回でいうLaravelアプリ）の開発に注力できます。",[13,3428,3429],{},"またVPSでも可能だと思いますが、GitHubとかと連携して本番環境・開発環境を構築できるのも魅力の一つです。まだ試験的な段階なので、AWSやVPSほどの自由度は今回のアプリではいらないかな？と思いHerokuにしました。",[23,3431,3433],{"id":3432},"herokuで構築した開発運用フロー","Herokuで構築した開発・運用フロー",[13,3435,3436],{},"下図のような感じです。バージョン管理などはGitHubを用いています。適宜、リモートリポジトリにpushしたらその変更が自動でHerokuのアプリ（個々のサーバーみたいなものだと思ってください。）に反映されます。",[1011,3438],{":src":3439,":width":1014},"'_mix\u002Fheroku_archtecture-768x446.jpg'",[13,3441,3442,3443,3446],{},"プルリクエストを送ると、 ",[2307,3444,3445],{},"そのリクエストに応じたアプリが自動的に構築されるように設定もできます。"," Stagingで最終確認をしてOKであれば公開環境にデプロイさせます。",[13,3448,3449],{},"そのため今回の記事では以下のような流れで構築してます。",[30,3451,3452,3455,3458,3461,3464,3467,3470,3473,3476,3479],{},[33,3453,3454],{},"Herokuのアカウント作成、アプリの作成",[33,3456,3457],{},"Herokuパイプラインの作成",[33,3459,3460],{},"Herokuアプリの設定",[33,3462,3463],{},"HerokuとGithubの連携",[33,3465,3466],{},"DBとの連携",[33,3468,3469],{},"環境変数の設定",[33,3471,3472],{},"Laravelアプリでのmigrationなど",[33,3474,3475],{},"本番環境との連携",[33,3477,3478],{},"SSL化する",[33,3480,3481],{},"API呼び出し確認",[23,3483,3485],{"id":3484},"herokuへのデプロイ作業を開始","Herokuへのデプロイ作業を開始",[299,3487,3489],{"id":3488},"herokuのアカウントを作成","Herokuのアカウントを作成",[13,3491,3492,3493,3496,3499],{},"当たり前ですがHerokuでのアカウントを作成します。これがないと何も始まりません。 ",[421,3494],{"href":3495,"target":2824},"https:\u002F\u002Fjp.heroku.com\u002F",[421,3497,3495],{"href":3495,"rel":3498},[425],"にアクセスして「新規登録」に進みます。",[1011,3501],{":src":3502,":width":1014},"'_mix\u002Fheroku-1.png'",[13,3504,3505],{},"名前やアドレスなど適宜入力します。アカウントを作成する段階では日本語でも大丈夫です。選択するプランですがとりあえず無料の物にしておきましょう。",[13,3507,3508,3512],{},[421,3509,3510],{"href":3510,"rel":3511},"https:\u002F\u002Fjp.heroku.com\u002Fpricing",[425]," にてプランの内容や価格を知ることができます。完全フリーの場合はSSL(httpsにするやつ)の設定が行えない、最低限のスペックしかないなど本当に試験的にサーバー上にアップする程度に使用します。",[13,3514,3515],{},"商用で運用する場合は有料のStandardなどにアップグレードしましょう。ここでは開発サーバーをフリー版、本番環境をhobbyにしておきます。",[299,3517,3519],{"id":3518},"herokuアプリを作成する","Herokuアプリを作成する。",[13,3521,3522,3523,3526],{},"今回デプロイするLaravelのソース達を入れる、Herokuアプリを作成します。 ",[2307,3524,3525],{},"Herokuアプリというのは簡単にいうとサーバーそのものだと思ってください。"," Herokuアプリに、使用するソフトやドキュメントルートの設定、環境変数の設定、連携するGithubのレポジトリなどの設定を行うことで、Laravelが正常に動いてアプリケーションサーバー（サービスを提供するサーバー）として機能するようになります。",[13,3528,3529],{},"ここでは実務の開発体制をとるのでpipelineを構築します。",[413,3531,3533],{"id":3532},"pipe-linesを構築する","pipe linesを構築する",[13,3535,3536,3539],{},[2307,3537,3538],{},"pipe linesというのは「開発、レビュー、ステージング、本番」など複数の段階に分けてアプリを動かすことができるHerokuアプリのグループのことをいいます。"," もう少し簡単にいうと、",[13,3541,3542],{},"修正した物開発中のものをテストする「開発サーバー」、お客さんが触り実際にサービスが提供される「本番サーバー」を同時に構築して管理しやすくしたアプリ達です。今回は開発用・確認用、本番の２つの構成にします。",[13,3544,3545],{},"アカウントを作ると個人のダッシュボード画面に移動します。そこの「New」をクリックし、[Create New pipeline」を選択します。",[1011,3547],{":src":3548,":width":1014},"'_mix\u002Fheroku_createnew-1024x306.png'",[13,3550,3551],{},"pipelineを作成すると、pipelineの名前と連携するリポジトリを選ぶことができます。ここではまだ連携しないので、名前だけ入力して「create pipeline」で作成。すると二つのアプリを加えることができるpipelineが作成されました。",[1011,3553],{":src":3554,":width":1014},"'_mix\u002Fdashnoard-1024x262.png'",[13,3556,3557,3558,3561],{},"そしてたらアプリの構築をしていきます。二つ作るのは少し面倒ですが仕方ないです。では ",[2307,3559,3560],{},"開発の方（staging）"," から作成していきます。「Add app」をクリックすると、既存のアプリを追加するか、新しく作るかを聞いてきます。ここでは「create new app」を選択。すると右側にアプリの基本情報を入力する欄が出てきます。",[13,3563,3564],{},"そしたらアプリの名前とリージョンを選択して「create app」します。フリープランの場合はアメリカかヨーロッパリージョンしか選べませんが、特に問題ないです。（その地域に隕石が落るとかしてサーバーが物理的にやられない限り）",[13,3566,3567],{},"アプリ名を英語で入力して「create」をクリックして作成されます。名前はとりあえず「staging-app-laravel」としておきます。",[299,3569,3571],{"id":3570},"build-packを設定する","Build packを設定する",[13,3573,3574],{},"開発用アプリの設定が可能になったのでダッシュボードから、アプリ名をクリックして移動します。すると下のようなアプリに関する設定ができる画面に移ります。設定画面でできることは後で使用するheroku CLIでも行うことができます。今回はGUIの方をつかっていきます。",[1011,3576],{":src":3577,":width":1014},"'_mix\u002Fapp_setting_dev-1024x497.png'",[13,3579,3580,3581,3584],{},"まずは ",[2307,3582,3583],{},"一番最初にbuild packというものを設定"," します。build packではnode.js,PHPなどを選択します。まあ使うサーバーサイドの言語を選ぶ物だと思ってください。上記の画面から「Settings」を選択します。",[13,3586,3587],{},"「Buildpacks」という項目があるので「Add builpack」でパックを追加します。",[1011,3589],{":src":3590,":width":3591,":center":3592},"'_mix\u002Fbuldpack-300x222.png'","'500px'","true",[13,3594,3595],{},"LaravelなのでPHPは必須です。そしてnode modulesもあるのでnode.jsも入れます。ここでサーバーサイドの言語を複数追加すれば各言語でアプリを動かすことができます。",[299,3597,3599],{"id":3598},"githubと連携してソースを入れる","Githubと連携してソースを入れる",[13,3601,3580,3602,3605],{},[2307,3603,3604],{},"Laravelアプリケーションのソースをこのアプリにぶち込む必要があります。"," herokuではGithubとの連携がオススメされているので、今回はその方法で行きます。上記画面から「Deploy」をクリックして移動します。",[1011,3607],{":src":3608,":width":1014},"'_mix\u002Fdeply_set-1024x257.png'",[13,3610,3611],{},"deployment method でGithubを選びます。すると連携するリポジトリ名を入力する欄が出現します。もし初めてherokuを触ってgithubと連携していない場合は「githubと連携する？」みたいな物が表示されたはずなので、言われる通りに連携します。",[13,3613,3614],{},"githubのアカウントを持っていてログイン状態が保持されていれば、githubの連携許可画面が表示されます。それで簡単に連携ができます。",[13,3616,3617],{},"連携が完了すると下のようにデプロイに関する設定が出てきます。githubを用いる場合は２通りのデプロイがあります。",[1239,3619,3620,3623],{},[33,3621,3622],{},"ブランチを指定して手動でデプロイ",[33,3624,3625],{},"ブランチに変化があったら自動でデプロイ",[13,3627,3628],{},[2307,3629,3630],{},"毎回herokuにログインするのも面倒なので「Enable Automatic Deploys」を選択して自動デプロイができるようにしておきます。",[1011,3632],{":src":3633,":width":1014},"'_mix\u002Fautodeploy-1024x421.png'",[13,3635,3636],{},"リポジトリにソースをプッシュすると以下のようにbuildしているのがダッシュボードで見ることができます。（たまに更新ボタンを押さないと表示されない時がある。）",[1011,3638],{":src":3639,":width":3591,":center":3592},"'_mix\u002Fbuildings-300x166.png'",[13,3641,3642],{},"しかしまだDBとの連携や環境変数の設定が済んでいないので、アプリをみたところで500エラーしか表示されません。次はDBの設定に移ります。",[299,3644,3646],{"id":3645},"jawsdbを設定する","JawsDBを設定する",[13,3648,3649,3650,3653,3654,3657],{},"herokuでは",[37,3651,3652],{},"Procfile"," を用いてドキュメントルート やwebサーバー&phpの設定ができますが、DBに関してはアドオンを利用する必要があります。herokuではpostgreSQLの使用を進めていますが、私が作成したLaravelアプリはmysql仕様で作っていたので ",[2307,3655,3656],{},"今回はmysqlをDBとして使用します。","（まあpostgreSQLに変更もできましたけど…）",[299,3659,3661],{"id":3660},"使用するdbのアドオンについて","使用するDBのアドオンについて",[13,3663,3664],{},"ちなみに、「heroku mysql DB」と検索すると **「ClearDB」というmysqlベースのDBアドオンを入れるように書かれた記事が多いですが、私はそこでハマった物がありました。**以下に記述しておきます。",[413,3666,3668],{"id":3667},"laravel-55以上の場合cleardbの使用はおすすめしない理由","Laravel 5.5以上の場合、ClearDBの使用はおすすめしない理由",[13,3670,3671],{},"ClearDBを用いててデータベースへマイグレーションをおこなった際に。こんなエラーが発生。",[60,3673,3675],{"className":62,"code":3674,"language":64,"meta":65,"style":65},"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",[37,3676,3677,3687,3691,3695,3798],{"__ignoreMap":65},[69,3678,3679,3681,3684],{"class":71,"line":72},[69,3680,354],{"class":106},[69,3682,3683],{"class":96}," artisan",[69,3685,3686],{"class":96}," migrate\n",[69,3688,3689],{"class":71,"line":79},[69,3690,284],{"class":88},[69,3692,3693],{"class":71,"line":85},[69,3694,284],{"class":88},[69,3696,3697,3700,3703,3706,3709,3712,3715,3718,3721,3724,3727,3730,3733,3736,3739,3741,3744,3747,3750,3753,3756,3759,3762,3765,3768,3771,3774,3777,3779,3782,3784,3786,3788,3791,3793,3795],{"class":71,"line":103},[69,3698,3699],{"class":106},"SQLSTATE[42000]:",[69,3701,3702],{"class":96}," Syntax",[69,3704,3705],{"class":96}," error",[69,3707,3708],{"class":96}," or",[69,3710,3711],{"class":96}," access",[69,3713,3714],{"class":96}," violation:",[69,3716,3717],{"class":116}," 1071",[69,3719,3720],{"class":96}," Specified",[69,3722,3723],{"class":96}," key",[69,3725,3726],{"class":96}," was",[69,3728,3729],{"class":96}," too",[69,3731,3732],{"class":96}," long",[69,3734,3735],{"class":92},";",[69,3737,3738],{"class":106}," max",[69,3740,3723],{"class":96},[69,3742,3743],{"class":96}," length",[69,3745,3746],{"class":96}," is",[69,3748,3749],{"class":116}," 767",[69,3751,3752],{"class":96}," bytes",[69,3754,3755],{"class":462}," (SQL: ",[69,3757,3758],{"class":96},"alter",[69,3760,3761],{"class":96}," table",[69,3763,3764],{"class":92}," `",[69,3766,3767],{"class":106},"users",[69,3769,3770],{"class":92},"`",[69,3772,3773],{"class":106}," add",[69,3775,3776],{"class":96}," unique",[69,3778,3764],{"class":92},[69,3780,3781],{"class":106},"users_email_unique",[69,3783,3770],{"class":92},[69,3785,1770],{"class":92},[69,3787,3770],{"class":92},[69,3789,3790],{"class":106},"email",[69,3792,3770],{"class":92},[69,3794,1777],{"class":92},[69,3796,3797],{"class":462},")\n",[69,3799,3800,3802,3804,3806,3808,3810,3812,3814,3816,3818,3820,3822,3824,3826,3828,3830,3832,3834,3836],{"class":71,"line":110},[69,3801,3699],{"class":106},[69,3803,3702],{"class":96},[69,3805,3705],{"class":96},[69,3807,3708],{"class":96},[69,3809,3711],{"class":96},[69,3811,3714],{"class":96},[69,3813,3717],{"class":116},[69,3815,3720],{"class":96},[69,3817,3723],{"class":96},[69,3819,3726],{"class":96},[69,3821,3729],{"class":96},[69,3823,3732],{"class":96},[69,3825,3735],{"class":92},[69,3827,3738],{"class":106},[69,3829,3723],{"class":96},[69,3831,3743],{"class":96},[69,3833,3746],{"class":96},[69,3835,3749],{"class":116},[69,3837,3838],{"class":96}," bytes\n",[13,3840,3841],{},"ローカルでは起きなかった謎のエラー。「上限は767bytesだけど、1071bytesのキーが設定されている」と怒っている。とりあえずmigrationが中途半端にUserテーブルだけ作って止まってしまったので、migrationをリフレッシュする。",[60,3843,3845],{"className":62,"code":3844,"language":64,"meta":65,"style":65},"php artisan migrate:fresh\n",[37,3846,3847],{"__ignoreMap":65},[69,3848,3849,3851,3853],{"class":71,"line":72},[69,3850,354],{"class":106},[69,3852,3683],{"class":96},[69,3854,3855],{"class":96}," migrate:fresh\n",[13,3857,3858],{},"このエラーを調べたところどうやら使用した「ClearDB」のmysqlが5.5と古い上に、Laravelで設定していたmysqlで用いる文字コードとの関係で起きていた。以下のQiita記事が非常に参考になった。",[2685,3860,3862,3863],{"className":3861},[2688,3290],"\n    ",[421,3864,3866],{"href":3865,"target":2824},"https:\u002F\u002Fqiita.com\u002Fbeer_geek\u002Fitems\u002F6e4264db142745ea666f","Laravel5.4以上、MySQL5.7.7未満 でusersテーブルのマイグレーションを実行すると Syntax error が発生する",[13,3868,3869],{},"対処は２つ。",[1239,3871,3872,3875],{},[33,3873,3874],{},"mysqlのバージョンを上げて5.7にする。",[33,3876,3877],{},"文字コードを上限を超えないものに変更する。",[13,3879,3880],{},"今回の場合はmysqlのバージョンを上げる方にした。しかしこちらのStackOver Flow にもあるようにClear DBで用いられているmysqlのバージョンを上げるのは無理らしい。。",[2685,3882,3862,3884],{"className":3883},[2688,3290],[421,3885,3887],{"href":3886,"target":2824},"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,3889,3890,3891,3895],{},"代わりに",[421,3892,3894],{"href":3893,"target":2824},"https:\u002F\u002Felements.heroku.com\u002Faddons\u002Fjawsdb","「JawsDB」","というのを使うとmysqlのバージョンが5.7のものが入ってくれる。",[13,3897,3898],{},"herokuでのmysql導入を調べると「ClearDBを入れろ」という記事が多く、言われたままにやったら起きたのでハマりポイントでした。",[299,3900,3902],{"id":3901},"jawsdbアドオンを入れる","jawsDBアドオンを入れる",[13,3904,3905,3906],{},"アドオンはherokuの追加機能のような物です。「Resources」を選ぶと「Add-ons」という項目がありそこでアドオンを導入できます。jawsDBと入力するとすぐに出てきます。 ",[2307,3907,3908],{},"「jawsDB MySQL」を選択して追加。",[1011,3910],{":src":3911,":width":3591,":center":3592},"'_mix\u002Fjawsdb-300x248.png'",[13,3913,3914],{},"データ容量などに応じたプランを選ぶことができます。ここではFreeでおk。「Provision」をクリックしてアプリに追加します。",[299,3916,3918],{"id":3917},"dbの接続情報を手に入れる","DBの接続情報を手に入れる",[13,3920,3921],{},"このjawsDBはherokuのアプリ内（というかサーバー）に組み込まれている訳ではないので、ホストやポートなどを指定する必要があります。jawsDBを導入するとそのherokuアプリを結びついたデータベースが作成されます。",[13,3923,3924],{},"その情報はアドオンのページで、jawsDBの箇所がリンクになっているのでそこをクリックします。",[1011,3926],{":src":3927,":width":3591,":center":3592},"'_mix\u002Faddons-768x194.png'",[13,3929,3930],{},[2307,3931,3932],{},"するとDBへの接続情報が書かれた画面が表示されます。ここに接続先ホスト、ユーザー名、パスワードなどが書いてあるので控えておきます。",[1011,3934],{":src":3935,":width":1014},"'_mix\u002Fjawsdashboard-768x360.png'",[13,3937,3938],{},"自前のmysqlコマンドやクラアントから普通にアクセスすることができます。では次にこれらの情報を元に環境変数を設定します。後少しでアプリがデプロイされます！頑張ってください！",[299,3940,3942],{"id":3941},"config-bars環境変数を設定","config bars(環境変数)を設定",[13,3944,3945],{},"DBの接続情報を手に入れたのでLaravel内で使用される環境変数を定義していきます。Laravelのファイル群に「.env」というのがあったと思います。そのファイルは環境変数を定義しており、DBのパスワード、APIキーなど重要な値や環境によって変化する値が格納されています。",[13,3947,3948,3949],{},"基本的には.envはgitでは管理せず、githubにはプッシュされないように除外してあります。",[2307,3950,3951],{},"ローカルとherokuではDBの接続情報も違うので異なる環境変数を設定する必要がります。",[13,3953,3954,3955,3958],{},"そのために ",[2307,3956,3957],{},"「Settings」の「config bars」にて環境変数を設定","できます。Laravelの.envを開いて必要な項目をコピーして適宜適切な値を入れておきましょう。",[1011,3960],{":src":3961,":width":1014},"'_mix\u002Fconfigure-768x115.png'",[299,3963,3964],{"id":3964},"マイグレーションを行う",[413,3966,3968],{"id":3967},"heroku-cliを入れる","heroku CLIを入れる",[13,3970,3971,3972,3976],{},"環境変数の設定を終えればLaravelのマイグレーションが使えるようになります。heroku CLIを用いてアプリをBashで操作します。ローカルに",[421,3973,3975],{"href":3974,"target":2824},"https:\u002F\u002Fdevcenter.heroku.com\u002Farticles\u002Fheroku-cli","heroku CLI","をダウンロードします。",[413,3978,3979],{"id":3979},"アプリをbashから操作する",[13,3981,3982],{},"macならターミナルを開いてログインをします。",[60,3984,3987],{"className":3985,"code":3986,"language":923},[921],"~ % 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",[37,3988,3986],{"__ignoreMap":65},[13,3990,3991],{},"ブラウザがいったん開いてログインが完了です。そしたらアプリをbashで操作します。以下のコマンドを唱えます。",[60,3993,3996],{"className":3994,"code":3995,"language":923},[921],"~ % heroku run bash -a staging-app-laravel\nRunning bash on ⬢ staging-app-laravel... done\n",[37,3997,3995],{"__ignoreMap":65},[13,3999,4000,4003],{},[2307,4001,4002],{},"「-a」でアプリ名を指定できます。バッシュの時はアプリ名を指定しないと怒られます。"," すると表示が少し変わりバッシュ操作が可能になります。",[13,4005,4006,4007,4010],{},"ホームディレクトリ 内にLaravelのソース達が配置されているのが確認できます。このLaravelアプリのルートで ",[37,4008,4009],{},"php artisan","を唱えることができます。それ以外のディレクトリなどで行うと「そんなコマンド知らんよ」と怒られます。ではマイグレーション。",[60,4012,4015],{"className":4013,"code":4014,"language":923},[921],"~ $ 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",[37,4016,4014],{"__ignoreMap":65},[13,4018,4019],{},"これでDBに必要なテーブルが設定されました。ユーザーテーブルに私たちが管理するための管理ユーザーを作成するシーダーを予め作成したおいたのでそれも実行。",[60,4021,4024],{"className":4022,"code":4023,"language":923},[921],"~ $ php artisan db:seed\n",[37,4025,4023],{"__ignoreMap":65},[13,4027,4028],{},"これでDBにもテーブルが挿入されたのでアプリが動くようになりました。",[299,4030,4032],{"id":4031},"herokuアプリをブラウザで開く","Herokuアプリをブラウザで開く",[13,4034,4035,4036],{},"ブラウザのHerokuの画面に戻り、pipe lineの一覧を開きます。 ",[2307,4037,4038],{},"開発アプリの「Open app」をクリックすると構築したLaravelアプリの画面が表示されました。",[13,4040,4041],{},"「500 Internal ServerError」などが表示されている場合は以下の４つを確かめましょう。",[30,4043,4044,4047,4050,4053,4056],{},[33,4045,4046],{},"$ heroku logs --tail -a your_app_name でログを確かめる。",[33,4048,4049],{},"Laravelアプリそのものにミスはないか？",[33,4051,4052],{},"環境変数の設定はあっているか？",[33,4054,4055],{},"DBに接続されているか？DBのテーブルの設定は正しいか？",[33,4057,4058],{},"ビルドのログを確かめてビルドが失敗していないか？",[13,4060,4061],{},"アプリのビルドが成功している場合はサーバー自体は構築できています。Laravelアプリや環境変数などに問題ある可能性が高いので確認してみましょう。",[299,4063,4065],{"id":4064},"次は本番のアプリ","次は本番のアプリ！！",[13,4067,4068],{},"ここまでもかなりボリューミーですが、今回は２つアプリを入れたので２つ目も設定します。ですがやることは開発の方でやったように",[1239,4070,4071,4074,4077,4080],{},[33,4072,4073],{},"ビルドパックを入れる",[33,4075,4076],{},"アドオンを入れる",[33,4078,4079],{},"環境変数を設定する",[33,4081,4082],{},"DBと接続してマイグレーションする",[13,4084,4085],{},"ぐらいです。そして私の運用ではgithubと本番のアプリは連携させず、stagingのアプリを本番に移行するので少し手間が省けます。stagingのものを本番に移行する場合はpipelineの画面で「promote to production」を選択すればすぐに反映されます。",[299,4087,4089],{"id":4088},"本番サーバーのssl化","本番サーバーのSSL化",[13,4091,4092],{},"最後に本番サーバーのSSL化です。無料プランのは自動でSSLは付与されません。証明書があれば付与することができますが無料のdynoはスペックが最低限な上、証明書取得でもお金がかかるので大人しくhobbyプラン以上の有料版にアップグレードしましょう。",[13,4094,4095],{},"本番アプリをhobbyプランに上げるとACM（自動証明書管理）が有効になり、証明書が発行できて有効になってます。SSL化はこれだけです。込み入った証明書や独自ドメイン の付与の際には少し設定が必要です。",[299,4097,4099],{"id":4098},"apiサーバーとして機能しているかを確かめる","APIサーバーとして機能しているかを確かめる",[13,4101,4102],{},"とりあえず本番環境までのデプロイが完了しました。Laravel Passport を用いて認証付きAPIを投げられるのでそのチェックをしてみます。とその前にlaravel passportの設定上必要なものがあるのでアプリ内にbashで入って以下を実行。",[60,4104,4107],{"className":4105,"code":4106,"language":923},[921],"~ $ php artisan passport:install\n",[37,4108,4106],{"__ignoreMap":65},[13,4110,4111],{},"Laravel Passport を用いて認証付きAPIを投げられるのでそのチェックをしてみます。シーダーで作成した管理ユーザーでログインします。Laravel Passport ではユーザーごとのアクセストークンが発行できるので、それを作成。（トークン取得画面のUIなどは適宜作成してください。Laravel側でも用意されています。）",[1011,4113],{":src":4114,":width":3591,":center":3592},"'_mix\u002Fapis-768x841.png'",[13,4116,4117],{},"アクセストークンを発行して手元にメモしておきます。「Talend API Tester」というChrome拡張機能を使ってこのHerokuアプリ宛にAPIを投げてみます。",[13,4119,4120],{},"アプリのドメイン名が表示されているのでコピー。外部からのAPIアクセスの際にはAPIキーをリクエストヘッダーに仕込んで送ると受け取る設定になっています。まずはAPIキーなして投げると、",[1011,4122],{":src":4123,":width":1014},"'_mix\u002Fapi_test_01-1-768x330.png'",[13,4125,4126,4127,4130],{},"おっ！401 Unauthorized が戻ってきました。 ",[2307,4128,4129],{},"とりあえず指定のAPIルーティングは存在して、リクエストが受け取れています。つまりAPIサーバーとして機能していることがわかります。"," ではヘッダに先ほどのキーを仕込んで、また諸所の設定値も入れて再度投げると、",[1011,4132],{":src":4133,":width":1014},"'_mix\u002Fapitest_02-768x438.png'",[13,4135,4136],{},"200 OK が返りました。プレビューを見るとJSONでのデータが戻ってきているので成功です。これでHerokuにLaravelで開発したAPIサーバー及び管理アプリを構築することができました。このサーバーに認証キー付きAPIを投げることでデータを取得できます。",[299,4138,4140],{"id":4139},"終了","終了〜〜〜",[13,4142,4143],{},"以上がherokuにlaravel で作成したAPIサービスを構築する手順でした。めちゃくちゃ長い記事になりましたが、VPSならもっと長くなっています笑。githubやSSL化そして他の管理を気にせず簡単にデプロイできるのでherokuが人気なのもわかります。Xserverとかは異なってスケーラブルでnodeとかpythonとかも入れられ、githubで管理できるのは大きいです。",[936,4145,4146],{},"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":65,"searchDepth":85,"depth":85,"links":4148},[4149,4153,4154],{"id":3349,"depth":79,"text":3350,"children":4150},[4151,4152],{"id":3374,"depth":85,"text":3375},{"id":3401,"depth":85,"text":3402},{"id":3432,"depth":79,"text":3433},{"id":3484,"depth":79,"text":3485,"children":4155},[4156,4157,4160,4161,4162,4163,4166,4167,4168,4169,4173,4174,4175,4176,4177],{"id":3488,"depth":85,"text":3489},{"id":3518,"depth":85,"text":3519,"children":4158},[4159],{"id":3532,"depth":103,"text":3533},{"id":3570,"depth":85,"text":3571},{"id":3598,"depth":85,"text":3599},{"id":3645,"depth":85,"text":3646},{"id":3660,"depth":85,"text":3661,"children":4164},[4165],{"id":3667,"depth":103,"text":3668},{"id":3901,"depth":85,"text":3902},{"id":3917,"depth":85,"text":3918},{"id":3941,"depth":85,"text":3942},{"id":3964,"depth":85,"text":3964,"children":4170},[4171,4172],{"id":3967,"depth":103,"text":3968},{"id":3979,"depth":103,"text":3979},{"id":4031,"depth":85,"text":4032},{"id":4064,"depth":85,"text":4065},{"id":4088,"depth":85,"text":4089},{"id":4098,"depth":85,"text":4099},{"id":4139,"depth":85,"text":4140},[2650],"2020-07-07",{},"\u002Farticles\u002Flalavel7-deplay-on-heroku",{"title":3334,"description":3334},"articles\u002Flalavel7-deplay-on-heroku",[354,2659,4185],"infrastructure","_mix\u002Fheroku_archtecture-768x446.jpg","i3Z8K7cn-JUTupKxFl5lgfnHLhl9EAJPK97f9yd7CX0",1780987146433]