コードロード

エラー討伐

【PHP】ある特定のAPIへリクエストすると勝手にリダイレクトされる

結論

リダイレクト時のステータスコード301 ではなく 302 を使う!

※もしくは明示的に指定しなければデフォルトで302になる

前提

一般ユーザーAでログインしているときに /user へリクエストすると、制限された情報を返すAPIがある。

一方、Adminユーザーでログインしているときに /user へリクエストすると、API側でAdminであれば /admin/user へリダイレクトさせる処理があった。

このときにリダイレクト処理を下記のようにしてしまっていた。

<?php

if ($user_id === ADMIN_USER_ID) {
    header("Location: /admin/user", true, 301);
    exit;
}

再現手順

  1. Adminユーザーでログインして /user へリクエストすると、API側で /admin/user にリダイレクトされる。(想定された挙動)
  2. Adminユーザーからログアウト
  3. 一般ユーザーAでログイン
  4. /user へリクエストすると、 /admin/user へなぜかリダイレクトされる。
  5. /admin/user 自体はAdminユーザーでないと認可していないので、 ステータスコード403 で返ってくる。(想定されていない挙動)
  6. 想定されたレスポンスが返ってこないのでエラーが出る。

原因

301 でリダイレクトさせるとブラウザ側でキャッシュしてしまうことが原因っぽい。

ちゃんとドキュメントに書いてあった。

www.php.net

A quick way to make redirects permanent or temporary is to make use of the $http_response_code parameter in header().

<?php

// 301 Moved Permanently

header("Location: /foo.php",TRUE,301);

// 302 Found

header("Location: /foo.php",TRUE,302);

header("Location: /foo.php");

// 303 See Other

header("Location: /foo.php",TRUE,303);

// 307 Temporary Redirect

header("Location: /foo.php",TRUE,307);

?>

The HTTP status code changes the way browsers and robots handle redirects, so if you are using header(Location:) it's a good idea to set the status code at the same time.  Browsers typically re-request a 307 page every time, cache a 302 page for the session, and cache a 301 page for longer, or even indefinitely.  Search engines typically transfer "page rank" to the new location for 301 redirects, but not for 302, 303 or 307. If the status code is not specified, header('Location:') defaults to 302.

参考

PHPで301リダイレクトする時の注意点。ブラウザのキャッシュで意図しないページにリダイレクトされる

【React】基礎のポイント振り返り

Reactの勉強記録。 こちらのトラハックさんのReact入門基礎編の振り返り

www.youtube.com

コンポーネント

見た目と機能を持つUI部品

大きく2つに分かれる

コンポーネントのいいところ

  • 再利用するため
    • 同じ記述を何度もする必要がない
  • コードの見通しをよくするため
    • 1コンポーネント=1ファイル
    • 責務を明確にする(何のためのパーツなのか)
    • 別ファイルに分ける
  • 変更に強くするため
    • 修正は1箇所

基本的な使い方

//App.jsx(親)
import Article from "./components/Article";

function App() {
    return (
        <div>
            <Article />
        </div>
    )
}

export default App;
// components/Article.jsx(子)
const Article = () => { // アロー関数バージョン
    return <h2>こんにちは</h2>
};

export default Article;

propsでデータを受け渡す

//App.jsx(親)
import Article from "./components/Article";

function App() {
    return (
        <div>
            <Article
                title={'勉強中'}
                content={'propsについて'}
            />
        </div>
    )
}

export default App;
// components/Article.jsx(子)
const Article = () => {
    return (
        <div>
            <h2>{props.title}</h2>
            <p>{props.content}</p>
        </div>
    )
};

export default Article;

#コンポーネントのimportとexport

JavaScriptのモジュール機能

  • プログラムをモジュールという単位に分割する
  • 原則、1ファイル=1モジュール
  • 必要なときに必要なモジュールのみ読み込む

default export(名前なしexport)

// アロー関数
const Title = (props) => {
    return <h2>{props.title}</h2>
};
export default Title;
// 名前付き関数
export default function Title(props) {
    return <h2>{props.title}</h2>
};
  • 推奨されるexport方法
  • 1ファイル=1export
  • 一度宣言したアロー関数をdefault export
  • 名前付き関数宣言と同時にdefault export

default import(名前なしimport)

// Article.jsx(export元)
const Article = (props) => {
    return (
        <div>
            <h2>{props.title}</h2>
            <p>{props.content}</p>
        </div>
    );
};
export default Article;
// App.jsx(import先)
import Article from "./components/Article";

function App() {
    return (
        <Article
            title={'タイトル'}
            content={'中身'}
        />
    );
};
  • default exportしたモジュールをそのまま読み込む

名前付きexport

// helper.js(1ファイルに便利関数が複数入っている)
export const addTax = (price) => {
    return Math.floor(price * 1.1)
}
export const getWild = () => {
    console.log('Get wild and touch')
}

// index.js
export {default as Article} from './Article'
export {default as Content} from './Content'
export {default as Title} from './Title'
// 別名(エイリアス)はなんでもいいけどファイル名にするのが一般的
  • 1ファイルから複数モジュールをexportしたいとき
  • Reactではエントリポイントでよく使う
  • エントリポイントでは別名exportも併用する

名前付きimport

// 直接コンポーネントを読み込むのではなく、エントリポイント(index.jsx)を通してimport
import {Content, Title} from "./index.jsx";

const Article = (props) => {
    return (
        <div>
            <Title title={props.title} />
            <Content content={props.content} />
        </div>
    );
};

export default Article;
  • 1ファイルから複数モジュールを読み込む
  • エントリポイントから複数コンポーネントを読み込む

コンポーネントの状態管理

Hooks

なぜstateを使うのか

  • Reactコンポーネント内の値を書き換えたい
    • ❌:DOMで直接書き換える
    • ⭕️:新しい値を使って再レンダリングさせる
  • Reactコンポーネントが再レンダリングするきっかけは?
    • stateが変更されたとき
    • propsが変更されたとき
      • 子から親にpropsを渡すことはできない
      • 親で更新用の関数を用意しておいて、子でその更新用の関数を実行して、親のpropsを変更させる

useStateの使い方

  1. userStatueによるstateの宣言
const [state, setState] = useState(initialState)
    現在の状態  更新関数                初期値
  1. stateの更新
setState(newState)
更新関数   新しい値
  1. 具体例
const [message, setMessage] = useState('test!!')
const [links, setLinkes] = useState(0)
const [isPublished, setIsPublished] = useState(false)
setIsPublished(true)

stateをpropsに渡す

// Article.jsx
const Article = (props) => {
    const [isPublished, setIsPublished] = useState(false)
    const publishArticle = () => {
        setIsPublished(true)
    }
    return (
        <div>
            <Title title={props.title} />
            <Content content={props.content} />
            <PublishButton isPublished={isPublished} onClick={publishArticle} />
        </div>
    );
};
// PublishButton.jsx
const PublishButton = (props) => {
    return (
        <button onClick={() => props.onClick()}>
            公開状態: {props.isPublished.toString()}
        </button>
    );
}
export default PublishButton;
  • 更新関数はそのままpropsとして渡さず関数化する
  • 関数をpropsに渡す時は注意する

propsへ関数を渡す際の注意点

  • OKな関数の渡し方
    • コールバック関数化関数自体を渡す
<PublishButton isPublished={isPublished} onClick={publishArticle} />

// コールバック関数
<PublishButton isPublished={isPublished} onClick={() => publishArticle()} />
  • NGな関数の渡し方(無限レンダリングが起こる)
    • propsに渡すときに関数を実行しない
// ()をつけて関数を実行してしまっている
<PublishButton isPublished={isPublished} onClick={publishArticle()} />

頻出するuseStateの使い方

引数を使って更新する

import React, {useState} from 'react';

const TextInput = () => {
    const [name, setName] = useState('')

    const handleName = (event) => {
        setName(event.target.value)
    }

    return (
        <input
            onChange={(event) => handelName(event)} // eventはonChangeイベントの戻り値
            type={'text'}
            value={name}
    />
    );
};
export default TextInput;
  • 入力フォームでよく使う
  • onChangeイベントでhandleName関数に渡す
  • handelName関数のパラメータであるeventを更新関数に渡す

prevStateを活用する

import React, {useState} from 'react';

const Counter = () => {
    const [count, setCount] = useState(0)
    const countUp = () => {
        setCount(prevState => prevState + 1)
    }
    const countDown = () => {
        setCount(prevState => prevState - 1)
    }

    return (
        <div>
            <p>現在のカウント数: {count}</p>
            <button onClick={countUp}>up</button>
            <button onClick={countDown}>down</button>
        </div>
    );
};
export default Counter;
  • useStateの更新関数で使える特殊はprevState
  • prevStateは更新前のstate
  • prevStateに変更を加えてreturn

こっちの書き方は良くない!

import React, {useState} from 'react';

const Counter = () => {
    const [count, setCount] = useState(0)
    const countUp = () => {
        setCount(count + 1)
    }
    const countDown = () => {
        setCount(count - 1)
    }

    return (
        <div>
            <p>現在のカウント数: {count}</p>
            <button onClick={countUp}>up</button>
            <button onClick={countDown}>down</button>
        </div>
    );
};
export default Counter;
  • countを直接書き換えている
  • 更新をしますとしてから、実際に更新されるまで多少のタイムラグが起こり、正しく変更されない(非同期で変更するため、実際に更新されるまでは前回の状態を参照している)

ON/OFFを切り替えるボタン

import React, {useState} from 'react';

const ToggleButton = () => {
    const [open, setOpen] = useState(false)
    const toggle = () => {
        setOpen(prevState => !prevState)
    }

    return (
        <button onClieck={toggle}>
            {open ? 'OPEN' : 'CLOSE'}
        </button>
    );
};
export default ToggleButton;
  • prevStateで受け取った値を ! で反転してreturnする
  • 三項演算子によってopenがtrue/falseで表示を切り替える

ライフサイクルと副作用(useEffect)

ライフサイクルとは

  • コンポーネントが生まれてから破棄されるまでの時間の流れ
  • ライフサイクルメソッドを使うと、時点に応じた処理を実行できる
  • Class Component時代は以下の3メソッドが頻出だった
    • componentDidMout()
    • componentDidUpdate()
    • componentWillUnmount()
  • Hooks時代はuseEffectでライフサイクルを表現

3種類のライフサイクル

副作用(effect)フックを使おう

constCounter = () => {
    const [count, setCount] = useState(0)

    const countUp = () => {
        setCount(prevState => prevState + 1)
    }

    const countDown = () => {
        setCount(prevState => prevState - 1)
    }

    // countというstateが変更されるたびに再レンダリング起きて、その度に起こされる処理
    useEffect(() => {
        console.log("Current count is...", count)
    })

    return (
        <div>
            <p>現在のカウント数: {count}</p>
            <button onClick={countUp}>up</button>
            <button onClick={countDown}>down</button>
        </div>
    );
};

第二引数の依存関係を理解する

// 毎回実行される
useEffect(() => {
    console.log("Current count is...", count)
})

// 初回レンダリング後のみ実行される
useEffect(() => {
    console.log("Current count is...", count)
}, [])

// triggerが変更されるたびに実行される
useEffect(() => {
    console.log("Current count is...", count)
}, [trigger])

// trigger1かtrigger2が変更されるたびに実行される
useEffect(() => {
    console.log("Current count is...", count)
}, [trigger1, trigger2])
  • useEffectの第二引数には配列を渡すことが可能
  • 第二引数はdeps(dependencies)と呼ばれ、副作用が引き起こされるどうかの依存関係となる

クリーンアップを理解する

const ToggleButton = () => {
    const [open, setOpen] = useState(false)
    const toggle = () => {
        setOpen(prevState => !prevState)
    }

    useEffect(() => {
        console.log("Current state is", open)
        if (open) {
            console.log("Subscribe database...")
        }
        return () => {
            console.log("Unsbscribe database!")
        }
    })

    return (
        <button onClieck={toggle}>
            {open ? 'OPEN' : 'CLOSE'}
        </button>
    );
};
  • コンポーネントないで外部データベースを購読したい
  • useEffectないで購読処理を呼び出す
  • 必要無くなったらクリーンアップ関数を使って掃除する
    • useEffectの中でreturnする

useEffect内でAPIを呼び出そう

useEffectのユースケース

  • APIやデータベースから非同期通信でデータを取得(fetch)する
  • 特定の値が変わったらデータを再取得(refetch)する

useEffect内で非同期通信

useEffect(() => {
    fetch(`https://api.github.com/users/${id}`)
        .then(res => res.json())
        .then(data => {
            console.log(data)
            setName(data.name)
        })
        .catch(error => {
            console.error(error)
        })
}, [id])
  • 初回レンダリング後に呼び出される
  • 第二引数に指定した値が変わるたびに再度呼び出される
  • 取得した値をuseStateの更新関数に渡す

【React】create-react-appするまでの環境構築手順

Reactの学習記録

こちらを使って勉強中。まずは環境構築手順の記録

youtu.be

はじめに

Create React App は React を学習するのに快適な環境であり、React で新しいシングルページアプリケーションを作成するのに最も良い方法です。

開発環境をセットアップして最新の JavaScript の機能を使えるようにし、快適な開発体験を提供し、そして本番環境用の最適化を行います。あなたのマシンに Node >= 14.0.0 及び npm >= 5.6 の環境が必要です。プロジェクトを作成するには次を実行します:

概要

  • create-react-appをするためにはnpmが必要
  • npmをインストールするためにはnodeが必要
  • nodeを使うにはnodebrewが必要
  • nodebrewをインストールするためにはhomebrewが必要 だからまずはmachomebrewをインストールするところから

手順

  1. Homebrewをインストール

     // インストールの確認
     % brew -v                         
     Homebrew 4.0.1
    
  2. nodebrewをインストール

     % brew install nodebrew
    
     % nodebrew -v 
     nodebrew 1.2.0
    
  3. nodeをインストール

     // インストールできるバージョン一覧を確認
     % nodebrew ls-remote
    
     // 安定バージョンをインストール
     % nodebrew install-binary stable
    
     // エラーが出てインストールできなければ
     % nodebrew setup
    
     // もう一度
     % nodebrew install-binary stable
    
     // インストールしたバージョンの確認
     % nodebrew ls
     v18.14.1
    
     current: none
    
     // インストールしたバージョンを使う
     % nodebrew use v18.14.1
    
     // もう一度確認 currentが設定されている
     % nodebrew ls
     v18.14.1
    
     current: v18.14.1
    
  4. nodeの環境パスを通す

     // パスを確認(シェルはzsh)
     % echo $SHELL
     /bin/zsh
    
     // パスを通す
     echo 'export PATH=$HOME/.nodebrew/current/bin:$PATH' >> ~/.zprofile
    
  5. 環境パスを通したら必ずターミナルを再起動

  6. nodeとnpmのバージョン確認。

     % node -v 
     v18.14.1
    
     % npm -v
     9.3.1
    
  7. create-react-app コマンドを使うのに 必要なバージョンを満たしているのでOK。

  8. 作業フォルダへ移動

     % cd workspace
    
  9. npm とは、ネットワーク上にある npm のコマンドを実行するということ

     % npx create-react-app react-basic
    
  10. ローカルサーバー起動

    % cd react-basic
    % npm start
    
  11. 自動で localhost3000 でブラウザが開いて無事完了!

【PHP】BCMath関数を使わないで正確に小数点同士を計算する

結論

X倍して整数にしてから、結果をXで割る。

事象

PHPで小数点以下を含む値の引き算だと、二進数表現の精度が落ちる影響で、期待した値にならないことがある。

<?php
echo(floor((0.1 + 0.7) * 10));
// 結果:7

8になるはずなのに、 floor() で小数点以下を切り捨てると、なぜか7になる。

これは 0.1 + 0.7 の結果が、精度の誤差で0.799999999999999933386618522491…となるかららしい。

対策

BCMathをインストールして、 bcsub() を使うと正しく計算できる、といろんな記事に書いてある。

でも、別途BCMathをインストールしてApachを再起動して〜てやると、少々面倒なのと影響範囲が大きかったので怖くて辞めた。PHPのバージョンも古かったので。

なので下記で対応。X倍して整数にしてから、結果をXで割る。

<?php
$a = (0.1 * 10) + (0.7 * 10);
echo($a / 10);
// 結果: 8

参考

rockstock2008.blog17.fc2.com

BCMathのインストール

ちなみに、別の環境でBCMathをインストールしたが、下記の記事が分かりやすかった。

saka24.blue

【PHP】セッション名にドットを含めて指定するとセッションが取得できなくなる

課題

セッション名を指定して session_start() すると、セッションが取得できなかったのでメモ。

結論

セッション名を指定するときに、「.」ドットは使わない!

セッション名を「.」ドットで繋いで session_start() すると、セッションを取得できない。

<?php
// lopgin.php

session_start([
  'name' => 'test.com.session'
]);

$_SESSION['user_id'] = $user_id;
<?php
// test.php

session_start([
  'name' => 'test.com.session'
]);

var_dump($_SESSION);
exit;

// array (size=0)
//  empty

やるなら「_」アンダーバーで繋ぐこと。

<?php
// lopgin.php

session_start([
  'name' => 'test_com_session'
]);

$_SESSION['user_id'] = $user_id;
<?php
// test.php

session_start([
  'name' => 'test_com_session'
]);

var_dump($_SESSION);
exit;

// array (size=1)
//  'user_id' => int 1

【PHP】CSV出力したいのにheader()関数が効かない

環境

やりたかったこと

PHPCSVを出力する機能を作っていて、レスポンスヘッダーを下記のように設定したかった。

Content-Disposition: attachment; filename=ファイル名.csv
Content-Type: text/csv

課題

下記のメソッドを使ってレスポンスしたのに、 header() で設定したはずのが反映されていない。

使用した関数

<?php
  /**
   * CSVダウンロード
   *
   * @param string $file_name
   * @param array $header
   * @param array $csv_list
   * @return void
   */
  public static function csvDownload($file_name, $header, $csv_list) {

    // 出力バッファをopen
    $stream = fopen('php://output', 'w');

    // 文字化け対策
    mb_convert_variables('SJIS-win', 'UTF-8', $header);

    // 列名を記入
    fputcsv($stream, $header);

    // 2行目以降を1行ずつストリームへ記入
    foreach ($csv_list as $line) {
        mb_convert_variables('SJIS-win', 'UTF-8', $line);
        fputcsv($stream, $line);
    }

    fclose($stream);

    // ファイルダウンロードさせるためにヘッダーを調整
    header("Content-Type: text/csv");
    header("Content-Disposition: attachment; filename=$file_name");
    readfile($file_name);
    exit;
  }

Postmanで確認したレスポンスヘッダ

解決策

先程のメソッドで、 header() 関数を使う順番を最初に持ってきた

<?php
/**
   * CSVダウンロード
   *
   * @param string $file_name
   * @param array $header
   * @param array $csv_list
   * @return void
   */
  public static function csvDownload($file_name, $header, $csv_list) {

    // ファイルダウンロードさせるためにヘッダーを調整
    header("Content-Type: text/csv");
    header("Content-Disposition: attachment; filename=$file_name");

    // 出力バッファをopen
    $stream = fopen('php://output', 'w');

    // 文字化け対策
    mb_convert_variables('SJIS-win', 'UTF-8', $header);

    // 列名を記入
    fputcsv($stream, $header);

    // 2行目以降を1行ずつストリームへ記入
    foreach ($csv_list as $line) {
        mb_convert_variables('SJIS-win', 'UTF-8', $line);
        fputcsv($stream, $line);
    }

    fclose($stream);

    readfile($file_name);
    exit;;
  }

すると、想定通りにレスポンスヘッダに反映されていた。

公式にも記載の通り、メソッド内でも先に書くようにということか?

覚えておいて頂きたいのは、header()  関数は、 通常の HTML タグまたは PHP からの出力にかかわらず、すべての実際の 出力の前にコールする必要があることです。 https://www.php.net/manual/ja/function.header

【Laravel】バリデーション失敗時に勝手にリダイレクトしないようにする(jsonを返させる)

課題

バリデーション失敗時に勝手に意図しないhtmlレスポンスを返す。

解決の過程

  1. ReactとLaravelで開発中に、データ更新時と登録時にたまに302レスポンスが返ってくる。
  2. コントローラまで処理が到達していない
  3. フォームリクエストで止まっているっぽい
  4. バリデーションに引っかかった時に302エラーを返しているよう
  5. ググったら、バリデーション失敗時は一個前のリクエストのページにリダイレクトしようとしているらしい。つまり、jsonで指定しているのにhtmlレスポンスになる。
  6. 勝手に前のページにリダイレクトしないようにしよう!

解決方法

Laravelはデフォルトで、バリデーション失敗時はリダイレクトされてHTMLレスポンスが返ってくるようになっているらしいので、このリダイレクトさせる処理をオーバーライドしてJSONが返ってくるようにする。

Illuminate\Foundation\Http\FormRequest クラスを継承した

app/Http/Requests/ApiRequest.phpを作成して、バリデーション失敗時の処理をオーバーライドさせる。

<?php

namespace App\Http\Requests;

use Illuminate\Contracts\Validation\Validator;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Http\Exceptions\HttpResponseException;

class ApiRequest extends FormRequest
{

    /**
     * @Override
     * 勝手にリダイレクトさせない
     * @param  \Illuminate\Contracts\Validation\Validator  $validator
     */
    protected function failedValidation(Validator $validator)
    {
        $data = [
            'message' => 'The given data was invalid.',
            'errors'  => $validator->errors()->toArray(),
        ];

        throw new HttpResponseException(response()->json($data, 200));
    }
}

使用例

フォームリクエストを使用する際は、デフォルトの FormRequest を継承するのではなく、上記のオーバーライドした ApiRequest を継承するようにする。

そうすれば、バリデーションに失敗してもJSONを返すようになる。

<?php

use App\Http\Requests\ApiRequest;  // 追記
#use Illuminate\Foundation\Http\FormRequest;  // 使用せず

class SampleRequest extends ApiRequest  // 継承先をデフォルトのFormRequestから変更
{
    /**
     * Determine if the user is authorized to make this request.
     *
     * @return bool
     */
    public function authorize()
    {
        return true;
    }

    /**
     * Get the validation rules that apply to the request.
     *
     * @return array
     */
    public function rules()
    {
        return [
            'name' => ['required'],
        ];
    }
}

参考

qiita.com

Laravel5.8 FormRequestでバリデーション失敗時にJsonResponseを返す - Qiita

FormRequestのバリデーションエラー時のリダイレクト先をカスタマイズする - Qiita

https://webxreal.com/laravel-validation/

blog.capilano-fw.com