20200204のPHPに関する記事は13件です。

【PHP】headerでリダイレクトしよう

はじめに

開発はフレームワーク(Laravel)を使いまくり、その中身(PHP)のことはあんまりわかってなかったので、
その勉強も兼ねて、PHPだけでリダイレクト処理を作ってみたときに学んだことをまとめてみます。

headerとは

生の HTTP ヘッダを送信する関数。
要するに、リダイレクトさせることができるPHP標準の関数です。笑

使用例

例えば、フォームから投稿する時を考えます。
まず、投稿ページからフォームで_POSTを飛ばします。

create.php
<form action="save.php" method="POST">
    <input type="text" name="post" value="">
    <input type="submit" name="送信">
</form>

そして、送った先でDBに接続して、保存の処理を行います。
こちらについては、またいつか詳細をまとめたいと思います!!
そして、headerで一覧ページ(index.php)にリダイレクトします。

save.php
//DB接続して、保存の処理

header('Location: index.php');
exit;

雑ですが、このように使います。
ちなみにですが、exitは必須のようなので注意しましょう。

外部サイトへリダイレクト

下記のように使用すると、URLを指定し外部のサイトに飛ばすことも可能です。

header("Location: http://www.example.com/");
exit;

以上です!!!
ここまで読んでいただきありがとうございました!!
疑問、気になるところがございましたら、質問、コメントよろしくお願いします!!!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【黒歴史】未経験半年で作成したWebアプリを、実務半年経ったのでリファクタリングしてみた

皆さんは最初に書いたコード覚えていますでしょうか。

目的

18年の12月にPHPを勉強し始め、19年の3月頃からLaravelを勉強し始めました。
その後、19年の5月20日から6月15日までLaravelを使ったオリジナルアプリを作りました。

当時Twitterで行われている100DaysOfCodeというものを行っており
活動時間とかわかればいいなーと思い作りました。

作った当初は良いと思っていたのですが、エンジニアとして現場に出て半年、久々に見るとひどいなと思い
何がだめだったのか、供養がてらリファクタリングしてみようとやってみました。(※厳密にはリファクタリングではありません)

なお、ユーザーが全く居ないのでデータ移行は考慮してないです

もっとこう出来る等ありましたら、ぜひ教えていただけますと幸いです。

作ったもの

動作リンクはこちら

Topページ

  • Twitterで登録・ログインを押すと、ソーシャルログインが行われマイページへリダイレクトします

マイページ

  • 画面下部の入力欄に継続したい活動の名称を登録できます
  • 登録した活動が一覧表示されます

投稿ページ

  • 活動の内容や時間などを入力し、投稿するとツイートされ、活動時間、活動日数、継続日数が記録されます
  • 画像には映っていませんが、投稿を削除でき、削除するとツイートも消えます。

簡単にまとめると

  • トップページから、ログイン・登録をおすと、Twitter認証が行われる
  • ログイン後、マイページへ移動でき、そこで活動を作成
  • 作成した活動の進捗などをTwitterにつぶやく
  • 合計活動時間や継続日数などが記録されていく

というものです。

環境

当時のデプロイした環境は下記になります。

PHP 7.2
FW Laravel 5.5
MySQL 
ConohaVPS

リファクタリング

1.DB関係

ER図はこんな感じです。
スクリーンショット 2020-01-06 1.44.55.png

マイグレーションファイルは下記のようになっています。

Schema::create('users', function (Blueprint $table) {
    $table->increments('id');
    $table->string('twitter_id')->unique();
    $table->string('twitter_name')->nullable();
    $table->string('twitter_nickname')->nullable();
    $table->string('twitter_avatar')->nullable();
    $table->string('twitter_oauth_token')->nullable();
    $table->string('twitter_oauth_token_secret')->nullable();
    $table->rememberToken();
    $table->timestamps();
    $table->softDeletes();
});

Schema::table('users', function (Blueprint $table) {
    $table->string('twitter_id', 191)->change();
});

Schema::create('activities', function (Blueprint $table) {
    $table->increments('id');
    $table->integer('task_id')->nullable()->default(0);
    $table->integer('user_id');
    $table->string('name')->nullable()->default(null);
    $table->string('hour')->nullable()->default(0);
    $table->integer('continuation_days')->nullable()->default(0);
    $table->timestamps();
    $table->softDeletes();
});

Schema::table('activities', function (Blueprint $table) {
    $table->integer('days_of_activity')->nullable()->default(0);
});

Schema::create('tweets', function (Blueprint $table) {
    $table->increments('id');
    $table->string('user_id')->nullable();
    $table->integer('activity_id');
    $table->string('tweet_id');
    $table->text('body')->nullable();
    $table->string('hour')->nullable()->default(0);
    $table->timestamps();
    $table->softDeletes();
});

何がだめだったか

まず、ほんとうにそのカラムが必要なのか考えていませんでした
行き当たりばったりで作った記憶があります。
正規化もよく分かっていませんでした。

命名も
半年ぶりに見るとなんのカラムを表しているのかわかりませんでした。
tweetsテーブルがtweet_idを持っていたり、task_idという謎のカラムもあり自分で作ったくせに悩みました。

わかりやすい命名とコメントをつけるべきでした。
また下記のように、カラムにもコメントをつけるべきでした。

$table->integer('hoge')->comment('ほげ');

次に型です。
tweetsテーブルのもつuser_idhourがなぜかstring(varchar)だったりします。

インデックスと制約についてですが、特に考量していませんでした。
存在は知っていましたが付け方を知らなかった気がします。

修正後

スクリーンショット 2020-02-03 1.14.00.png

tweetsテーブルをpostsテーブルに変更しました。

また、活動内容を投稿するたびにactivitiesの持っていた合計時間格納用カラムに加算してましたが、postsからSQLで集計するようにしたため削除。
継続日数もロジックでなんとかなりそうだったので削除しました。

usersはidをtwitterのユーザーidにし、他のカラム名も変更しています。

その他、外部キーも設定し、その他検索で使うと思われるものにindexを貼っています(この小規模ので必要かはわかりませんが)。

2.ルーティング

web.php
Route::get('/', function () {
    return view('top', ['user' => Auth::user()]);
})->name('top');

Route::prefix('auth/twitter')->group(function () {
    // ログインURL
    Route::get('/', 'Auth\LoginController@redirectToProvider')->name('login');
    // コールバックURL
    Route::get('/callback', 'Auth\LoginController@handleProviderCallback');
    // ログアウトURL
    Route::post('logout', 'Auth\LoginController@logout')->name('logout');
});
Route::group(['middleware' => ['auth', 'user.name'], 'prefix' => 'activity'], function () {
    Route::get('{user_name}/', 'ActivityController@index')->name('activity.index');
    Route::post('{user_name}/', 'ActivityController@store')->name('activity.store');
    Route::get('{user_name}/{activity}', 'ActivityController@show')->name('activity.show');
    Route::patch('{user_name}/{activity}', 'ActivityController@update')->name('activity.tweet');
    Route::delete('{user_name}/{activity}', 'ActivityController@destroy')->name('activity.delete');
    Route::delete('{user_name}/{activity}/{id}', 'ActivityController@deleteTweet')->name('tweet.delete');
});

図で表すとこのようになります。
スクリーンショット 2020-01-06 3.20.47.png

何がだめだったか

もっとまとめられました。

修正後

web.php
<?php

Route::view('/', 'top')->name('top');

Route::group(['prefix' => 'auth/twitter'], function () {
    // ログインURL
    Route::get('/', 'Auth\LoginController@redirectToProvider')->name('login');
    // コールバックURL
    Route::get('/callback', 'Auth\LoginController@handleProviderCallback');
    // ログアウトURL
    Route::post('logout', 'Auth\LoginController@logout')->name('logout');
});
Route::group(['prefix' => '{user_name}', 'middleware' => ['auth', 'user.name']], function () {
    Route::resource('activity', 'ActivityController')->only(['index', 'store', 'show', 'update', 'destroy']);
    Route::resource('activity/{activity}/post', 'PostController')->only(['store', 'destroy']);
    Route::get('activity/{activity}/post/latest', 'PostController@fetchLatest')->name('post.latest');
});

topページはRoute::viewというものがあるので、それを用います。
また、Route::prefixgroupに変更
resourceでまとめられるものをまとめています。

投稿に関するものをActivityControllerで処理していたので(継続日数や活動時間を投稿のたびに、活動へアップデートしていた)
PostControllerへ切り離しました。

topで使用されているAuth::user()ですが、他のviewで共通して使用していたのでViewComposerを用いて自動的に必要なViewに渡すようにしています。

3.Controller

Controller共通

Controller内でバリデーションや登録等すべての処理をしていましたが、

  • FormRequestでバリデーション
  • コンストラクタインジェクションした、サービスクラスにバリデーション済みデータを渡す
  • 処理した結果を受け取る

上記のように変更しました。
Modelはリレーション、アクセサ、スコープを書く用にしています。

ActivityController.php
    /**
     * 活動の保存
     * @param  ActivityStoreRequest $request
     * @return \Illuminate\Http\RedirectResponse
     */
    public function store(ActivityStoreRequest $request)
    {
        $result = DB::transaction(function () use ($request) {
            return $this->activity_service->createActivity($request->validated());
        });
        return redirect()->back()->with($result ? 'success' : 'error', $result ? '新規追加しました' : '追加に失敗しました');
    }

Login処理

下記はTwitterの認証画面で承認された際のリダイレクトを処理するauth/twitter/callbackの部分です。

Auth/LoginController.php
    /**
     * ユーザーのアクセストークンを取得する
     *
     * @return \Illuminate\Http\Response
     */
    public function handleProviderCallback()
    {

        try {

            $twitter_user = Socialite::driver('twitter')->user();  
            //アクセストークン取得
            $token = $twitter_user->token;
            $token_secret = $twitter_user->tokenSecret;
            if (!$twitter_user || !$token || !$token_secret)
                throw new \RuntimeException('ユーザー取得の失敗');


            //ユーザーの取得または生成
            $user = User::firstOrCreate(['twitter_id' => $twitter_user->id]);
            //最新状態に更新
            $update_result = $user->update(
                [
                    'twitter_name' => $twitter_user->name,
                    'twitter_nickname' => $twitter_user->nickname,
                    'twitter_avatar' => $twitter_user->user['profile_image_url_https'],
                    'twitter_oauth_token' => $token,
                    'twitter_oauth_token_secret' => $token_secret
                ]
            );

            if (!$user || !$update_result)
                throw new \RuntimeException('ユーザー情報の生成または更新の失敗');

            Auth::login($user, true);
            return redirect()->route('activity.index', $twitter_user->nickname)->with('success', 'ログインしました');

        } catch (Exception $e) {
            Log::error($e->getMessage());
            return redirect()->route('top')->with('error', 'Twitterアカウント取得に失敗しました');
        }

        return redirect()->route('top')->with('erorr', 'エラーが発生しました。再度お試しください');
    }

何がだめだったか

まず、謎に通らない箇所にリダイレクト処理があります。
作成・更新がありますがトランザクションがありません。
また、取得できなかったら、作成、その後更新をしていますが、updateOrCreateで済みます。

Serviceに切り出したりしたらもっとスッキリできそうな気がするのですがこれで進みました。

Auth/LoginController.php
    /**
     * ユーザーのアクセストークンを取得、ログインをする
     *
     * @return \Illuminate\Http\Response
     */
    public function handleProviderCallback()
    {
        // twitterからアカウント情報取得
        try {
            $account_info = Socialite::driver('twitter')->user();
        } catch (\Exception $e) {
            Log::error($e->getMessage());
            return redirect()->route('top')->with('error', 'Twitterアカウント取得に失敗しました');
        }
        $auth_user = DB::transaction(function () use ($account_info) {
            $user = User::updateOrCreate(['id' => $account_info->id], [
                'name' => $account_info->name,
                'nickname' => $account_info->nickname,
                'avatar' => $account_info->avatar,
                'token' => $account_info->token,
                'token_secret' => $account_info->tokenSecret
            ]);
            Auth::login($user, true);
            return $user;
        });

        return redirect()->route('activity.index', $auth_user->nickname)->with('success', 'ログインしました');
    }

4.フロント周り

なにがだめだったか

もともとこのアプリはHTML、CSSは無料テンプレートのを使っていたのですが、
不必要なライブラリなどたくさん読み込んでいました。
しかも、minify化(コードの圧縮化)がされていないものでした。

また、jQueryを使っていたのですが、
Bladeにベタ書きしており、更にBlade自体コンポーネントへ分割していなかったので非常に見づらくなっていました。

ライブラリ類はnpmからインストールし、
Laravel Mixを用いてjsのコードを圧縮しました。

Bladeも@push@include@componentを使い切りだせそうなものは切り出しました
スクリーンショット 2020-02-03 20.44.23.png

5.Log

これは未だにどうするのが良いのか、はっきりと分かってはいません
もともとは、エラー箇所にLogを書き込む処理おいているだけでした。

今回は
Eloquentは作成や更新などイベントが起こるとそれをフックに、そのタイミングで処理ができるので、そこにLog処理を置きました。

下記のようにすると、Userが作成されたタイミングでLogが書かれます

php artisan make:observer UserObserver --model=User
UserObserver.php
<?php

namespace App\Observers;

use App\User;
use Illuminate\Support\Facades\Log;

class UserObserver
{
    /**
     * Handle the user "created" event.
     *
     * @param  \App\User  $user
     * @return void
     */
    public function created(User $user)
    {
        Log::info('ユーザーが新規作成されました', ['user' => $user->nickname]);
    }
}

6.最適化

一番最初、完成した後やったことは
.envAPP_ENVproductionにし、APP_DEBUGfalseにしただけでした。

なにがだめだったか

公式にかかれている最適化処理があるのに関わらず、行っていませんでした。

オートローダー最適化

composer install --optimize-autoloader --no-dev

設定ローディングの最適化

php artisan config:cache

ルートロードの最適化

php artisan route:cache

その他の最適化として、Opcacheの有効化を行いました。

OPcacheとは

スクリプト言語は毎回呼び出される度にプログラムの解析・解釈が行われています。 コンパイル型の言語に比べてスクリプト言語は、「呼び出し毎にプログラムを解析する」点にオーバーヘッドがあると言えます。

スクリプト言語においても、ソースコードが変更されていないのであればコンピュータが解釈をし直す必要がないのではないかと考えられます。 よって、コンピュータが解析済みの「バイトコード」をキャッシュして再利用すればよく、そういう機能をアクセラレータとよびます。 PHPのアクセラレータ機能を利用することでPHPを解釈する段取りをスキップすることができ、処理が高速になる

他に、HTMLの圧縮を行いました。
htmlmin/htmlminというパッケージを使用し、Bladeからコンパイルされたファイルを圧縮しています。
自身で作ったディレクティブ(@hogeのようなもの)があると、うまく使えません。

その他個人的な

ツイートの文字数カウント

今まで、jsでkeyUpのたびにカウントする、Laravel側はmax:140のバリデーションというガバルールだったのを
twitter-text-jsを使って、Twitterと同様のカウントにしました。
Laravelも同様にパッケージを使いTwitterと同様のルールでバリデーションできるようにしました。
js:詳しくはこちら
Laravel:詳しくはこちら

継続日数のカウント

今まで投稿するたびに、合計時間も活動日数も継続日数もいちいち計算して活動(activities)のカラムへ保存していました。
投稿から取得して計算するように変更したところ、コード量がごっそり減りました。

Before
SC000019.JPG

After
SC000020.JPG

おわりに

半年前のコードが完全に他人のもので、想像以上に時間がかかりました。
頭の中で考えてとりあえず作るということのデメリットが享受できました。
今後は事前の設計をしっかりして、物を作りたいです。

また半年したらこのコードもだめだなと思えるような成長を遂げていたいものです。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

未経験半年で作成したWebアプリを、実務半年経ったのでリファクタリングしてみた

皆さんは最初に書いたコード覚えていますでしょうか。

目的

18年の12月にPHPを勉強し始め、19年の3月頃からLaravelを勉強し始めました。
その後、19年の5月20日から6月15日までLaravelを使ったオリジナルアプリを作りました。

当時Twitterで行われている100DaysOfCodeというものを行っており
活動時間とかわかればいいなーと思い作りました。

作った当初は良いと思っていたのですが、エンジニアとして現場に出て半年、久々に見るとひどいなと思い
何がだめだったのか、供養がてらリファクタリングしてみようとやってみました。(※厳密にはリファクタリングではありません->若干挙動に修正が入っているため)

なお、ユーザーが全く居ないのでデータ移行は考慮してないです

もっとこう出来る等ありましたら、ぜひ教えていただけますと幸いです。

作ったもの

動作リンクはこちら

Topページ

  • Twitterで登録・ログインを押すと、ソーシャルログインが行われマイページへリダイレクトします

マイページ

  • 画面下部の入力欄に継続したい活動の名称を登録できます
  • 登録した活動が一覧表示されます

投稿ページ

  • 活動の内容や時間などを入力し、投稿するとツイートされ、活動時間、活動日数、継続日数が記録されます
  • 画像には映っていませんが、投稿を削除でき、削除するとツイートも消えます。

簡単にまとめると

  • トップページから、ログイン・登録をおすと、Twitter認証が行われる
  • ログイン後、マイページへ移動でき、そこで活動を作成
  • 作成した活動の進捗などをTwitterにつぶやく
  • 合計活動時間や継続日数などが記録されていく

というものです。

環境

当時のデプロイした環境は下記になります。

PHP 7.2
FW Laravel 5.5
MySQL 
ConohaVPS

リファクタリング

1.DB関係

ER図はこんな感じです。
スクリーンショット 2020-01-06 1.44.55.png

マイグレーションファイルは下記のようになっています。

Schema::create('users', function (Blueprint $table) {
    $table->increments('id');
    $table->string('twitter_id')->unique();
    $table->string('twitter_name')->nullable();
    $table->string('twitter_nickname')->nullable();
    $table->string('twitter_avatar')->nullable();
    $table->string('twitter_oauth_token')->nullable();
    $table->string('twitter_oauth_token_secret')->nullable();
    $table->rememberToken();
    $table->timestamps();
    $table->softDeletes();
});

Schema::table('users', function (Blueprint $table) {
    $table->string('twitter_id', 191)->change();
});

Schema::create('activities', function (Blueprint $table) {
    $table->increments('id');
    $table->integer('task_id')->nullable()->default(0);
    $table->integer('user_id');
    $table->string('name')->nullable()->default(null);
    $table->string('hour')->nullable()->default(0);
    $table->integer('continuation_days')->nullable()->default(0);
    $table->timestamps();
    $table->softDeletes();
});

Schema::table('activities', function (Blueprint $table) {
    $table->integer('days_of_activity')->nullable()->default(0);
});

Schema::create('tweets', function (Blueprint $table) {
    $table->increments('id');
    $table->string('user_id')->nullable();
    $table->integer('activity_id');
    $table->string('tweet_id');
    $table->text('body')->nullable();
    $table->string('hour')->nullable()->default(0);
    $table->timestamps();
    $table->softDeletes();
});

何がだめだったか

まず、ほんとうにそのカラムが必要なのか考えていませんでした
行き当たりばったりで作った記憶があります。
正規化もよく分かっていませんでした。

命名も
半年ぶりに見るとなんのカラムを表しているのかわかりませんでした。
tweetsテーブルがtweet_idを持っていたり、task_idという謎のカラムもあり自分で作ったくせに悩みました。

わかりやすい命名とコメントをつけるべきでした。
また下記のように、カラムにもコメントをつけるべきでした。

$table->integer('hoge')->comment('ほげ');

次に型です。
tweetsテーブルのもつuser_idhourがなぜかstring(varchar)だったりします。

インデックスと制約についてですが、特に考量していませんでした。
存在は知っていましたが付け方を知らなかった気がします。

修正後

スクリーンショット 2020-02-03 1.14.00.png

tweetsテーブルをpostsテーブルに変更しました。

また、活動内容を投稿するたびにactivitiesの持っていた合計時間格納用カラムに加算してましたが、postsからSQLで集計するようにしたため削除。
継続日数もロジックでなんとかなりそうだったので削除しました。

usersはidをtwitterのユーザーidにし、他のカラム名も変更しています。

その他、外部キーも設定し、その他検索で使うと思われるものにindexを貼っています(この小規模ので必要かはわかりませんが)。

2.ルーティング

web.php
Route::get('/', function () {
    return view('top', ['user' => Auth::user()]);
})->name('top');

Route::prefix('auth/twitter')->group(function () {
    // ログインURL
    Route::get('/', 'Auth\LoginController@redirectToProvider')->name('login');
    // コールバックURL
    Route::get('/callback', 'Auth\LoginController@handleProviderCallback');
    // ログアウトURL
    Route::post('logout', 'Auth\LoginController@logout')->name('logout');
});
Route::group(['middleware' => ['auth', 'user.name'], 'prefix' => 'activity'], function () {
    Route::get('{user_name}/', 'ActivityController@index')->name('activity.index');
    Route::post('{user_name}/', 'ActivityController@store')->name('activity.store');
    Route::get('{user_name}/{activity}', 'ActivityController@show')->name('activity.show');
    Route::patch('{user_name}/{activity}', 'ActivityController@update')->name('activity.tweet');
    Route::delete('{user_name}/{activity}', 'ActivityController@destroy')->name('activity.delete');
    Route::delete('{user_name}/{activity}/{id}', 'ActivityController@deleteTweet')->name('tweet.delete');
});

図で表すとこのようになります。
スクリーンショット 2020-01-06 3.20.47.png

何がだめだったか

もっとまとめられました。

修正後

web.php
<?php

Route::view('/', 'top')->name('top');

Route::group(['prefix' => 'auth/twitter'], function () {
    // ログインURL
    Route::get('/', 'Auth\LoginController@redirectToProvider')->name('login');
    // コールバックURL
    Route::get('/callback', 'Auth\LoginController@handleProviderCallback');
    // ログアウトURL
    Route::post('logout', 'Auth\LoginController@logout')->name('logout');
});
Route::group(['prefix' => '{user_name}', 'middleware' => ['auth', 'user.name']], function () {
    Route::resource('activity', 'ActivityController')->only(['index', 'store', 'show', 'update', 'destroy']);
    Route::resource('activity/{activity}/post', 'PostController')->only(['store', 'destroy']);
    Route::get('activity/{activity}/post/latest', 'PostController@fetchLatest')->name('post.latest');
});

topページはRoute::viewというものがあるので、それを用います。
また、Route::prefixgroupに変更
resourceでまとめられるものをまとめています。

投稿に関するものをActivityControllerで処理していたので(継続日数や活動時間を投稿のたびに、活動へアップデートしていた)
PostControllerへ切り離しました。

topで使用されているAuth::user()ですが、他のviewで共通して使用していたのでViewComposerを用いて自動的に必要なViewに渡すようにしています。

3.Controller

Controller共通

Controller内でバリデーションや登録等すべての処理をしていましたが、

  • FormRequestでバリデーション
  • コンストラクタインジェクションした、サービスクラスにバリデーション済みデータを渡す
  • 処理した結果を受け取る

上記のように変更しました。
Modelはリレーション、アクセサ、スコープを書く用にしています。

ActivityController.php
    /**
     * 活動の保存
     * @param  ActivityStoreRequest $request
     * @return \Illuminate\Http\RedirectResponse
     */
    public function store(ActivityStoreRequest $request)
    {
        $result = DB::transaction(function () use ($request) {
            return $this->activity_service->createActivity($request->validated());
        });
        return redirect()->back()->with($result ? 'success' : 'error', $result ? '新規追加しました' : '追加に失敗しました');
    }

Login処理

下記はTwitterの認証画面で承認された際のリダイレクトを処理するauth/twitter/callbackの部分です。

Auth/LoginController.php
    /**
     * ユーザーのアクセストークンを取得する
     *
     * @return \Illuminate\Http\Response
     */
    public function handleProviderCallback()
    {

        try {

            $twitter_user = Socialite::driver('twitter')->user();  
            //アクセストークン取得
            $token = $twitter_user->token;
            $token_secret = $twitter_user->tokenSecret;
            if (!$twitter_user || !$token || !$token_secret)
                throw new \RuntimeException('ユーザー取得の失敗');


            //ユーザーの取得または生成
            $user = User::firstOrCreate(['twitter_id' => $twitter_user->id]);
            //最新状態に更新
            $update_result = $user->update(
                [
                    'twitter_name' => $twitter_user->name,
                    'twitter_nickname' => $twitter_user->nickname,
                    'twitter_avatar' => $twitter_user->user['profile_image_url_https'],
                    'twitter_oauth_token' => $token,
                    'twitter_oauth_token_secret' => $token_secret
                ]
            );

            if (!$user || !$update_result)
                throw new \RuntimeException('ユーザー情報の生成または更新の失敗');

            Auth::login($user, true);
            return redirect()->route('activity.index', $twitter_user->nickname)->with('success', 'ログインしました');

        } catch (Exception $e) {
            Log::error($e->getMessage());
            return redirect()->route('top')->with('error', 'Twitterアカウント取得に失敗しました');
        }

        return redirect()->route('top')->with('erorr', 'エラーが発生しました。再度お試しください');
    }

何がだめだったか

まず、謎に通らない箇所にリダイレクト処理があります。
作成・更新がありますがトランザクションがありません。
また、取得できなかったら、作成、その後更新をしていますが、updateOrCreateで済みます。

Serviceに切り出したりしたらもっとスッキリできそうな気がするのですがこれで進みました。

Auth/LoginController.php
    /**
     * ユーザーのアクセストークンを取得、ログインをする
     *
     * @return \Illuminate\Http\Response
     */
    public function handleProviderCallback()
    {
        // twitterからアカウント情報取得
        try {
            $account_info = Socialite::driver('twitter')->user();
        } catch (\Exception $e) {
            Log::error($e->getMessage());
            return redirect()->route('top')->with('error', 'Twitterアカウント取得に失敗しました');
        }
        $auth_user = DB::transaction(function () use ($account_info) {
            $user = User::updateOrCreate(['id' => $account_info->id], [
                'name' => $account_info->name,
                'nickname' => $account_info->nickname,
                'avatar' => $account_info->avatar,
                'token' => $account_info->token,
                'token_secret' => $account_info->tokenSecret
            ]);
            Auth::login($user, true);
            return $user;
        });

        return redirect()->route('activity.index', $auth_user->nickname)->with('success', 'ログインしました');
    }

4.フロント周り

なにがだめだったか

もともとこのアプリはHTML、CSSは無料テンプレートのを使っていたのですが、
不必要なライブラリなどたくさん読み込んでいました。
しかも、minify化(コードの圧縮化)がされていないものでした。

また、jQueryを使っていたのですが、
Bladeにベタ書きしており、更にBlade自体コンポーネントへ分割していなかったので非常に見づらくなっていました。

ライブラリ類はnpmからインストールし、
Laravel Mixを用いてjsのコードを圧縮しました。

Bladeも@push@include@componentを使い切りだせそうなものは切り出しました
スクリーンショット 2020-02-03 20.44.23.png

5.Log

これは未だにどうするのが良いのか、はっきりと分かってはいません
もともとは、エラー箇所にLogを書き込む処理おいているだけでした。

今回は
Eloquentは作成や更新などイベントが起こるとそれをフックに、そのタイミングで処理ができるので、そこにLog処理を置きました。

下記のようにすると、Userが作成されたタイミングでLogが書かれます

php artisan make:observer UserObserver --model=User
UserObserver.php
<?php

namespace App\Observers;

use App\User;
use Illuminate\Support\Facades\Log;

class UserObserver
{
    /**
     * Handle the user "created" event.
     *
     * @param  \App\User  $user
     * @return void
     */
    public function created(User $user)
    {
        Log::info('ユーザーが新規作成されました', ['user' => $user->nickname]);
    }
}

6.最適化

一番最初、完成した後やったことは
.envAPP_ENVproductionにし、APP_DEBUGfalseにしただけでした。

なにがだめだったか

公式にかかれている最適化処理があるのに関わらず、行っていませんでした。

オートローダー最適化

composer install --optimize-autoloader --no-dev

設定ローディングの最適化

php artisan config:cache

ルートロードの最適化

php artisan route:cache

その他の最適化として、Opcacheの有効化を行いました。

OPcacheとは

スクリプト言語は毎回呼び出される度にプログラムの解析・解釈が行われています。 コンパイル型の言語に比べてスクリプト言語は、「呼び出し毎にプログラムを解析する」点にオーバーヘッドがあると言えます。

スクリプト言語においても、ソースコードが変更されていないのであればコンピュータが解釈をし直す必要がないのではないかと考えられます。 よって、コンピュータが解析済みの「バイトコード」をキャッシュして再利用すればよく、そういう機能をアクセラレータとよびます。 PHPのアクセラレータ機能を利用することでPHPを解釈する段取りをスキップすることができ、処理が高速になる

他に、HTMLの圧縮を行いました。
htmlmin/htmlminというパッケージを使用し、Bladeからコンパイルされたファイルを圧縮しています。
自身で作ったディレクティブ(@hogeのようなもの)があると、うまく使えません。

その他個人的な

ツイートの文字数カウント

今まで、jsでkeyUpのたびにカウントする、Laravel側はmax:140のバリデーションというガバルールだったのを
twitter-text-jsを使って、Twitterと同様のカウントにしました。
Laravelも同様にパッケージを使いTwitterと同様のルールでバリデーションできるようにしました。
js:詳しくはこちら
Laravel:詳しくはこちら

継続日数のカウント

今まで投稿するたびに、合計時間も活動日数も継続日数もいちいち計算して活動(activities)のカラムへ保存していました。
投稿から取得して計算するように変更したところ、コード量がごっそり減りました。

Before
SC000019.JPG

After
SC000020.JPG

おわりに

半年前のコードが完全に他人のもので、想像以上に時間がかかりました。
頭の中で考えてとりあえず作るということのデメリットが享受できました。
今後は事前の設計をしっかりして、物を作りたいです。

また半年したらこのコードもだめだなと思えるような成長を遂げていたいものです。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

『 Laravel DB.com 』コード書かずにCRUD作成【最新版 2020/02/01 〜利用方法まとめ〜】

「 Laravel DB.com 」 、利用方法まとめ

2020年2月1日リニューアルしたので、使用方法をまとめたいと思います。
en.jpg

Laravel DB.com ってなに?

「LaravelDB.com」は、ER図(テーブルの設計)を作成することで、”Migration” OR ”CRUD(テストデータ)”のコードまで生成できるサービス(ツール)です。

確認したLaravel動作環境

  • 開発環境: Laravel5.5 ~ 6.x / PHP7.2.1 / MySQL5.6.38
  • 確認環境: Mac: Chromeブラウザ

メリット

  • インストール不要
  • テーブル設計をクラウドに保存可能 (他アプリの設計に使い回せる!)
  • Migration自動生成
  • CRUD自動生成
  • テストデータ(Faker)自動生成
  • Validation自動生成(テーブル設計時のカラム型を参照して作成してる。※Enum型は自分でCODE修正必要かも)
  • Webサービスだから、どのPCでも同じデータを活用して作成できる

さあ、 LaravelDB.com はじめて行きましょう!

操作方法(Youtubu)

【 CRUD: 新しいプロジェクトを用意 】

最低限Laravelがインストールした状態を準備します!
1. この時点ではまだLOGINは作らないように!
2. .env と MySQLのDB準備はしておきましょう。
image.png

【 LaravelDB.comへ ログイン 】

Googleアカウントのみログイン可能です。(画面右上 OR ページ下部の中央)
LaravelDB.com
en.jpg

【 LaravelDB.comの POINT!! 】

ER図を作成していきますが重要事項があります!

<<重要>>
主キーのAutoIncrementは どのテーブルも『 id 』 と固定すること!!

image.png

LaravelのEloquentModel を便利に使うためには、どのテーブルも 『 id 』 と固定するのが吉です。
理由は「 モデル名::find($id); 」と便利に使う場合は 主キーのAutoIncrement 『 id 』 名にしておかないと動作しないからです( Laravelを勉強してる人はなんとなく知ってることでしょうか )。
※ laravelDB.comのCRUDでも生成されるファイルは「 ○○○::find($id); 」を使ってるためそ!
※ laravelに最初から入っているテーブル「user」「password_resets」は作成しないこと(migrate時に上書きしてしまいます)
※慣れれば簡単ですが、少しだけ慣れる時間は必要ですね。


【 CRUDを生成してみる! 】

  1. まずは、ER図を簡単に作ってみましょう!! その後に右メニュー「 ER図のLoad/Save 」をクリックすると以下画面が表示されます。
  2. 外部キーを沢山つけてる場合は、Migrationの順番が重要なので試す場合は「OFF」がオススメです。
  3. 「 Laravel( CRUD BUILD) 」 ボタンをクリックでファイルが生成されます!!

【 ダウンロードファイルを確認しましょう 】

  1. Zip圧縮ファイルがダウンロードされるので、Zipを展開(解凍)して中を見てみましょう!。CRUDに必要なファイルが一式入っています。
  2. 下図:左側(CRUDファイル)、右図laravelのフォルダ

【移行方法】
Macの場合: 「Optionキー + ドラッグ&ドロップ」 → 「結合」でフォルダの上書きではなく、結合になります。
Winの場合:「ドラッグ&ドロップ」で結合できます。

ダウンロードした各フォルダを上記の方法で移動しましょう!

【 LaravelのHOMEページを表示しましょう 】

私の環境では「http://localhost/

【 「 コマンド 」 を3つ順番に打ちましょう! 】

<<重要>>
 DBや.env設定など最低限の設定を完了しておくこと!!

php artisan migrate

composer update

//テストデータいらない場合は打たなくてもOK
php artisan db:seed

以上3つです。

【 完成: ブラウザで確認しましょう! 】

http://localhost/ (URLは開発環境によって異なります) で確認しましょう!
あなたが作ったテーブルのCRUD画面へ遷移できますよ!!

welcome.blade.php

作成されたindex/show/editの画面
基本的な処理が最初から出来てます!捗りそうですね!
image.png

Validationがスゴイ! → テーブル設計に合わせて生成

例でざっくりテーブルをER図で書いてみました。
以下"t_gsusers"テーブルを中心に見ていきます。

生成されたコントローラーのcreate(),edit()にはこういったvalidationが挿入されます。

TGsusersController
            $this->validate($request, [
                "name" => "required|max:128", //string('name',128)
                "lid" => "required|max:128", //string('lid',128)
                "lpw" => "required|max:128", //string('lpw',128)
                "m_department_id" => "required|integer", //integer('m_department_id')
                "m_position_id" => "required|integer", //integer('m_position_id')
                "m_prefectures_id" => "required|integer", //integer('m_prefectures_id')
                "m_kanri_id" => "required|integer", //integer('m_kanri_id')
                "m_lifeflg_id" => "required|integer", //integer('m_lifeflg_id')
                "m_test_id" => "required|integer", //integer('m_test_id')

            ]);

{{old('name')}} → 入力項目を補完

validationで未入力等ではじかれた場合、入力した文字を消さずに表示します。

date,datetime型 → 入力フォー厶type="date"

  • 画面表示側もtype='date'
  • validation側もdateチェックが入ります。

ER図外部キー設定→JOIN自動生成 QueryBuilderをController側にコメントで用意

イチからQueryBuilderを使用して、JOINを書くのは大変です。ベースになる部分は生成してくれます。使用する場合はコメントになってるのでコメントを外し、上記に書かれてるコードのEloquentModelの1行をコメントして使います。

※機械的に作成してるため、より良い記述に変更は必要になります。

動作した後はコードを見ると良いでしょう

  • Routing
  • Controller
  • Views
  • Model 後は、自分で変更をいれていくだけですね。

日本発 Laravelツール

日本から海外でも使われるプロダクトになることを願っております。

Twitter: LaravelDB.com


以上

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

最新版『 Laravel DB.com 』コード書かずにCRUD作成~利用方法まとめ~

「 Laravel DB.com 」 、利用方法まとめ

2020年2月2日リニューアルしたので、使用方法をまとめたいと思います。
en.jpg

Laravel DB.com ってなに?

「LaravelDB.com」は、ER図(テーブルの設計)を作成することで、”Migration” OR ”CRUD(テストデータ)”のコードまで生成できるサービス(ツール)です。

確認したLaravel動作環境

  • 開発環境: Laravel5.5 ~ 6.x / PHP7.2.1 / MySQL5.6.38
  • 確認環境: Mac: Chromeブラウザ

メリット

  • インストール不要
  • テーブル設計をクラウドに保存可能 (他アプリの設計に使い回せる!)
  • Migration自動生成
  • CRUD自動生成
  • テストデータ(Faker)自動生成
  • Validation自動生成(テーブル設計時のカラム型を参照して作成してる。※Enum型は自分でCODE修正必要かも)
  • Webサービスだから、どのPCでも同じデータを活用して作成できる

さあ、 LaravelDB.com はじめて行きましょう!

操作方法(Youtubu)

【 CRUD: 新しいプロジェクトを用意 】

最低限Laravelがインストールした状態を準備します!
1. この時点ではまだLOGINは作らないように!
2. .env と MySQLのDB準備はしておきましょう。
image.png

【 LaravelDB.comへ ログイン 】

Googleアカウントのみログイン可能です。(画面右上 OR ページ下部の中央)
LaravelDB.com
en.jpg

【 LaravelDB.comの POINT!! 】

ER図を作成していきますが重要事項があります!

<<重要>>
主キーのAutoIncrementは どのテーブルも『 id 』 と固定すること!!

image.png

LaravelのEloquentModel を便利に使うためには、どのテーブルも 『 id 』 と固定するのが吉です。
理由は「 モデル名::find($id); 」と便利に使う場合は 主キーのAutoIncrement 『 id 』 名にしておかないと動作しないからです( Laravelを勉強してる人はなんとなく知ってることでしょうか )。
※ laravelDB.comのCRUDでも生成されるファイルは「 ○○○::find($id); 」を使ってるためそ!
※ laravelに最初から入っているテーブル「user」「password_resets」は作成しないこと(migrate時に上書きしてしまいます)
※慣れれば簡単ですが、少しだけ慣れる時間は必要ですね。


【 CRUDを生成してみる! 】

  1. まずは、ER図を簡単に作ってみましょう!! その後に右メニュー「 ER図のLoad/Save 」をクリックすると以下画面が表示されます。
  2. 外部キーを沢山つけてる場合は、Migrationの順番が重要なので試す場合は「OFF」がオススメです。
  3. 「 Laravel( CRUD BUILD) 」 ボタンをクリックでファイルが生成されます!!

【 ダウンロードファイルを確認しましょう 】

  1. Zip圧縮ファイルがダウンロードされるので、Zipを展開(解凍)して中を見てみましょう!。CRUDに必要なファイルが一式入っています。
  2. 下図:左側(CRUDファイル)、右図laravelのフォルダ

【移行方法】
Macの場合: 「Optionキー + ドラッグ&ドロップ」 → 「結合」でフォルダの上書きではなく、結合になります。
Winの場合:「ドラッグ&ドロップ」で結合できます。

ダウンロードした各フォルダを上記の方法で移動しましょう!

【 LaravelのHOMEページを表示しましょう 】

私の環境では「http://localhost/

【 「 コマンド 」 を3つ順番に打ちましょう! 】

<<重要>>
 DBや.env設定など最低限の設定を完了しておくこと!!

php artisan migrate

composer update

//テストデータいらない場合は打たなくてもOK
php artisan db:seed

以上3つです。

【 完成: ブラウザで確認しましょう! 】

http://localhost/ (URLは開発環境によって異なります) で確認しましょう!
あなたが作ったテーブルのCRUD画面へ遷移できますよ!!

welcome.blade.php

作成されたindex/show/editの画面
基本的な処理が最初から出来てます!捗りそうですね!
image.png

Validationがスゴイ! → テーブル設計に合わせて生成

例でざっくりテーブルをER図で書いてみました。
以下"t_gsusers"テーブルを中心に見ていきます。

生成されたコントローラーのcreate(),edit()にはこういったvalidationが挿入されます。

TGsusersController
            $this->validate($request, [
                "name" => "required|max:128", //string('name',128)
                "lid" => "required|max:128", //string('lid',128)
                "lpw" => "required|max:128", //string('lpw',128)
                "m_department_id" => "required|integer", //integer('m_department_id')
                "m_position_id" => "required|integer", //integer('m_position_id')
                "m_prefectures_id" => "required|integer", //integer('m_prefectures_id')
                "m_kanri_id" => "required|integer", //integer('m_kanri_id')
                "m_lifeflg_id" => "required|integer", //integer('m_lifeflg_id')
                "m_test_id" => "required|integer", //integer('m_test_id')

            ]);

{{old('name')}} → 入力項目を補完

validationで未入力等ではじかれた場合、入力した文字を消さずに表示します。

date,datetime型 → 入力フォー厶type="date"

  • 画面表示側もtype='date'
  • validation側もdateチェックが入ります。

ER図外部キー設定→JOIN自動生成 QueryBuilderをController側にコメントで用意

イチからQueryBuilderを使用して、JOINを書くのは大変です。ベースになる部分は生成してくれます。使用する場合はコメントになってるのでコメントを外し、上記に書かれてるコードのEloquentModelの1行をコメントして使います。

※機械的に作成してるため、より良い記述に変更は必要になります。

動作した後はコードを見ると良いでしょう

  • Routing
  • Controller
  • Views
  • Model 後は、自分で変更をいれていくだけですね。

日本発 Laravelツール

日本から海外でも使われるプロダクトになることを願っております。

Twitter: LaravelDB.com


以上

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

最新版『 Laravel DB.com 』コード書かずに超スピード開発~(DEMO動画あり)~

Laravel DB.com ってなに?

「LaravelDB.com」は、ER図(テーブルの設計)を作成することで、”Migration” OR ”CRUD(テストデータ)”のコードまで生成できるサービス(ツール)です。

DEMO動画(操作方法)

en.jpg


「 Laravel DB.com 」 、利用方法まとめ

2020年2月2日リニューアルしたので、使用方法をまとめたいと思います。
注意)以前に見たことがある人は「PWA」でChcheAPIが効いてて古い画面のままかもしれません。
その場合には、[開発者ツール]→[Application]→[Clear Storage]でキャッシュを削除してください。

確認したLaravel動作環境

  • 開発環境: Laravel5.5 ~ 6.x / PHP7.2.1 / MySQL5.6.38
  • 確認環境: Mac: Chromeブラウザ

メリット

  • インストール不要
  • テーブル設計をクラウドに保存可能 (他アプリの設計に使い回せる!)
  • Migration自動生成
  • CRUD自動生成
  • テストデータ(Faker)自動生成
  • Validation自動生成(テーブル設計時のカラム型を参照して作成してる。※Enum型は自分でCODE修正必要かも)
  • Webサービスだから、どのPCでも同じデータを活用して作成できる

さあ、 LaravelDB.com はじめて行きましょう!

【 CRUD: 新しいプロジェクトを用意 】

最低限Laravelがインストールした状態を準備します!
1. この時点ではまだLOGINは作らないように!
2. .env と MySQLのDB準備はしておきましょう。
image.png

【 LaravelDB.comへ ログイン 】

Googleアカウントのみログイン可能です。(画面右上 OR ページ下部の中央)
LaravelDB.com
en.jpg

【 LaravelDB.comの POINT!! 】

ER図を作成していきますが重要事項があります!

<<重要>>
主キーのAutoIncrementは どのテーブルも『 id 』 と固定すること!!

image.png

LaravelのEloquentModel を便利に使うためには、どのテーブルも 『 id 』 と固定するのが吉です。
理由は「 モデル名::find($id); 」と便利に使う場合は 主キーのAutoIncrement 『 id 』 名にしておかないと動作しないからです( Laravelを勉強してる人はなんとなく知ってることでしょうか )。
※ laravelDB.comのCRUDでも生成されるファイルは「 ○○○::find($id); 」を使ってるためそ!
※ laravelに最初から入っているテーブル「user」「password_resets」は作成しないこと(migrate時に上書きしてしまいます)
※慣れれば簡単ですが、少しだけ慣れる時間は必要ですね。


【 CRUDを生成してみる! 】

  1. まずは、ER図を簡単に作ってみましょう!! その後に右メニュー「 ER図のLoad/Save 」をクリックすると以下画面が表示されます。
  2. 外部キーを沢山つけてる場合は、Migrationの順番が重要なので試す場合は「OFF」がオススメです。
  3. 「 Laravel( CRUD BUILD) 」 ボタンをクリックでファイルが生成されます!!

【 ダウンロードファイルを確認しましょう 】

  1. Zip圧縮ファイルがダウンロードされるので、Zipを展開(解凍)して中を見てみましょう!。CRUDに必要なファイルが一式入っています。
  2. 下図:左側(CRUDファイル)、右図laravelのフォルダ

【移行方法】
Macの場合: 「Optionキー + ドラッグ&ドロップ」 → 「結合」でフォルダの上書きではなく、結合になります。
Winの場合:「ドラッグ&ドロップ」で結合できます。

ダウンロードした各フォルダを上記の方法で移動しましょう!

【 LaravelのHOMEページを表示しましょう 】

私の環境では「http://localhost/

【 「 コマンド 」 を3つ順番に打ちましょう! 】

<<重要>>
 DBや.env設定など最低限の設定を完了しておくこと!!

php artisan migrate

composer update

//テストデータいらない場合は打たなくてもOK
php artisan db:seed

以上3つです。

【 完成: ブラウザで確認しましょう! 】

http://localhost/ (URLは開発環境によって異なります) で確認しましょう!
あなたが作ったテーブルのCRUD画面へ遷移できますよ!!

welcome.blade.php

作成されたindex/show/editの画面
基本的な処理が最初から出来てます!捗りそうですね!
image.png

Validationがスゴイ! → テーブル設計に合わせて生成

例でざっくりテーブルをER図で書いてみました。
以下"t_gsusers"テーブルを中心に見ていきます。

生成されたコントローラーのcreate(),edit()にはこういったvalidationが挿入されます。

TGsusersController
            $this->validate($request, [
                "name" => "required|max:128", //string('name',128)
                "lid" => "required|max:128", //string('lid',128)
                "lpw" => "required|max:128", //string('lpw',128)
                "m_department_id" => "required|integer", //integer('m_department_id')
                "m_position_id" => "required|integer", //integer('m_position_id')
                "m_prefectures_id" => "required|integer", //integer('m_prefectures_id')
                "m_kanri_id" => "required|integer", //integer('m_kanri_id')
                "m_lifeflg_id" => "required|integer", //integer('m_lifeflg_id')
                "m_test_id" => "required|integer", //integer('m_test_id')

            ]);

{{old('name')}} → 入力項目を補完

validationで未入力等ではじかれた場合、入力した文字を消さずに表示します。

date,datetime型 → 入力フォー厶type="date"

  • 画面表示側もtype='date'
  • validation側もdateチェックが入ります。

ER図外部キー設定→JOIN自動生成 QueryBuilderをController側にコメントで用意

イチからQueryBuilderを使用して、JOINを書くのは大変です。ベースになる部分は生成してくれます。使用する場合はコメントになってるのでコメントを外し、上記に書かれてるコードのEloquentModelの1行をコメントして使います。

※機械的に作成してるため、より良い記述に変更は必要になります。

動作した後はコードを見ると良いでしょう

  • Routing
  • Controller
  • Views
  • Model 後は、自分で変更をいれていくだけですね。

日本発 Laravelツール

日本から海外でも使われるプロダクトになることを願っております。

Twitter: LaravelDB.com


以上

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

コード書かずに超スピード開発~(DEMO動画あり)~最新版『 Laravel DB.com 』

Laravel DB.com ってなに?

「LaravelDB.com」は、ER図(テーブルの設計)を作成することで、”Migration” OR ”CRUD(テストデータ)”のコードまで生成できるサービス(ツール)です。

DEMO動画(操作方法)

en.jpg

注意)以前に見たことがある人は「PWA」でChcheAPIが効いてて古い画面のままかもしれません。
その場合には、[開発者ツール]→[Application]→[Clear Storage]でキャッシュを削除してください。

「 LaravelDB.com 」の特徴

  • Webサービスだから「 家・会社のパソコン 」でも同じデータを活用できる
  • テーブル設計をクラウドに保存可能 ( 他アプリの設計に使い回せる! )
  • Migration自動生成 / CRUD自動生成 / テストデータ(Faker)自動生成
  • Validation(テーブル設計時のカラム型を参照して自動生成 ※Enum型は自分でCODE修正必要かも...)
  • 確認環境: Mac: Chromeブラウザ
  • 開発環境: Laravel5.5 ~ 6.x / PHP7.2.1 / MySQL5.6.38

【 CRUD: 新しいプロジェクトを用意 】

最低限Laravelがインストールした状態(Welcomeページ表示)を準備します!
1. この時点ではまだLOGINは作らないように!
2. .env と MySQLのDB準備はしておきましょう。
image.png

【 LaravelDB.comへ ログイン 】

Googleアカウントのみログイン可能です。(画面右上 OR ページ下部の中央)
LaravelDB.com
en.jpg

【 LaravelDB.comの POINT!! 】

ER図を作成していきますが重要事項があります!

<<重要>>
主キーのAutoIncrementは どのテーブルも『 id 』 と固定すること!!

image.png

LaravelのEloquentModel を便利に使うためには、どのテーブルも 『 id 』 と固定するのが吉です。
理由は「 モデル名::find($id); 」と便利に使う場合は 主キーのAutoIncrement 『 id 』 名にしておかないと動作しないからです( Laravelを勉強してる人はなんとなく知ってることでしょうか )。
※ laravelDB.comのCRUDでも生成されるファイルは「 ○○○::find($id); 」を使ってるためそ!
※ laravelに最初から入っているテーブル「user」「password_resets」は作成しないこと(migrate時に上書きしてしまいます)
※慣れれば簡単ですが、少しだけ慣れる時間は必要ですね。


【 CRUDを生成してみる! 】

  1. まずは、ER図を簡単に作ってみましょう!! その後に右メニュー「 ER図のLoad/Save 」をクリックすると以下画面が表示されます。
  2. 外部キーを沢山つけてる場合は、Migrationの順番が重要なので試す場合は「OFF」がオススメです。
  3. 「 Laravel( CRUD BUILD) 」 ボタンをクリックでファイルが生成されます!!

【 ダウンロードファイルを確認しましょう 】

  1. Zip圧縮ファイルがダウンロードされるので、Zipを展開(解凍)して中を見てみましょう!。CRUDに必要なファイルが一式入っています。
  2. 下図:左側(CRUDファイル)、右図laravelのフォルダ

【移行方法】
Macの場合: 「Optionキー + ドラッグ&ドロップ」 → 「結合」でフォルダの上書きではなく、結合になります。
Winの場合:「ドラッグ&ドロップ」で結合できます。

ダウンロードした各フォルダを上記の方法で移動しましょう!

【 LaravelのHOMEページを表示しましょう 】

私の環境では「http://localhost/

【 「 コマンド 」 を3つ順番に打ちましょう! 】

<<重要>>
 DBや.env設定など最低限の設定を完了しておくこと!!

php artisan migrate

composer update

//テストデータいらない場合は打たなくてもOK
php artisan db:seed

3つ目のコマンドはテストデータ登録の実行コマンド

Faker注意点有り!

テストデータ登録「 database/seeds/以下 」Fakerファイルの中はこんな感じです。
TUsersTableSeeder_php_—_test3.jpg
自動で文字が入ってるので、精度はまあこんなもんですかね。※文字列、数値、日付は自動で判定して入れてくれてるようです。
Fakerで使えるタイプ一覧、 自身で変更して使いたい場合はこちらを参考にすることをオススメします。

「address」や「url」を使う場合はカラムサイズに気をつけましょう!

【 完成: ブラウザで確認しましょう! 】

http://localhost/ (URLは開発環境によって異なります) で確認しましょう!
あなたが作ったテーブルのCRUD画面へ遷移できますよ!!

welcome.blade.php

作成されたindex/show/editの画面
基本的な処理が最初から出来てます!捗りそうですね!
image.png

Validationがスゴイ! → テーブル設計に合わせて生成

例でざっくりテーブルをER図で書いてみました。
以下"t_gsusers"テーブルを中心に見ていきます。

生成されたコントローラーのcreate(),edit()にはこういったvalidationが挿入されます。

TGsusersController
            $this->validate($request, [
                "name" => "required|max:128", //string('name',128)
                "lid" => "required|max:128", //string('lid',128)
                "lpw" => "required|max:128", //string('lpw',128)
                "m_department_id" => "required|integer", //integer('m_department_id')
                "m_position_id" => "required|integer", //integer('m_position_id')
                "m_prefectures_id" => "required|integer", //integer('m_prefectures_id')
                "m_kanri_id" => "required|integer", //integer('m_kanri_id')
                "m_lifeflg_id" => "required|integer", //integer('m_lifeflg_id')
                "m_test_id" => "required|integer", //integer('m_test_id')

            ]);

{{old('name')}} → 入力項目を補完

validationで未入力等ではじかれた場合、入力した文字を消さずに表示します。

date,datetime型 → 入力フォー厶type="date"

  • 画面表示側もtype='date'
  • validation側もdateチェックが入ります。

ER図外部キー設定→JOIN自動生成 QueryBuilderをController側にコメントで用意

イチからQueryBuilderを使用して、JOINを書くのは大変です。ベースになる部分は生成してくれます。使用する場合はコメントになってるのでコメントを外し、上記に書かれてるコードのEloquentModelの1行をコメントして使います。

※機械的に作成してるため、より良い記述に変更は必要になります。

動作した後はコードを見ましょう!

最低限のコードは出来上がってます!あとは、仕様に合わせて修正・追加していきましょう!!!
- Routing
- Controller
- Views
- Model
後は、自分で変更をいれていくだけですね。

Laravel専用ツール!!

日本から世界へ!世界で使われるプロダクトになることを願っております。
是非、応援の程よろしくお願いいたしますm(_ _)m

Twitter: LaravelDB.com


LaravelDB.com解説ページ一覧

LaravelDB.com 対応カラム一覧

https://qiita.com/daisu_yamazaki/items/92dc3cc599a264c3fb0f

LaravelDB.com テーブル命名ルール

https://qiita.com/daisu_yamazaki/items/1cb5987cc6d1008def82

LaravelDb.com integerの注意点

https://qiita.com/daisu_yamazaki/items/f2e6d58cfa20fa81fd54

LaravelDB.com Faker(テストデータ投入)

https://qiita.com/daisu_yamazaki/items/57669e8fa2c256d85c95

以上

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Laravel で Cloud KMS を使って暗号化/復号化をしてみた。

こんにちわ! @ktoshi です!

以前、Cloud KMSをGoで使う 記事を書きましたが、今回は PHP(というよりLaravel)で使ってみようと思います。
実際はGoより先にこっちで使い始めてたのですが、諸々あり後発の記事になりました。
そんなわけではじめましょー。

Cloud KMS

暗号鍵をセキュアに管理できるGCPのサービスです。
自動ローテーションやIAMでの権限管理でセキュアなアクセスを実現できる上、APIを利用した暗号化/復号化を行えるので非常に柔軟な利用が可能です。
類似したサービスはAWSの「KMS」や Azureの「Key Vault」などがあります。

Key Ring と Key

Cloud KMS では Key を Key Ring で管理しています。
関係性は字のごとく、鍵と鍵束ですね。
ある程度同じ用途で使用される鍵については同じ Key Ring でまとめておくとよいでしょう。
実際に暗号化などで利用されるのは Key となります。

目的

Cloud KMS に暗号鍵を用意し、Laravel + Cloud KMS で文字列を暗号化/復号化する。

実践

では実際に暗号化/復号化を行いましょう。
大きく分けて作業は3つです。

  • 環境準備
  • 暗号鍵の準備
  • 暗号化
  • 復号化

※ 下記手順の[PROJECT_NAME]、[KEY_RING_NAME]、[KEY_NAME] は適宜置き換えてください。

前提条件

前提として下記が導入されていることとします。
なお、バージョンは私の環境でのものです。

  • PHP: 7.3.10
  • composer: v1.9.0
  • Laravel: 6.1
  • gcloud: 268.0.0
  • terraform: v0.12.16

環境準備

PHPでCloudKMSを扱うためのライブラリをインストールします。
composer でインストールをします。

composer require google/cloud-kms

暗号鍵の準備

暗号をするにはまず暗号鍵を準備しないことには始まりません。
私は terraform を利用しましたが、 gcloud コマンドでも簡単に作成できます。
terraform や gcloud のインストールについては話がそれるので割愛します。

terraform の場合

# 鍵束の作成
resource "google_kms_key_ring" "sample_key_ring" {
  name     = "[KEY_RING_NAME]"
  location = "global"
}

# 鍵の作成
resource "google_kms_crypto_key" "sample_crypto_key" {
  name     = "[KEY_NAME]"
  key_ring = google_kms_key_ring.sample_key_ring.self_link
}

gcloud コマンドの場合

# 鍵束の作成
$ gcloud kms keyrings create [KEY_RING_NAME] --location global --project [PROJECT_NAME]

# 鍵の作成
$ gcloud kms keys create [KEY_NAME] --location global --keyring [KEY_RING_NAME] --purpose encryption --project [PROJECT_NAME]

これで暗号鍵の準備は完了です。

暗号化

では、実際に Laravel で暗号化を行います。
今回は処理を行っている箇所のみ抜粋しています。

use Google\Cloud\Kms\V1\KeyManagementServiceClient;

public function encryption(String $str)
{
    $kms = new KeyManagementServiceClient();
    $projectId = '<PROJECT_NAME>';
    $location = '<PROJECT_LOCATION>';
    $keyRingId = '<KEY_RING_NAME>';
    $keyId = '<KEY_NAME>';
    $cryptoKeyName = $kms->cryptoKeyName($projectId, $location, $keyRingId, $keyId);
    return $kms->encrypt($cryptoKeyName, $str)->getCiphertext()
}

これで暗号化した文字列を返すことができます。
なお、Cloud KMS では本気で暗号化されるため、帰ってきた文字列が UTF-8 ではなくなり、
DBなどに直接突っ込むことができません。
DBへ突っ込むときなどは base64_encode を使ってエンコードしておくと良いです。

復号化

暗号化処理はできたので、次に復元しましょう。
やってることはほぼ、暗号化と一緒です。

use Google\Cloud\Kms\V1\KeyManagementServiceClient;

public function decryption(String $str)
{
    $kms = new KeyManagementServiceClient();
    $projectId = '<PROJECT_NAME>';
    $location = '<PROJECT_LOCATION>';
    $keyRingId = '<KEY_RING_NAME>';
    $keyId = '<KEY_NAME>';
    $cryptoKeyName = $kms->cryptoKeyName($projectId, $location, $keyRingId, $keyId);
    return $kms->decrypt($cryptoKeyName, $str)->getPlaintext()
}

これで復号化された文字列を取得できます。
なお、暗号化の際に Base64 でエンコードしている場合はデコードしてから渡しましょう
私はこれを忘れてめちゃくちゃ時間食いました。。。

まとめ

前回、Goで行った処理と同様のものをPHPで行いました。
結局やることとしては、暗号化鍵を取得して、それを使って暗号化/復号化をするだけですね。
前の記事でも記載しましたが、今後セキュアでかつ可逆暗号化が必要となるケースがあると思います。
そんなときにぜひこの記事が参考になればと思います。
それでは、みなさまも引き続き安全で快適な暗号化ライフを。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

PHPでフォームを作成する基本

フォームの基本

  • 入力画面
  • 確認画面
  • 完了画面

index.php → confirm.php → thanks.php
と作る場合が多い

その場合、formのaction属性をそれぞれの飛び先に指定する

入力フォーム


    <form method="post" action=""> <?php //送信先が空なので、自分自身(index.php)を再度呼び出す
                                    ?>
      <div class="element_wrap">
        <label>氏名</label>
        <input type="text" name="your_name" value="<?php if (!empty($_POST['your_name'])) {
                                                      echo $_POST['your_name'];
                                                    } ?>">
        <!-- POSTパラメータが空じゃない場合のみvalue属性にセット -->
      </div>
      <div class="element_wrap">
        <label>メールアドレス</label>
        <input type="mail" name="email" value="<?php if (!empty($_POST['email'])) {
                                                  echo $_POST['email'];
                                                } ?>">
      </div>
      <div class="element_wrap">
        <label>性別</label>
        <label for="gender_male"><input id="gender_male" type="radio" name="gender" value="male">男性</label>
        <label for="gender_female"><input id="gender_female" type="radio" name="gender" value="female">女性</label>
      </div>
      <div class="element_wrap">
        <label>年齢</label>
        <select name="age">
          <option value="">選択してください</option>
          <option value="1">〜19歳</option>
          <option value="2">20歳〜29歳</option>
          <option value="3">30歳〜39歳</option>
          <option value="4">40歳〜49歳</option>
          <option value="5">50歳〜59歳</option>
          <option value="6">60歳〜</option>
        </select>
      </div>
      <div class="element_wrap">
        <label>お問い合わせ内容</label>
        <textarea name="contact"></textarea>
      </div>
      <div class="element_wrap">
        <label for="agreement"><input id="agreement" type="checkbox" name="agreement" value="1">プライバシーポリシーに同意する</label>
      </div>
      <input type="submit" name="btn_confirm" value="入力内容を確認する">
    </form>


確認ページ


    <form method="post" action="">
      <div class="element_wrap">
        <label>氏名</label>
        <p><?php echo $_POST['your_name']; ?></p>
      </div>
      <div class="element_wrap">
        <label>メールアドレス</label>
        <p><?php echo $_POST['email']; ?></p>
      </div>
      <div class="element_wrap">
        <label>性別</label>
        <p><?php if($_POST['gender'] === "male" ){ echo '男性'; } else {echo '女性' ; }?></p>
      </div>
      <div class="element_wrap">
        <label>年齢</label>
        <p>
          <?php if( $_POST['age'] === "1") {
            echo '〜19歳';
          }elseif($_POST['age'] === "2") {
            echo '20歳〜29歳';
            }elseif($_POST['age'] === "3") {
            echo '30歳〜39歳';
            }elseif($_POST['age'] === "4"){
            echo '40歳〜49歳';
            }elseif($_POST['age'] === "5"){
            echo '50歳〜59歳';
            }elseif($_POST['age'] === "6"){
            echo '60歳〜';
            }
          ?>
        </p>
      </div>
      <div class="element_wrap">
        <label>お問い合わせ内容</label>
        <p><?php echo nl2br($_POST['contact']); ?></p>
      </div>
      <div class="element_wrap">
        <p>
          <?php
          if($_POST['agreement'] === "1") {
            echo '同意する';
          } else {
            echo '同意しない';
          }
          ?>
        </p>
      </div>
      <!-- 入力値の受け渡し用 -->
      <input type="submit" name="btn_back" value="戻る">
      <input type="submit" name="btn_submit" value="送信">
      <input type="hidden" name="your_name" value="<?php echo $_POST['your_name']; ?>">
      <input type="hidden" name="email" value="<?php echo $_POST['email']; ?>">
      <input type="hidden" name="gender" value="<?php echo $_POST['gender']; ?>">
      <input type="hidden" name="age" value="<?php echo $_POST['age']; ?>">
      <input type="hidden" name="contact" value="<?php echo $_POST['contact']; ?>">
      <input type="hidden" name="agreement" value="<?php echo $_POST['agreement']; ?>">
    </form>

ページの切り替え

    <form method="post" action=""> <?php //送信先が空なので、自分自身(index.php)を再度呼び出す

formがこうなっているので、自分自身を再び呼び出している。

そのため、別ファイルに記述するのではなく、同じファイルに全ての内容を記述していく。

(ただし、action="confirm.php"とした場合は、しっかり別ファイルが必要になるので注意)

ページの切り替え方法


/*---------------------------------------
    ページ切り替え
-----------------------------------------*/

$page_flag = 0;

if (!empty($_POST['btn_confirm'])) {
  //$_POST[‘btn_confirm’]の値が渡されているか確認
  $page_flag  = 1;
} elseif (!empty($_POST['btn_submit'])) {
  //$_POST[‘btn_submit’]の値が渡されているか確認
  $page_flag = 2;
}
//入力ページや確認ページの表示をスイッチするフラグになる
//$_POST[‘btn_confirm’]があれば、確認ページへ進む

//[0]入力
//[1]確認
//[2]完了

どのボタンが押されたかによって、フラグを立て、
条件分岐させている

例えば、

<input type="submit" name="btn_confirm" value="入力内容を確認する">

コレだと、$_POST['btn_submit']が送信されるため、$page_flagには1が入る。

フォーム記載欄に、下記のように条件分岐させればオッケー。

  <?php if ($page_flag === 1) : //確認ページの内容 ?>

  <?php elseif ($page_flag === 2) : //完了ページの内容 ?>

    <p>送信が完了しました</p>

  <?php else : //入力ページの内容 ?>

  <?php endif; ?>


メール送信機能


/*---------------------------------------
    メール送信機能

// 変数の設定
$to = "testtaro@gray-code.com";
$subject = "メール送信のテスト";
$text = "メール本文です。";

// メール送信
mb_send_mail( $to, $subject, $text);
-----------------------------------------*/

//変数とタイムゾーン
$header = null; //メールの件名に入る文字列
$auto_reply_subject = null;
$auto_reply_text = null;
date_default_timezone_set('Asia/Tokyo');

// ヘッダー情報を設定
$header = "MIME-Version: 1.0\n";
$header .= "From: GRAYCODE <noreply@gray-code.com>\n";
$header .= "Reply-To: GRAYCODE <noreply@gray-code.com>\n";

//件名を設定
$auto_reply_subject = 'お問い合わせありがとうございます';

//本文設定
$auto_reply_text = "この度はお問い合わせありがとうございます。下記の内容でお問い合わせを受け付けました。\n\n";
$auto_reply_text .= "お問い合わせ日時:" . date("Y-m-d H:i") . "\n";
$auto_reply_text .= "氏名:" . $_POST['your_name'] . "\n";
$auto_reply_text .= "メールアドレス:" . $_POST['email'] . "\n\n";
$auto_reply_text .= "CODE株式会社";

//メール送信
mb_send_mail($_POST['email'], $auto_reply_subject, $auto_reply_text);


/**--------運営者側へ送るメール */

// 運営側へ送るメールの件名
$admin_reply_subject = "お問い合わせを受け付けました";

// 本文を設定
$admin_reply_text = "下記の内容でお問い合わせがありました。\n\n";
$admin_reply_text .= "お問い合わせ日時:" . date("Y-m-d H:i") . "\n";
$admin_reply_text .= "氏名:" . $_POST['your_name'] . "\n";
$admin_reply_text .= "メールアドレス:" . $_POST['email'] . "\n\n";

// 運営側へメール送信
mb_send_mail('webmaster@gray-code.com', $admin_reply_subject, $admin_reply_text, $header);


?>

入力値のサニタイズ

セキュリティ対策の一つ「入力値のサニタイズ」

フォームにHTMLやJSなどのプログラミングコードを入力しても実行されてないようにする。

→無効化したテキストデータにする

hemlspecialchars($string,$flags,$encodinf,$h_flag)

  • $string 対象となる文字列
  • $flags エンコードのフラグ
  • $encodinf 文字コード -$h_flag HTMl変換フラグ

HTMLエンティティへ変換し、無効化することができる。
(HTMLエンティティとは、文字列の素の姿)

htmlspecialchars

フォームにサニタイズを実装する

$clean = array();

//サニタイズ
if (!empty($_POST)) {
  foreach ($_POST as $key => $value) {
    $clean[$key] = htmlspecialchars($value, ENT_QUOTES);
  }
}

POSTパラメータが存在するかを判断、
そもそもPOSTがなかったら、サニタイズも必要ないので、ifで条件分岐させる

foreachでPOSTパラメータを配列ごと渡して、入力値を1つずつ取り出してサニタイズする。
その後、値を$cleanに格納

サニタイズされた入力値は$cleanに入っているので、$cleanの配列から値を取り出すように変更すればOK

バリデーション

  • 氏名 – 未入力チェック、文字の長さチェック
  • メールアドレス – 未入力チェック、形式チェック
  • 性別 – 未入力チェック、形式チェック
  • 年齢 – 未入力チェック、形式チェック
  • お問い合わせ内容 – 未入力チェック
  • プライバシーポリシーに同意 – 未入力チェック、形式チェック

さらに、「未入力チェック」は全項目に用意する

未入力チェック

$error = array();

if (!empty($clean['btn_confirm'])) {
  //$_POST[‘btn_confirm’]の値が渡されているか確認

  $error = validation($clean);

  if( empty($error)) {
    $page_flag  = 1;
  }


function validation($data) {
  $error = array();

  //氏名のバリデーション
  if( empty($data['your_name'])) {
    $error[] = "氏名は必ず入力して下さい";
  }
  return $error;
}

バリデーション用にvalidation関数を用意
関数の返り血でエラーが合ったかどうかを判断する。

empty関数で、入力がない場合、$errorへメッセージを追加

  $error = validation($clean);

  if( empty($error)) {
    $page_flag  = 1;
  }

エラーが見つからない場合は、$page_flagに1を設定し、ページを確認ページに切り替える

エラーメッセージの表示

  <?php if (!empty($error)) { ?>
  <ul class="error_list">
    <?php foreach ($error as $value) : ?>
    <li><?php echo $value; ?></li>
    <?php endforeach; ?>
  </ul>
  <?php } ?>

ここはもう簡単で、
emptyで$errorがあるかを判断して出力するだけ。

$errorは配列なので、foreachでループして出力する

入力値の長さ(文字数)を調べる

  if (empty($data['your_name'])) {
    $error[] = "氏名は必ず入力して下さい";
  } elseif( 20 < mb_strlen($data['your_name'])) {
    $error[] = "指名は20文字以内で入力して下さい";
  }

mb_strlenは、引数で渡した文字列の長さを返す

アドレスの形式を調べる

  if (empty($data['email'])) {
    $error[] = "メールアドレスは必ず入力して下さい";
  } elseif( !preg_match( '/^[0-9a-z_.\/?]+@([0-9a-z-]+\.)+[0-9a-z-]+$/',$data['email'])) {
    $error[] = "メールアドレスは正しい形式で入力して下さい";
  }

メールアドレス正規表現

特定の値が入力されているかを調べる

  if (empty($data['gender'])) {
    $error[] = "性別は必ず入力して下さい";
  } elseif( $data['gender'] !== 'male' && $data['gender'] !=='female') {
    $error[] = "性別は必ず入力して下さい";
  }

ラジオボタンなど、いずれかの入力が必須の場合

特定の範囲が入力されているか調べる

// 年齢のバリデーション
if( empty($data['age']) ) {
    $error[] = "「年齢」は必ず入力してください。";

} elseif( (int)$data['age'] < 1 || 6 < (int)$data['age'] ) {
    $error[] = "「年齢」は必ず入力してください。";
}

セレクトボックスなど、

value属性の値が1より小さい(0以下)、または6より大きい(7以上)の場合にエラーと判断しています。

チェックボックスのチェックを調べる

  if (empty($data['agreement'])) {
    $error[] = "プライバシーポリシーを確認して下さい";
  } elseif ((int) $data['agreement'] !== 1) {
    $error[] = "プライバシーポリシーをご確認ください。";
  }

セッションを作る

// セッションの書き込み
session_start();
$_SESSION['page'] = true;

確認ページ

session_start()でセッション開始の合図を出し、$_SESSION[‘page’]に「true」を格納

完了ページ

    session_start();
    if( !empty($_SESSION['page']) && $_SESSION['page'] === true ) {

        // セッションの削除
        unset($_SESSION['page']);

//$_SESSION['page']が空じゃない時のみ、完了ページの処理を実行する


    } else {
        $page_flag = 0;
    }
<?php
// var_dump($_POST);


//変数の初期化

/*---------------------------------------
    ページ切り替え
-----------------------------------------*/

$page_flag = 0;
$clean = array();
$error = array();

//サニタイズ
if (!empty($_POST)) {
  foreach ($_POST as $key => $value) {
    $clean[$key] = htmlspecialchars($value, ENT_QUOTES);
  }
}


if (!empty($clean['btn_confirm'])) {
  //$_POST[‘btn_confirm’]の値が渡されているか確認

  $error = validation($clean);

  if (empty($error)) { //エラーがなかった時、確認ページへ
    $page_flag  = 1;

    // セッションの書き込み
    session_start();
    $_SESSION['page'] = true;
  }
} elseif (!empty($clean['btn_submit'])) {
  //$_POST[‘btn_submit’]の値が渡されているか確認
  $page_flag = 2;
}
//入力ページや確認ページの表示をスイッチするフラグになる
//$_POST[‘btn_confirm’]があれば、確認ページへ進む

//[0]入力
//[1]確認
//[2]完了


session_start();
if (!empty($_SESSION['page']) && $_SESSION['page'] === true) {

  // セッションの削除
  unset($_SESSION['page']);



  /*---------------------------------------
    メール送信機能

// 変数の設定
$to = "testtaro@gray-code.com";
$subject = "メール送信のテスト";
$text = "メール本文です。";

// メール送信
mb_send_mail( $to, $subject, $text);
-----------------------------------------*/

  //変数とタイムゾーン
  $header = null; //メールの件名に入る文字列
  $auto_reply_subject = null;
  $auto_reply_text = null;
  date_default_timezone_set('Asia/Tokyo');

  // ヘッダー情報を設定
  $header = "MIME-Version: 1.0\n";
  $header .= "From: GRAYCODE <noreply@gray-code.com>\n";
  $header .= "Reply-To: GRAYCODE <noreply@gray-code.com>\n";

  //件名を設定
  $auto_reply_subject = 'お問い合わせありがとうございます';

  //本文設定
  $auto_reply_text = "この度はお問い合わせありがとうございます。下記の内容でお問い合わせを受け付けました。\n\n";
  $auto_reply_text .= "お問い合わせ日時:" . date("Y-m-d H:i") . "\n";
  $auto_reply_text .= "氏名:" . $clean['your_name'] . "\n";
  $auto_reply_text .= "メールアドレス:" . $clean['email'] . "\n\n";
  $auto_reply_text .= "CODE株式会社";

  //メール送信
  mb_send_mail($clean['email'], $auto_reply_subject, $auto_reply_text);


  /**--------運営者側へ送るメール */

  // 運営側へ送るメールの件名
  $admin_reply_subject = "お問い合わせを受け付けました";

  // 本文を設定
  $admin_reply_text = "下記の内容でお問い合わせがありました。\n\n";
  $admin_reply_text .= "お問い合わせ日時:" . date("Y-m-d H:i") . "\n";
  $admin_reply_text .= "氏名:" . $clean['your_name'] . "\n";
  $admin_reply_text .= "メールアドレス:" . $clean['email'] . "\n\n";

  // 運営側へメール送信
  mb_send_mail('webmaster@gray-code.com', $admin_reply_subject, $admin_reply_text, $header);
} else {
  $page_flag = 0;
}


function validation($data)
{
  $error = array();

  //氏名のバリデーション
  if (empty($data['your_name'])) {
    $error[] = "氏名は必ず入力して下さい";
  } elseif (20 < mb_strlen($data['your_name'])) {
    $error[] = "指名は20文字以内で入力して下さい";
  }


  if (empty($data['email'])) {
    $error[] = "メールアドレスは必ず入力して下さい";
  } elseif (!preg_match('/^[0-9a-z_.\/?]+@([0-9a-z-]+\.)+[0-9a-z-]+$/', $data['email'])) {
    $error[] = "メールアドレスは正しい形式で入力して下さい";
  }


  if (empty($data['gender'])) {
    $error[] = "性別は必ず入力して下さい";
  } elseif ($data['gender'] !== 'male' && $data['gender'] !== 'female') {
    $error[] = "性別は必ず入力して下さい";
  }


  if (empty($data['age'])) {
    $error[] = "年齢は必ず入力して下さい";
  } elseif ((int) $data['age'] < 1 || 6 < (int) $data['age']) {
    $error[] = "年齢は必ず入力して下さい";
  }

  if (empty($data['contact'])) {
    $error[] = "お問い合わせ内容は必ず入力して下さい";
  }

  if (empty($data['agreement'])) {
    $error[] = "プライバシーポリシーを確認して下さい";
  } elseif ((int) $data['agreement'] !== 1) {
    $error[] = "プライバシーポリシーをご確認ください。";
  }


  return $error;
}

?>

<!DOCTYPE>
<html lang="ja">

<head>
  <title>お問い合わせフォーム</title>
  <link rel="stylesheet" href="style.css">
</head>

<body>
  <h1>お問い合わせフォーム</h1>

  <?php if ($page_flag === 1) : //  ----------------確認ページの内容-----------------------
  ?>

  <form method="post" action="">
    <div class="element_wrap">
      <label>氏名</label>
      <p><?php echo $clean['your_name']; ?></p>
    </div>
    <div class="element_wrap">
      <label>メールアドレス</label>
      <p><?php echo $clean['email']; ?></p>
    </div>
    <div class="element_wrap">
      <label>性別</label>
      <p><?php if ($clean['gender'] === "male") {
              echo '男性';
            } else {
              echo '女性';
            } ?></p>
    </div>
    <div class="element_wrap">
      <label>年齢</label>
      <p>
          <?php if ($clean['age'] === "1") {
            echo '〜19歳';
          } elseif ($clean['age'] === "2") {
            echo '20歳〜29歳';
          } elseif ($clean['age'] === "3") {
            echo '30歳〜39歳';
          } elseif ($clean['age'] === "4") {
            echo '40歳〜49歳';
          } elseif ($clean['age'] === "5") {
            echo '50歳〜59歳';
          } elseif ($clean['age'] === "6") {
            echo '60歳〜';
          }
          ?>
        </p>
    </div>
    <div class="element_wrap">
      <label>お問い合わせ内容</label>
      <p><?php echo nl2br($clean['contact']); ?></p>
    </div>
    <div class="element_wrap">
      <p>
          <?php
          if ($clean['agreement'] === "1") {
            echo '同意する';
          } else {
            echo '同意しない';
          }
          ?>
        </p>
    </div>
    <!-- 入力値の受け渡し用 -->
    <input type="submit" name="btn_back" value="戻る">
    <input type="submit" name="btn_submit" value="送信">
    <input type="hidden" name="your_name" value="<?php echo $clean['your_name']; ?>">
    <input type="hidden" name="email" value="<?php echo $clean['email']; ?>">
    <input type="hidden" name="gender" value="<?php echo $clean['gender']; ?>">
    <input type="hidden" name="age" value="<?php echo $clean['age']; ?>">
    <input type="hidden" name="contact" value="<?php echo $clean['contact']; ?>">
    <input type="hidden" name="agreement" value="<?php echo $clean['agreement']; ?>">
  </form>

  <?php elseif ($page_flag === 2) : // ----------------完了ページの内容-----------------------
  ?>

  <p>送信が完了しました</p>

  <?php else : //----------------入力ページの内容-----------------------
  ?>

  <?php if (!empty($error)) { ?>
  <ul class="error_list">
    <?php foreach ($error as $value) : ?>
    <li><?php echo $value; ?></li>
    <?php endforeach; ?>
  </ul>
  <?php } ?>

  <form method="post" action=""> <?php //送信先が空なので、自分自身(index.php)を再度呼び出す
                                    ?>
    <div class="element_wrap">
      <label>氏名</label>
      <input type="text" name="your_name" value="<?php if (!empty($clean['your_name'])) {
                                                      echo $clean['your_name'];
                                                    } ?>">
      <!-- POSTパラメータが空じゃない場合のみvalue属性にセット -->
    </div>
    <div class="element_wrap">
      <label>メールアドレス</label>
      <input type="mail" name="email" value="<?php if (!empty($clean['email'])) {
                                                  echo $clean['email'];
                                                } ?>">
    </div>
    <div class="element_wrap">
      <label>性別</label>
      <label for="gender_male"><input id="gender_male" type="radio" name="gender" value="male">男性</label>
      <label for="gender_female"><input id="gender_female" type="radio" name="gender" value="female">女性</label>
    </div>
    <div class="element_wrap">
      <label>年齢</label>
      <select name="age">
        <option value="">選択してください</option>
        <option value="1">〜19歳</option>
        <option value="2">20歳〜29歳</option>
        <option value="3">30歳〜39歳</option>
        <option value="4">40歳〜49歳</option>
        <option value="5">50歳〜59歳</option>
        <option value="6">60歳〜</option>
      </select>
    </div>
    <div class="element_wrap">
      <label>お問い合わせ内容</label>
      <textarea name="contact"></textarea>
    </div>
    <div class="element_wrap">
      <label for="agreement"><input id="agreement" type="checkbox" name="agreement" value="1">プライバシーポリシーに同意する</label>
    </div>
    <input type="submit" name="btn_confirm" value="入力内容を確認する">
  </form>

  <?php endif; ?>
</body>

</html>


参考記事

フォームの基本構造を作成する | GRAYCODE PHPプログラミング

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Moodle マニュアル 3.8 - アップグレードの警告

原文

アップグレードの警告

もしあなたが安定板からのアップグレードの最中の警告からこのページにたどり着いたのなら、おそらく我々のブランチのいくつかの変更が影響しています。

もしあなたが最も最近の Moodle の安定版にとどまる事を意図しているのなら、適切な MOODLE_3x_STABLE ブランチ (MOODLE_32_STABLE, MOODLE_33_STABLE その他のように) を使用してチェックアウトを変更する必要があります。

Git を使用しているのなら、リモートブランチを追跡する新しいローカルブランチを作成してそれにスイッチする必要があります。

git checkout -b MOODLE_38_STABLE origin/MOODLE_38_STABLE
git pull

38 を関係する Moodle のバージョンに置き換えてください。最初の行により、MOODLE_38_STABLE という新しいローカルブランチが作成され追跡するリモートブランチも同時に設定されます。2行目は、git-pull コマンドがアップストリームから最近の変更をフェッチしあなたのローカルブランチにマージします。あなたの Moodle を次の週にアップデートするには、ただ 2行目を実行するだけでよいです。

Git により管理されている Moodle のバージョンについては、我々の dev:Moodle_versions(翻訳準備中)ページを参照してください。

関連項目

Git ガイド

カテゴリ:インストール

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

変数で計算結果を保管

変数

  • PHPの場合、変数名を$から始める必要がある
    • 変数は$から初めて、英語や英語_数字などで作成する
    • 例:$abc$abc_123
      • $合計 などの日本語も一応使えるけど基本的に使わない
  • 記号や空白が含まれていたり、先頭が数字の場合は使えない
  • 大文字小文字の設定は自由だけど、基本的にPHPの変数は小文字で書いたほうがいい
    • 変数名は小文字と大文字を完全区別するので、一部でも異なると「別の変数」と見なされてしまうので注意
    • 例えば全て小文字の場合、一部を大文字にすると別の変数となってしまう
  • 変数は、最初に『代入』と言う操作を行う
<?php $sum = 100+1050+200; ?>
  • 変数名(上記の場合は$sum)の次に=を書き、その後に記憶したい内容や、保管したい内容を記述する
  • ルールはprintのパラメーターと同じで'(クォーテーション記号)を入れてあげるとそのまま文字列として記憶される
    • クォーテーション記号がなければ計算式として「計算結果」を代入してくれる
  • 格納した変数を使いたい場合は=を使わず変数名だけを記述する
  • 変数を使うメリット
    • 何度も同じ計算式を使う場合にわかりやすくなる
    • 計算式が何を意味しているのわかりやすくなる
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

オブジェク指向

オブジェクト

  • 関連する変数(値)と関数(動作)が一つにまとまり、そのまとまりに名前がついたもの。
    • 関連した変数や関数を「一つのオブジェクト内」で宣言することで、管理しやすくなる。
    • オブジェクトには「クラス」「インスタンス」と言う2つの側面がある。
  • クラス
    • 設計図。オブジェクトを作るには、まずクラスを宣言する必要がある。
    • 設計図なので、どんな変数(値)でどんな関数(動作)にするのか考えて、クラス内部で宣言する。
    • 関連した変数や関数はこの時点(クラス)で決定。
  • インスタンス

    • クラス(設計図)から生成された実体。モノ。
    • 設計図だけあっても関数を使うことができないので、クラスから実体を生成する。
  • メモ

<?php
$today = new DateTime();
print($today -> format('G時 i分 s秒'));
?>
  • $today と format は -> で繋がっている
    • これは$todayと言うDateTimeメソッドの、インスタンスのメソッドを使っている
  • formatメソッドの使い方
    • dateとだいたい同じ
    • ()の中のパラメーター(例の場合は時間)をそれぞれ求めてフォーマット化してくれるメソッド
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ZipArchiveで突然"ZipArchive::addFromString(): Invalid or uninitialized Zip object"が出る様になった話

はじまり

ZipArchiveで以下の様なコードを実行していたが、PHPのアップデートを行ってから急にZipArchive::addFromString(): Invalid or uninitialized Zip object が出て正常に動作しないようになった。

<?php
$zip = new ZipArchive;
$zip->open(tempnam(sys_get_temp_dir(), 'foo-'), ZipArchive::CREATE);
$zip->addFromString('bar.txt', 'hogehoge');
$zip->close();

調べてみると $zip->open() の部分で失敗している様子。

原因の調査

PHPの確認

アップデート前のサーバがたまたまあったので、適当なコードを書いて比較してみることにした。
CentOS 7でPHPはremi-php72でインストール。

検証用コード
<?php
$zip = new ZipArchive;
var_dump($zip->open(tempnam(sys_get_temp_dir(), 'foo-'), ZipArchive::CREATE));
var_dump($zip->open(sys_get_temp_dir().'foo.zip', ZipArchive::CREATE));

PHP 7.2.26

$ php test.php
bool(true)
bool(true)

どちらも通る。

PHP 7.2.27

$ php test.php
int(19) 
bool(true)

tempnam()を使っている方が通らなくなった。
int(19) はzip archiveでは無いファイルを開こうとした場合に返ってくるエラーコードの模様。しかし、そもそも空のファイルのはず…

PHPのソースコードでzip関連のコミットログを追いかけて見るも怪しい箇所は見つからず。PHPでは無くlibzipに原因があるのかもと思い確認してみる。

libzipの確認

PHP 7.2.26

# yum list installed php libzip5
libzip5.x86_64                   1.5.2-1.el7.remi                    @remi-safe
php.x86_64                       7.2.26-1.el7.remi                   @remi-php72

PHP 7.2.27

# yum list installed php libzip5
libzip5.x86_64                   1.6.0-1.el7.remi                    @remi
php.x86_64                       7.2.27-1.el7.remi                   @remi-php72

GitHubのリポジトリで変更内容を確認する。

1.6.0 [2020-01-24]

Avoid using umask() since it's not thread-safe.
Set close-on-exec flag when opening files.
Do not accept empty files as valid zip archives any longer.
Add support for XZ compressed files (using liblzma).
Add support for cancelling while closing zip archives.
Add support for setting the time in the on-disk format.

ん…?

Do not accept empty files as valid zip archives any longer.
Do not accept empty files as valid zip archives any longer.
Do not accept empty files as valid zip archives any longer.

該当部分の変更内容を確認するかぎり、libzipの仕様が変更になったとわかる。

まとめ

ZipArchiveの使い方によっては不具合が出る場合がある。ただしバグではなく仕様変更なので元に戻ることは期待できない。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む