2016年7月31日日曜日

AndroidでIncompatibleClassChangeErrorが発生した場合の対処。

オフィス狛 技術部です。

Android StudioのGradle設定で、dependencies・compileに設定している
外部プラグインのバージョンを最新にして、ビルド後にアプリを起動したら、以下のエラーが発生しました。

Caused by: java.lang.IncompatibleClassChangeError: The method 'java.io.File android.support.v4.content.ContextCompat.getNoBackupFilesDir(android.content.Context)' was expected to be of type virtual but instead was found to be of type direct (declaration of 'java.lang.reflect.ArtMethod' appears in /system/framework/core-libart.jar)
   at com.google.android.gms.iid.zzd.zzeb(Unknown Source)
   at com.google.android.gms.iid.zzd.(Unknown Source)
   at com.google.android.gms.iid.zzd.(Unknown Source)
   at com.google.android.gms.iid.InstanceID.zza(Unknown Source)
   at com.google.android.gms.iid.InstanceID.getInstance(Unknown Source)

「クラスの変換に互換性が無い」、「android.support.v4」というヒントがあります。
何より直前に行った「外部プラグインのバージョンを最新に」という作業が原因なのは間違いありません。

幸いにも、外部プラグインはソースも提供している物だったので、
早速、外部プラグイン側のGradleを見てみると・・・
dependencies {
    compile 'com.android.support:support-v4:24.0.0'
    compile 'com.android.support:appcompat-v7:24.0.0'
    compile 'com.android.support:recyclerview-v7:24.0.0'
    compile 'com.android.support:support-annotations:24.0.0'
}

となっています。
一方、本体のプロジェクトの方のSupport Libraryのバージョンは・・・
dependencies {
    compile 'com.android.support:support-v4:23.1.1'
    compile 'com.android.support:appcompat-v7:23.1.1'
    compile 'com.android.support:recyclerview-v7:23.1.1'
    compile 'com.android.support:support-annotations:23.1.1'
}

となっています。これが原因ですね。

プラグインの Support Library バージョンと、
本体のプロジェクトの Support Library バージョンが異なっていると、
今回のようなエラーが発生する可能性があります。

本体の方のバージョンを上げれば解決なのですが、
ちょっと影響範囲が不明なので、今回は、外部プラグインのバージョンを元に戻す、という事で対応しました。

一気にバージョン上げると、動かなくなる可能性があるので、
Support Library の更新は、定期的にやりましょう、という事ですね。

2016年7月30日土曜日

URLConnection(HttpURLConnection)と向き合おう~POSTメソッドでフォーム情報(application/x-www-form-urlencoded)を送信する~

オフィス狛 技術部です。

URLConnection(HttpURLConnection)と向き合うために、今回はPOSTメソッドを使います。
POSTはPOSTでも特にHTMLのフォームタグでPOSTを行ったときと同様のデータを送信します。

今回の目標

  • HttpURLConnectionを使ってPOSTメソッドを行う。
  • ContentTypeはapplication/x-www-form-urlencodedの形式にする。
  • POSTメソッドのレスポンスデータを取得する。

HttpURLConnectionを使ったPOSTのやり方

HttpURLConnection はGET/POSTに関係なくメソッドの呼び出し順序は大体決まっています。
順序は下記のとおりです。
  • ステップ1 :接続URLを決める。
  • ステップ2 :URLへのコネクションを取得する。
  • ステップ3 :接続設定(メソッドの決定,タイムアウト値,ヘッダー値等)を行う。
  • ステップ4 :コネクションを開く
  • ステップ5 :リクエストボディの書き込みを行う。(POSTのみ行う)
  • ステップ6 :レスポンスの読み出しを行う。
  • ステップ7 :コネクションを閉じる。

ステップ1:接続URLを決める。

接続先URLを決めるには java.net.URL を使います。
URL url = new URL("[接続先のURLを設定してください。]");

ステップ2:URLへのコネクションを取得する。

接続URLのコネクションを取得することができます。
URL url = [ステップ1:接続URLを決めるを参照してください];
HttpURLConnection urlConnection = null;
try {
    urlConnection = (HttpURLConnection) url.openConnection();
} catch (IOException e) {
    e.printStackTrace();
}

ステップ3:接続設定(メソッドの決定,タイムアウト値,ヘッダー値等)を行う。

今回はPOSTを使ったデータの送受信を行うので特に下記の設定を行う必要があります。
  • setRequestMethod を使いPOSTを使う設定をする
  • リクエストボディの Content-Type を application/x-www-form-urlencoded に設定する。
  • リクエストボディにデータを書き込むため、setDoOutput を使い許可をする。
  • レスポンスボディのデータを読み出しするため、setDoInput を使い許可をする。
//接続タイムアウトを設定する。
urlConnection.setConnectTimeout(100000);
//レスポンスデータ読み取りタイムアウトを設定する。
urlConnection.setReadTimeout(100000);
//ヘッダーにUser-Agentを設定する。
urlConnection.setRequestProperty("User-Agent", "Android");
//ヘッダーにAccept-Languageを設定する。
urlConnection.setRequestProperty("Accept-Language", Locale.getDefault().toString());
//ヘッダーにContent-Typeを設定する
urlConnection.addRequestProperty("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8");
//HTTPのメソッドをPOSTに設定する。
urlConnection.setRequestMethod("POST");
//リクエストのボディ送信を許可する
urlConnection.setDoOutput(true);
//レスポンスのボディ受信を許可する
urlConnection.setDoInput(true);

ステップ4:コネクションを開く

コネクションを設定した内容で接続し、実際にデータの送受信ができる状態にします。
このメソッド以降は「接続先にデータを送信する OutputStream 」と「接続先からデータを受信する InputStream 」を使うことができるようになります。
//ステップ4.コネクションを開く
urlConnection.connect();

ステップ5:リクエストボディの書き込みを行う。

「application/x-www-form-urlencoded」の送信データは「key1=value&key2=value...」形式の文字列を送信する必要があります。
また、key と value はそれぞれURLエンコードする必要があります。そのため Uri.Builder クラスを使い送信文字列を作成します。
書き込みが終わったら必ずストリームを閉じましょう。
//ステップ5:リクエストボディの書き出しを行う。
OutputStream outputStream = urlConnection.getOutputStream();
HashMap keyValues = new HashMap<>();
keyValues.put("key" , "value");
if (keyValues.size() > 0) {
    Uri.Builder builder = new Uri.Builder();
    //HashMapを[key=value]形式の文字列に変換する
    Set keys = keyValues.keySet();
    for (String key : keys) {
        //[key=value]形式の文字列に変換する。
        builder.appendQueryParameter(key , keyValues.get(key);
    }
    //[key=value&key=value…]形式の文字列に変換する。
    String join = builder.build().getEncodedQuery();
    PrintStream ps = new PrintStream(outputStream);
    ps.print(join);
    ps.close();
}
outputStream.close();

ステップ6:レスポンスボディの読み出しを行う。

主なレスポンスデータは下記のメソッドを使って取得します。
  • getResponseCode を使うことでHTTPステータスコードを取得できます。
  • getInputStream を使うことレスポンスボディのデータストリームを取得できます。

//ステップ6:レスポンスボディの読み出しを行う。
int statusCode = urlConnection.getResponseCode();

String responseData = "";
InputStream stream = urlConnection.getInputStream();
StringBuffer sb = new StringBuffer();
String line = "";
BufferedReader br = new BufferedReader(new InputStreamReader(stream, "UTF-8"));
while ((line = br.readLine()) != null) {
    sb.append(line);
}
try {
    stream.close();
} catch (Exception e) {
    e.printStackTrace();
}
responseData = sb.toString();
今回はレスポンスボディのデータストリームを文字列に変換しています。

ステップ7:コネクションを閉じる。

使ったコネクションはしっかりと閉じましょう。
//ステップ7:コネクションを閉じる。
urlConnection.disconnect();

サンプルコード

今回のサンプルコードの仕様は下記のとおりです。

  • URLとハッシュマップを引数で渡す。
  • 指定したURLにPOSTメソッドで接続する。
  • Content-Typeはapplication/x-www-form-urlencodeを使用する。
  • リクエストボディにハッシュマップからapplication/x-www-form-urlencodeに適合する文字列を生成し送信する。
  • レスポンスボディデータは文字列として扱う。
  • 非同期処理から呼び出されることを想定している。
  • 呼び出し側には文字列を返却する。
    public String execute(String argStrApiUrl, HashMap<String,string> formItems) {
        String ret = "";
        HttpURLConnection urlConnection = null;
        try {
            //ステップ1.接続URLを決める。
            URL url = new URL(argStrApiUrl);

            //ステップ2.URLへのコネクションを取得する。
            urlConnection = (HttpURLConnection) url.openConnection();

            //ステップ3.接続設定(メソッドの決定,タイムアウト値,ヘッダー値等)を行う。
            //接続タイムアウトを設定する。
            urlConnection.setConnectTimeout(100000);
            //レスポンスデータ読み取りタイムアウトを設定する。
            urlConnection.setReadTimeout(100000);
            //ヘッダーにUser-Agentを設定する。
            urlConnection.setRequestProperty("User-Agent", "Android");
            //ヘッダーにAccept-Languageを設定する。
            urlConnection.setRequestProperty("Accept-Language", Locale.getDefault().toString());
            //ヘッダーにContent-Typeを設定する
            urlConnection.addRequestProperty("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8");
            //HTTPのメソッドをPOSTに設定する。
            urlConnection.setRequestMethod("POST");
            //リクエストのボディ送信を許可する
            urlConnection.setDoOutput(true);
            //レスポンスのボディ受信を許可する
            urlConnection.setDoInput(true);

            //ステップ4.コネクションを開く
            urlConnection.connect();
            //ステップ5:リクエストボディの書き出しを行う。
            OutputStream outputStream = urlConnection.getOutputStream();
            if (formItems.size() > 0) {
                Uri.Builder builder = new Uri.Builder();
                Set keys  = formItems.keySet();
                for (String key : keys) {
                    //[key=value&key=value…]形式の文字列に変換する。
                    builder.appendQueryParameter(key , formItems.get(key));
                }
                String join = builder.build().getEncodedQuery();
                PrintStream ps = new PrintStream(outputStream);
                ps.print(join);
                ps.close();
            }
            outputStream.close();

            //ステップ6.レスポンスボディの読み出しを行う。
            int responseCode = urlConnection.getResponseCode();
            ret = convertToString(urlConnection.getInputStream());
            Log.d("execute", "URL:" + argStrApiUrl);
            Log.d("execute", "HttpStatusCode:" + responseCode);
            Log.d("execute", "ResponseData:" + ret);
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (urlConnection != null) {
                //7.コネクションを閉じる。
                urlConnection.disconnect();
            }
        }
        return ret;
    }

    public String convertToString(InputStream stream) throws IOException {
        StringBuffer sb = new StringBuffer();
        String line = "";
        BufferedReader br = new BufferedReader(new InputStreamReader(stream, "UTF-8"));
        while ((line = br.readLine()) != null) {
            sb.append(line);
        }
        try {
            stream.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return sb.toString();
    }

今回のまとめ

  • POSTメソッドではリクエストボディを使ったデータ送信を許可するために setDoOutput にtrueを渡すこと。
  • application/x-www-form-urlencoded に適合した送信文字列を作成するときは Uri.Builder クラスを使用すること。
  • テキストとファイルを同時に送信する Content-Type「multipart/form-data」とは異なるので注意すること。

2016年7月27日水曜日

URLConnection(HttpURLConnection)と向き合おう~GETメソッドでデータを取得する~

オフィス狛 技術部です。

URLConnection(HttpURLConnection)と向き合うためには、まずGETメソッドから始めましょう。
「HTTPにおけるGETメソッドとは何か?」はGoogleさんにお任せします。

今回の目標

  • HttpURLConnection を使ってGETメソッドを行う。
  • HTTP通信を行う際にクエリ文字列を使ってデータを付加する。
  • GETメソッドのレスポンスデータを取得する。

HttpURLConnection の使い方

HttpURLConnection は GET/POSTに関係なくメソッドの呼び出し順序は大体決まっています。
順序は下記のとおりです。

  • ステップ1 : 接続URLを決める。
  • ステップ2 : URLへのコネクションを取得する。
  • ステップ3 : 接続設定(メソッドの決定,タイムアウト値,ヘッダー値等)を行う。
  • ステップ4 : コネクションを開く
  • ステップ5 : リクエストボディの書き込みを行う。(POSTのみ行う)
  • ステップ6 : レスポンスの読み出しを行う。
  • ステップ7 : コネクションを閉じる。

ステップ1~3の操作は設定を行います、ステップ4の操作で実際にコネクションを開き、ステップ5以降は「接続先にデータを送信する OutputStream 」と「接続先からデータを受信する InputStream 」を使いデータの送受信を行います。
早速、順番に見ていきましょう。

ステップ1:接続URLを決める。

接続先URLを決めるにはjava.net.URLを使います。
また、今回はGETメソッドということでURLにクエリ文字列を付加する必要があります。
サンプルでは HashMap<String,String> のkey:value をそのままクエリ文字列に変換するようにしています。

String urlString = "[接続先のURLに書き換えてください。]";
HashMap<String,string> queryParams = new HashMap<>();
queryParams.put("key", "value");
URL url;
try {
    if (queryParams == null) {
        url = new URL(urlString);
    } else {
        Uri.Builder builder = new Uri.Builder();
        Set keys = queryParams.keySet();
        for (String key : keys) {
            builder.appendQueryParameter(key, queryParams.get(key));
        }
        url = new URL(urlString + builder.toString());
    }
} catch (IOException e) {
    e.printStackTrace();
}

ポイントはクエリ文字列の作成にはUri.Builderを使うことです。
このクラスを使うことでクエリ文字列を作ることが簡単になります。

本来ならURLクラスを使わずUri.Builderだけを使ってスキームやドメイン、パス等を指定すべきなのですが目標に関係なくコードが複雑化してしまうため上記の方法をとりました。

ステップ2:URLへのコネクションを取得する。

接続URLのコネクションを取得することができます。
URL url = [ステップ1:接続URLを決めるを参照してください];
HttpURLConnection urlConnection = null;
try {
    urlConnection = (HttpURLConnection) url.openConnection();
} catch (IOException e) {
    e.printStackTrace();
}
URL#openConnection はコネクションを開き通信可能状態になりそうな名前ですが、実際は開いただけで接続されていないため通信はできません。

ステップ3:接続設定(メソッドの決定,タイムアウト値,ヘッダー値等)を行う。

接続先URLとのコネクションを取得したので、通信内容について設定を行っていきます。
下記の項目を設定することができます。
  • setConnectTimeout を使うことで接続タイムアウトを設定できます。
  • setReadTimeou を使うことでレスポンスデータ読み取りタイムアウトを設定できます。
  • setRequestProperty を使うことでヘッダーに値を設定できます。
  • setRequestMethod を使うことでGETやPOSTなどのHTTPリクエストメソッドを設定できます。
  • setDoOutput を使うことでコネクション上でリクエストボディ送信の許可/不許可を設定できます。
  • setDoInput を使うことでコネクション上でレスポンスデータ受信の許可/不許可を設定できます。

HttpURLConnection urlConnection = [ステップ2:URLへのコネクションを取得するを参照してください];
//ステップ3:接続設定(メソッドの決定,タイムアウト値,ヘッダー値等)を行う。
//接続タイムアウトを設定する。
urlConnection.setConnectTimeout(100000);
//レスポンスデータ読み取りタイムアウトを設定する。
urlConnection.setReadTimeout(100000);
//ヘッダーにUser-Agentを設定する。
urlConnection.addRequestProperty("User-Agent", "Android");
//ヘッダーにAccept-Languageを設定する。
urlConnection.addRequestProperty("Accept-Language", Locale.getDefault().toString());
//HTTPのメソッドをGETに設定する。
urlConnection.setRequestMethod("GET");
//リクエストのボディ送信を許可しない
urlConnection.setDoOutput(false);
//レスポンスのボディ受信を許可をする
urlConnection.setDoInput(true);

setRequestMethod、setDoOutput、setDoInput の設定値の関係性について特に注意が必要です。
・デフォルト値について
   setRequestMethod はGET、setDoOutput はfalse、setDoInput はtureが設定されている状態になっています。
・setRequestMethod と setDoOutput について
   setRequestMethodでGETを指定した状態で、このメソッドにtrueを渡すとGETをPOSTに書き換えてしまいます。
確かにGETメソッドでリクエストボディは使わないのでその通りですが、特にメッセージもなく通信が失敗します。
この3つのメソッドは必ず呼ぶようにして、各設定値が矛盾を起こさないようにしましょう。

ステップ4:コネクションを開く

コネクションを設定した内容で接続し、実際にデータの送受信ができる状態にします。
このメソッド以降は「接続先にデータを送信する OutputStream 」と「接続先からデータを受信する InputStream 」を使うことができるようになります。
//ステップ4:コネクションを開く
urlConnection.connect();

ステップ5:リクエストボディの書き込みを行う。

GETメソッドではリクエストボディを送信しません、そのため OutputStream を取得してはいけません。
取得しようとすると例外が発生します。

ステップ6:レスポンスボディの読み出しを行う。

主なレスポンスデータは下記のメソッドを使って取得します。
  • getResponseCode を使うことでHTTPステータスコードを取得できます。
  • getInputStream を使うことレスポンスボディのデータストリームを取得できます。

//ステップ6:レスポンスボディの読み出しを行う。
int statusCode = urlConnection.getResponseCode();

String responseData = "";
InputStream stream = urlConnection.getInputStream();
StringBuffer sb = new StringBuffer();
String line = "";
BufferedReader br = new BufferedReader(new InputStreamReader(stream, "UTF-8"));
while ((line = br.readLine()) != null) {
    sb.append(line);
}
try {
    stream.close();
} catch (Exception e) {
    e.printStackTrace();
}
responseData = sb.toString();

今回はレスポンスボディのデータストリームを文字列に変換しています。

ステップ7:コネクションを閉じる。

使ったコネクションはしっかりと閉じましょう。
//ステップ7:コネクションを閉じる。
urlConnection.disconnect();
上記の7ステップで URLConnection を使ったGETメソッドを実装することができます。

サンプルコード

今回のサンプルコードの仕様は下記のとおりです。

  • URLとクエリ文字列を引数で渡し、クエリ文字列付きのURLでGETメソッドを実行する。
  • レスポンスボディデータは文字列として扱う。
  • 非同期処理から呼び出されることを想定している。
  • 呼び出し側には文字列を返却する。

    /**
     * GETメソッドで文字列を取得する。
     *
     * @param urlString   アクセスするURL
     * @param queryParams クエリパラメーター
     * @return レスポンス
     */
    public String execute(String urlString, HashMap<String,string> queryParams) {
        HttpURLConnection urlConnection = null;
        int responseCode = 0;
        String responseData = "";
        try {
            //ステップ1:接続URLを決める。
            URL url;
            if (queryParams == null) {
                url = new URL(urlString);
            } else {
                //クエリ文字列を付加する。
                Uri.Builder builder = new Uri.Builder();
                Set keys = queryParams.keySet();
                for (String key : keys) {
                    builder.appendQueryParameter(key, queryParams.get(key));
                }
                url = new URL(urlString + builder.toString());
            }
            //ステップ2:URLへのコネクションを取得する。
            urlConnection = (HttpURLConnection) url.openConnection();

            //ステップ3:接続設定(メソッドの決定,タイムアウト値,ヘッダー値等)を行う。
            //接続タイムアウトを設定する。
            urlConnection.setConnectTimeout(100000);
            //レスポンスデータ読み取りタイムアウトを設定する。
            urlConnection.setReadTimeout(100000);
            //ヘッダーにUser-Agentを設定する。
            urlConnection.setRequestProperty("User-Agent", "Android");
            //ヘッダーにAccept-Languageを設定する。
            urlConnection.setRequestProperty("Accept-Language", Locale.getDefault().toString());
            //HTTPのメソッドをGETに設定する。
            urlConnection.setRequestMethod("GET");
            //リクエストのボディ送信を許可しない
            urlConnection.setDoOutput(false);
             //レスポンスのボディ受信を許可する
            urlConnection.setDoInput(true);
            //ステップ4:コネクションを開く
            urlConnection.connect();

            //ステップ6:レスポンスボディの読み出しを行う。
            responseCode = urlConnection.getResponseCode();
            responseData = convertToString(urlConnection.getInputStream());
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (urlConnection != null) {
                //ステップ7:コネクションを閉じる。
                urlConnection.disconnect();
            }
        }
        Log.d("execute", "URL:" + urlString);
        Log.d("execute", "HttpStatusCode:" + responseCode);
        Log.d("execute", "ResponseData:" + responseData);
        return responseData;
    }

    public String convertToString(InputStream stream) throws IOException {
        StringBuffer sb = new StringBuffer();
        String line = "";
        BufferedReader br = new BufferedReader(new InputStreamReader(stream, "UTF-8"));
        while ((line = br.readLine()) != null) {
            sb.append(line);
        }
        try {
            stream.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return sb.toString();
    }

今回のまとめ

  • HttpURLConnection を使った場合、HTTPメソッドに関係なく7ステップを実装すること。
  • HTTPメソッドに合わせてsetDoOutputとsetDoInputを設定すること。

URLConnection(HttpURLConnection)と向き合おう

オフィス狛 技術部です。

 ApacheHTTPClient がAndroid 6.0で廃止されてからもう少しで1年ですね。

「useLibrary 'org.apache.http.legacy'」がいまだに残る Gradle。
取り消し線を表示し「早くこのコードを消してくれ」と訴えてくる AndroidStudio。

Android 7.0も出そうなので、そろそろ向き合おうと思います。
ApacheHTTPClitentを使ったHTTP通信」から「URLConnection(HttpURLConnection)を使ったHTTP通信」への乗り換えについて複数回に分けて書きます。

今回は ApacheHTTPClitent がしていてくれたことの確認を行います。
そもそも、ApacheHTTPClitent はHTTP通信を行う上で必要な機能を提供してくれるクラスです。
下記はその機能の一部です。
  • HTTPリクエストのリクエストデータ(ヘッダーやボディ)の設定方法の提供
  • HTTPリクエストの実行
  • レスポンスの取り扱い
  • クッキーの管理
  • 認証
  • コネクションの管理

一部機能を挙げましたが、多機能すぎるため保守性・拡張性・互換性に問題が出始めたようです。
これらの問題を解決するために軽量で柔軟な URLConnection が登場したわけです。

では HttpURLConnection は何をしてくれるのかというと、「HTTP通信に必要最低限の機能」を提供してくれます。
  • HTTPリクエストのリクエストデータ(ヘッダー)の設定方法の提供
  • HTTPリクエストの実行
  • 認証
  • コネクションの管理

一部機能を挙げましたが、特に「リクエストデータ(ボディ)の設定」と「レスポンスデータの解析」は自前で実装することになります。
 ApacheHTTPClitent が覆い隠していたリクエストとレスポンスの実装がプログラマーに提供されたような印象を受けます。

次回以降はサンプルコードを提示し、要点を説明していきます。
目次は下記のとおりです。
  • GETメソッドでデータを取得する。
  • POSTメソッドでフォーム情報(application/x-www-form-urlencoded)を送信する。
  • POSTメソッドでJSONを送信する。
  • POSTメソッドでファイルを送信する。
  • POSTメソッドでマルチパート情報(multipart/form-data)を送信する。
  • レスポンスの解析
  • Cookieの管理方法

今回のまとめ

  • ApacheHTTPClitent から URLConnectionへの乗り換えは必須です。
  • URLConnection ではリクエストデータとレスポンスデータに対する操作は、自前で実装する必要があります。

2016年7月23日土曜日

小石につまずくとけっこう痛い Codeigniter3 ファイル命名規約


オフィス狛 技術部です。

ブログになかなか手がつけられないなと思うこと早1年。。。
久々の投稿です。

弊社では、Android,iOS,PHP( Codeigniter ), Java , Node.js を用いて開発を行っていますが、
今回は PHP ( Codeigniter3 )で地味につまずいたことを書こうと思います。

まあそもそも

 Codeigniter は日本語の情報が少ない!!!

ということでよくつまずきます。
なので、自分の調べた備忘録も兼ねて、ここにつまずきろく(記録)をためていきます。

今回の地味につまづいたことは、

『ファイル名は大文字から始める。』

ということ。

 Codeigniter3 からの規約で本家サイトを見れば当たり前のように書いてあるのですが、
なぜか本番にデプロイするときまで気が付かず。

原因は、

Windowsはファイル名の大文字小文字を無視する(Macも)

という点。

 Codeigniter2 だとファイル名の規約はなかったので、
昔使っていたファイルをまんま利用するとファイル名の頭が小文字のままのものもあり、、、

Windowsでは何事もなく動く。
本番環境はLinux(AWS EC2 Amazon Linux)へデプロイ、
URLへアクセス。。。できずエラー!!!となります。(そんなファイルねぇよと怒られます)

↓のようなチェックツールもあるので、デプロイ前に一度チェックしてみると良いです。
https://github.com/kenjis/codeigniter3-filename-checker

足元の確認は大事ですね。

,