- 投稿日:2019-12-02T19:35:21+09:00
Laravelにてテーブルの既存カラムをtinyint型に変更できない問題
環境
- Laravel 5.7.28
- MySQL 8.0.17
遭遇した問題
DBの容量を節約するため、既存カラムのデータ型をintからtinyintに変更するマーグレーションを実行した際、下記のようなエラーが表示された。
「tinyintegerなんてカラム型はないよ」と怒られているので、「あ、マイグレーションファイルにtinyIntegerではなくtinyintegerとタイピングしちゃったかな」なんて思って確認してみるも、間違っていない。ちなみにマイグレーションファイルの中身は以下のような感じです。
<?php use Illuminate\Support\Facades\Schema; use Illuminate\Database\Schema\Blueprint; use Illuminate\Database\Migrations\Migration; class ChangeContactTypeOfUsers extends Migration { /** * Run the migrations. * * @return void */ public function up() { Schema::table('users', function (Blueprint $table) { $table->tinyInteger('contact_type')->change(); }); } /** * Reverse the migrations. * * @return void */ public function down() { Schema::table('users', function (Blueprint $table) { $table->integer('contact_type')->change(); }); } }原因
Laravel公式ドキュメントのMigrationの項を当たってみると、以下の通り。
Only the following column types can be "changed"
の部分を和訳すると、「以下のカラムの型のみが変更可能です」となります。
試してみたところ、tinyintから別の型に変更することはできましたが、逆にtinyintには変更できませんでした。
つまり、Laravelではカラムの型をtinyintに変更することはできないということです。また、例えばtinyintからintに変更するマイグレーションが通ったとしても、ロールバック時にはコケるので、tinyint型のカラムの型変更は行わないほうがよいでしょう。
tinyintに変更することで、格納可能サイズを超過するデータが生じる恐れがあるため、このような仕様となっているのでしょうか。
詳しい方いらっしゃいましたらコメントにてご教授いただけると幸いです。対処法
一旦該当カラムをドロップし、再度tinyint型として追加することで対処しました。
結局、型変更のために2つのマイグレーションファイルの作成が必要になってしまいました。※2019/12/4 追記
職場の方にご指摘いただき気付いたのですが、DBファサードを利用してSQLクエリを実行すれば解決する問題でした。DB::statement("alter table users modify tel_type tinyint;");学び
テーブルを作成する際には、格納され得るデータを予め想定し、適切にデータ型を決定する必要があるということを学習しました。
参考文献
- 投稿日:2019-12-02T18:01:58+09:00
【Laravel6】Vueがデフォルトでインストールされなくなっていた件【Vue.js】
前提
Laravelは5.3以降、yarn(npm)でインストールコマンドを実行すると
vueがデフォルトでインストールされるようになっていました。package.jsonから色々消えた
Laravel6ではいくつかのパッケージがデフォルトから除外されたようで、
package.jsonの中身がすっきりしています。Vue.js
masterのpackage.jsonからvueがいなくなっていました。
2019/6/28に消されたようで、
当然、この日付以降に取得したLaravelでは、
yarn installをしてもvueでプロジェクトが作れません。
なので、手動で追加する必要があります。例(追記参照:非正攻法のようです。)$yarn add --dev vue vue-router一方、Laravel5.8を確認してみるとvueは残っており、
更新履歴的に特に消える気配はなさそうなので、Laravel6からの方針と思われます。他の消えたパッケージ
上記コミットを見てわかる通り、下記パッケージも同様にデフォルトでインストールされなくなりました。
- bootstrap
- jquery
- popper.js
以上。
コミットメッセージを見ても特に消された経緯がわからなかったので、
本件について何か情報をお持ちの方がいらしたらコメントを頂けると幸いです。追記
既存記事に解決策がありました
Laravel6でBootstrap, jQueryを使う方法
composer require laravel/ui php artisan ui vue
を使うのが正攻法のようです。
公式記事に解説がありました
- 投稿日:2019-12-02T16:08:06+09:00
Laravel SQLiteからMySQLに変更したら外部キー制約のマイグレーションが失敗してハマった
>php artisan -V Laravel Framework 6.1.0以下のusersに対して、user_idを外部キー制約として持つsubscribesテーブルを作りました。
Schema::create('users', function (Blueprint $table) { $table->bigIncrements('id'); $table->string('name'); $table->string('email')->unique(); $table->timestamp('email_verified_at')->nullable(); $table->string('password'); $table->rememberToken(); $table->timestamps(); });SQLiteで動作していたマイグレーションだったので、そのまま
php artisan migrate
したらエラーになりました。実行したマイグレーションSchema::create('subscribes', function (Blueprint $table) { $table->bigIncrements('id'); $table->integer('user_id')->unsigned(); $table->string('channel_id'); $table->string('channel_title'); $table->timestamps(); // userが削除されたとき、それに関連するも一気に削除する制約 $table->foreign('user_id') ->references('id') ->on('users') ->onDelete('cascade'); });Illuminate\Database\QueryException: SQLSTATE[HY000]: General error: 1215 Cannot add foreign key constraint (SQL: alter table `subscribes` add constraint `subscribes_user_id_foreign` foreign key (`user_id`) references `users` (`id`) on delete cascade)PHP - 【Laravel】外部キー制約があるテーブルのmigrateができません|teratail という記事をみて、
user_id
カラムを作るときの記述が異なることに気づいてその部分を修正することで、解決しました!修正点- $table->integer('user_id')->unsigned(); + $table->unsignedBigInteger('user_id');正しいマイグレーションSchema::create('subscribes', function (Blueprint $table) { $table->bigIncrements('id'); $table->unsignedBigInteger('user_id'); $table->string('channel_id'); $table->string('channel_title'); $table->timestamps(); // userが削除されたとき、それに関連するも一気に削除する制約 $table->foreign('user_id') ->references('id') ->on('users') ->onDelete('cascade'); });型が違っているからエラーになったっぽいのですが、それじゃあSQLiteはなぜ通った?という疑問が…。
- 投稿日:2019-12-02T16:06:33+09:00
Laravel+Vue.js+MySQLで入力内容の途中保存機能を実装してみた
グレンジ Advent Calendar 2019 4日目担当の soyo と申します。
グレンジでクライアントエンジニアをしております。
とはいえ、今年の記事もクライアントとはまったく関係ありません。普段Googleフォームなどでアンケートを回答する際に、
「あれ、途中で保存することができないの?」って自分はたまに思います。ユーザーが一項目ずつ入力したらサーバーに送信してデータベースに記録するから、
ページに再度アクセスしたら記録されている情報を自動的に反映するまで、
PHPを使って簡単に実装してみました。目標
「ラジオボタンの選択内容」と「テキストの入力内容」を途中保存できるようにする
開発環境
- macOS 10.14.6
- PHP 7.3.8
- Laravel 6.6.0
- MySQL 8.0.18
フロントエンド
Vue.jsで入力内容の操作
今回の戦場はLaravelプロジェクトのwelcome画面にします。
まずはそこにVue.jsを導入して、ラジオボタン3つとテキストボックス1つを置きます。resources/views/welcome.blade.php... <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script> ... <div id="app"> ラジオボタン<br/> <input type="radio" value="1" v-model="radio">選択肢1<br/> <input type="radio" value="2" v-model="radio">選択肢2<br/> <input type="radio" value="3" v-model="radio">選択肢3<br/> <br/> テキスト入力<br/> <input type="text" v-model="text" placeholder="内容を入力"> <br/> </div> ...public/js/main.jsconst app = new Vue({ el: '#app', data: { radio: '2', text: 'あいうえお' }, });これで
radio
とtext
でラジオボタンとテキストボックスを操作することができます。
UUIDの作成と保存
javascriptで適当なUUIDを生成する方法がありまして、
生成したUUIDをJavaScript Cookieでローカルに保存するようにします。resources/views/welcome.blade.php... <script src="https://cdn.jsdelivr.net/npm/js-cookie@beta/dist/js.cookie.min.js"></script> ...public/js/main.jsconst app = new Vue({ el: '#app', data: { radio: '', text: '', uuid: '' }, methods: { initUUID: function() { if (Cookies.get('uuid') !== undefined) { this.uuid = Cookies.get('uuid'); return; } var d = new Date().getTime(); var d2 = (performance && performance.now && (performance.now() * 1000)) || 0; this.uuid = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { var r = Math.random() * 16; if (d > 0){ r = (d + r) % 16 | 0; d = Math.floor(d / 16); } else { r = (d2 + r) % 16 | 0; d2 = Math.floor(d2 / 16); } return (c === 'x' ? r : (r & 0x3 | 0x8)).toString(16); }); // とりあえず期限を10年にする Cookies.set('uuid', this.uuid, { expires: 3650 }); } } }); app.initUUID();これで画面を開く度にcookieからuuidを取得し、存在しない場合は生成できるようになりました。
サーバーとの通信
サーバーとの通信はaxiosで行います。
resources/views/welcome.blade.php... <script src="https://cdnjs.cloudflare.com/ajax/libs/axios/0.18.0/axios.min.js"></script> ...public/js/main.jsconst app = new Vue({ ... methods: { saveData: function(key, value) { let postData = { 'user_id': this.uuid, 'key': key, 'value': value }; axios.post("/saveData", postData).then(response => { // 成功 }).catch(error => { // 失敗 }); }, loadData: function () { let postData = { 'user_id': this.uuid }; axios.post("/loadData", postData).then(response => { // 成功 }).catch(error => { // 失敗 }); } } });送信する内容についてですが、
文字を入力する度に送信してしまうとサーバーに負荷をかける可能性がありますので、
今回は連続する入力を無視してくれるLodashのdebounceで制御します。resources/views/welcome.blade.php... <script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.15/lodash.min.js"></script> ... <div id="app"> ラジオボタン<br/> <input type="radio" value="1" v-model="radio" @click="isRadioSelecting = true">選択肢1<br/> <input type="radio" value="2" v-model="radio" @click="isRadioSelecting = true">選択肢2<br/> <input type="radio" value="3" v-model="radio" @click="isRadioSelecting = true">選択肢3<br/> <br/> テキスト入力<br/> <input type="text" v-model="text" @input="isTextTyping = true" placeholder="内容を入力"> <br/> </div> ...public/js/main.jsconst app = new Vue({ el: '#app', data: { ... isTextTyping: false, isRadioSelecting: false, ... }, watch: { radio: _.debounce(function() { this.isRadioSelecting = false; }, 1000), text: _.debounce(function() { this.isTextTyping = false; }, 2000), isRadioSelecting: function(selecting) { if (selecting) { return; } this.saveData('radio', this.radio); }, isTextTyping: function(typing) { if (typing) { return; } this.saveData('text', this.text); }, }, ... });これでラジオボタンは選択停止後1秒、テキストボックスは入力停止後2秒からサーバーにデータを送るようになりました。
最後に、ステータスをわかるためにvue2-notifyを使ってプッシュ通知を表示させます。
resources/views/welcome.blade.php... <script src="https://cdnjs.cloudflare.com/ajax/libs/element-ui/2.4.0/index.js"></script> <link rel="stylesheet" href="https://unpkg.com/element-ui/lib/theme-chalk/index.css"> ...使い方の例
this.$notify.info({ title: '受信', message: '内容読み取り完了' });これで、フロントエンドの方は必要な機能を揃えました。
完成したコードはこの記事の最後にまとめております。サーバーサイド
データベース構造
テストのため、すごくシンプルなテーブルを作ります。
+------------+ | database() | +------------+ | vue_test | +------------+ +------------+ | TABLE_NAME | +------------+ | user_input | +------------+ +-------------+-----------+ | COLUMN_NAME | DATA_TYPE | +-------------+-----------+ | id | int | | user_id | varchar | | radio | int | | text | varchar | +-------------+-----------+リクエストデータ処理クラス
ユーザー入力内容をデータベースに書き込む・読み取り処理を行います。
app/Http/Controllers/UserInputController.php<?php namespace App\Http\Controllers; use Illuminate\Http\Request; use Illuminate\Support\Facades\DB; class UserInputController extends Controller { public function saveData(Request $request) { DB::table('user_input')->updateOrInsert( [ 'user_id' => $request->input('user_id') ], [ $request->input('key') => $request->input('value') ] ); } public function loadData(Request $request) { $user_id = $request->input('user_id'); $data = [ 'result' => DB::table('user_input')->where('user_id', $user_id)->first() ]; return $data; } }ルーティング
routes/web.php... Route::post('/saveData', 'UserInputController@saveData'); Route::post('/loadData', 'UserInputController@loadData'); ...コードまとめ
resources/views/welcome.blade.php<!DOCTYPE html> <html> <head> <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script> <script src="https://cdn.jsdelivr.net/npm/js-cookie@beta/dist/js.cookie.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.15/lodash.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/axios/0.18.0/axios.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/element-ui/2.4.0/index.js"></script> <link rel="stylesheet" href="https://unpkg.com/element-ui/lib/theme-chalk/index.css"> </head> <body> <div id="app"> ラジオボタン<br/> <input type="radio" value="1" v-model="radio" @click="isRadioSelecting = true">選択肢1<br/> <input type="radio" value="2" v-model="radio" @click="isRadioSelecting = true">選択肢2<br/> <input type="radio" value="3" v-model="radio" @click="isRadioSelecting = true">選択肢3<br/> <br/> テキスト入力<br/> <input type="text" v-model="text" @input="isTextTyping = true" placeholder="内容を入力"> <br/> </div> </body> <script src="{{ asset('/js/main.js') }}"></script> </html>public/js/main.jsconst app = new Vue({ el: '#app', data: { radio: '', text: '', isTextTyping: false, isRadioSelecting: false, uuid: '' }, watch: { radio: _.debounce(function() { this.isRadioSelecting = false; }, 1000), text: _.debounce(function() { this.isTextTyping = false; }, 2000), isRadioSelecting: function(selecting) { if (selecting) { return; } this.saveData('radio', this.radio, 'ラジオボタン'); }, isTextTyping: function(typing) { if (typing) { return; } this.saveData('text', this.text, 'テキスト入力'); }, }, methods: { initUUID: function() { if (Cookies.get('uuid') !== undefined) { this.uuid = Cookies.get('uuid'); return; } var d = new Date().getTime(); var d2 = (performance && performance.now && (performance.now() * 1000)) || 0; this.uuid = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { var r = Math.random() * 16; if (d > 0){ r = (d + r) % 16 | 0; d = Math.floor(d / 16); } else { r = (d2 + r) % 16 | 0; d2 = Math.floor(d2 / 16); } return (c === 'x' ? r : (r & 0x3 | 0x8)).toString(16); }); Cookies.set('uuid', this.uuid, { expires: 3650 }); }, saveData: function(key, value, description) { let postData = { 'user_id': this.uuid, 'key': key, 'value': value }; axios.post("/saveData", postData).then(response => { this.$notify.info({ title: '送信', message: '内容保存済み:' + description }); }).catch(error => { this.$notify.error({ title: '送信', message: '送信に失敗しました' }) }); }, loadData: function () { let postData = { 'user_id': this.uuid }; axios.post("/loadData", postData).then(response => { let data = response.data['result']; if (data == null) { this.$notify.info({ title: '受信', message: '新規ユーザー' }); return; } this.radio = data['radio']; this.text = data['text']; this.$notify.info({ title: '受信', message: '内容読み取り完了' }); }).catch(error => { this.$notify.error({ title: '受信', message: '受信に失敗しました' }) }); } } }); app.initUUID(); app.loadData();app/Http/Controllers/UserInputController.php<?php namespace App\Http\Controllers; use Illuminate\Http\Request; use Illuminate\Support\Facades\DB; class UserInputController extends Controller { public function saveData(Request $request) { DB::table('user_input')->updateOrInsert( [ 'user_id' => $request->input('user_id') ], [ $request->input('key') => $request->input('value') ] ); } public function loadData(Request $request) { $user_id = $request->input('user_id'); $data = [ 'result' => DB::table('user_input')->where('user_id', $user_id)->first() ]; return $data; } }routes/web.php<?php /* |-------------------------------------------------------------------------- | Web Routes |-------------------------------------------------------------------------- | | Here is where you can register web routes for your application. These | routes are loaded by the RouteServiceProvider within a group which | contains the "web" middleware group. Now create something great! | */ Route::get('/', function () { return view('welcome'); }); Route::post('/saveData', 'UserInputController@saveData'); Route::post('/loadData', 'UserInputController@loadData');最後に
Vue.jsが使いやすくて、サードパーティのライブラリもたくさんあって、
導入と実装がかなり楽でした(cocos2d-xとunityと比べるとねw)また、項目を増やす度にテーブルにカラムを追加するのはさすがに面倒ですね。
その場合はテーブルのスキーマをユーザーID、項目ID、内容にして、
select文でユーザーIDと項目IDで検索して、その結果を処理して反映すればいいと思います。ありがとうございました。
- 投稿日:2019-12-02T15:31:39+09:00
Github ActionsでLaravelのテストとECRへpushするまで
目標
Laravelで作成したアプリに対して、Github Actionsを利用してテストの実行及びECRへのイメージのpushまで行います。
前提条件
- Laravelアプリケーション作成済み
- Laravel実行用イメージ作成済み
- ECRのリポジトリ作成済み
Github Actionsについて
いわゆるCI / CDです。細かい部分は既に多くご紹介されているので省略いたします。
公式Public Repositoryだと無料みたいですね。
PrivateだとFreeプランでは1ヶ月2000分までだとか。Laravelのテスト実行
こちらは既にテンプレートが用意されており、変更するところといえば実行タイミングくらいでしょうか。MySQLやRedisが欲しい場合は別途追加する必要があります。
今回はテストなのでDBもsqliteで、実行タイミングもmasterへのpushという指定をしていますが、実際はかなり柔軟に設定できます。
リファレンスname: Laravel on: push: branches: - master jobs: laravel-tests: runs-on: ubuntu-latest steps: - uses: actions/checkout@v1 - name: Copy .env run: php -r "file_exists('.env') || copy('.env.example', '.env');" - name: Install Dependencies run: composer install -q --no-ansi --no-interaction --no-scripts --no-suggest --no-progress --prefer-dist - name: Generate key run: php artisan key:generate - name: Create Database run: | mkdir -p database touch database/search.sqlite - name: Execute tests (Unit and Feature tests) via PHPUnit env: DB_CONNECTION: sqlite DB_DATABASE: database/database.sqlite run: vendor/bin/phpunitmasterへのpushが完了すると、
という感じで結果を見ることができます。
特にテストも追加していないデフォルトの状態でテストを実行した場合、実行時間は合計35秒でした。このあと何回か実施したところ50秒くらいのもあったので、そんくらいのブレでおさまる感じみたいですね。ECRへのpush
テストだけで終わるのもさみしいので、ECRへイメージをpushするところまでやりたいと思います。
こちらも既にテンプレートが用意されているというか、もっといえばECSへのデプロイまでやってくれるテンプレートがありましたので、まずはそのうちのECRへのイメージのpushのところを使ってやってみようと思います。on: push: branches: - master name: Deploy to Amazon ECS jobs: deploy: name: Deploy runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v1 - name: Configure AWS credentials uses: aws-actions/configure-aws-credentials@v1 with: aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} aws-region: ap-northeast-1 - name: Login to Amazon ECR id: login-ecr uses: aws-actions/amazon-ecr-login@v1 - name: Build, tag, and push image to Amazon ECR id: build-image env: ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }} ECR_REPOSITORY: sample_repository IMAGE_TAG: ${{ github.sha }} run: | docker build -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG . docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG echo "::set-output name=image::$ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG"変更するところはリポジトリ名です。今回は「sample_repository」というリポジトリ名にしているので、こちらを変更しています。
ECR_REPOSITORY: sample_repositoryまた、「${{ secrets.AWS_ACCESS_KEY_ID }}」という形で利用できる変数は以下を参考に設定してみてください。今回設定するのは以下2点です。
- AWS_ACCESS_KEY_ID
- AWS_SECRET_ACCESS_KEY
終わりに
今回の設定のままだとイメージのタグが${{ github.sha }}で設定されてしまうので、ブランチのタグから取得するような形にするときれいにイメージを保存できそうですね。
- 投稿日:2019-12-02T04:16:55+09:00
Laravelでセッションを利用できるまでの準備の流れ
laravelでのsession
laravelではセッションの起点が
\Illuminate\Session\Middleware\StartSession
というミドルウェアになります。/Illuminate/Session/Middleware/StartSessionpublic function handle($request, Closure $next) { if (! $this->sessionConfigured()) { return $next($request); } $request->setLaravelSession( $session = $this->startSession($request) ); $this->collectGarbage($session); $response = $next($request); $this->storeCurrentUrl($request, $session); $this->addCookieToResponse($response, $session); $this->saveSession($request); return $response; }
$this->sessionConfigured()
でセッションを開始するかどうかを判定しています。laravelではセッションの設定は
config/session
に記述していきます。そのファイルがなかったり、driverがnullだったりするとセッションは使われません。sessionの開始
$request->setLaravelSession()
はリクエストのプロパティに生成したセッションをセットしているだけなので、実質的な処理は$this->startSession
になります。/Illuminate/Session/Middleware/StartSessionprotected function startSession(Request $request) { return tap($this->getSession($request), function ($session) use ($request) { $session->setRequestOnHandler($request); $session->start(); }); }sessionの取得
$this->getSession()
でセッションの取得を行っていきます。/Illuminate/Session/Middleware/StartSessionpublic function getSession(Request $request) { return tap($this->manager->driver(), function ($session) use ($request) { $session->setId($request->cookies->get($session->getName())); }); }
/Illuminate/Session/SessionManager
のdriver()
を呼んでドライバーの取得をしています。driver()
自体はスーパークラスの/Illuminate/Support/Manager
に実装されており、最終的にSessionManager::createNativeDriver
が呼ばれます。ドライバーとはセッションの値をどこに保存するかを決めるもので、デフォルトではfileになっていて、databaseなどにも変更できるようです。
/Illuminate/Session/SessionManagerprotected function createNativeDriver() { $lifetime = $this->config->get('session.lifetime'); return $this->buildSession(new FileSessionHandler( $this->container->make('files'), $this->config->get('session.files'), $lifetime )); }
$this->buildSession()
にセッション用のファイルハンドラを渡してセッションを生成してそうです。/Illuminate/Session/SessionManagerprotected function buildSession($handler) { return $this->config->get('session.encrypt') ? $this->buildEncryptedSession($handler) : new Store($this->config->get('session.cookie'), $handler); }
$this->config->get('session.encrypt')
はデフォルトではfalseなので、ここで本体である/Illuminate/Session/Store
が生成されています。ここで第一引数に渡している文字列はcookieのNameとして使われています。(APP_NAME_session)ブラウザでも確認することができますね!
セッションIDのセット
セッションをのインスタンスを取得したので、
getSession()
に戻って、tap関数でidをセットしていきます。$session->setId($request->cookies->get($session->getName()));先程
new Store()
の第一引数に渡した文字列を使って、cookieからidを取得しています。もしidがなければidを生成してセットしてくれます。セッションの開始
無事にセッションを取得できたので、セッションを開始してきます。
(
$session->setRequestOnHandler($request)
はデフォルトでは関係ないので割愛)/Illuminate/Session/Middleware/StartSessionprotected function startSession(Request $request) { return tap($this->getSession($request), function ($session) use ($request) { $session->setRequestOnHandler($request); $session->start(); }); }/Illuminate/Session/Storepublic function start() { $this->loadSession(); if (! $this->has('_token')) { $this->regenerateToken(); } return $this->started = true; }
loadSession()
でfileからセッションに格納している値を読み出してきます。その中に
_token
(CSRF用)がなければトークンを生成します。以上でセッションを利用する準備が整いました!
- 投稿日:2019-12-02T03:09:50+09:00
Laravel Mixで圧縮時にconosole.logを削除する
Laravel Mixがterser-webpack-pluginの設定を中継してくれるので、
webpack.mix.jsに、この設定を含めておくだけでnpm run prod
時にconosole.logが削除される。
npm run watch
やnpm run dev
するときには設定をスルーしてくれる。webpack.mix.js//console.log削除設定 mix.options({ terser: { terserOptions: { compress: { drop_console: true } } } });以下バージョンで動作確認済み
package.json"laravel-mix": "^4.1.4",ありがたやありがたや
参考リンク
- 投稿日:2019-12-02T03:09:50+09:00
Laravel Mixで圧縮時にconosole.*を削除する
Laravel Mixがterser-webpack-pluginの設定を中継してくれるので、
webpack.mix.jsに、以下の設定を含めておくだけでnpm run prod
時にconosole.logが削除される。
npm run watch
やnpm run dev
するときには設定をスルーしてくれる。webpack.mix.js//console.log削除設定 mix.options({ terser: { terserOptions: { compress: { drop_console: true } } } });以下バージョンで動作確認済み
package.json"laravel-mix": "^4.1.4",ありがたやありがたや
参考リンク
- 投稿日:2019-12-02T02:00:46+09:00
Mac × MAMP × Laravelで接続した時に起こった事
事象
・なんかLaravelインストールしたらMAMPの「MY WEBSITE」に接続できなくなった
※正確に言うと、「The requested URL /~ was not found on this server.」の404エラーが出ました。原因
・Laravelインストールしたと思ったらできてなかった....
→composer導入した後に、インストールしたつもりになってた・Laravel側で複数設定が必要なファイルに対して追加できていなかった。
■database.php
→使用しているデータベースの情報を追加できていなかった・MAMP側の設定ファイルでLaravelの追加ができていなかった
■httpd-vhosts.conf
→設定されているPort番号がLaravelの行で設定している
Port番号と一致していなかった。
NameVitualHost *:oooo
※上記とここのファイルでLaravelの追加が必要
■httped.conf
→ここでVitural hostsの下の行でコメントアウトされている文章の#を外す
(ここは出来てきた)所感
・ターミナル時にコマンド実行して大量のメッセージが流れた時に、
読み取る前にやった気になってたかも
・MAMPを初めて導入した時より早く事象の解決が出来た気がした。//MAMP導入時→12,3時間くらい
//今回→6,7時間くらい参考元
PKunitoさんとCodedayの作者様、理解しやすい記事を作成いただいてありがとうございました。
・Laravel開発:1.環境構築をMAMPを使用して作成する
https://qiita.com/PKunito/items/6a3bb187ca3c67de4519・MAMPを使用してLaravelアプリをMySQLに接続する方法
https://codeday.me/jp/qa/20190324/474003.html
- 投稿日:2019-12-02T01:53:11+09:00
Laradockを用いてDocker/Apache/PHP7.2/MySQL/Laravelの開発環境を構築する
Laradockクローン
ルートディレクトリにて、下記を実行。
$ git clone https://github.com/LaraDock/laradock.git
.envをenv-exampleからコピーして作成。
$ cd laradock
$ cp env-example .env
プロジェクト作成まずは、ワークスペースを起動。
$ cd laradock
$ docker-compose up -d workspace
ワークスペースに入る。
$ docker-compose exec workspace bash
Laravelのプロジェクトを作成composer create-project laravel/laravel web
dockerを一旦終了
exit
$ docker-compose down
laradock/.envのpathを作成したプロジェクトに変更。Point to the path of your applications code on your host
APP_CODE_PATH_HOST=../new_project
apache2の設定変更
ハマったところ。
apache2を使用するので、laradock/apache2/sites/default.apache.confを変更。
ServerName localhost
DocumentRoot /var/www/public/
Options Indexes FollowSymLinks各バージョンを指定(.env)
PHP_VERSION=7.2
MYSQL_VERSION=latest
(mysql/Dockerfile)
ARG MYSQL_VERSION=5.7dockerにてコンテナを起動。
$ docker-compose up -d mysql apache2 workspace
localhostにアクセス。docker-compose stop
既存のLaravelプロジェクトを配置する場合
webに展開
docker-compose exec --user=laradock workspace bash # workspaceへ入る
composer install
laradock@hoge:/var/www$ exit # workspaceから抜ける
$ docker-compose restart # コンテナ再起動
http://localhost/ にアクセスLaravel のプロジェクトを Homestead 環境で 起動させました。
http://localhost:8000/ にアクセスするとエラーがでました。RuntimeException がでる
RuntimeException
No application encryption key has been specified.
encryption key がないとあります。key を生成する
php artisan key:generate
Application key [base64:Wdhku6YSePiOh0XjqauthSaeOhzwRKxasFjbuuHXz0w=] set successfully.
Application key が生成されました。再度アクセス
http://localhost:8000/ にアクセスすると Laravel の初期画面が表示されました
- 投稿日:2019-12-02T00:22:18+09:00
Laradockを利用してLaravelの開発環境を作る
この記事は CODEBASE okinawa Advent Calendar 2019 2日目の記事です
本記事では、Laradockを使って、Laravelの開発環境を構築する手順について書こうと思います。
普段の開発環境は、Laradockを利用していないのですが、Laravelを最近さわりはじめたという方からLaradockでの開発環境構築につまずいた(migrateに失敗する)と相談を受けた際に、サクッと解決方法を提示できなかったので、改めて自分でも触ってみてみました (PHPフレームワークLaravel Webアプリケーション開発を参考にした、と話していたので、本記事でも同じような手順で書いてみました )
Laradockとは
Laradock is a full PHP development environment based on Docker.
ざっくりの説明だと、Laravelの開発環境をDockerをベースでサクッと提供してくれるものです。(詳しくは 公式のページ をみていただけると、わかりやすいかもです )
環境構築手順
前提
- Dockerがインストールされている事
- gitがインストールされている事
Laradockのダウンロード
- 以下のコマンドを実行する
# 作業ディレクトリ作成 $ mkdir laravel_docker # 移動 $ cd laravel_docker # laradockをclone $ git clone https://github.com/LaraDock/laradock.gitコンテナの初期化
- 以下のコマンドを実行する
# laradockディレクトリに移動 $ cd laradock # envファイルをコピー $ cp env-example .env # コンテナの起動 (若干時間かかる) $ docker-compose up -d nginx mysql workspace phpmyadmin # コンテナがちゃんと起動してるか確認 $ docker ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 42218ff4fdeb laradock_phpmyadmin "/docker-entrypoint.…" 10 seconds ago Up 8 seconds 0.0.0.0:8080->80/tcp laradock_phpmyadmin_1 ce8875359faa laradock_nginx "/bin/bash /opt/star…" 47 seconds ago Up 9 seconds 0.0.0.0:80->80/tcp, 0.0.0.0:443->443/tcp laradock_nginx_1 229e9c9b0fb9 laradock_php-fpm "docker-php-entrypoi…" 49 seconds ago Up 47 seconds 9000/tcp laradock_php-fpm_1 3f398da51a14 laradock_workspace "/sbin/my_init" 50 seconds ago Up 48 seconds 0.0.0.0:2222->22/tcp laradock_workspace_1 761b70cc0bd2 laradock_mysql "docker-entrypoint.s…" 51 seconds ago Up 10 seconds 0.0.0.0:3306->3306/tcp, 33060/tcp laradock_mysql_1 8f28241ced91 docker:dind "dockerd-entrypoint.…" 51 seconds ago Up 49 seconds 2375-2376/tcp laradock_docker-in-docker_1Laravelプロジェクトの作成
- 以下のコマンドを実行する
# workspaceコンテナに接続 $ docker-compose exec --user=laradock workspace bash # Laravelプロジェクト作成 $ composer create-project laravel/laravel sampleapp --prefer-dist "6.*.*" # コンテナから出る $ exit
laradockディレクトリの
.env
ファイルの設定を変更する変更前
# Point to the path of your applications code on your host APP_CODE_PATH_HOST=../
- 変更後
# Point to the path of your applications code on your host APP_CODE_PATH_HOST=../sampleapp
- 変更した設定を反映させるため、コンテナを再起動させる
# サービスの停止 $ docker-compose stop # サービスの起動 $ docker-compose up -d nginx mysql
- ブラウザから
http://localhost
にアクセスして、LaravelのWelcom画面が表示を確認できたら
php artisan migrate
ができることを確認する
- エラーその1(DBに接続できてない)
laradock@fd5cdbe63d4b:/var/www$ php artisan migrate Illuminate\Database\QueryException : SQLSTATE[HY000] [2002] Connection refused (SQL: select * from information_schema.tables where table_schema = laravel and table_name = migrations and table_type = 'BASE TABLE') at /var/www/vendor/laravel/framework/src/Illuminate/Database/Connection.php:665 661| // If an exception occurs when attempting to run a query, we'll format the error 662| // message to include the bindings with SQL, which will make this exception a 663| // lot more helpful to the developer instead of just the database's errors. 664| catch (Exception $e) { > 665| throw new QueryException( 666| $query, $this->prepareBindings($bindings), $e 667| ); 668| } 669| Exception trace: 1 PDOException::("SQLSTATE[HY000] [2002] Connection refused") /var/www/vendor/laravel/framework/src/Illuminate/Database/Connectors/Connector.php:70 2 PDO::__construct("mysql:host=127.0.0.1;port=3306;dbname=laravel", "root", "", []) /var/www/vendor/laravel/framework/src/Illuminate/Database/Connectors/Connector.php:70 Please use the argument -v to see more details.
DBに接続できてないのはlaravelの
.env
の設定ができていないっぽいので、Laradockのデフォルトの設定に合わせて修正しました変更前
DB_CONNECTION=mysql DB_HOST=127.0.0.1 DB_PORT=3306 DB_DATABASE=laravel DB_USERNAME=root DB_PASSWORD=
- 変更後(Laradockのデフォルトの設定に合わせる)
DB_CONNECTION=mysql DB_HOST=mysql DB_PORT=3306 DB_DATABASE=default DB_USERNAME=default DB_PASSWORD=secret
上記修正後、あらためて
php artisan migrate
すると以下のエラーが発生エラーその2(MySQLサーバー側で使われている認証方式がLaravel側でサポートされていないっぽい)
laradock@fd5cdbe63d4b:/var/www$ php artisan migrate Illuminate\Database\QueryException : SQLSTATE[HY000] [2054] The server requested authentication method unknown to the client (SQL: select * from information_schema.tables where table_schema = default and table_name = migrations and table_type = 'BASE TABLE') at /var/www/vendor/laravel/framework/src/Illuminate/Database/Connection.php:665 661| // If an exception occurs when attempting to run a query, we'll format the error 662| // message to include the bindings with SQL, which will make this exception a 663| // lot more helpful to the developer instead of just the database's errors. 664| catch (Exception $e) { > 665| throw new QueryException( 666| $query, $this->prepareBindings($bindings), $e 667| ); 668| } 669| Exception trace: 1 PDOException::("PDO::__construct(): The server requested authentication method unknown to the client [caching_sha2_password]") /var/www/vendor/laravel/framework/src/Illuminate/Database/Connectors/Connector.php:70 2 PDO::__construct("mysql:host=mysql;port=3306;dbname=default", "default", "secret", []) /var/www/vendor/laravel/framework/src/Illuminate/Database/Connectors/Connector.php:70 Please use the argument -v to see more details.
- MySQLユーザに対する認証方式を変更する
$ docker exec -it laradock_mysql_1 sh # mysql -uroot -proot -A mysql> alter user 'default'@'%' identified with mysql_native_password by 'secret';
- laradockの
mysql/my.cnf
を修正する(default_authentication_plugin=mysql_native_password
を追加する)# The MySQL Client configuration file. # # For explanations see # http://dev.mysql.com/doc/mysql/en/server-system-variables.html [mysql] [mysqld] sql-mode="STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION" character-set-server=utf8 default_authentication_plugin=mysql_native_password // ⬅️追加
- 上記修正した後にあらためて
php artisan migrate
する。うまくいったのでこれで大丈夫そうlaradock@fd5cdbe63d4b:/var/www$ php artisan migrate Migration table created successfully. Migrating: 2014_10_12_000000_create_users_table Migrated: 2014_10_12_000000_create_users_table (0.07 seconds) Migrating: 2014_10_12_100000_create_password_resets_table Migrated: 2014_10_12_100000_create_password_resets_table (0.04 seconds) Migrating: 2019_08_19_000000_create_failed_jobs_table Migrated: 2019_08_19_000000_create_failed_jobs_table (0.02 seconds)まとめ
- 今までLaradockを使ったことなかったけど、Laravelの環境構築すぐできて便利
- いろんなものが入ってるので、わかりにくところは多そう
- 困ってる方の質問を受けた時にサクッとこれぐらいこたえれれるようにがんばろ
参考
- 投稿日:2019-12-02T00:13:51+09:00
LaravelのMailMessageクラスをカスタマイズする
php artisan make:notification
で通知のひな型を生成するとNotification
を継承した以下の様なコードが現れます。public function toMail($notifiable) { return (new MailMessage) ->line('The introduction to the notification.') ->action('Notification Action', url('/')) ->line('Thank you for using our application!'); }このひな型の中では、
MailMessage
クラスを使ってメールの文章が構築されていますが、このMailMessage
クラスに関するドキュメントがあまりないため、恐らく多くのケースで、まったく別のコードに置き換えるか、雰囲気でカスタマイズしていたりするのではないでしょうか?
MailMessage
クラスと上手く付き合う事で、HTMLメールとテキストメールの2パターン分のbladeを作成&更新する手間から解放される事ができます。
この記事では、MailMessage
クラスの仕組みとカスタマイズ方法を解説していきます。Laravelのバージョン
Laravel 5.7
文章の構築
文章の構築は
subject()
,greeting()
,line()
などのメソッドを呼び出して行います。
各メソッドがメール上のどこに出力されるかは、以下の図の通りです。
左の画像が構築されるHTMLメールで、右の画像が構築する処理、赤矢印で対応を表しています。上図の補足ですが、
subject
はメールの件名となります。
①には.env
のAPP_ENV
の値が出力されます。
②はリンクになっておりaction()
の第二引数で指定したURLが出力されます。
また、level()
の指定により色が変わります。
③はアクションボタンの出力がある場合のみ出力される注意書きです。
英語が出力されている箇所については、@lang
を通して出力されているので
Translation Stringsを使って置き換える事ができます。テキストメールしか対応していないメールクライアントの場合、以下の様に表示されます。
使用されるbladeについて
デフォルトでは以下のbladeを使用してレンダリングされます。
HTMLメールの場合
- ①メール全体のレイアウト
- ②本文部分のヘッダー/フッター
- ③本文部分のコンテンツ
テキストメールの場合
- ④メール全体のレイアウト
- ⑤本文部分のヘッダー/フッター
- ⑥本文部分のコンテンツ
①の内容を確認すると、本文部分のテキストを
Illuminate\Mail\Markdown::parse()
を通しているためHTMLメールではmarkdown記法が使える様です。
逆に、④では、Illuminate\Mail\Markdown::parse()
を通していない他、strip_tags関数を通してHTMLタグを取り除いている事が分かります。
なお、③と⑥は同じファイルです。bladeのカスタマイズ方法
③⑥は
MailMessage
のmarkdown('your-blade-name')
で指定したbladeに差し替える事ができます。
①②は/resources/views/vendor/mail/html
以下に同名のbladeを配置すると、そちらが使われます。
④⑤は/resources/views/vendor/mail/markdown
以下に同名のbladeを配置すると、そちらが使われます。Markdownについて
MailMessageではMarkdown記法が使用でき、パースにはparsedownが使用されています。(デモページ)
使える文法はGitHub Flavoredに準拠している様です。なお、テキストメールには変換前のmarkdownがそのまま出力されます。
スタイルのカスタマイズについて
HTMLメールで適応されるCSSをthemeと呼んでいる様です。
デフォルトで適応されるCSS(theme)は以下となります。
Illuminate/Mail/resources/views/html/themes/default.cssカスタマイズしたい場合は
/resources/views/vendor/mail/html/themes/default.css
を作成すると、そちらが使われます。改行する方法
デザインの都合上、1つの
line()
の中で改行したいケースがあります。
(改行を含む文字列を入力しても、出力時には取り払われてしまう。brタグも同様。)
その場合は以下の様に書く必要があります。->line(new HtmlString("1行目のテキスト<br>\n2行目のテキスト"))キモは
<br>
と\n
を両方書くことです。
これにより、HTMLメールとテキストメールの両方で改行されます。最後に
以上が、MailMessageの仕組みとカスタマイズ方法となります。
お役に立てましたら「いいね」を頂けると嬉しいです!!
- 投稿日:2019-12-02T00:00:42+09:00
Laravel のモデルクラスをどこに配置するか問題について考えてみる
この記事について
Laravel Advent Calender 2019 2日目の記事です。
Laravel では、モデルクラスの置き場所が決められておらず、デフォルトで作成される User クラスは app 直下に置かれています。とはいえ、app 直下にすべてのモデルクラスを置いてしまうと、ツリービューで見たときの視認性が悪くなってしまうので、できれば役割やコンテキストごとに分割して配置したい、という気持ちになります。
これまで10近く Laravel を使ったアプリケーションに携わってきて、様々な構成を見てきましたが、わりと最近はひとつの形に収斂されてきてる印象を受けるので、問題提起を兼ねて、様々なパターンについてメリット/デメリットを考察しつつ、どういう配置がいいのか探ってみようという試みです。
はじめに
環境
- Laravel 6.6.0
Model/モデルの定義
本記事では「Model」(アルファベット表記のもの)は Eloquent Model を指し、「モデル」(カタカナ表記のもの)は概念的なものを指します。本記事ではビューとコントローラー以外はすべてモデルとして扱います。
パターン一覧
- デフォルト
- Models
- Entities, ValueObjects, Services, etc
- アプリケーションと独立した Domain
初期構成
必要になったらディレクトリができるので、初期状態はすっきりしています。
$ tree -L 1 app app ├── Console ├── Exceptions ├── Http ├── Providers └── User.phpこれに、Events, Notifications, Policies といった標準で規定されたディレクトリが加わります(これらもカスタマイズは可能ですが、特別な理由がなければそのまま使うほうがいいでしょう。
パターン1: デフォルト
前述の通り、app 直下に配置するパターンです。
配置例
tree -L 1 app app ├── Console ├── Customer.php ├── Deliverer.php ├── Exceptions ├── Http ├── Order.php ├── Providers └── User.phpメリット
php artisan make:model Hoge
と実行するとapp/Hoge.php
ができます。最少のタイプ数で作成できるのがメリットです。デメリット
こちらも前述の通り、ツリービューで見たときにずらずらと Model のファイルが並んでしまうので、視認性が悪くなります。
所感
中には、ツリービューは見ないあるいは視認性の悪さは気にならない、という方もいるかもしれませんが、私は無理だったので、数個程度のファイルでアプリケーションが構成されているのでなければ、こちらのパターンは選択しないでしょう。
パターン2: Models
app/Models 以下に配置するパターンです。
Laravel4 の時代には models ディレクトリがあったんですが、5 になってなくなりました。4時代から触っていて、それに慣れていたので、なくなったときは、えーなんでなくしたの?と思いました。
配置例
$ tree -L 2 -d app app ├── Console ├── Exceptions ├── Http │ ├── Controllers │ └── Middleware ├── Models │ ├── Base │ ├── Delivery │ └── Order └── Providersメリット
パターン3 との対比になりますが、このパターンだと、役割ごとではなくコンテキストあるいは集約ルートごとの分割が容易になります。
注文と配送というコンテキストがあるとして、Models 以下のようにコンテキストごとに分割して配置することができます。さらにコンテキストごとに Entities, Services などをつくってもいいでしょう。
デメリット
こちらもパターン3 との対比になりますが、コンテキストがひとつないしはそれほど多くなく、コンテキストごとに振る舞いが変わらないようなドメインの場合は、階層が増えるだけであまり意味がなくなってしまうかもしれません。
所感
いまのところこれがいちばんしっくりきています。Policy や Observer のような Model に密接に関わるクラスをどこに配置するか(デフォルトか Models 以下か)というのは悩ましいところではあるんですが、いまのところはデフォルトがいいのかな、と感じています。
パターン3: Entities, ValueObjects, Services, etc
app/Entities, app/Services など、モデルの種類ごとにディレクトリを切って配置するパターンです。最近はわりとこのパターンに遭遇することが多いです(書籍やインターネット上のリソースで推奨しているものがあるんでしょうか)。
配置例
$ tree -L 2 -d app app ├── Console ├── Entities │ ├── Deliver │ └── Order ├── Exceptions ├── Http │ ├── Controllers │ └── Middleware ├── Providers ├── Services │ ├── Deliver │ └── Order └── ValueObjects ├── Deliver └── Orderメリット
パターン2 との対比になりますが、コンテキストごとに分けたいのであれば、種類ごとのディレクトリの下で分割する形になります。その結果、種類ごとのディレクトリの下にそれぞれディレクトリができることになるので、コンテキストがひとつあるいはごく少なければ、いちばん簡潔な構成かもしれません。
デメリット
普段これでやっててあまりデメリットは感じてないですが、強いて挙げるとすれば、クラスの種類に引きずられて、関連の強いクラスが分断されてしまう恐れがあるとか、実態は値オブジェクトでないのに ValueObjects の中にあって混乱する、とかでしょうか。
所感
パターン2を選ぶか3を選ぶか、というのは、極論で言えば好みの問題、ということになる気はします。個人的には、モデルの種類(Entity なのか Service なのか)というのはあまり気にならなくて、どちらかといえば、どのコンテキストのクラス(オブジェクト)なのか、のほうに意識があるので、パターン2 を推しますが、チームでよく話し合って決めればいいのかな、と思います。
パターン4: アプリケーションと独立した Domain
最近また盛り上がりを感じるドメイン駆動設計的な、クリーンアーキテクチャ的な、フレームワークへの依存性をゼロにする、あるいは極力小さくする、という戦略のもとにつくられる、ディレクトリ構成です。
私はこのような方針で Laravel を採用しているプロジェクトには関わったことがなく、細かいメリット・デメリットは想像の範囲内でしかわからないので、配置例のみ記載することにします。もし実際に採用されている方がいれば、コメントにてメリット・デメリットを教えていただけるとありがたいです。
実装の詳細はこちらの記事を参考にするといいかもしれません。
配置例
$ tree -L 2 -d ./app ./domain ./app ├── Console ├── Exceptions ├── Http │ ├── Controllers │ └── Middleware ├── Infrastructure │ └── Repositories └── Providers ./domain ├── Delivery │ ├── Entities │ └── Repositories └── Order ├── Entities └── Repositories上記例では、リポジトリパターンを導入し、domain/{Context}/Repositories 配下には interface を、app/Infrastructure/Repositories 配下には実装クラスをそれぞれ配置します。原則的に domain 以下は POPO (Plain Old PHP Object) なクラスになるので、フレームワークに対して疎結合にできるメリットがあります。
あと、「Policy や Observer のような Model に密接に関わるクラス」との関係をどうするか、という問題があって、これは、
- 使わないで自前で仕組みを用意する
- 使うが中身はドメインモデルに委譲できるようにする
といった解決策がありそうです。そこら辺も事前に決めておく必要があるでしょう。
おわりに
個人的にはパターン2 のように app/Models 以下にすべてを配置する形を推したいですが、パターン3 のようにクラスの種類ごとにディレクトリを切る形でもいまのところそれほど不満はありません。
上記以外のメリット・デメリットを感じている方、あるいは上記以外で、ウチではこんな構成でやってます、というのがあれば、メリット・デメリット合わせて教えていただけると助かります