- 投稿日:2021-03-03T23:08:02+09:00
Visual Studio Code で PHP を 使うための初期設定をしてみた!!!
はじめに
ずっと愛用していた「Brackets」のサポートが9月1日に終了してしまうみたいです(´;ω;`)
Brackets を開いたときに「Visual Studio Code」に移行してね、と書いていたのでとってもシンプルで使いやすかったBrackets ともお別れします!!!
とりあえずPHPが使えればいいので、ささっさっとやってみました。
Visual Studio Codeのインストール
Windows版でやっていきます!!
Visual Studio Code ダウンロード設定
インストールできたら開いて
左側の上から5番目の四角のやつで拡張機能をインストールしていきます!日本語にするのは
「Japanese Language Pack for Visual」
ってやつです!次に
フォルダーを開くで使用したいフォルダを開きます。
次に、ファイル(F)→ユーザ設定→設定 で 「settings.json]を編集します。
settings.jsonを
{ "liveServer.settings.donotShowInfoMsg": true, "liveServer.settings.proxy": { "enable": true, "baseUri": "/", "proxyUri": "http://localhost/使用したいフォルダ名/" }, "json.schemas": [ ], "editor.fontLigatures": null }と記述します!
デバック
PHPを指定すると、
launch.json
ができます。{ // IntelliSense を使用して利用可能な属性を学べます。 // 既存の属性の説明をホバーして表示します。 // 詳細情報は次を確認してください: https://go.microsoft.com/fwlink/?linkid=830387 "version": "0.2.0", "configurations": [ { "name": "Listen for XDebug", "type": "php", "request": "launch", "port": 9000 }, { "name": "Launch currently open script", "type": "php", "request": "launch", "program": "${file}", "cwd": "${fileDirname}", "port": 9000 } ] }![1614778787613.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1153081/4b32f1bf-6f81-4c08-49c7-8286aabffe79.jpeg)これでデバックもできます!
動かす!
最後にindex.htmlなどの右上にある青いマークを押して
こんな感じでHTMLがでて・・・
最後に
ここもではできたのですがMySQLにどうやってつなげるのかがまだわかってないので調べ倒そうと思います。。。
「Brackets」今までありがとう!
- 投稿日:2021-03-03T22:59:02+09:00
Laravel 画像の投稿機能とシンボリックリンク
はじめに
Laravelにて簡単な画像付きレシピ投稿アプリを作成しています。
画像のアップロード機能を実装した際にシンボリックリンクという言葉を初めて知ったので、備忘録として記録しておくことにしました。シンボリックリンクとは
Windouwsでいうところのショートカットのようなもの。
シンボリックリンクファイルを通して大元のフォルダを参照することができます。やりたいこと
投稿フォームから画像を選択し送信ボタンを押すと一覧で表示されるという仕組み。
画像ファイルを選択し送信ボタンをクリック
一覧画面へ画像を表示
ここでレシピ投稿アプリなのに料理の写真ねーじゃん!とツッコまれる
画像の保存先
さて、コントローラーで受け取った画像ファイルは基本的に
storage/app/public/image
内に保存させます。RecipeController.phppublic function create(Request $request){ $recipe = new Recipe; $form = $request->all(); //画像が送信されたら保存して $recipe->image_pathカラム にパスを保存する if (isset($form['image'])) {//変数に値が入っているかをチェック $path = $request->file('image')->store('image');// 画像をstorage/app/public/image配下に保存 $recipe->image_path = basename($path);//パスからを取得したファイル名をimage_pathカラムに保存 } else { $recipe->image_path = null; } unset($form['_token']); unset($form['image']); $recipe->fill($form)->save(); return redirect('/home'); }ただ、実際にアプリケーションを起動するときに公開されるのはpublicディレクトリのみです。画像は
storage/app/public/image
に保存されているので画像データを持ってくることはできません。
そんなときにpublicディレクトリ
からstorage/app/public/image
へのショートカット機能の役割を果たすのががシンボリックリンクです。シンボリックリンクの貼り方
artisanコマンドから簡単にできます。
まずpublicディレクトリへ移動します。$ cd public以下のコマンドを実行します。
$ php artisan storage:linkするとpublic内にstorageフォルダができます。(ショートカットが作成されるの方がわかりやすいかも)
矢印ができます。
public/image
と進んでいくと、storage/app/public/image
と同じ画像ファイルが存在していることがわかるかと思います。ビューに画像を表示させる
画像表示部分のみ
home.blade.php<img src="{{ asset('storage/image/' . $recipe->image_path) }}" width="100%" heigth="100%" alt="">
asset('ファイルパス')
はpublicディレクトリのパスを返す関数です。
丁寧に書くと、app/public/storage/image/[画像のパス名]
となるわけです。
さっきシンボリックリンクを貼ったおかげでこのフォルダパスを通すことができました。終わり。
- 投稿日:2021-03-03T22:17:24+09:00
Class 'App\Providers\view' not found 解決方法
エラー:Class 'App\Providers\view' not found
参考書Laravel入門(p.101)の「クロージャ(無名関数)でコンポーザ処理を作る」で、サービスプロバイダを登録しビューコンポーザを利用する為に、/helloにアクセスしたところ、
Class 'App\Providers\view' not foundエラーが発生!
このエラーは、viewが適切に読み込まれていないという意味なので、
viewを読み込むよう、HelloServiceProvider.phpに下記のuse宣言を追加すればOKです。use Illuminate\Support\Facades\View;参考記事はこちら
https://qiita.com/janet_parker/items/4b63804fe7f8aa3e74c6
https://qiita.com/niiyz/items/5b83ef5255a1ec64d9d6
https://stackoverflow.com/questions/63655253/error-class-app-providers-view-not-found
- 投稿日:2021-03-03T22:15:39+09:00
Laravelでquery parametersで空文字渡ってくるはずが謎にnullが取れる件
はじめに
こんにちは。楽天モバイルの楽天回線エリアに住んでますが、パートナー回線しか繋がらない筆者です。
さて、Laravel使ってまして、query parametersで空文字来てるはずなのに、nullになっちゃうことに地味に小一時間悩んだので共有します。
結論:こいつの仕業
Kernel.phpprotected $middleware = [ \Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class, // これ ];こいつが空文字をnullに変換しているんです...
地味に小一時間悩んだ話
以下を例にすると、2のときにnullになっているという恐ろしさ...。
そして、3のときも空文字が入っていたもののそれがnullに変換されてsetされているので、デフォルト値の1が適用されないという恐ろしさ...。HomeController.phppublic function index(Request $request) { // 1: http://localhost?hoge=1 $hoge = $request->input('hoge'); dd($hoge); // 1 // 2: http://localhost?hoge= $hoge = $request->input('hoge'); dd($hoge); // null // 3: http://localhost?hoge= $hoge = $request->input('hoge', 1); dd($hoge); // null }勝手に変えてほしくない人は
以下コメントアウトしましょう
あと、ちゃんとコメント書いておきましょう
https://github.com/laravel/laravel/blob/8.x/app/Http/Kernel.php#L23おわりに
便利なのかもしれないのですが、良し悪しあると思います
何かのお役に立てていれば幸いです。
それでは。
- 投稿日:2021-03-03T21:04:55+09:00
Could not open input file: artisan解決方法
エラー:Could not open input file: artisan
Laravelでサービスプロバイダを作成する為に、
$ php artisan make:provider HelloServiceProvider上記のコマンドをターミナルで叩くと、
Could not open input file: artisanというエラーが発生します。
このコマンドは、arisanファイルのあるディレクトリにいないと実行できない為、
エラーが発生するということは、コマンドを実行するカレントディレクトリが間違っている可能性があります。現在どこのディレクトリで実行しているか確認しましょう。
こちらの場合だと、$cd docker-laravel-demo/backendこれで、artisanファイルのあるbackendディレクトリまで移動し、
$ php artisan make:provider HelloServiceProviderサービスプロバイダを作成するコマンドを叩くと、
Controller created successfully.こちらが表示され、Providersディレクトリ内にHelloServiceProviderファイルが作成されます。
- 投稿日:2021-03-03T20:13:09+09:00
爆速で検索できる顔文字サイトを開発した話
きっかけ
某S●mejiに対抗するため、えりんぎという顔文字サイトを2020年1月に公開
志半ばで中途半端な状態のまま放置してドメインを失効しそうになる
閉鎖しようと思ったが友人がずっと使ってくれてたのを知るというわけでバグだらけの「えりんぎ」をリニューアルすることに
作ったもの
顔文字をカテゴリやキーワードで検索できるサイト
えもしぇあ https://emoshare.net製作期間は3週間で60時間くらい
※顔文字を登録する作業が10時間以上占めている( ;꒳; )LighthouseでPerformanceが100にならなかったけど爆速
※Google Analytics入れたら92に下がった( ;꒳; )
技術
Laravel 6.x (普段使っている安心安定のLaravel)
Vue3 (Composition API 使ってみたかった)
Tailwind CSS (Laravel8で採用されたらしいので使ってみたかった)実装方針
実装に時間が掛かりそうなものはできるだけ除外
コンポーネントはあまり細かく分けない
新しいことはあまりしない(躓いたら諦める)実装したい機能
検索したときにページ遷移せずいい感じで表示したい
タップやクリックでクリップボードにコピー
コピーした履歴を持ちたい
できるだけ爆速で表示したい
いいね機能
顔文字にキーワードを持たせて検索しやすくする(管理画面で設定するのめんどくさくなって途中から設定してない)
投稿(めんどくさくなってやめた)
ダークモード(めんどくさくなってやめた)
多言語化する(顔文字に日本語を含めなくなるのでやめた)画面設計
DB設計
実装
いいねボタン
これ参考にした
タグクラウド
チェックボックスの状態でON/OFFを切り替える
vue.js// 一部省略 <label :for="'category' + category.id"> <input :id="'category' + category.id" type="checkbox" class="category-input hidden" /> <div class="category-button-wrapper"> <span class="category-button"> #{{ category.name }} </span> </div> </label>style.scss.category-input { &:checked { + .category-button-wrapper { .category-button { color: #fff; background: #f59e0b; border-color: #f59e0b; } } } &:active { + .category-button-wrapper{ .category-button { color: #fff; background: #fbbf24; border-color: #fbbf24; } } } }
:checked
と:hover
で実装したところスマホでうまく動かなかったが:hover
を:active
に変えたら動いたコピー履歴
Local Storageでコピーした顔文字を保持
エラー画面
地味に 401, 403, 404, 405, 419, 429, 500, 503 エラーに対応した
gzip圧縮
cssとjsを圧縮する
zopfli使ったら結構小さくなったtailwind.config.jsmodule.exports = { purge: [ './resources/js/components/**/*.vue', './resources/views/**/*php' ], darkMode: false, // or 'media' or 'class' theme: { extend: {}, }, variants: { extend: {}, }, plugins: [], }webpack.mix.jsconst mix = require('laravel-mix') const tailwindcss = require('tailwindcss') const CompressionPlugin = require('compression-webpack-plugin') const zopfli = require('node-zopfli') mix.js('resources/js/app.js', 'public/js').vue() .sass('resources/sass/app.scss', 'public/css') .options({ processCssUrls: false, postCss: [ tailwindcss('./tailwind.config.js') ], }).webpackConfig({ plugins: [ new CompressionPlugin({ test: /\.(css)|(js)$/, algorithm: (content, options, fn) => { zopfli.gzip(content, options, fn); }, }) ] }).htaccess# gzip対応 RewriteCond %{HTTP:Accept-Encoding} gzip RewriteCond %{REQUEST_FILENAME}\.gz -s RewriteRule ^(.+) $1.gz <FilesMatch "\.css\.gz$"> ForceType text/css AddEncoding x-gzip .gz </FilesMatch> <FilesMatch "\.js\.gz$"> ForceType application/x-javascript AddEncoding x-gzip .gz </FilesMatch>デプロイ
レンタルサーバーにデプロイする方法忘れたのでこれを参考にした
ヘテムルにLaravelをデプロイする方法 (Laravel5.8)最後に
Tailwind CSSのおかげで命名する時間を節約できたのはよかった
あとレスポンシブ対応も楽だった
Google Analytics入れたらBest Practicesが100にならないんだけどどうしたらいいの?
- 投稿日:2021-03-03T18:57:22+09:00
LaravelでNotification::fake()を使ったテストでNotification::assertSentToがうまく通らない
経緯
Laravelのテストで、Notification Fake
https://readouble.com/laravel/8.x/ja/mocking.html
を使用して、テスト時に実際にはslackに通知を飛ばさないようにしておいて、
Notification::assertSentTo
で、通知が送信される処理は行われていることをテストしようとして、Notification::assertSentTo( new AnonymousNotifiable, SlackNotification::class );と書いたのだけど、どうあがいても
The expected [App\Notifications\SlackNotification] notification was not sent. Failed asserting that false is true.と怒られてしまう。
指定の仕方とか色々試したけど、assertSentToではどうあがいてもうまくテストが通らなかった。。。。解決策
Notification::assertTimesSent
を使用することで解決Notification::assertTimesSent( 1, SlackNotification::class );第一引数に通知が送信されるべき回数(期待値)を指定してやればOK
テストでは一旦通知内容は確認しなくてOKだったので、通知が飛ばされる回数をテストしてやる形でテストが通った通知が送られる処理の直前で
Notification::assertTimesSent( 0, SlackNotification::class );と記述しても通ったので、とりあえず問題なさそう。
assertSentTo使う場合はどう書けばいいのかモヤモヤするけど、一旦忘れる!
追記
本記事を公開後、ちょっと調べてたら、以下の記事を見つけた
https://qiita.com/harukei/items/26f0a1c0d6128f681c00
Notification::fake() が Mail::fake() を上から潰しているようです。
確かに僕のコードでも
public function test__invoke() { Mail::fake(); Notification::fake();とNotification::fake() と Mail::fake()の両方を使用しているので、assertSentToがうまく動作しないのはこのせいなのかもしれない。。。
- 投稿日:2021-03-03T17:34:43+09:00
PHPだけでRESTAPIを再現してみた
動機
最近はReactなどフロントばっかり触っているのですが、データ処理などのバックエンドを書くのは正直面倒くさいです。
普通であればLaravelのRESTAPI用のライブラリを使うのですが、Laravelの勉強はしたくないので、生のPHPだけでRESTAPIを再現してみることにしました。なお、RESTAPIっぽいルーティングを再現するために以下のサイトを参考にしました。
要求
APIの設計については、次の要件を満たすこととします。なお、以下のサイトを参考にしました。
- HTTPメソッド(GET,POST,PUT,DELETE)で操作を行う。
- バージョンをURLに含める。
- 作成・更新後は変更後のリソースの情報を返す。
- リクエスト・レスポンスのボディはJSONである。
- 適切なHTTPステータスコードを用いる。
実行環境
実行環境は以下の通りです。
- PHP 7.4.3
- Apache 2.4.41
- MySQL 8.0.23データベース(MySQL)の設定
今回は、
sample_database
のデータベースに、sample_table
のテーブルを作成しました。またテーブルの中に、id
,name
,age
のカラムを設定しています。CREATE TABLE `sample_table` ( `id` int NOT NULL, `name` varchar(32) NOT NULL, `age` tinyint UNSIGNED DEFAULT NULL );ディレクトリ構成
root/ ├ config/ │ └ database.php ├ v1/ │ ├ controllers/ │ │ └ sample.php │ ├ index.php │ └ .htaccess ├ v2/ │ └... └ DB.phpデータベースへの接続
データベースへの接続は、
/DB.php
,/config/database.php
で行っています。/DB.php<?php include(__DIR__ . "/config/database.php"); class DB { function pdo() { global $setting; try{ $driver_option = [ PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_EMULATE_PREPARES => false, ]; $pdo = new PDO($setting["dsn"],$setting["user"],$setting["password"],$driver_option); }catch(PDOException $error){ header("Content-Type: application/json; charset=utf-8", true, 500); echo json_encode(["error" => ["type" => "server_error","message"=>$error->getMessage()]]); die(); } return $pdo; } }/config/database.php<?php $setting = [ "dsn" => "mysql:dbname=sample_database;host=localhost;charset=utf8mb4", "user" => "user", "password" => "password" ];これらのファイルは公開する必要がないので、公開フォルダの外側に設置するのが望ましいでしょう。
特に、/config/database.php
のファイルはデータベースの接続情報が入っているので、.gitignore
に追加するなど、外部流出には気をつけましょう。サーバ(Apache)の設定
/v1/.htaccessRewriteEngine On RewriteRule ^ index.php [L]これで
/v1
直下へのアクセスはすべて/v1/index.php
に飛ばされます。バージョン管理
/v1
,/v2
,...と増やしていくことで、RESTAPIのバージョン管理ができます。ルーティング
URLに応じて適切なファイルで処理をするためのルーティングです。
/v1/index.php<?php include(__DIR__ . "/../DB.php"); preg_match('|'.dirname($_SERVER["SCRIPT_NAME"]).'/([\w%/]*)|', $_SERVER["REQUEST_URI"], $matches); $paths = explode('/',$matches[1]); $file = array_shift($paths); $file_path = './controllers/'.$file.'.php'; if(file_exists($file_path)){ include($file_path); $class_name = ucfirst($file)."Controller"; $method_name = strtolower($_SERVER["REQUEST_METHOD"]); $object = new $class_name(); $response = json_encode($object->$method_name(...$paths)); $response_code = $object->code ?? 200; // header("Access-Control-Allow-Origin: *"); header("Content-Type: application/json; charset=utf-8", true, $response_code); echo $response; }else{ header("HTTP/1.1 404 Not Found"); exit; }ルーティング:解説
まずURLをわかりやすいように配列に変換します。
URLがxxx.com/yy/v1/sample/arg1/arg2/...
の場合、$paths
には、sample
以降の値が/
ごとに区切られて配列に格納されます。3行目で配列がshiftされるので、$file
はsample
、$paths
はarg1
以降の配列となります。preg_match('|'.dirname($_SERVER["SCRIPT_NAME"]).'/([\w%/]*)|', $_SERVER["REQUEST_URI"], $matches); $paths = explode('/',$matches[1]); $file = array_shift($paths);次に処理を行うファイルを決定します。
$file
がsample
の場合は、/controllers/sample.php
のファイルで処理が行われます。$file_path = './controllers/'.$file.'.php'; if(file_exists($file_path)){ (中略) }else{ //ファイルが存在しなければエラー(404) header("HTTP/1.1 404 Not Found"); exit; }次に実際にファイルを読み込みます。
ファイル内のクラス名class_name
は、(ファイル名の先頭を大文字にした単語)+Controller
になります。例えば$file
がsample
の場合は、SampleController
となります。
method_name
にはHTTPメソッド(get,post,put,delete)などが入ります。include($file_path); $class_name = ucfirst($file)."Controller"; $method_name = strtolower($_SERVER["REQUEST_METHOD"]);次にクラスのインスタンスを作成し、そのメソッドに値を渡し、返り値を表示するようにします。
$object->$method_name(...$paths)
で、クラス内に作成したHTTPメソッドに対応するメソッド(getなど)が引数$paths
で実行されます。
また、プロパティ$code
の値がHTTPステータスコードになります。$object = new $class_name(); $response = json_encode($object->$method_name(...$paths)); $response_code = $object->code ?? 200; // header("Access-Control-Allow-Origin: *"); // CORSでOriginエラーになる場合は、コメントを外す header("Content-Type: application/json; charset=utf-8", true, $response_code); echo $response;コントローラ
このファイルで実際に処理を行います。
ファイル名とクラス名には、命名規則があります。(前項参照)/v1/controllers/sample.php<?php class SampleController { public $code = 200; public $url; public $request_body; function __construct() { $this->url = (empty($_SERVER['HTTPS']) ? 'http://' : 'https://').$_SERVER['HTTP_HOST'].mb_substr($_SERVER['SCRIPT_NAME'],0,-9).basename(__FILE__, ".php")."/"; $this->request_body = json_decode(mb_convert_encoding(file_get_contents('php://input'),"UTF8","ASCII,JIS,UTF-8,EUC-JP,SJIS-WIN"),true); } public function get($id=null):array { $db = new DB(); echo $_GET["data"]; if($this->is_set($id)){ return $this->getById($db, $id); }else{ return $this->getAll($db); } } private function getById($db,$id):array { $sql = "SELECT * FROM sample_table WHERE id = :id"; $sth = $db->pdo()->prepare($sql); $sth->bindValue(":id",$id); $res = $sth->execute(); if($res){ $data = $sth->fetch(PDO::FETCH_ASSOC); if(!empty($data)){ return $data; }else{ $this->code = 404; return ["error" => [ "type" => "not_in_sample" ]]; } }else{ $this->code = 500; return ["error" => [ "type" => "fatal_error" ]]; } } private function getAll($db):array { $sql = "SELECT * FROM sample_table"; $sth = $db->pdo()->prepare($sql); $res = $sth->execute(); if($res){ return $sth->fetchAll(PDO::FETCH_ASSOC); }else{ $this->code = 500; return ["error" => [ "type" => "fatal_error" ]]; } } public function post():array { $post = $this->request_body; if(!array_key_exists("id",$post) || !array_key_exists("name",$post) || !array_key_exists("age",$post)){ $this->code = 400; return ["error" => [ "type" => "invalid_param" ]]; } $db = new DB(); $pdo = $db->pdo(); $sql = "INSERT INTO sample_table (id, name, age) VALUES (:id, :name, :age)"; $sth = $pdo->prepare($sql); $sth->bindValue(":id",$post["id"]); $sth->bindValue(":name",$post["name"]); $sth->bindValue(":age",$post["age"]); $res = $sth->execute(); $id = $pdo->lastInsertId(); if($res){ $this->code = 201; header("Location: ".$this->url.$id); return []; }else{ $this->code = 500; return ["error" => [ "type" => "fatal_error" ]]; } } public function put($id=null):array { if(!$this->is_set($id)){ $this->code = 400; return ["error" => [ "type" => "invalid_url" ]]; } $original_data = $this->get($id); if(empty($original_data)){ $this->code = 404; return ["error" => [ "type" => "not_in_sample" ]]; } $put = array_merge($original_data, $this->request_body); $db = new DB(); $sql = "UPDATE sample_table SET name=:name,age=:age WHERE id=:id"; $sth = $db->pdo()->prepare($sql); $sth->bindValue(":id",$id); $sth->bindValue(":name",$put["name"]); $sth->bindValue(":age",$put["age"]); $res = $sth->execute(); if($res){ return [$this->get($id)]; }else{ $this->code = 500; return ["error" => [ "type" => "fatal_error" ]]; } } public function delete($id=null):array { if(!$this->is_set($id)){ $this->code = 400; return ["error" => [ "type" => "invalid_url" ]]; } $db = new DB(); $sql = "DELETE FROM sample WHERE id = :id"; $sth = $db->pdo()->prepare($sql); $sth->bindValue(":id",$id); $res = $sth->execute(); if($res){ $this->code = 204; return []; }else{ $this->code = 500; return ["error" => [ "type" => "fatal_error" ]]; } } public function options():array { header("Access-Control-Allow-Methods: OPTIONS,GET,HEAD,POST,PUT,DELETE"); header("Access-Control-Allow-Headers: Content-Type"); return []; } private function is_set($value):bool { return !(is_null($value) || $value === ""); } }コントローラ:解説
コントローラ内のメソッドの返り値は連想配列(PHP)であることが必要です。
__construct
$code
:HTTPステータスコード$url
:このリクエストのURL(/v1/sample/arg1/arg2/...
の場合は、/v1/sample/
まで)$request_body
:リクエストボディ(JSONに対応)public $code = 200; public $url; public $request_body; function __construct() { $this->url = (empty($_SERVER['HTTPS']) ? 'http://' : 'https://').$_SERVER['HTTP_HOST'].mb_substr($_SERVER['SCRIPT_NAME'],0,-9).basename(__FILE__, ".php")."/"; $this->request_body = json_decode(mb_convert_encoding(file_get_contents('php://input'),"UTF8","ASCII,JIS,UTF-8,EUC-JP,SJIS-WIN"),true); }※万が一、リクエストボディをhtmlのinputタグで実装する場合(
application/x-www-form-urlencoded
)は、以下の記事を参考にしてJSONで送るようにするか、parse_str(file_get_contents('php://input'),$request_body);
のように直接展開してください。
get
getメソッド:データの取得
なお、引数には$paths
の値が展開されて入ります。よって/v1/sample/:id/:key
の形である場合、get($id=null,$key=null)
とします。public function get($id=null):array { $db = new DB(); if($this->is_set($id)){ //(null or 空白)でないことを判定 return $this->getById($db, $id); }else{ return $this->getAll($db); } } private function getById($db,$id):array { (中略) //一部取得する } private function getAll($db):array { (中略) //全件取得する }post
postメソッド:データの追加
必要なキー(項目)がリクエストボディにあるかどうかをarray_key_exists
でチェックします。public function post():array { $post = $this->request_body; if(!array_key_exists("id",$post) || !array_key_exists("name",$post)){ //$postに必要なキーが存在することをチェック $this->code = 400; return ["error" => [ "type" => "invalid_param" ]]; } $db = new DB(); $pdo = $db->pdo(); $sql = "INSERT INTO sample_table (id, name, age) VALUES (:id, :name, :age)"; $sth = $pdo->prepare($sql); $sth->bindValue(":id",$post["id"]); $sth->bindValue(":name",$post["name"]); $sth->bindValue(":age",$post["age"]); $res = $sth->execute(); // $id = $pdo->lastInsertId(); // IDをAutoIncrementで設定する場合は、これでidを取得 if($res){ $this->code = 201; //新しいリソースが生成されたことを示す header("Location: ".$this->url.$post["id"]); //生成されたリソースを取得するURL return []; }else{ $this->code = 500; return ["error" => [ "type" => "fatal_error" ]]; } }※リクエストボディのキーチェックを行っていないもの(今回では
age
が該当)は、以下のいずれかを満たす必要があります。
- データベースの該当するカラムで、
NOT NULL制約
が外れているbindValue(":age",$post["age"]);
→bindValue(":age",$post["age"]??0);
のようにNULL演算子を用いてデフォルト値を設定するput
putメソッド:データの上書き
public function put($id=null):array { if(!$this->is_set($id)){ //(null or 空白)であればエラー(400) $this->code = 400; return ["error" => [ "type" => "invalid_url" ]]; } $original_data = $this->get($id); if(empty($original_data)){ //該当するデータがなければエラー(404) $this->code = 404; return ["error" => [ "type" => "not_in_sample" ]]; } $put = array_merge($original_data, $this->request_body); //元のデータにリクエストボディの値を上書き $db = new DB(); $sql = "UPDATE sample_table SET name=:name,age=:age WHERE id=:id"; $sth = $db->pdo()->prepare($sql); $sth->bindValue(":id",$id); $sth->bindValue(":name",$put["name"]); $sth->bindValue(":age",$put["age"]); $res = $sth->execute(); if($res){ return [$this->get($id)]; }else{ $this->code = 500; return ["error" => [ "type" => "fatal_error" ]]; } }delete
deleteメソッド:データの削除
public function delete($id=null):array { if(!$this->is_set($id)){ //(null or 空白)であればエラー(400) $this->code = 400; return ["error" => [ "type" => "invalid_url" ]]; } $db = new DB(); $sql = "DELETE FROM sample WHERE id = :id"; $sth = $db->pdo()->prepare($sql); $sth->bindValue(":id",$id); $res = $sth->execute(); if($res){ $this->code = 204; //処理は成功したが、返すデータがないことを示す return []; }else{ $this->code = 500; return ["error" => [ "type" => "fatal_error" ]]; } }options
CORSを利用したリクエストにおいて、単純なリクエスト以外のものは、プリフライトリクエストと呼ばれます。プリフライトリクエストでは、はじめに
OPTIONS
メソッドにより、そのリソースが利用可能かどうかを判断します。public function options():array { header("Access-Control-Allow-Methods: OPTIONS,GET,POST,PUT,DELETE"); header("Access-Control-Allow-Headers: Content-Type"); return []; }使用しないHTTPメソッドは、
Access-Control-Allow-Methods
から削除するようにしてください。
終わりに
今回使用したソースコードは、GitHubにあげています。
また、PHPを中心にした記事はこちらにも書いています。
- 投稿日:2021-03-03T16:11:48+09:00
PHPで簡単なアクセスログを取ろう!
今回作るもの
ユーザーがページにアクセスしたら
・日時
・ユーザーのIPアドレス
・ユーザーのホスト名
・ユーザーが利用しているポート番号
・アクセスされているページのURL
・リクエストメソッド
・遷移元URLあまり深く考えずに選びましたが、必要に応じて追加してください。
コード
log.php<?php //保存するフォルダ(相対パス) $directory='log/'; //日時取得 $time=date("Y/m/d H:i:s"); //ユーザーのipアドレスを取得 $user_id=$_SERVER["REMOTE_ADDR"]; if(!isset($_SERVER["REMOTE_HOST"])||$_SERVER["REMOTE_HOST"]==""){ $_SERVER["REMOTE_HOST"]=gethostbyaddr($_SERVER['REMOTE_ADDR']); } //ユーザーのホスト名を取得 $user_host=$_SERVER["REMOTE_HOST"]; //ユーザーのポートを取得 $user_port=$_SERVER["REMOTE_PORT"]; if(empty($_SERVER["HTTPS"])){ $url='http://'; }else{ $url='https://'; } //アクセス先URL取得 $url .=urldecode($_SERVER["HTTP_HOST"].$_SERVER["REQUEST_URI"]); $url='"'.$url.'"'; $request=$_SERVER["REQUEST_METHOD"]; //元URLを取得 if(!isset($_SERVER['HTTP_REFERER'])||$_SERVER['HTTP_REFERER']==""){ $motourl="empty"; }else{ $motourl = urldecode($_SERVER['HTTP_REFERER']); } $motourl='"'.$motourl.'"'; //ファイル出力 $fp = fopen($directory.date("Y_m_d").'.csv', "a"); //一応文字コード指定 $str = mb_convert_encoding($time.','.$user_id.','.$user_host.','.$user_port.','.$url.','.$request.','.$motourl."\n", "UTF-8"); fwrite($fp,$str); // ファイルを閉じる fclose($fp);読み込む
require('log.php');//ファイルへの相対パスは変更してくださいアクセスログを記録したいページにこのコードを記述すればいろんなところで使いまわすことができます
以上!
- 投稿日:2021-03-03T15:04:19+09:00
【Laravel】認証済みユーザーが"/register"にアクセスするとリダイレクトされるのはなぜか
初めに
タイトルの通りです。
RegisterController
コントローラーやRegisterUsers
トレイトばかりに目が向いていて、ミドルウェアという選択肢を全く考えられなかったことへの戒めに記事に残しておきます。前提
環境
- PHP
7.4.15
- Laravel
6.20.16
- laravel/ui
1.3.0
今回の認証機能は
laravel/ui
の以下のコマンドphp artisan ui:auth vueによって提供されるものをそのまま使いました。
結論
先に答えを述べて、それから何故それが答えになるのかの説明をしていきたいと思います。
結論を言いますと、
App\Http\Controllers\Auth\RegisterController
クラスのコンストラクタにapp/Http/Controllers/Auth/RegisterController.phppublic function __construct() { $this->middleware('guest'); }という処理によって、
guest
という名前のミドルウェアが適用されているからでした。
guest
ミドルウェアが追加されているコントローラは、認証済みユーザーからはアクセスできないようにミドルウェアによってガードされるようになります。ではこの処理がどのように動作しているかを見てみましょう。
ミドルウェア
guest
guest
ミドルウェアがどんな処理を行うかはApp\Http\Kernel
をみればわかります。
App\Http\Kernel
は
- 適用させるミドルウェアを登録したり
- グループ分けを行ったり
- 今回の
guest
のようにミドルウェアに対して個別に呼び出しを行えるように別名を付けたりといったミドルウェアに関する情報を扱っています。
App\Http\Kernel
クラスを見てみると、app/Http/kernel.php// 前略 protected $routeMiddleware = [ 'auth' => \App\Http\Middleware\Authenticate::class, 'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class, 'bindings' => \Illuminate\Routing\Middleware\SubstituteBindings::class, 'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class, 'can' => \Illuminate\Auth\Middleware\Authorize::class, 'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class, 'password.confirm' => \Illuminate\Auth\Middleware\RequirePassword::class, 'signed' => \Illuminate\Routing\Middleware\ValidateSignature::class, 'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class, 'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class, ]; // 後略
$routeMiddleware
プロパティに、[ミドルウェア名 => それの実装クラス]
というような形式の連想配列の形で、あるミドルウェアに対してどの実装クラスを実行するかの情報が保存されています。このプロパティ中の
guest
キーに対応するものは\App\Http\Middleware\RedirectIfAuthenticated
というクラスです。名前からして、認証済みであればリダイレクトを行うという処理を担っていることが明らかですね。
ではこのクラスの実装を見てみましょう。
RedirectIfAuthenticated
クラスの処理app/Http/Middleware/RedirectIfAuthenticated.php<?php namespace App\Http\Middleware; use App\Providers\RouteServiceProvider; use Closure; use Illuminate\Support\Facades\Auth; class RedirectIfAuthenticated { public function handle($request, Closure $next, $guard = null) { if (Auth::guard($guard)->check()) { return redirect(RouteServiceProvider::HOME); } return $next($request); } }コメントを省くとこれだけの処理です。
リクエストを受けたときに実行されるメソッドである
handle()
メソッドの中身を見てみましょう。まず
Auth::guard($guard)->check()
で既にユーザー情報があるかを判断しています。既にユーザー情報がある、つまりログイン済みであれば
return redirect(RouteServiceProvider::HOME)で
RouteServiceProvider::HOME
にリダイレクトする、という処理を行っています。この処理から、
App\Providers\RouteServiceProvider
クラスのHOME
定数を変更すればリダイレクト先を変更できるということもここから読み取れますね。ユーザー情報がまだ無ければこのミドルウェアでは何もせず、
$next($response)
によって次のミドルウェアへ処理を渡しています。以上のことから、確かにこのミドルウェアによって認証済みユーザーに対してのアクセスガードが実装されていることが分かりました。
終わりに
認証済みユーザーが
/register
にアクセスしたときにリダイレクトする処理は、guest
ミドルウェア、つまり\App\Http\Middleware\RedirectIfAuthenticated
クラスによって提供されていることが分かりました。ここでは各処理の詳細については深くは追わず、簡略化した説明となっています。
もし各処理の詳細(たとえば、ミドルウェアの実行される流れだったり、リダイレクト関数の実装)が気になる方がいらっしゃいましたら、ぜひ一度
laravel
のソースコードを深堀して追ってみるという経験をなさってみてはいかがでしょうか。
- 投稿日:2021-03-03T14:27:07+09:00
xdebug3 でデバッグする
いつのまにかxdebugが更新されて古い設定のままだと動かなくなってたのでiniファイルの変更点をざっくり
xdebug.ini
の変更点変更前(xdebug2)
確かこんな感じ
zend_extension=xdebug xdebug.remote_enable =1 xdebug.remote_autostart=1 xdebug.remote_host=host.docker.internal xdebug.remote_port=9000変更後(xdebug3)
動いた設定
zend_extension=xdebug xdebug.mode=debug xdebug.start_with_request=1 xdebug.client_host=host.docker.internal xdebug.client_port=9003デフォルトのポート番号も9000から9003に変更になっているようです。
IDE側を直すか、xdebug側を変えるかはお好みで。前提
使用している
Dockerfile
です。FROM php:8.0-fpm RUN pecl install xdebug \ && docker-php-ext-enable xdebug COPY docker-php-ext-xdebug.ini /usr/local/etc/php/conf.d/docker-php-ext-xdebug.ini参考
- 投稿日:2021-03-03T12:05:29+09:00
EUC-JPでpreg_replace()を使いたいので代替になる方法を見つけた
結論
配列を渡す機能は代替できないが、参照は(もっと柔軟な形で)代替できる。そう、
mb_ereg_replace_callback()
を使えばね。環境
PHP5.4.1以降、mbstringが有効になっている必要があります。
前提
PHPのマルチバイト文字対応は、UTF-8が前提になっているものがあります。
preg_replace()
関数もその一つです。この関数はマルチバイト文字に対応していますが、UTF-8以外の文字コードには対応していません。
しかし例えばEUC-JPなど、どうしても古い文字コードを使わなければならない環境はあると思います。
そんな中でこれの代替になりそうなのは、mb_ereg_replace()
ですが、これは配列渡しや参照(${0}
とかのやつです)が利用できないという弱点があります。
でも、UTF-8以外のマルチバイト文字で参照を利用したreplaceをやりたい!という方のための記事です。説明
UTF-8以外のマルチバイト文字で参照を利用したreplaceが出来ないかというと、そんなことなかったです。
mb_ereg_replace_callback()
がありました。
この関数は第2引数に関数をとることができます。その関数の引数に、第1引数の正規表現でマッチした部分が配列として入ります。これを利用すれば参照が使えます。しかも、関数の中で使えるのでもっと柔軟な形で利用できます。サンプルコード
<?php // 念のため利用するエンコーディングをEUC-JPに設定しておきます。 // php.iniの設定次第では、この行は要りません。 mb_internal_encoding("EUC-JP"); // echo mb_internal_encoding() . "\n"; $text = "あいうえお かきくけこ さしすせそ たちつてと\n"; $text .= "なにぬねの はひふへほ まみむめも やいゆえよ\n"; $text .= "らりるれろ わおん\n"; // パターンは正規表現ですが、スラッシュで挟む必要はありません。 // また、少なくともEUC-JPを用いた場合、.は改行文字も含むようです。 $pattern = "([^ \n]+)([ \n])"; // 第2引数は無名関数を使うことができます。 // これにより、コールバック関数としてだけ使いたい関数があちこちに散らばるのを抑制できます。 // もちろん名前をつけた関数を利用することもできます。 // その方法はPHPの公式マニュアルを読まれるといいでしょう。 $result = mb_ereg_replace_callback( $pattern, function ($matches) { if ($matches[2] === "\n") { return $matches[1] . "," . $matches[1] . $matches[2]; } else { return $matches[1] . "," . $matches[1] . ","; }; }, $text ); echo $text; echo "\n"; echo $result; // $resultの中身は以下です。 // あいうえお,あいうえお,かきくけこ,かきくけこ,さしすせそ,さしすせそ,たちつてと,たちつてと // なにぬねの,なにぬねの,はひふへほ,はひふへほ,まみむめも,まみむめも,やいゆえよ,やいゆえよ // らりるれろ,らりるれろ,わおん,わおん echo "\n";ここで、第2引数の関数の引数にマッチした部分が入ります。その添字と中身の関係は、通常の参照の関係と同じで、0にマッチした部分全体、1以降はサブパターンにマッチしたテキストが入ります。
まとめ
こんな感じで参照を使ったpreg_replace()の代替ができました。ちなみにpreg_replace()の方でコールバック関数を使いたいのならば、
preg_replace_callback()
があるようです。
どうでもいいですが、Qiitaって全角文字は等幅フォントじゃないのですね。この記事書いてて気付きました。参考文献
https://www.php.net/manual/ja/function.mb-ereg-replace-callback.php
https://www.php.net/manual/ja/function.mb-ereg-replace.php
https://www.php.net/manual/ja/function.preg-replace.php
https://www.php.net/manual/ja/function.preg-replace-callback.php
- 投稿日:2021-03-03T02:36:37+09:00
PrivateTmp に残ったファイルの処理
systemd が作成する PrivateTmp に、ユーザがアップロードするファイルが大量に残っていてOSの容量を圧迫していたので、適当に消すスクリプトを作成。
/root/remove_files_in_php_fpm_private_tmp.sh
#!/bin/bash PHP_FPM_PRIVATE_TEMP=$(find /tmp -type d -name "*php-fpm*") PHP_FPM_PRIVATE_TEMP="${PHP_FPM_PRIVATE_TEMP}/tmp" find "${PHP_FPM_PRIVATE_TEMP}" -type f -exec rm {} \; exit 0cron で適当に実行
# crontab -e ...(snip)... # removing php-fpm private temp files (not a directory) 5 * * * * /root/remove_files_in_php_fpm_private_tmp.sh >/dev/null ...(snip)...ちなみに、ディレクトリを消しちゃうと、アップロードができなくなる。
その場合は、php-fpm を再起動すると、PrivateTmp が再作成される。
- 投稿日:2021-03-03T02:06:00+09:00
フレームワークを使わないPHP+Javascript TIPS
サイトマップを作る
記載するほどでもないが、サイトマップを作るコード。
最終的にxmlファイルを生成するが、phpファイルのままでもいけるかもしれない。make_stemap.php$xml = '<?xml version="1.0" encoding="utf-8" ?>'."\n"; $xml .= '<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">'; foreach($array_movie_id as $value){ $xml .= "<url>"; $xml .= "<loc>https://~/".$value['key']."</loc>"; $get_date = $value['key2']; $mod_date = substr($get_date, 0, -9); $xml .= "<lastmod>".$mod_date."</lastmod>"; $xml .= "</url>"; } $xml .= "</urlset>"; <urlset> file_put_contents($_SERVER['DOCUMENT_ROOT'].'/xml/sitemap.xml', $xml);ページを複製する
通常1つのページを作成し、PPOSTで渡した値で表示を変えるのが通例だが、
SEOの観点から複数のページを作成したいときがある。元ファイルがJavascriptの場合
定型部分をJavascriptで記載し、incファイル化
ob_start()関数を使用。元ファイルがJavascriptなのでPHPの変数の影響を受けることなく記載可能。a.incコード内部はJavascriptで記載。b.phpデータベースに接続等 ob_start(); //バッファ制御スタート include("inc/a.inc"); $text = ob_get_clean(); //バッファ制御終了&変数を取得 $something = 可変部分 $something .= $text;元ファイルがphpの場合
複製元でデータベースに接続していたり、include関数を使っていたりすると複製するためのPHPの段階では上手く回っても、複製先ではphpファイルを読み込まない。
foreachの中でディレクトリも作成し、その中にコピーを作成。
a.php$path = '../directory'; foreach($array as $value){ if(in_array($value['key'], $file_name_all)){ }else{ mkdir($path."/".$value['key']); copy('b.php', $path."/".$value['key']."/index.php"); } } ?>$value[key]というディレクトリーの中にindex.phpというファイルが生成される。
indexと名前がつけばURLにディレクトリを入れるだけでそのページを表示するので、楽。ajaxによる非同期通信(jQueryを用いた場合)
毎回迷うので、備忘録もかねて。
a.php//データベースに書き込むためのphp $.ajax({ url: 'b.php', type: 'post', dataType: 'json', data: { // 送信データを指定。上記のURLに$_POST[~]として送信。 name: $('#name').val(),//jQueryの文法。id="name"のvalueを取得。 id: $('#id').val(), } })読み込みについては後日記載。
- 投稿日:2021-03-03T00:05:21+09:00
簡単なAPIをちょっとした工夫で完成度を高める話
PHPで簡単なAPIを作った話という記事を読みまして、自分ならどう改善していくかを考えました。
Step 1. 整形する
=
+
.
などの演算子の周りを空白で空けてあげます。これだけで何か見ためが綺麗めになります。<?php $front = $_GET['front']; $back = $_GET['back']; $result = $front + $back; $response['result'] = $result; $response['status'] = "true"; $response['formula'] = $front . '+' . $back; print json_encode($response, JSON_UNESCAPED_UNICODE);私はPHPを以下のように起動しました。
% php -S localhost:3939 easyapi.phpこの状態で http://localhost:3939/ にアクセスするとAPIが動作するという寸法です。
Step 2. 入力値を安全に受け取る
さて、この状態で何のクエリパラメータも渡さずにAPIにアクセスすると、このような表示1になってしまいます。
さあ、このようなエラー表示が出力に混じってしまうようではAPIとして役目を果たせません。実際にJSONとしてデコードできませんからね。
このような場合にどうするかは2通りの考えがあります。
- A: 値が渡されなかった場合は、0が渡されたということにして計算を続ける
- B: 値が渡されなかった場合は、計算できないのでエラーを返す
Aパターンの路線でいくならば、以下のように
?? 0
というものを付け足すだけです。<?php $front = $_GET['front'] ?? 0; $back = $_GET['back'] ?? 0; // 以下は同じこれはnull合体演算子という、なんだかかっちょいい名前のPHPの機能です。
// ↓ わざと長く書くと if (isset($_GET['front'])) { $front = $_GET['front']; } else { $front = 0; } // ↓ ちょっと短く書くと $front = isset($_GET['front']) ? $_GET['front'] : 0;ということで、
$front = $_GET['front'] ?? 0
と書くことで、すっきり書けるようになるのでした。これでめでたしなのでしょうか。……いいえ、 もっと悪いこと ができます。
?front=1&back=2
: 正常なパターン?front=&back=
: 空文字列を入力 = 異常?front=a&back=b
: アルファベットの文字を入力 = 異常?front[]=1&back=2
: 整数が入った配列と整数を入力 = 異常?front[]=1&back[]=2
: 整数が入った配列と整数を入力 = 異常? 正常?以上のようなクエリパラメータを入力すると、たとえば以下のような表示になります。
これではやはりAPIとしての仕事をまっとうできるとは言えませんよね。
APIは「値が渡されない」だけではなく、「意図しない形式の値が渡される」ことを必ず考えなければいけないのです。
それは特別なことではなく、靴を脱いだら揃えたり、用を足したら手を洗うくらい当たり前のことです。
別に洗わなくたって氏にはしない? あ、そう…というわけで、今後はBパターンで入力値検証に合格しなければ計算を進めない方針で進めます。
Step 3. 入力値を型安全に受け取る
@mpywの$_GET, $_POSTなどを受け取る際の処理 - Qiitaを読んできてください。読みましたね? おめでとうございます。これであなたもPHPマスターです。
復習しましょう。
filter_input()
またはfilter_var()
と検証フィルタを活用するのです。<?php $front = filter_input(INPUT_GET, 'front', FILTER_VALIDATE_INT); $back = filter_input(INPUT_GET, 'back', FILTER_VALIDATE_INT); if ($front === false || $front === null || $back === false || $back === null) { $response = [ 'result' => null, 'status' => false, 'formula' => '', ]; } else { $result = $front + $back; $response['result'] = $result; $response['status'] = "true"; $response['formula'] = $front . '+' . $back; } print json_encode($response, JSON_UNESCAPED_UNICODE);
FILTER_VALIDATE_INT
は入力値が整数かどうかを検証するフィルタです。もし小数として受け取りたければFILTER_VALIDATE_FLOAT
を使います。
$_GET['front']
のように値を取り出したときは文字列型(string)なのでしたが、filter_input(INPUT_GET, 'front', FILTER_VALIDATE_INT)
のように取り出すと整数型(int)に、filter_input(INPUT_GET, 'front', FILTER_VALIDATE_FLOAT)
のように取り出すと浮動小数点型(float)に値がキャストされるのでした。とても便利ですね。不正な値が渡された場合は
false
に、そもそも値が渡されなかった場合はnull
になるのでした。よって、このような判定になるのです。$front === false || $front === null || $back === false || $back === null
は冗長ですが、empty($front) || empty($back)
などと書くと0
も弾かれてしまいます。$is_unexpected_value = fn($v) => in_array($v, [false, null], true); if ($is_unexpected_value($front) || $is_unexpected_value($back)) { // ...もっとも、今回の場合は
int
,false
,null
の3バターンしかないので、以下のように書いた方が簡単でした。if (!is_int($front) || !is_int($back)) { // ...この条件は以下のように書き換えても構いません。
if (!(is_int($front) && is_int($back))) {これはド・モルガンの法則 - Wikipediaによるものです。私は算数がとても苦手ですが名前がおもしろいのでよく覚えています。
Step 4. 適切なHTTPヘッダを設定する
PHPはデフォルトでは
Content-type: text/html; charset=UTF-8
として取り扱うので、HTML以外のデータ形式を出力する場合はContent-Type
を適切にセットする必要があります。
header('Content-Type: application/json'); header('X-Content-Type-Options: nosniff'); echo json_encode($response, JSON_UNESCAPED_UNICODE);
- X-Content-Type-Options - HTTP | MDN
- 機密情報を含むJSONには X-Content-Type-Options: nosniff をつけるべき - 葉っぱ日記
- X-Content-Type-Options: nosniff はIE以外にも必要 – yohgaki's blog
これはWebブラウザの推測による予期しない振舞いを避けるためのものです。
細かいようですが、これらはWebからアクセスされうるAPIでは基本的に設定すべきものです。なぜ付ける必要があるのかわからなければ、必ず付けてください。
Step 5. 細かいところもこだわっていこう
神は細部に宿ります。
<?php declare(strict_types=1); error_reporting(E_ALL); $front = filter_input(INPUT_GET, 'front', FILTER_VALIDATE_INT); $back = filter_input(INPUT_GET, 'back', FILTER_VALIDATE_INT); $response = [ 'result' => null, 'status' => false, 'formula' => '', ]; if (!in_array($_SERVER['REQUEST_METHOD'], ['GET', 'HEAD'])) { http_response_code(405); } elseif (!is_int($front) || !is_int($back)) { http_response_code(400); } else { $response = [ 'result' => $front + $back, 'status' => true, 'formula' => "{$front} + {$back}", ]; } header('Content-Type: application/json'); header('X-Content-Type-Options: nosniff'); echo json_encode($response, JSON_UNESCAPED_UNICODE);
declare(strict_types=1);
- これを付けると関数呼び出し時に引数の型が間違ってると判定が厳しくなります
- ただし今回はPHPの内部関数しか使ってないので、あまり意味がないんですけど
error_reporting(E_ALL)
- これを付けるとNoticeなどのエラーも見逃されなくなります
- 今回はエラーが発生しないように作ってありますが、細かいエラーを握り潰さないという気概を示しておきます
- HTTPメソッドは
GET
とHEAD
以外は許可せず405
を返すことにします
- [405 Method Not Allowed - HTTP | MDN]
- 一般的なWeb APIはHTTPメソッドが固定されています
- フレームワークを使わないPHPではHTTPメソッドの判定が甘くなりがちです (https://developer.mozilla.org/ja/docs/Web/HTTP/Status/405)
HEAD
メソッドをサポートする必要があるかといえばないのですが、気分です- 意図したパラメータが渡されなかった場合は
400
を返すことにしますまとめ
今回は外部ライブラリやフレームワークなどを使わずにPHPで安全なAPIを実装するために気にするべきことをまとめました。これらは普段フレームワークを使って開発していたとしても、PHPを使っていれば基本的に気にするべき事柄です。残念なことにフレームワークを使うだけで即座に安全になるといった性格のものではありません。(フレームワークの機能をきちんと選択すれば避けられるかもしれませんが)
以下は読者への課題とします。
- 好きな個数の数を計算するAPIを実装する
- 好きな演算の種類を選んで実行時エラーを起こさずに計算できるAPIを実装する
- 使い慣れているフレームワークでAPIを実装してみて不正な入力にどう対応できるか確認する
私からは以上です。
おまけ: 内部関数と厳密な型検査
今回は
pow()
を再実装することで検証してみましょう。
<?php declare(strict_types=1); var_dump(pow('2', '10')); var_dump(mypow('2', '10')); function mypow(int|float $base, int|float $exp): int|float { return $base ** $exp; }動作確認してみましょう。
pow()
とmypow()
はdeclare(strict_types=1);
を外すと同じになります。
PHPに詳しいかたはすぐに気付くでしょうが、最新のPHP 8.0ではなく7.4とかいう古臭いバージョンを使っています ↩