- 投稿日:2021-12-24T23:00:31+09:00
[CakePHP2] baserCMSをLaravelで動かしてみる
はじめに CakePHP3がリリースされてから、夢のように年月が経ってしまいました。そのCakePHP3も来年にはサポート終了となり、現行バージョンはCakePHP4、すでにCakePHP5の開発も始まっています。そして、残念ながら、CakePHP2は今年の6月にサポート終了となっています。 しかし、CakePHP2で作られたアプリケーションをCakePHP3以降にアップグレードするのは困難です。特にbaserCMSの場合には、CMSという性質上、仮にbaserCMS自体をアップグレードできたとしても、今度は利用者の方がアプリケーション、作成したテーマ、プラグインなどをアップグレードする必要があります。 もっと簡単に、新機能だけをつまみ食いするような形で段階的なアップグレードを行えないかずっと悩んでいたのですが、よい考えは浮かびませんでした。 そんな中、3年ほど前に仕事の都合でLaravelという別のフレームワークを触る機会がありました。 そして私は気付きました。気付いてしまったのです。 CakePHP2はCakePHP3以降にアップグレードするよりもLaravelに移行する方が遥かに簡単だということに。 本稿ではbaserCMSを実際にLaravelで動作させてみることで、CakePHP2からLaravelへの移行がいかに簡単かをご紹介したいと思います。 必須要件 PHP >= 7.3 Composer >= 2.0 最新のLaravel 8.xをインストールするためにPHPのバージョンは7.3以上である必要があります。PHPのバージョンが7.3未満でも7.2.5以上であれば代わりに6.xをインストールすることもできますが、保守期間が8.xよりも少し短くなっています。 また、LaravelではComposerを使用しますのでComposerも必要です。 インストール まずはbaserCMSをインストールガイドにしたがってインストールしましょう。テーマは既定のBcSampleを選択してください。インストールが完了したら、簡単な動作確認をしておきましょう。 Laravel化 baserCMSをLaravelで動作させるまでにかかる時間は5分くらいだと思います。 オートロード設定 では、早速Laravel化していきましょう。 Laravelではオートロードの設定が必要です。baserCMSのROOTにcomposer.jsonがありますので、それを開いて設定を追加します。 { ... "autoload": { "psr-4": { "Baser\\": "lib/Baser", "App\\": "app" } }, ... } 設定を追加したら、dump-autoloadを実行します。 composer dump-autoload 関連パッケージ取得 まず、ComposerでLaravelのフレームワーク本体を取得します。 composer require "laravel/framework:^8.0" 次にOstoandelプラグインを取得します。CakePHP2をLaravelで動作させるためのプラグインとして開発しました。まだ、リリース前なのでdev-masterを指定してください。 composer require "ostoandel/ostoandel:dev-master" 最後にLaravelのアプリケーションを取得します。コマンドが少し違うので注意してください。 composer create-project --no-install --no-scripts "laravel/laravel:^8.0" プラグイン読込 app/Config/bootstarp.phpでOstoandelプラグインを読み込んでください。pathは必須です。 CakePlugin::load('Ostoandel', ['path' => VENDORS . '/ostoandel/ostoandel/plugin/']); Laravel化コマンド実行 Ostoandelプラグインを読み込んだらLaravel化コマンドを実行します。ROOT以下のファイルを書き換えますので、必要に応じてバックアップを取ってください。 cake Ostoandel.laravelize retry関数 baserCMSの場合にはもうひとつだけ追加の手順があります。lib/Baser/basics.phpを開いてretry関数が定義されないようにしてください。Laravelにも同名の関数があり、名前が衝突してしまうためです。 if (!function_exists('retry')) { function retry($times, callable $callback, $interval = 0) { ... } } さて、これでbaserCMSがLaravelで動作するようになったはずです。さっそくトップページを開き直してみましょう。 baserCMSのトップページが開けば成功です。おつかれさまでした。エラーになってしまった場合には手順を見直してみてください。 といっても、見た目の変化がないのであまり実感がないかもしれませんね。では、存在しないページを開いてみましょう。 ね? ちゃんとLaravelのエラー画面が表示されたでしょう? 互換性のない機能 Laravelへの移行後もまったく変わらず動作するかというと、一部、互換性のない機能があります。 config関数 CakePHPでは名前を指定して設定ファイルを取り込むための関数です。 Laravelでは設定値を読み書きするための関数になっており、Ostoandelではこちらが使用されます。 もし、アプリケーション中でconfig関数を使用している場合は、代わりにincludeなどを使用するようにしてください。 cache関数 CakePHPではファイルを指定してキャッシュの読み書きをするための関数です。 Laravelではキーを指定してキャッシュの読み書きをする関数になっていて、ふるまいが異なっています。 CakePHPではすでに廃止予定としてマークされているため使用されていない場合が多いと思いますが、もし使用している場合には、代わりにCacheファサードなどを使用するようにしてください。 env関数 CakePHPとLaravel、いずれにおいても\$_SERVER、\$_ENVまたは環境変数から値を取得する関数なので、ある程度の互換性がありますが、一部のふるまいが異なっています。 CakePHPでは以下のキーについては他のキーを参照するなどして別の値を返すことがあります。 HTTPS SCRIPT_NAME REMOTE_ADDR DOCUMENT_ROOT PHP_SELF CGI_MODE HTTP_BASE HTTPSについてはoff以外の値の場合には真を、offの場合には偽を返します。 その他のキーについての詳細はenv関数の実装を参照してください。 一方、Laravelでは一部の値について型の変換や空文字への変換が行われます。 true または (true) false または (false) empty または (empty) null または (null) また、Laravelではキーが存在しない場合に返す既定の値を第二引数で指定することができます。 複数のデータ元に同名のキーが存在する場合の優先順位についてもCakePHPとLaravelでは異なっています。CakePHPでは\$_SERVER、\$_ENV、環境変数の順番でキーを探しますが、Laravelでは\$_ENV、\$_SERVER、環境変数の順番でキーを探します。 以上のようにCakePHPとLaravelではenv関数のふるまいに若干の違いがありますが、中でも重要なのはHTTPSの扱いの違いです。CakePHPでは特別な変換が行われて真偽を返しますが、Laravelでは変換対象にはなりませんのでonやoffを文字列で返します。 OstoandelではLaravelのenv関数を使用しますが、HTTPSについては影響が大きいため、優先順位の差を利用して、\$_SERVER['HTTPS']の値に応じて\$_ENV['HTTPS']に真偽値を設定するようにしています。結果としてenv('HTTPS')は真偽を返すようになりますし、何らかの理由で\$_SERVER['HTTPS']を直接参照している処理があったとしても、これまで通りに動作すると思います。 コントローラーでの型宣言 CakePHP2ではControllerクラスで定義されていないpublicメソッドで、かつメソッド名がアンダースコアで始まらないメソッドは、既定のルーティングではアクションとして扱われます。 実はこの仕様は脆弱性に繋がる可能性もあります。 baserCMSではsendMailというメソッドがBcAppControllerに存在しますが、このメソッドはControllerクラスでは定義されておらず、publicで、メソッド名がアンダースコアで始まっていないため、本来であればこれもアクションとして呼び出すことが可能です。そのため、baserCMSではバックトレースを確認することで安全を担保しているようです。 class BcAppController exends Controller { ... public function sendMail($to, $title = '', $body = '', $options = []) { $dbg = debug_backtrace(); if (!empty($dbg[1]['function']) && $dbg[1]['function'] === 'invokeArgs') { $this->notFound(); } ... } ... } もし、バックトレースの確認がなければ、悪意のある第三者がメールを送信できてしまっていたでしょう。 こうしたアクションではないpublicな共通メソッドが、ひょっとするとアプリケーションによっては型宣言のために守られているということもあるかもしれません。仮に以下のような共通メソッドがあったとします。 App::uses('CakeEmail', 'Network/Email'); class AppController exends BcAppController { ... public function sendCakeEmail(CakeEmail $email) { ... } ... } CakeEmailの型宣言があるため、仮に既定のルーティングを使用していたとしても、引数としてCakeEmailのインスタンスを渡すことまでは不可能ですので、このsendCakeEmailメソッドはアクションとして呼び出すことは通常はできません。 しかし、Laravel化した場合には、あとで紹介するサービスコンテナーによる依存性の注入の機能により、CakeEmailは自動的にインスタンス化されます。結果として、こうしたメソッドが脆弱性に繋がってしまう可能性があります。 コントローラーに共通メソッドを用意する場合には、protectedで宣言するか、メソッド名をアンダースコアで始めるようにしてください。 何らかのインターフェイスを実装する必要があるなど、どうしてもpublicで、かつアンダースコアから始まらないメソッドを用意する必要がある場合は、methodsプロパティ―から手動で除外する方法もあります。 class AppController extends BcAppController { ... public function __construct($request = null, $response = null) { parent::__construct($request, $response); $this->methods = array_diff($this->methods, ['sendCakeEmail']); } public function sendCakeEmail(CakeEmail $email) { ... } ... } 運用中のアプリケーションをLaravel化させる場合には、アプリケーションにこうしたメソッドが存在しないかどうか、事前に確認した上でLaravel化するようにしてください。 Laravelの機能つまみ食い 以下の手順はすべて任意です。すべてに目を通す必要はありません。目ぼしい機能だけつまみ食いしてください。 なお、以下の節はあくまでbaserCMSの本体をLaravelに移行する想定で記載していますので、本体のコードを直接修正してしまっていますが、実際にお使いのアプリケーションをLaravel化する場合には、baserCMSのコアは修正すると以降のアップグレードを受けるのが難しくなってしまいますので、なるべく触らないようにしましょう。 エラー処理 インストールの手順で存在しないページの表示が変わってしまっていたと思います。これはCakePHPのエラー処理はもはや実行されていないためです。 でも、元通りにbaserCMSのエラー画面を表示したいかもしれませんね。 Laravelではエラー処理はapp/Exceptions/Handler.phpで行われていますので、OstoandelのLaravelExceptionRendererを使用するように修正します。 namespace App\Exceptions; ... use Ostoandel\Exceptions\LaravelExceptionRenderer; class Handler extends ExceptionHandler { ... public function render($request, $exception) { try { return (new LaravelExceptionRenderer($exception))->render(); } catch (\Throwable $e) { return parent::render($request, $exception); } } } renderメソッドをオーバーライドしてOstoandel版のExceptionRendererを使用するように変更しました。 これで元のエラー画面が表示されるようになるはずです。 デバッグバー Laravelでの開発を行いやすくするためにデバッグバーを導入してみましょう。 Composerからデバッグバーをインストールします。 composer require --dev "barryvdh/laravel-debugbar" インストール後、config/app.phpにサービスプロバイダーを追加します。 return [ ... 'providers' => [ ... Barryvdh\Debugbar\ServiceProvider::class, ], ... ]; ページを再読込すれば画面の左下にLaravelのロゴが現われるはずです。 ただ、このままだとちょっと問題もあります。あとで紹介するBladeViewを使用するとデバッグバーがエラーで表示されなくなってしまうのです。 あとでプルリクエストを出してみようと思っていますが、ここではデバッグバー側のコードを直接修正してしまいます。 namespace Barryvdh\Debugbar\DataCollector; ... class ViewCollector extends TwigCollector { ... public function addView($view) { ... } ... } ViewCollectorクラスのaddViewメソッドの引数から型宣言をなくしました。インターフェイスで宣言してくれていたらよかったのですが、クラスで宣言されているためBladeViewを渡せませんでした。 でも、これでBladeViewを使用してもデバッグバーを使用できるはずです。 データベース デバッグバーを見てみると、現状ではQueriesタブに何も表示されません。baserCMSのクエリーはCakePHPが実行されているため、Laravelはクエリーが実行されていることに気付いていないのです。 これはあまりよい状態ではありません。 現時点ではすべてのクエリーをCakePHPが実行している都合、問題はデバッグバーでクエリーを確認できないことくらいですが、たとえばLaravelのEloquentモデルを部分的に導入したいと考えたとしましょう。 そうなれば、CakePHPとLaravelがそれぞれクエリーを実行することになります。 当然、トランザクションも別になってしまいますので、一貫性のある処理を書くことができなくなってしまいます。 この状態を脱するためにDboSourceにLaravelのConnectionを使用してクエリーを実行させることもできます。 まずはLaravel側のデータベースの設定を行います。.envを開いて以下の設定を環境に合わせて変更しましょう。 DB_CONNECTION=mysql DB_HOST=127.0.0.1 DB_PORT=3306 DB_DATABASE=laravel DB_USERNAME=root DB_PASSWORD= 次にconfig/database.phpを開いて適切なprefixを設定してください。また、strictについてはfalseにする必要があります。これはCakePHPが緩いクエリーを実行する場合があり、厳格なモードだとエラーになってしまうためです。 return [ ... 'connections' => [ ... 'mysql' => [ ... 'prefix' => 'mysite_', ... 'strict' => false, ... ], ... ], ... ]; 続いてCakePHP側の修正を行います。 baserCMSでは、データベース接続に標準のDboSourceを拡張したBcMysqlクラスを使用しているようですので、こちらを以下のように改造します。 use Ostoandel\Traits\LaravelDatabase; class BcMysql extends Mysql { use LaravelDatabase; ... } ページを更新してみましょう。 デバッグバーのQueriesタブにbaserCMSが実行したクエリーが表示されるようになったはずです。 おなじみのapp/Config/database.phpの設定はdatasourceの指定を除いて不要になりました。 class DATABASE_CONFIG { public $default = [ 'datasource' => 'Database/BcMysql', ]; public $test = [ 'datasource' => 'Database/BcMysql', ]; } なお、CakePHP側のdefaultの設定はLaravel側のdefaultの設定に読み替えられます。Laravel側のdefaultをmysqlから変更したい場合には、.envでDB_CONNECTIONにpgsqlなどを指定します。 また、testの設定はPHPUnitを実行する場合には必要になるので、mysqlの設定を参考に追加するといいでしょう。 return [ ... 'connections' => [ ... 'test' => [ ... ], ... ], ... ]; なお、データベースの接続をLaravelに任せるようにすると、副作用としてcakeコマンドが動かなくなってしまいますが、安心してください。Ostoandelではcakeコマンドはartisanコマンドのサブコマンドとして実行できるようになっています。 php artisan cake bake ストレージ CakePHPでは一時ファイルなどはapp/tmpに置かれましたが、Laravelではstorageに置かれます。 置き場所をstorageに統一したい場合には、app/Providers/AppServiceProvider.phpで関連する定数を定義します。おすすめの設定は以下の通りです。 namespace App\Providers; ... class AppServiceProvider extends ServiceProvider { ... public function register() { defined('TMP') || define('TMP', storage_path('framework/')); defined('CACHE') || define('CACHE', storage_path('framework/cache/')); defined('LOGS') || define('LOGS', storage_path('logs/')); } ... } 定数のほとんどはCakeServiceProviderのbootメソッド中で定義されますので、AppServiceProviderのregisgterメソッドで先に定義することができます。 ルーティング Laravelの書き方でルーティングを定義することもできます。routes/web.phpを以下のように変更してみます。 Route::prefix('admin')->namespace('\Baser\Controller')->group(function() { Route::any('favorites/ajax_add', 'FavoritesController@admin_ajax_add'); Route::any('favorites/ajax_delete', 'FavoritesController@admin_ajax_delete'); Route::any('favorites/ajax_edit/{id}', 'FavoritesController@admin_ajax_add'); Route::any('favorites/update_sort', 'FavoritesController@admin_update_sort'); }); Route::fallbackToCake(); Laravelではコントローラーやアクションにワイルドカードを使用することはできません。必要なルーティングは明示的に定義する必要があります。 また、コントローラーには任意の名前空間を使用することができます。利用者のアプリケーションと名前空間を分けるために例ではBaser\Controllerを指定してみました。まだFavoritesControllerはグローバル名前空間に定義されているので修正を行いましょう。 FavoritesControllerで名前空間を使用するように変更します。 namespace Baser\Controller; use AppController; ... class FavoritesController extends AppController { ... public $name = 'Favorites'; ... } 注意点として、名前空間を使用した場合にはnameプロパティ―を指定する必要があります。CakePHPはコントローラーの名前からモデルやビューを推測しますが、名前が指定されていない場合はクラス名を使用します。名前空間を使用するとクラス名も変わるため、モデルやビューを正しく取得できなくなってしまいます。ただ、baserCMSでは最初から指定されているみたいですね。 もう一つの注意点として、CakePHPのルーティングにフォールバックしない場合、コントローラーのコンストラクターにはCakeRequestとCakeResponseは渡されません。 これはLaravelのルーティングの仕様によるものなのですが、baserCMSのBcAppControllerのコンストラクターは以下のようなコードになっているため、CakeRequestが渡されないと動作しませんでした。 class BcAppController extends Controller { ... public function __construct($request = null, $response = null) { parent::__construct($request, $response); ... $isRequestView = $request->is('requestview'); ... } ... } このままだと動作しないので、コンストラクターの代わりにconstructClassesをオーバーライドするように変更します。 class BcAppController extends Controller { ... public function constructClasses() { $request = $this->request; ... $isRequestView = $request->is('requestview'); ... parent::constructClasses(); } } constructClassesはコンストラクターが呼ばれた直後に呼び出されるので、この変更による影響はないはずです。 それから、BcRedirectMainSiteFilterクラスも修正する必要があります。このクラスはコントローラーの存在確認のためにCakePHPのコントローラーを探してしまうため、コントローラーを名前空間に入れるとエラーになってしまうようです。Laravelのルーティングでコントローラーが取得できている場合にはチェックしないように修正します。 class BcRedirectMainSiteFilter extends DispatcherFilter { ... protected function _existController($request) { if (request()->route()->controller) { return true; } ... } } さて、お気に入りの登録や削除を行ってみましょう。ちゃんと動くはずです。 Bladeテンプレート CakePHP2からCakePHP3以降へのアップグレードを試みたことのある方は、ビューをアップグレードすることさえ難しいことに気付いたはずです。 ビューをアップグレードするには、その依存関係にあるヘルパー、リクエスト、レスポンス、イベントマネージャー等をアップグレードする必要があります。それらにも依存関係があり、そうやって修正が次々に別の層に波及して、結局アプリケーション全体を修正するはめになってしまうのです。 一方、Laravelに移行するのであれば、一部のビューだけ部分的に導入することさえ可能です。実際にbaserCMSの一部のビューにLaravelのBladeテンプレートを使用してみましょう。 まず、BcAppViewクラスを継承したBladeViewクラスを作成します。Ostoandelも標準でBladeViewを用意してあったのですが、baserCMSは独自の仕様でビューを探すため、そのままでは使用できませんでした。なお、BladeViewクラスではLaravelのViewインターフェイスを実装する必要があります。LaravelBladeトレイトを使用するのが簡単です。 use Ostoandel\Traits\LaravelBlade; App::uses('BcAppView', 'View'); class BladeView extends BcAppView implements \Illuminate\Contracts\View\View { use LaravelBlade; } 次に、PagesControllerのdisplayアクションを以下のように変更します。 class PagesController extends AppController ... public function display() { $this->viewClass = 'Blade'; ... } } 先ほど作成したBladeViewが使用するように変更しました。 トップページを開けば、デバッグバーのViewsタブにBladeViewが処理したテンプレートの一覧が表示されるようになるはずです。 BladeはPHPの構文はのまま解釈しますので、\$thisとしてViewのインスタンスさえ渡すことができれば、既存の.ctpファイルを処理することができるのです。 早速Bladeの構文も使ってみましょう。app/webroot/theme/bc_sample/Pages/templates/default.phpを修正します。 @extends('/Layouts/default') @section('content') <?php $this->layout = 'empty'; $this->BcPage->content(); ?> @endsection CakePHPの代わりにBladeにレイアウトを描画させてみました。 なお、emptyレイアウトを指定しているのは、レイアウトが重複して描画されないようにするためです。レイアウトをfalseに設定するのも誤りではありませんが、View.beforeLayout/View.afterLayoutイベントが実行されなくなってしまうので、空のレイアウトを指定する方がよいと思います。 また、@extendsディレクティブの第一引数にはCakePHPが理解できるパスを渡してあげてください。LaravelのFileViewFinderの代わりにCakePHPのViewにファイルを探させているからです。 さて、トップページを開いてみましょう。ちゃんとページが表示されるはずです。 ところで、Bladeの@sectionディレクティブの中身を取得するには、対応する@yieldディレクティブが必要なはずですがレイアウト側は修正していません。なぜちゃんとコンテンツが表示されているのでしょう? その答えは、BladeViewではfetchメソッドと@yieldディレクティブが等価になっているからです。仮に一部のビューをBladeに移行したとしてもレイアウトまで修正する必要はありません。段階的にビューをBladeに移行した後、最後にレイアウトを移行することもできます。 Eloquentモデル CakePHPのモデルの代わりにEloquentモデルを使用することもできます。 artsianコマンドからモデルを作成します。対応するCakePHPのモデルと同じ名前にしてください。 php artisan make:model Content app/Models/Content.phpが作成されるはずです。なお、Laravel 6.xをインストールした場合、Eloquentモデルのファイルはapp/Content.phpに作成されます。 さて、作成されたばかりのこのEloquentモデルをCakePHPのモデルと連携できるようにします。継承するクラスの名前空間をIlluminateからOstoandelに変更してください。 namespace App\Models; ... use Ostoandel\Database\Eloquent\Model; class Content extends Model { ... } これでEloquentモデルがCakePHPのモデルと連携できるようになりました。早速どこかで使ってみましょう。 モデルの取得 ContentsControllerの_createAdminIndexConditionsByTableメソッドに以下のような処理があります。 $content = $this->Content->find('first', ['fields' => ['lft', 'rght'], 'conditions' => ['Content.id' => $data['Content']['folder_id']], 'recursive' => -1]); $conditions['Content.rght <'] = $content['Content']['rght']; $conditions['Content.lft >'] = $content['Content']['lft']; コンテンツ一覧の表形式表示時に絞り込み検索で使われるクエリーのようです。recursiveに-1を指定しているので、Contentモデルだけを取得する単純なクエリーですね。 実行されるクエリーは以下の通りです。 SELECT `Content`.`lft`, `Content`.`rght` FROM `cms`.`mysite_contents` AS `Content` WHERE `Content`.`id` = 6 AND `Content`.`deleted` = '0' LIMIT 1 これをEloquentモデルを使用して書き換えると下のようになります。 $content = \App\Models\Content::where('id', $data['Content']['folder_id'])->first(['lft', 'rght']); $conditions['Content.rght <'] = $content->rght; $conditions['Content.lft >'] = $content->lft; ちょっとすっきりしました。返値も配列からオブジェクトになったので、オブジェクト演算子も使用できるようになりました。 実行されたクエリーは以下の通りです。 select `lft`, `rght` from `mysite_contents` where `id` = '6' and `deleted` = 0 limit 1 ところで、idを指定しただけの単純な命令だったはずですが、実行されたクエリーを見てみると追加の条件としてdeletedが勝手に指定されているのがわかると思います。これはContentモデルがSoftDeleteビヘイビアーを使用しているためです。 でも、CakePHPのモデルはともかく、Eloquentモデルが実行したクエリーでもdeletedが指定されているのはちょっと不思議じゃないでしょうか? LaravelにもSoftDeletesトレイトというのが存在しますが、今回は使用していません。 実はEloquentモデルがクエリーを実行する際にModel.beforeFindイベントを発生させていて、それをSoftDeleteビヘイビアーが捕まえて、Eloquentモデルによって構築されたクエリーを書き換えているからなのです。 アソシエーションの取得 アソシエーションを取得したい場合はどうでしょう。 PagesControllerのadmin_editアクションに以下のようなクエリーがあります。 $this->Page->recursive = 2; $this->request->data = $this->Page->read(null, $id); CakePHP2でも仮にContainableBehaviorを使用していれば以下のように書けそうですね。 $this->Page->contain('Content.User'); $this->request->data = $this->Page->read(null, $id); Eloquentモデルではクエリービルダーのwithメソッドを使用すると同じように書くことができます。 Contentモデルの時と同じ手順でPageのEloquentモデルを作成します。 php artisan make:model Page また、UserモデルはLaravelが既定で提供しているモデルが存在するためmake:modelコマンドがエラーになりますので、既存のファイルの名前を変更するか、--forceオプションをつけて上書きしましょう。 php artisan make:model --force User 継承するクラスの名前空間をOstoandelに変更するのも忘れないでください。 Eloquentモデルを修正したら、admin_editアクションを以下のように修正します。 $this->request->data = \App\Models\Page::with('Content.User')->find($id)->toCakeArray(); Laravel側ではアソシエーションは定義する必要はありません。CakePHPのモデルの設定からアソシエーションを判断しています。 モデルの保存 UsersControllerのadmin_addメソッドに以下のような保存処理があります。 $this->User->create($this->request->data); if ($this->User->save()) { $this->request->data['User']['id'] = $this->User->id; ... } else { ... } これをEloquentモデルを使用するように変更してみましょう。以下のように書き直します。 $user = new \App\Models\User(); $user->guard(['id']); $user->fill($this->request->data['User']); if ($user->save()) { $this->request->data['User']['id'] = $user->id; ... } else { ... } こんな感じになります。 さて、ユーザー登録を行ってみましょう。 データベースを覗いてみるとちゃんと保存されているようですが、パスワードがハッシュ化されていますね。いつの間にハッシュ化されたのでしょう? 実はOstoandelが提供するEloquentモデルのsaveメソッドはCakePHPのモデルのラッパーに過ぎません。 Model.beforeSaveやModel.afterSaveを捕まえて何かをするとうのはありそうなことなので、Eloquentモデルから保存を行った場合にも同じように動作するようにしておきました。 モデルの削除 モデルの削除も保存と同様にCakePHPのモデルのラッパーとしてふるまいます。 PagesControllerのadmin_deleteアクションでは、Pageモデルのdeleteを呼んでいます。 class PagesController extends AppController { ... public function admin_delete() { if (empty($this->request->data['entityId'])) { return false; } if ($this->Page->delete($this->request->data['entityId'])) { return true; } return false; } ... } このメソッドはModelのdeleteをオーバライドしていて、データベースからレコードを削除するのに加えて、テンプレートも削除しますが、これをEloquentモデルを使用して書き換えてみます。 class PagesController extends AppController { ... public function admin_delete() { $page = \App\Models\Page::find($this->request->data('entityId')); return !$page || $page->delete(); } ... } こんな感じです。 ただ、実際に画面からコンテンツをゴミ箱に入れ、ゴミ箱を空にしてみたところ、削除時にパスが見つからなくてテンプレートは消えませんでした。ゴミ箱に入れた時点でparent_idが消えてしまうのが原因みたいです。でも、ちゃんとPageモデルのdeleteメソッドは呼ばれているはずです。 サービスコンテナー Laravelの主要機能であるサービスコンテナーを使用して、コントローラーに依存性の注入を行ってみましょう。CakePHPでも4.2から試験的に導入されている機能ですね。 もっとも、実はCakePHP2にも似たような機能ならすでにあります。 CakePHPでは使用するコントローラーで使用したいモデルはusesプロパティ―にクラス名を書くだけで、UserモデルやUserGroupモデルをコントローラーのプロパティ―として使用できるようになると思います。 class UsersController extends AppController { ... public $uses = ['User', 'UserGroup']; ... public function admin_edit($id) { ... if (empty($this->request->data)) { $this->request->data = $this->User->read(null, $id); } else { ... } ... } } ところで、このモデルたちはどこでどのように作られたのでしょう。というか、何気なく使っていますが、本当にそれぞれUserクラス、UserGroupクラスのインスタンスなんでしょうか? 実際のところ、コントローラーは自分が使用するモデルたちがどこでどのように作られたのかなんて気にしません。それはClassRegistryの領分だからです。もちろん通常はUserモデルはUserクラスのインスタンスになるはずですが、すでにその派生クラスがClassRegistryに格納されていればそれが使用されます。ユニットテストを実行した場合には、ClassRegistryはテスト用のデータベースを参照する設定でモデルをインスタンス化してくれますよね。 LaravelのサービスコンテナーはそんなClassRegistryの上位互換です。Laravelのサービスコンテナーを使用すると、たとえばモデルのインスタンスをプロパティ―ではなく、メソッドの引数として取得することができます。 先ほどのアクションをLaravelのサービスコンテナーを使用して書き直すとこうなります。 class UsersController extends AppController { ... public function admin_edit(User $userRepository, $id) { ... if (empty($this->request->data)) { $this->request->data = $userRepository->read(null, $id); ... } else { ... } ... } ... } Userクラスのインスタンス化の方法はサービスプロバイダーで指定します。指定しなくても勝手にインスタンス化されますが、モデルを勝手にインスタンス化されるとユニットテストで困りますので、とりあえずClassRegistryでも使っておきましょう。 namespace App\Providers; class AppServiceProvider extends \Illuminate\Support\ServiceProvider\ServiceProvider { ... public function register() { $this->app->singleton('User', function() { return \ClassRegistry::init('User'); }); } ... } だいぶLaravelっぽいコードになってきましたね。 リクエスト リクエストはCakePHPではCakeRequestを使用します。CakeRequestはルーティング、コントローラー、コンポーネント、ビュー、ヘルパーなど、アプリケーションの多くの層で使用されているため、簡単にLaravelに移行することは難しいでしょう。たとえば、フォームヘルパーに初期値を表示させるためにはCakeRequestのdataプロパティ―に値を設定する必要があります。 ただ、参照のみであればLaravelのRequestを使用することができます。 SiteConfigsControllerのadmin_check_sendmailアクションは下のような処理になっています。 class SiteConfigsController extends AppController { ... public function admin_check_sendmail() { if (empty($this->request->data['SiteConfig'])) { $this->ajaxError(500, __d('baser', 'データが送信できませんでした。')); } $this->siteConfigs = $this->request->data['SiteConfig']; ... } ... } LaravelのRequestを使用して書き直すと下のようになります。 use Illuminate\Http\Request; ... class SiteConfigsController extends AppController { ... public function admin_check_sendmail(Request $request) { $data = data_get($request->post(), 'data.SiteConfig'); if (!$data) { $this->ajaxError(500, __d('baser', 'データが送信できませんでした。')); } $this->siteConfigs = $data; ... } ... } レスポンス レスポンスにはCakePHPではCakeResponseを使用します。CakeResponseもCakeRequest同様に多くの層で利用されているため、やはりすぐにLaravelのResponseに置き換えるのは困難かもしれません。 部分的に導入することから始めましょう。たとえば、UsersControllerのログアウト処理は以下のようになっています。 class UsersController extends AppController { ... public function admin_logout() { ... $this->redirect($redirect); } ... } ControllerのredirectメソッドはCakeResponseがレスポンスを送信し、既定ではexitまで呼び出しますが、それだとLaravelのミドルウェアのレスポンス処理の部分が動作しません。 このリダイレクトは以下のように書き直すことができます。 class UsersController extends AppController { ... public function admin_logout() { ... $this->autoRender = false; return redirect($redirect); } ... } 代わりにLaravelのredirectヘルパーを使用しました。レスポンスを返却させるためにreturnも使用しています。また、テンプレートを描画されては困りますのでautoRenderはfalseに指定しています。 ただし、この方法だとController.beforeRedirectイベントが発生しません。アプリケーションがこのイベントを必要としている場合には動作しません。 代わりにLaravelResponseトレイトを使用する方法もあります。 use Ostoandel\Traits\LaravelResponse; ... class AppController extends BcAppController { use LaravelResponse; ... } LaravelResponseトレイトは、exitを実行する代わりにHttpResponseExceptionを投げます。 こうしておくことで、Controller.beforeRedirectイベントは残しつつ、ミドルウェアもレスポンスを捕まえることができるようになるはずです。 ミドルウェア ミドルウェアを使用することもできます。CakePHPでも3.4から導入されている仕組みです。 CakePHP2の概念ではディスパッチャーフィルターに該当します。 なお、Laravelではコントローラーでもミドルウェアの指定ができますが、Ostoandelではルーティングでのみサポートしています。コントローラーでもサポートは可能なのですが、getMiddlewareメソッドを定義するとCakePHPではアクションになってしまうのでやめておきました。 CSRF防御 VerifyCsrfTokenミドルウェアをSecurityComponentが提供するCSRF防御の代わりに使用してみましょう。 まずはVerifyCsrfTokenを有効化します。 namespace App\Http; ... class Kernel extends HttpKernel { ... protected $middlewareGroups = [ 'web' => [ ... \App\Http\Middleware\VerifyCsrfToken::class, ... ], ... ]; ... } 次にSecurityComponentのCSRF防御を無効にします。 class AppController extends BcAppController { public function beforeFilter() { parent::beforeFilter(); $this->Security->csrfCheck = false; } } さて、この状態でCSRF防御が有効になっているか確認してみましょう。 試しにログイン画面からログインを行ってみると419エラーが返されます。CakePHPのFormHelperが描画したフォームが、LaravelのCSRFトークンを送信できていないからですね。ちゃんと防御できているようです。 では、FormHelperがLaravelのCSRFトークンを送信できるようにしましょう。 まず、SecrityComponentが生成したCSRFトークンをBcAppControllerのbeforeRenderメソッド中でLaravelのものに置き換えます。 class BcAppController extends Controller { ... public function beforeRender() { $this->request->params['_Token']['key'] = $this->getToken(); parent::beforeRender(); ... } ... protected function getToken() { return csrf_token(); } ... } 次に、VerifyCsrfTokenミドルウェアを修正してFormHelperが送信するトークンを受け取れるようにします。 namespace App\Http\Middleware; ... class VerifyCsrfToken extends Middleware { ... protected function getTokenFromRequest($request) { $token = $request->input('data._Token.key') ?? $request->input('_Token.key'); if ($token) { $request->merge(['_token' => $token]); } return parent::getTokenFromRequest($request); } } これでCSRF防御を突破できるようになったはずです。 なお、baserCMSではSecurityComponentのcsrfUseOnceオプションが有効になっており、リクエスト毎に毎回新しいトークンを取得し直すためにjquery.bcToken.jsという独自のライブラリーを使用しているようです。 ただ、LaravelにはcsrfUseOnceと同等のオプションはないため、この変更によりCSRFトークンはセッション毎の固定になります。トークンを取得し直しても同じ値が返されるだけになっていますが、安全上は問題ありません。OWSAPでもCSRFトークンはセッション毎に発行すればよいことになっています。 Cookie CakePHPではCookieの読み書きはCookieComponentを使用して行います。Cookieの暗号化・複合化を行うのもCookieComponentの仕事でした。 LaravelではEncryptCookiesミドルウェアが暗号化・複合化を行います。CakePHPでも3.4以降はEncryptedCookieミドルウェアに置き換わっていますので、いずれの道に進むにしてもCookieComponentとはこの辺で別れを告げる必要がありそうです。 まずはEncryptCookiesミドルウェアを有効化します。 namespace App\Http; ... class Kernel extends HttpKernel { ... protected $middlewareGroups = [ 'web' => [ \App\Http\Middleware\EncryptCookies::class, ... ], ... ]; ... } 次に、Cookieの暗号化を例外を設定します。CookieComponentを使用している箇所が残っている場合、二重に暗号化されてしまうためです。 namespace App\Http\Middleware; ... class EncryptCookies extends Middleware { ... protected $except = [ 'CakeCookie', ]; } Cookieを書き込む場合はResponseクラスを使用できればよいのですが、すぐには難しいかもしれません。 代わりにCookieファサードのqueueメソッド使用して書き込むとよいでしょう。AddQueuedCookiesToResponseミドルウェアが後でResponseにCookieを追加してくれます。 実例を見てみましょう。UsersControllerでCookieComponentを使用している箇所があります。 class UsersController { ... public function admin_login() { ... $this->Cookie->destroy(); ... } ,.. public function setAuthCookie($data) { ... $this->Cookie->httpOnly = true; $this->Cookie->write(Inflector::camelize(str_replace('.', '', BcAuthComponent::$sessionKey)), $cookie, true, '+2 weeks'); // 3つめの'true'で暗号化 } ... public function admin_logout() { ... $this->Cookie->delete(Inflector::camelize(str_replace('.', '', BcAuthComponent::$sessionKey))); ... } } Cookieファサードを使用して書き直すと下のようになります。 use Illuminate\Http\Request; use Illuminate\Support\Facades\Cookie; ... class UsersController { ... public function admin_login(Request $request) { ... $cookieKey = Inflector::camelize(str_replace('.', '', BcAuthComponent::$sessionKey)); Cookie::expire($cookieKey); ... } ... public function setAuthCookie($data) { ... $cookieKey = Inflector::camelize(str_replace('.', '', BcAuthComponent::$sessionKey)); Cookie::queue($cookieKey, json_encode($cookie), strtotime('+2 weeks') / 60); } ... public function admin_logout() { ... $cookieKey = Inflector::camelize(str_replace('.', '', BcAuthComponent::$sessionKey)); Cookie::expire($cookieKey); ... } ... } LaravelではCookieを暗号化している場合、値に配列を使用することができないため、json_encodeを呼んでいます。 また、リダイレクト時にResponseにCookieが書き込まれるようにLaravelResponseトレイトも使用する必要があります。もし使用していなければレスポンスの節を参照して使用してください。 Cookieを読み出す場合にはRequestクラスのcookieメソッドを使用します。 上で書き込んだCookieはBcAuthConfigureComponentで使用されているようです。 $cookie = $Controller->Cookie->read($cookieKey); CookieComponentの代わりにRequestクラスを使用するように書き直してみましょう。今回は書き込む時にjson_encodeを呼んでいたので、読み出す時にはjson_encodeを呼ぶ必要があります。 $cookie = json_decode(request()->cookie($cookieKey), true); これでCookieComponentを使用せずに同じ動作をするようになるはずです。 セッション セッションについてはLaravelとCakePHPがそれぞれ管理しています。 一元化するにはconfig/app.phpでOstoandel版のCakeSessionを使用するように設定します。 Ostoandel版のCakeSessionはLaravelのSessionファサードのラッパーになっています。 return [ ... 'aliases' => [ ... 'CakeSession' => Ostoandel\Fake\CakeSession::class, ... ], ]; ただ、baserCMSではこれだけでは動作しません。理由は以下の3点です。 \$_SESSIONを使用している箇所がある session_idなどPHPのセッション系関数を使用している箇所がある exitを使用している箇所がある LaravelのセッションはPHPのセッションの仕組みを使わず、StartSessionミドルウェアによって独自に実装されています。PHPのセッションはsession_startによって開始され、プログラムの終了時に保存されますが、このふるまいはミドルウェアの概念とは相容れません。 StartSessionミドルウェアではリクエスト処理前にセッションを開始し、リクエスト処理後に保存します。PHPのセッションの仕組みは用しませんので、session_startは呼ばれず、\$_SESSIONも定義されず、session_register_shutdownも登録しませんので、プログラム終了時にセッションが保存されることもありません。 したがって、\$_SESSIONを使用していたり、session_idなどPHPのセッション系関数を使用していたり、exitを使用していたりすると、期待通りに動作しません。 Ostodandelでは、こうした場合のためにcakeセッションドライバーを用意しています。このドライバーはPHPのセッションの仕組みを利用します。つまり、\$_SESSIONや、session_idなどのPHPのセッション系関数を使用することができ、またexitが呼ばれたタイミングでセッションを保存します。 cakeセッションドライバーを利用するには.envでSESSION_DRIVERにcakeを指定します。 SESSION_DRIVER=cake SESSION_COOKIE=BASERCMS SESSION_COOKIEにはBASERCMSを指定しました。EncryptCookiesミドルウェアを使用している場合、このCookieは暗号化から除外する必要があります。 namespace App\Http\Middleware; ... class EncryptCookies extends Middleware { ... protected $except = [ 'CakeCookie', 'BASERCMS', ]; } baserCMSではミドルウェアが実行される前にすでにセッションを開始しているからです。 認証 CakePHPのAuthComponentの代わりにLaravelのAuthenticateミドルウェアを使用することもできます。Laravelのルーティングを使用しますので、先にルーティングの節もご一読ください。 さて、まずはログインとログアウトをLaravelに任せるところまでをやってみます。 既定ではデータベースのパスワードはBlowfishPasswordHasherでハッシュ化されている必要がありますが、baserCMSではSimplePasswordHasherでハッシュ化しているようなのでAppServiceProviderでEloquentUserProviderにCakeHasherを渡すようにします。 namespace App\Providers; ... use Illuminate\Auth\EloquentUserProvider; use Ostoandel\Hashing\CakeHasher; ... class AppServiceProvider extends ServiceProvider { public function boot() { Auth::provider('eloquent', function($app, $config) { return new EloquentUserProvider(new CakeHasher(), $config['model']); }); ... } } 次にEloquent版のUserモデルでAuthenticatableインターフェイスを実装します。対応するトレイトが存在するので、それを利用するのが簡単です。 namespace App\Models; ... use Ostoandel\Database\Eloquent\Model; class User extends Model implements \Illuminate\Contracts\Auth\Authenticatable { use \Illuminate\Auth\Authenticatable; ... } 次にBcAuthComponentのlogin/logoutメソッドの代わりに、LaravelのAuthファサードのattempt/logoutメソッドを使用するようにUsersControllerを修正します。 use Illuminate\Support\Arr; use Illuminate\Support\Facades\Auth; class UsersController extends AppController { ... public function admin_login_exec() { ... $credentials = Arr::only($this->request->data['User'], ['name', 'password']); if (Auth::attempt($credentials)) { return true; } ... } ... public function admin_login() { ... if ($this->request->data) { $credentials = Arr::only($this->request->data['User'], ['name', 'password']); Auth::attempt($credentials); ... } } ... public function admin_logout() { ... Auth::logout(); $logoutRedirect = Router::normalize($this->BcAuth->logoutRedirect); ... } ... } なお、BcAuthConfigureComponentでも呼ばれているようなので、そちらも修正する必要があります。 さて、これだけではCakePHP側に認証状態が伝わりませんので、Laravelのイベントを捕まえて、CakePHP側にも認証状態を伝えましょう。 app/Providers/EventServiceProvider.phpを開いてイベントリスナーを登録します。 namespace App\Providers; ... ... class EventServiceProvider extends ServiceProvider { ... protected $listen = [ ... \Illuminate\Auth\Events\Login::class => [ \App\Listeners\LoginListener::class, ], \Illuminate\Auth\Events\Logout::class => [ \App\Listeners\LogoutListener::class, ], ]; ... } まだイベントリスナーのクラスが存在しませんので、artisanコマンドで作成しましょう。 php artisan event:generate app/Listenersの下にイベントリスナークラスが作成されますので、まずはLoginListenerを修正します。 namespace App\Listeners; ... use Illuminate\Auth\Events\Login; use Illuminate\Support\Facades\Session; ... class LoginListener { ... public function handle(Login $event) { Session::put(\BcAuthComponent::$sessionKey, $event->user->load(['UserGroup', 'Favorite'])->toArray()); } } baserCMSでは認証情報にUserGroupとFavoriteも必要なようでしたので、Loginイベント中でセットしました。 UserGroupとFavoriteのEloquentモデルが必要ですので、artisanコマンドで作成します。 php artisan make:model UserGroup php artisan make:model Favorite 継承するクラスの名前空間をOstoandelに変更するのも忘れないでください。 次にLogoutListenerを修正します。 namespace Appf\Listeners; ... use Illuminate\Auth\Events\Logout; use Illuminate\Support\Facades\Session; ... class LoginListener { ... public function handle(Logout $event) { Session::forget(\BcAuthComponent::$sessionKey); } } これでAuthファサードを使用してログイン・ログアウトができるようになったはずです。 続いてAuthenticateミドルウェアを使用するように変更します。簡単そうなDashboardControllerを例にします。 namespace Baser\Controller; use AppController; use BcUtil; use CakePlugin; ... class DashboardController extends AppController { ... public $name = 'Dashboard'; ... public function beforeFilter() { parent::beforeFilter(); $this->BcAuth->allow(); } ... } Authenticateミドルウェアによって守られていることを確かめるために、この例ではbeforeFilterメソッドをオーバーライドしてBcAuthComponentによるアクセス制限をなくしました。 また、DashboardControllerを名前空間に入れたことにも注意してください。名前空間に入れるのは必須というわけではありませんが、入れた方が安全です。 理由としては、これまで見てきたミドルウェアは原則すべてのアクションに適用させるものでしたが、Authenticateミドルウェアの場合には、認証を必要とするアクションと必要しないアクションがあるはずです。そして、認証を必要とするアクションに対して、万が一ミドルウェアが適用されないとアプリケーションを危険にさらしてしまいます。 名前空間に入れてしまえば、CakePHPのルーターでは接続できなくなります。したがって、意図せず既定のルーティングにフォールバックしてしまって、ミドルウェアが適用されずにアクションが呼び出されてしまう危険性をなくせるのです。 さて、routes/web.phpを開いてAuthentcateミドルウェアをDashboardControllerに適用してみましょう。 Route::prefix('admin')->namespace('\Baser\Controller')->middleware('auth')->group(function() { Route::any('/', 'DashboardController@admin_index'); }); Route::fallbackToCake(); 最後に、app/Http/Middleware/Authenticate.phpを開いてリダイレクト先を修正します。 namespace App\Http\Middleware; class Authenticate extends Middleware { ... protected function redirectTo($request) { if (! $request->expectsJson()) { return \Router::url(['controller' => 'users', 'action' => 'login', 'admin' => true]); } } } では、ダッシュボードにアクセスしてみましょう。ログイン画面にリダイレクトされるはずです。 おわりに いかがでしたか? CakePHP2をCakePHP3以降にアップグレードしようとすると壮大な計画を立てる必要があります。でも、Laravelに移行するなら5分後にはもう新機能を利用できるなんてワクワクしませんか? もっとも、baserCMSはすでにCakePHP4へのアップグレードの準備を着々と進められているようで、Ostoandelがお役に立てる機会はないかもしれませんが、もしCakePHP2からCakePHP3以降へのアップグレードに苦労されている方がいらっしゃいましたら、ぜひ本稿を参考に開発環境などで試験的に導入いただいてフィードバックをいただければ嬉しいです。 でも、ひょっとしたら、baserCMSがLaravelで作られたCMSとして生まれ変わり、マスコットキャラクターのべっしー君も、ららべっしー君へと進化する日が来たりして。
- 投稿日:2021-12-24T14:37:54+09:00
[初学者]Laravelとは何か?Webサイトを作る流れを見て理解する
この記事で何が分かるのか view,controller,ルーティングとは何か migration,seederのやり方 これらがどういう組み合わせで使われいるのか 以上について説明していきます。 環境 想定する環境 Ubuntu 18.4.6 (これはOracle VM VirtualBox上で動かしています) php 8.0.13 Laravel 8.73.1 MySQL 14.14 今回はローカル環境で動かします。なおこれらがすでに構築されている前提で話を進めます。構築の仕方はすでに先駆者が記事をいっぱい残していると思うので探してみてください。 viewファイルの作成 viewファイルとは、どのようにWebページとして表示するかが書かれている、いわゆるフロントエンドの処理方法を決めるファイルです。デフォルトではresources/viewにあり、welcome.blade.phpのみが存在してると思います。 bladeとは? Laravelで使われているテンプレートエンジンのことです。これがあることで、特定のユーザーだけにデータを表示させるといったとき使える「if文」やひな形になるviewファイルを作り、それ違うviewファイルで一部内容を書き換えて使いまわすときに使える「継承」などの処理をviewファイルに記載できるようになります。ファイルの拡張子を.blade.phpとすることで使えます。 ここのディレクトリにviewファイルを新規作成してみましょう。今回はindex.blade.phpとします。 これはコマンドで作れないので手動で作成してください。 作成したら次のように入力してみましょう。 resources/view/index.blade.php <h1>indexだよ</h1> Controllerの作成 Controllerとは、ルーティングで設定されて呼び出されたときに実行するプログラムが書かれているファイルです。ただ指定したviewファイルを表示させることもできますし、表示させたいviewファイルに渡してあげたい変数があればそれを準備してviewファイルと一緒に渡してあげることもできますし(例えば自分のアカウントの情報とか)、ユーザーに入力してもらったデータをMySQLに格納するといった処理もできます。デフォルトではapp/Http/Controllersに入ります。 では下記のコマンドを入力して Controller を作りましょう。 terminal $ php artisan make:controller TestController --resource なお"--resource"は、書くとcreate()を始めとした7つのメソッドを一緒に自動的に生成してくれるオプションです。これは追加したり編集したり削除したりするもの(=リソース)に使うコントローラーを作成する際に使ってください。 それでは作成したTestControllerに中身を記述していきましょう。とりあえずindexメソッドのみ記述していきます。 app/Http/Controllers/TestController.php ~~ public function index() { return view('index'); } ~~ これでTestControllerのindexメソッドが呼び出されるとindex.blade.phpが呼び出されこのviewファイルに書かれている内容が表示されるようになりました。 ルーティングの設定 ルーティングとは、クライアントから要求されたURLに対してどういう処理をするか定義することです。デフォルトではroute/web.phpに書くことになります。具体的に見ていきましょう。 route/web.php ~~ Route::get('/home', [App\Http\Controllers\TestController::class, 'index']); ~~ このように記述することで、/homeに対してhttpリクエストメソッドであるgetメソッドを使ってリクエストされるとTestControllerのindexメソッドを実行するようになります。TestControllerのindexメソッドにはindex.blade.phpを表示させるように記述されているので、ようやくこれで指定したURLにアクセスするとwebページが表示される一連の流れが完成しました。ターミナルで terminal $ php artisan serve と入力してhttp://localhost:8000/index にアクセスしてみてください。「indexだよ」と表示されていると思います。 これが基本となるのでぜひ覚えてください。ちなみにctrl+cでサーバーを閉じることができます。 migrationをしてみよう Webページにいろいろなデータを表示させたい場合、controllerでデータベースから値を取得してviewファイルに渡してあげる…などが考えられますが、このデータベースを構築するにはどうすればいいでしょうか。 このときlaravelの重要な機能としてmigrationというものがあり、これを使うことでデータベースにテーブルを作成できます。 テーブルを作成するならMySQLに直でコマンド打てばいいじゃんと思うかもしれないですが、これを使うメリットとして、gitなどを使って複数人で開発するときに簡単に共通のDBを構築できる(DBはローカルにしか保存しないので)、やり直しの際にmigrationコマンドをもう一度実行すればいいだけになる、そもそもmigrationでのデータベース操作の方が楽、といったことが挙げられます。 それでは実際にmigrationファイルを作成していきましょう。ターミナルで terminal $ php artisan make:migration create_users_table --create=users と入力してください。database/migrations配下にxxxx_xx_xx_xxxxxx_create_users_table.phpが作られたと思います。ちなみにこのように"--create=(変数名)"オプションを利用すると雛形が作られるようになります。 作られたファイルを開くと database/migrations/xxxx_xx_xx_xxxxxx_create_users_table.php <?php use Illuminate\Database\Schema\Blueprint; use Illuminate\Database\Migrations\Migration; class User extends Migration { /** * Run the migrations. * * @return void */ public function up() { Schema::create('users', function (Blueprint $table) { $table->increments('id'); $table->timestamps(); }); } /** * Reverse the migrations. * * @return void */ public function down() { Schema::dropIfExists('users'); } } のようになっていると思います。このようにcreate_tableにおけるmigrationファイルにはテーブル作成時の挙動がup関数に、テーブル削除時の挙動がdown関数に記述されています。これを少し書き換えてusersテーブルにカラムを追加していきましょう。 database/migrations/xxxx_xx_xx_xxxxxx_create_users_table.php ~~ public function up() { Schema::create('users', function (Blueprint $table) { $table->increments('id'); $table->string('name'); $table->string('email'); $table->timestamps(); }); } ~~ 書き換えたらmigrateしていきましょう。ターミナルで terminal $ php artisan migrate と入力することでmigrateが実行されます。これでテーブルが作られました。MySQLに入って\dコマンド等で確認してみてください。 seedしてみよう seedとはテーブルにレコードを挿入する機能です。これを用いることで、デフォルトで入れておきたいデータをあらかじめ挿入することやテストデータを簡単に挿入することができます。デフォルトではdatabase/seedersの中に入ります。 それではseederファイルを作ってみましょう。ターミナルで terminal $ php artisan make:seeder UserTableSeeder と入力することでseederファイルが作られます。作られたファイルを開いて database/seeders/UserTableSeeder.php <?php namespace Database\Seeders; use Illuminate\Database\Seeder; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Hash; class UserTableSeeder extends Seeder { /** * Run the database seeds. * * @return void */ public function run() { DB::table('users')->truncate();//テーブルの中身の初期化をしている $user = [ [ 'name' => 'test', 'email' => 'test@example.com', ] ]; DB::table('users')->insert($user); } } と書き換えましょう。書き換えたらseederファイルを実行していきます。ターミナルで terminal $ php artisan db:seed --class=UserTableSeeder と入力することでこのseederファイルが実行されます。これでダミーデータが入ったので、MySQLに入って"SELECT * FROM users;"等で確認してみてください。 controllerからviewに値を受け渡す 実際にcontrollerからviewに値を引き渡す流れを確認しましょう。TestControllerを書き換えてMySQLから値を取得しviewに渡すようにします。 app/Http/Controllers/TestController.php ~~ public function index() { $users = DB::table('users') ->select('id', 'name', 'email') ->get(); return view('index', compact('users')); } ~~ このように、viewファイルにDBから受け取った値を渡すときはcompactメソッドを使うということが分かると思います。なお今回はDBを操作する方法としてDBファザードを利用していますが他の方法としてモデルクラスを利用する方法もあります。詳しくはこちらを参照してください。 それではviewにこのデータを表示させるよう記述します。 resources/view/index.blade.php <h1>index</h1> <table> <tr> <th>ID</th> <th>名前</th> <th>メールアドレス</th> </tr> @foreach($users as $user) <tr> <td>{{$user->id}}</td> <td>{{$user->name}}</td> <td>{{$user->email}}</td> </tr> @endforeach </table> これによってviewにDBのデータが表示されるようになりました。ターミナルで terminal $ php artisan serve と入力してhttp://localhost:8000/index にアクセスして確認してみてください。 まとめ ここまででWebページを表示させる一連の流れとデータベースの利用方法が分かったと思います。ここでは書かなかったさらに重要な要素としてモデルやミドルウェアあたりがあります(自分があんまりわからないから書かなかった)。ミドルウェアを使うことでユーザー登録の際のメール認証やログ出力などができるようになるらしい。余裕がある人は調べてみてください。
- 投稿日:2021-12-24T11:24:10+09:00
【アウトプット】地域を限定的にしたフードロス削減サービスを作った
1.はじめに はじめまして!! 茶木(ちゃき)と申します。本名です。 宜しくお願い致します! 【Twitterアカウント】 https://twitter.com/chaki01288 現在は転職の為にウェブカツにてLaravelやVue.js、AWSを学んでいます。 アウトプットでWEBサービスやWordpressのテーマ販売サイトを作成しています。 今回はその一環としてフードロス削減サービスを作りました。 私事で恐縮ですが 自分自身、趣味が筋トレ、ランニングで6年以上続けているのですが、それらの努力が 無に帰す程お酒が大好きでコロナ禍で家でずっと飲んでました。 最近は少し落ち着いてきたのでたまに近所の居酒屋で飲んだりします。 (オミクロンでまた自粛中) 酔いも相まって、そこで働いてるおじいさんやおばあさんと仲良くなったりすると 何か出来る事はないかと思っちゃうんですよね。 2.目的 目的は前述した通りフードロス削減です。 なぜそのサービスを作ろうかと思ったか、その背景は以下の通りです。 ・世界のフードロス 突然ですが皆さんは日本の食品廃棄物てどのくらいあるか知ってますか? なんと、 年間2,550万tに及びます。 これは世界3位です。 ちなみに1位は中国、2位がアメリカです。 そして、その食品廃棄物の4分の1がフードロスなんです。 話しを戻しますが、正直世界がどうとかはあまり興味なくて、自分の手の届く範囲だけで 良いから地域で助け合い出来ないかなって思いまして、自分の好きな居酒屋さんや飲食店の 経費削減(フードロス)に貢献したいと思い作りました。 ・個人経営の居酒屋あるある 個人経営の居酒屋や飲食店で結構見かけると思うんですが、お客さんとの距離が やっぱり近いんですよね。 そしてフランチャイズとかではないから融通も利く。 例えば「こんなメニュー欲しい」とか「あんな飲み物作って」とか、本社を通したりするわけではないので フットワークも軽く出来ちゃうわけです。 それをリクエストしてくれたお客さんが毎日通ってくれて、毎回それを注文してくれるなら 良いと思います。 しかし現実問題そんな事はあまりなくて、メニュー数だけが無駄に増えていきます。 そしてメニューがあるって事はそれを作るための材料も抱えておかないといけない。 たまに顔出して頼んでくれるもんだからメニューを無くしたりもできない。 そうなると原価率だけ上がって、客数も単価も上がらないと利益が出ません。 そういった背景からこのサービスを作りました。 3.サービス概要 1.食材を登録または検索 新規登録するとまずは食材を登録、または食材を探す画面が表示されます。 お知らせ等もこの画面で見ます。 2.地域を限定して表示 登録してある食材はユーザーの登録した住所の近隣のお店の物しか表示されません。 このサービスのコンセプトが「手の届く範囲で良いから助け合う」だからです。 3.気になる食材があれば「いいね」 気になる食材があれば「気になるボタン」を押します。 相手ユーザーに通知され、気になるリストに登録されます。 4.その中からメッセージを送る相手を選ぶ 誰でも投稿者に自由にメッセージを送ることが出来ても相手からしたら煩わしいと思ったので メッセージでやりとりする相手は「いいね」を押したユーザーの中からか、食材の登録者自身が決める仕様にしました。 5.メッセージ等はPUSH通知でお知らせ PUSHERを使用しているので サイトにいる限りはリロードしなくても相手からのメッセージが届いた時や、気になるボタンを押してもらった時に通知が届きます。 参考:PUSHERの参考記事はこちら 5.賞味期限前日で自動削除 当然ですが賞味期限の切れた食材が表示されているとまずいので 賞味期限前日になったらその食材は自動で削除されます。 それに紐づくlikesテーブル,messagesテーブルも削除されます。 プロジェクト内のstorageフォルダ内の実際の画像ファイルも削除されるので プロジェクト内の肥大化を防ぎます。 食材を登録する際も賞味期限が当日のものは登録できません。 6.成約済みから3日経過で自動削除 メッセージを通して食材を渡すユーザーが決まったら、食材を登録したユーザーが「成約済み」ボタンを押します。 こちらも上記同様、成約済みのデータがいつまであってもパフォーマンスが落ちるので3日経つと削除されます。 4.こだわったポイント 1.登録した食材を使った人気レシピ表示 楽天レシピAPIと連携させて、登録した食材のおススメレシピTOP4を 表示させています。 ただ単に食材が登録されているよりも、その食材を使ってどんな料理を作るれるか。という体験をユーザーに想起させる事でフードロス削減に繋がりやすいかもしれないと考えました。 本当はクラシルかクックパッドのAPIがあればそちらを利用したかったです。 2.非同期処理 動的セレクトボックス 今までだとDBのカテゴリー情報等を展開する時は変数の中にDBから取得した配列を入れて、「foreach」で展開。というのが 当たり前でしたが、今回は楽天レシピの膨大なカテゴリーの中からユーザーがカテゴリーを選択します。その中から該当のものを選ぶのは大変なので大カテゴリーを選んだら中カテゴリーは動的に絞られる様にしました。 ページングやタブの切り替え ページングやタブもvueの非同期処理でDBの情報を展開しました。 タブを切り替えてもページ情報の保持が少し大変でした。 住所検索 こちらも入力した郵便番号をvueで取得してaxiosでPHP側に渡し、DB内の全国の住所情報と照らし合わせて 住所を自動入力できるようにしました。 3.食材は直接会いに行って受け取る カード決済機能とか実装しようと思ったんですが、このサービスのもう一つの狙いで「地域のコミュニティの活性化」があります。地域を限定したのもこれが理由で、コンセプトである「手の届く範囲で良いから助け合う」これを実現させる為にFace to Faceは不可欠だと思いました。 それゆえ自分の住んでる場所から歩いて行ける距離のお店しか表示されません。 それによる「ついで消費」も発生すれば良いかな。と 4.その他 食材の画像を登録する際は自動でリサイズしてstorageフォルダに保存したり テーブルを削除したらちゃんとプロジェクト内のimageファイルも削除等 とにかくプロジェクト内の肥大化を防ぐのとファイル一つ一つの軽量化を 心掛けました。 使用言語 Laravel5.8 JavaScript(Jquery,Vue.js) 外部サービスやAPI 楽天レシピ・・・レシピ取得 PUSHER・・・リアルタイム通知 vuejs-pagenate・・・ページネーション Intervention Image・・・画像リサイズ、圧縮 Croppie・・・プロフィール画像切り抜き登録 maatwebsite・・・CSVデータをDBにエクスポート ER図 今後実装したい事 まだ開発環境なのですが、デプロイするタイミングで追加したい機能です。 ソーシャルログイン機能 メールリマインダー サイトを開いていなくてもPUSH通知 課題 収益化 無料で公開しているうちはまだまだ3流、4流だと思うので今後どの様にして 収益化させるかが課題。 数ヵ月後に月額利用料(Larevel cashieを使ったサブスク決済等)、追加機能利用で料金発生、広告スペース設置 等々? そもそもターゲットが厳しいかもしれないのでポジショニングをずらさないといけないかもしれません。 個人経営の飲食店がターゲットだとそんなにお金持っていないだろうし。。。 色々考えないといけないですがまずは世に出すのが大切だと思ってます。 終わりに 最後まで読んで誠に頂きありがとうございました! 本番環境に移せたらまた記事を更新したいなと思います。 ご指摘等ありましたら、バシバシお願い致します。