default eye-catch image.

flattenでcollectionを平坦化する

Laravelで多次元配列を1次元化するflatten()が便利だった. 連想配列の値のみを収集して1次元配列にしてくれる。 $ ./vendor/bin/sail artisan tinker Psy Shell v0.11.1 (PHP 8.1.2 — cli) by Justin Hileman >>> $collection = collect([ \'hoge\' => [1, 2, 3], ... \'fuga\' => \'aiueo\', ... \'foo\' => 1, ... \'bar\' => null ... ]); => IlluminateSupportCollection {#3529 all: [ \"hoge\" => [ 1, 2, 3, ], \"fuga\" => \"aiueo\", \"foo\" => 1, \"bar\" => null, ], } >>> $collection->flatten(); => IlluminateSupportCollection {#3525 all: [ 1, 2, 3, \"aiueo\", 1, null, ], }

default eye-catch image.

Laravel8 Jetstreamを導入した状態でsocialiteによるSNS認証を両立させる

Laravel8が大きく変わっていたので前回の記事で再入門した。 sailコマンドでコンテナの外からartisanコマンドを叩けて便利。 [clink url=\"https://ikuty.com/2021/05/16/laravel8-sail/\"] Laravel5,6あたりでSocialiteパッケージによりSNS認証を簡単に実装することができた. Laravel8+JetstreamにSocialiteを導入してSNS認証してみた. Jetstreamをインストールし,Jetstreamのrouteがある状態でSocialiteが機能するようにした. JetstreamのAuthはlaravel/uiのようにお手軽にrouteを変更できない様子. 今回はそれには触れず, 最低限の修正でJetstreamとSocialiteを両立させてみる. [arst_toc tag=\"h4\"] Jetstream導入 sailコマンド経由でインストールしていく。 composer, artisanだけでなく, npmもsailで実行できる. # jetstreamをインストールする $ ./vendor/bin/sail composer require laravel/jetstream # livewireをインストールする migrationファイルを作成する $ ./vendor/bin/sail artisan jetstream:install livewire # 作成したmigrationを実行する $ ./vendor/bin/sail artisan migrate # npm install , npm run dev $ ./vendor/bin/sail npm install $ ./vendor/bin/sail npm run dev migrationで作られたテーブル達を確認する. sailからmysqlを叩くことはできそうだが、さらに-eオプションでSQLを続けられなかった。 sail mysqlでいつものmysql clientに繋がる. sailはあくまでもユーザインターフェースなのでこれで良いか. $ ./vendor/bin/sail mysql mysql> show tables; +------------------------+ | Tables_in_example_app | +------------------------+ | failed_jobs | | migrations | | password_resets | | personal_access_tokens | | sessions | | users | +------------------------+ http://localhostを叩くと、認証機能が追加されていることを確認できる。 registerから登録してログインすると認証後URL (./dashboard) にredirectされる. profileに進むとまぁ普通に使いそうな機能が既にインプリメントされていることがわかる. routeの確認 Jetstreamをインストールした直後にJetstreamにより作られたrouteを確認してみる. いやー.. too much過ぎだろう... $ ./vendor/bin/sail artisan route:list +--------+----------+----------------------------------+---------------------------------+---------------------------------------------------------------------------------+-----------------------------------------------------------+ | Domain | Method | URI | Name | Action | Middleware | +--------+----------+----------------------------------+---------------------------------+---------------------------------------------------------------------------------+-----------------------------------------------------------+ | | GET|HEAD | / | | Closure | web | | | GET|HEAD | api/user | | Closure | api | | | | | | | AppHttpMiddlewareAuthenticate:sanctum | | | GET|HEAD | dashboard | dashboard | Closure | web | | | | | | | AppHttpMiddlewareAuthenticate:sanctum | | | | | | | IlluminateAuthMiddlewareEnsureEmailIsVerified | | | GET|HEAD | forgot-password | password.request | LaravelFortifyHttpControllersPasswordResetLinkController@create | web | | | | | | | AppHttpMiddlewareRedirectIfAuthenticated:web | | | POST | forgot-password | password.email | LaravelFortifyHttpControllersPasswordResetLinkController@store | web | | | | | | | AppHttpMiddlewareRedirectIfAuthenticated:web | | | GET|HEAD | livewire/livewire.js | | LivewireControllersLivewireJavaScriptAssets@source | | | | GET|HEAD | livewire/livewire.js.map | | LivewireControllersLivewireJavaScriptAssets@maps | | | | POST | livewire/message/{name} | livewire.message | LivewireControllersHttpConnectionHandler | web | | | GET|HEAD | livewire/preview-file/{filename} | livewire.preview-file | LivewireControllersFilePreviewHandler@handle | web | | | POST | livewire/upload-file | livewire.upload-file | LivewireControllersFileUploadHandler@handle | web | | | | | | | IlluminateRoutingMiddlewareThrottleRequests:60,1 | | | GET|HEAD | login | login | LaravelFortifyHttpControllersAuthenticatedSessionController@create | web | | | | | | | AppHttpMiddlewareRedirectIfAuthenticated:web | | | POST | login | | LaravelFortifyHttpControllersAuthenticatedSessionController@store | web | | | | | | | AppHttpMiddlewareRedirectIfAuthenticated:web | | | | | | | IlluminateRoutingMiddlewareThrottleRequests:login | | | POST | logout | logout | LaravelFortifyHttpControllersAuthenticatedSessionController@destroy | web | | | GET|HEAD | register | register | LaravelFortifyHttpControllersRegisteredUserController@create | web | | | | | | | AppHttpMiddlewareRedirectIfAuthenticated:web | | | POST | register | | LaravelFortifyHttpControllersRegisteredUserController@store | web | | | | | | | AppHttpMiddlewareRedirectIfAuthenticated:web | | | POST | reset-password | password.update | LaravelFortifyHttpControllersNewPasswordController@store | web | | | | | | | AppHttpMiddlewareRedirectIfAuthenticated:web | | | GET|HEAD | reset-password/{token} | password.reset | LaravelFortifyHttpControllersNewPasswordController@create | web | | | | | | | AppHttpMiddlewareRedirectIfAuthenticated:web | | | GET|HEAD | sanctum/csrf-cookie | | LaravelSanctumHttpControllersCsrfCookieController@show | web | | | GET|HEAD | two-factor-challenge | two-factor.login | LaravelFortifyHttpControllersTwoFactorAuthenticatedSessionController@create | web | | | | | | | AppHttpMiddlewareRedirectIfAuthenticated:web | | | POST | two-factor-challenge | | LaravelFortifyHttpControllersTwoFactorAuthenticatedSessionController@store | web | | | | | | | AppHttpMiddlewareRedirectIfAuthenticated:web | | | | | | | IlluminateRoutingMiddlewareThrottleRequests:two-factor | | | GET|HEAD | user/confirm-password | password.confirm | LaravelFortifyHttpControllersConfirmablePasswordController@show | web | | | | | | | AppHttpMiddlewareAuthenticate | | | POST | user/confirm-password | | LaravelFortifyHttpControllersConfirmablePasswordController@store | web | | | | | | | AppHttpMiddlewareAuthenticate | | | GET|HEAD | user/confirmed-password-status | password.confirmation | LaravelFortifyHttpControllersConfirmedPasswordStatusController@show | web | | | | | | | AppHttpMiddlewareAuthenticate | | | PUT | user/password | user-password.update | LaravelFortifyHttpControllersPasswordController@update | web | | | | | | | AppHttpMiddlewareAuthenticate | | | GET|HEAD | user/profile | profile.show | LaravelJetstreamHttpControllersLivewireUserProfileController@show | web | | | | | | | AppHttpMiddlewareAuthenticate | | | | | | | IlluminateAuthMiddlewareEnsureEmailIsVerified | | | PUT | user/profile-information | user-profile-information.update | LaravelFortifyHttpControllersProfileInformationController@update | web | | | | | | | AppHttpMiddlewareAuthenticate | | | POST | user/two-factor-authentication | two-factor.enable | LaravelFortifyHttpControllersTwoFactorAuthenticationController@store | web | | | | | | | AppHttpMiddlewareAuthenticate | | | | | | | IlluminateAuthMiddlewareRequirePassword | | | DELETE | user/two-factor-authentication | two-factor.disable | LaravelFortifyHttpControllersTwoFactorAuthenticationController@destroy | web | | | | | | | AppHttpMiddlewareAuthenticate | | | | | | | IlluminateAuthMiddlewareRequirePassword | | | GET|HEAD | user/two-factor-qr-code | two-factor.qr-code | LaravelFortifyHttpControllersTwoFactorQrCodeController@show | web | | | | | | | AppHttpMiddlewareAuthenticate | | | | | | | IlluminateAuthMiddlewareRequirePassword | | | GET|HEAD | user/two-factor-recovery-codes | two-factor.recovery-codes | LaravelFortifyHttpControllersRecoveryCodeController@index | web | | | | | | | AppHttpMiddlewareAuthenticate | | | | | | | IlluminateAuthMiddlewareRequirePassword | | | POST | user/two-factor-recovery-codes | | LaravelFortifyHttpControllersRecoveryCodeController@store | web | | | | | | | AppHttpMiddlewareAuthenticate | | | | | | | IlluminateAuthMiddlewareRequirePassword | +--------+----------+----------------------------------+---------------------------------+---------------------------------------------------------------------------------+-----------------------------------------------------------+ Socialite導入 Laravel5とか6あたりではSocialiteパッケージを導入することでSNS認証を簡単に作れた. Laravel8+Jetstreamでも同じように作れるのか試してみた. 以下の記事を参考にさせていただきました. 【Laravel】JetstreamでSNS認証(ソーシャルログイン) # Socialite インストール ./vendor/bin/sail composer require laravel/socialite # google用provider インストール ./vendor/bin/sail composer require socialiteproviders/google OAuth idとsecret を取得しておく. (id,secretの発行にはこちらを参考にさせていただきました.) Callback redirect先のURLとして http://localhost/login/google/callback を登録する. Socialite実装 .envにOAuth認証id,secret,redirectURLを書く. .env自体はhostから編集すれば良い. GOOGLE_KEY=\"*****-*******.apps.googleusercontent.com\" GOOGLE_SECRET=\"****-****\" GOOGLE_REDIRECT_URI=\"http://localhost/login/google/callback\" config/servicesに以下の設定を追加する. \'google\' => [ \'client_id\' => env(\'GOOGLE_KEY\'), \'client_secret\' => env(\'GOOGLE_SECRET\'), \'redirect\' => env(\'GOOGLE_REDIRECT_URI\'), ], Routeを追加する. Laravel7までとLaravel8ではRouteの書き方が異なる. Laravel7までは app/Providers/RouteServiceProvider.php に名前空間が定義されているため, Routeに書くコントローラの名前空間を書かなくても自動的に解決してくれた. 例えば, LoginController::class と書くと, 自動的にApp/Http/Controllers/LoginController::class と解釈された. Laravel8では, 名前空間を省略できなくなった. Route::prefix(\'login/{provider}\')->where([\'provider\'=> \'google\'])->group(function(){ Route::get(\'/\',[AppHttpControllersAuthLoginController::class, \'redirectToProvider\'])->name(\'sns_login.redirect\'); Route::get(\'/callback/\',[AppHttpControllersAuthLoginController::class, \'handleProviderCallback\'])->name(\'sns_login.callback\'); }); Socialite Providerを config/app.php のproviders に追加する /* * Socialite Providerをconfig/app.php の providers に追加する */ \'providers\' => [ ... SocialiteProvidersManagerServiceProvider::class, ... ], app/Providers/EventServiceProvider.php を以下の通り変更する. <?php namespace AppProviders; use IlluminateAuthEventsRegistered; use IlluminateAuthListenersSendEmailVerificationNotification; use IlluminateFoundationSupportProvidersEventServiceProvider as ServiceProvider; use IlluminateSupportFacadesEvent; use SocialiteProvidersManagerSocialiteWasCalled; //追加 class EventServiceProvider extends ServiceProvider { /** * The event listener mappings for the application. * * @var array */ protected $listen = [ Registered::class => [ SendEmailVerificationNotification::class, ], // 追加 SocialiteProvidersManagerSocialiteWasCalled::class => [ \'SocialiteProviders\\Google\\GoogleExtendSocialite@handle\', ], ]; /** * Register any events for your application. * * @return void */ public function boot() { // } } SNS認証によるログインを担うコントローラを自力で作成する. $ ./vendor/bin/sail artisan make:controller Auth\\LoginController Controller created successfully. 作成したコントローラの中身は以下の通り. <?php namespace AppHttpControllersAuth; use AppHttpControllersController; use AppModelsUser; use IlluminateHttpRequest; use LaravelSocialiteFacadesSocialite; use IlluminateSupportFacadesHash; use IlluminateSupportStr; class LoginController extends Controller { // メディア側へのリダイレクト public function redirectToProvider(Request $request) { $provider = $request->provider; return Socialite::driver($provider)->redirect(); } // メディア側から返されるユーザー情報 public function handleProviderCallback(Request $request) { $provider = $request->provider; $sns_user = Socialite::driver($provider)->user(); $sns_email = $sns_user->getEmail(); $sns_name = $sns_user->getName(); // 登録済ならログイン。未登録ならアカウント登録してログイン if(!is_null($sns_email)) { $user = User::firstOrCreate( // Userモデルに、レコードがあれば取得、なければ保存 [ \'email\' => $sns_email ], [ \'email\' => $sns_email, \'name\' => $sns_name, \'password\' => Hash::make(Str::random()) ]); auth()->login($user); session()->flash(\'oauth_login\', $provider.\'でログインしました。\'); return redirect(\'/\'); } return \'情報が取得できませんでした。\'; } } viewを作成する. ファイル名は app/View/auth/login.blade.php. Routeで書いた sns_login_redirect ページに遷移するリンクがあるだけ. <div> <a href=\"{{ route(\'sns_login.redirect\', \'google\') }}\">Google </div> Welcomeページのログインを修正 普通は何らかの画面が既にあってそこにSocialiteを組み込むと思うが, 今回は何もないので, とりあえずWelcomeページのログインをSocialite用に書き換えてみる. Jetstreamのrouteを変えようとしたが闇が深そうなので見なかったことにする. ちょっとJetstreamは出来が良くないのかなー.. デフォルトのWelcomeページのログインは, Jetstreamが生成する /login に合わせて作られてある. このままだと, Jetstreamが作った認証機構が動く. 例えば以下のように変更するとWelcomeページのログインをSocialiteのものに差し替えることができる. route(\'login\')をroute(\'sns_login.redirect\',\'google\')に変更した. また, registerは不要なので, registerへの遷移リンクを削除した. <body class=\"antialiased\"> <div class=\"relative flex items-top justify-center min-h-screen bg-gray-100 dark:bg-gray-900 sm:items-center py-4 sm:pt-0\"> @if (Route::has(\'sns_login.redirect\')) <div class=\"hidden fixed top-0 right-0 px-6 py-4 sm:block\"> @auth <a href=\"{{ url(\'/dashboard\') }}\" class=\"text-sm text-gray-700 underline\">Dashboard @else <a href=\"{{ route(\'sns_login.redirect\',\'google\') }}\" class=\"text-sm text-gray-700 underline\">Log in @endauth </div> @endif ... 動作確認 未ログインの状態で http://localhost を開くと, Welcome画面が表示され, Login への遷移リンクが表示される. Loginを押下すると, Googleのログイン画面に遷移する. アカウントを選択すると, http://localhost/login/google/callback にredirectがかかる. もし当サイトにアカウントがなければ,アカウントを作成する. アカウントがあれば,そのユーザでログインする. 晴れて, Googleアカウントと同じメールアドレスを持つユーザでログインした状態でダッシュボード(./dashbaord)が開く.

default eye-catch image.

Laravel8 sailで環境構築

とにかく進歩が早いLaravel。 セマンティックバージョニングになった6あたりから結構な速度で機能を乗せて来た感がある. 付いていくのがなかなか大変というのはある. 開けた口に無理やり食べ物を押し込んでくるような強引さの中にセンスの良さを感じ取れるので、 ちょっと付いて行ってみることにする. [arst_toc tag=\"h4\"] Laravel sail Laravel公式が用意するDocker開発環境を操作する軽量なコマンドラインインターフェース. ポイントは、コンテナの外部からコンテナ内のLaravelに対してコマンドを実行できる点. dockerコマンドをラップし、コンテナの内部で実行した結果を応答する仕組みとなっている. フルスタックフレームワークであるLaravelらしく何でも内包してしまう. composerやartisanコマンド実行のために、わざわざdockerコマンドを叩くのは辛い. sailが無いとdockerコマンドを叩きまくるか、コンテナに入って作業する必要がある. sailを使うことで、コンテナの中に入らず外からかsailコマンドを実行できる. こんな風にするとdockerの上位に来る仕組みを作れるのか、と結構感動. sailでプロジェクトを作る 既存のプロジェクトにsailを導入するパターンと、新規にプロジェクトを作成するパターンの2通りがある. 今回は新規にプロジェクトを作成していく. https://laravel.build/example-app というURLはShellScriptのコードを返す. withの後ろにインストールしたいミドエウウェアを指定する. 今回はmysqlだけ. カンマ区切りで複数指定可. $ mkdir -p ~/hoge && cd ~/hoge $ curl -s \"https://laravel.build/example-app?with=mysql\" | bash $ cd example-app && ./vendor/bin/sail up ちなみに、https://laravel.build/example-appは以下のShellScriptを返す. そのShellScriptは何をやっているかというと. laravelsail/php80-composerというイメージからコンテナを起動する. laravel newコマンドでプロジェクトを作成する. artisan sail:installコマンドを実行する. ディレクトリのOwnerを変更する. (パスワードが要求される) docker info > /dev/null 2>&1 # Ensure that Docker is running... if [ $? -ne 0 ]; then echo \"Docker is not running.\" exit 1 fi docker run --rm -v $(pwd):/opt -w /opt laravelsail/php80-composer:latest bash -c \"laravel new example-app && cd example-app && php ./artisan sail:install --with=mysql\" cd example-app CYAN=\'33[0;36m\' LIGHT_CYAN=\'33[1;36m\' WHITE=\'33[1;37m\' NC=\'33[0m\' echo \"\" if sudo -n true 2>/dev/null; then sudo chown -R $USER: . echo -e \"${WHITE}Get started with:${NC} cd example-app && ./vendor/bin/sail up\" else echo -e \"${WHITE}Please provide your password so we can make some final adjustments to your application\'s permissions.${NC}\" echo \"\" sudo chown -R $USER: . echo \"\" echo -e \"${WHITE}Thank you! We hope you build something incredible. Dive in with:${NC} cd example-app && ./vendor/bin/sail up\" fi sailでコンテナを立ち上げる 要はdocker-compose upをラップしたsail upコマンドを叩く. PHPのbundlerであるcomposerの仕様上, vendor 以下にモジュールがインストールされる. sailコマンドも ./vendor/bin/ に入っている. そこで ./vendor/bin/sail up を実行する. $ cd example-app $ ./vendor/bin/sail up dockerそのものなので, Ctrl+Cで落ちる. もちろん、./vendor/bin/sail up -d によりバックグラウンドで立ち上がる. $ ./vendor/bin/sail up -d ブラウザからhttp://localhostを開く あっさり開けた. ちなみに Dockerfile内で /usr/local/bin/start-containerを実行している. start-container内ではsupervisordによりLaravelのビルトインサーバをデーモン化している. #!/usr/bin/env bash if [ ! -z \"$WWWUSER\" ]; then usermod -u $WWWUSER sail fi if [ ! -d /.composer ]; then mkdir /.composer fi chmod -R ugo+rw /.composer if [ $# -gt 0 ];then exec gosu $WWWUSER \"$@\" else /usr/bin/supervisord -c /etc/supervisor/conf.d/supervisord.conf fi supervisord.confは以下の通り. [supervisord] nodaemon=true user=root logfile=/var/log/supervisor/supervisord.log pidfile=/var/run/supervisord.pid [program:php] command=/usr/bin/php -d variables_order=EGPCS /var/www/html/artisan serve --host=0.0.0.0 --port=80 user=sail environment=LARAVEL_SAIL=\"1\" stdout_logfile=/dev/stdout stdout_logfile_maxbytes=0 stderr_logfile=/dev/stderr stderr_logfile_maxbytes=0 sailでLaravelのバージョンを確認してみる 試しにコンテナの外からsailコマンドでartisan --versionを実行してみる. まるでコンテナの外からartisanコマンドを打っているような感覚. 良いと思う. $ ./vendor/bin/sail artisan --version Laravel Framework 8.41.0

default eye-catch image.

(今さら)Docker composeでWordPress環境を用意する

Hello World. docker-composeを使ってコンテナ間の繋がりを定義してみるデモに超速で入門する。 ゼロから書くと不要な時間を要するので、こちらを参考にさせていただいた。 写経する中でポイントを咀嚼していく。 ~/dockercompose_test というディレクトリを作成し、 その中で作業する。 docker-compose.yml 構成を記述する設定ファイル。ymlで書く。 ansibleでymlには慣れているので嬉しい。 version: \"3\" services: db: image: mysql:5.7 #container_name: \"mysql57\" volumes: - ./db/mysql:/var/lib/mysql restart: always environment: MYSQL_ROOT_PASSWORD: root_pass_fB3uWvTS MYSQL_DATABASE: wordpress_db MYSQL_USER: user MYSQL_PASSWORD: user_pass_Ck6uTvrQ wordpress: image: wordpress:latest #container_name: \"wordpress\" volumes: - ./wordpress/html:/var/www/html - ./php/php.ini:/usr/local/etc/php/conf.d/php.ini restart: always depends_on: - db ports: - 8080:80 environment: WORDPRESS_DB_HOST: db:3306 WORDPRESS_DB_NAME: wordpress_db WORDPRESS_DB_USER: user WORDPRESS_DB_PASSWORD: user_pass_Ck6uTvrQ ルートはservices. 名称の理解がフワフワだが、コンテナをサービスと読んでいることが多いくらいの認識. 配下に db と wordpress が存在する。おなじみの構成を定義している。 db dbの定義についてパラメタを追っていく. パラメタ 値 説明 image mysql57 pull してくるイメージを書く. mysql57という名前のイメージをpullする. volumes - ./db/mysql:/var/lib/mysql コンテナ内の/var/lib/mysql を ホストの./db/mysql にマウントする(で良いか?) restart always 再起動ポリシー(コンテナ終了時に再起動するための仕組み)を設定する.再起動ポリシーを設定しておくことで、Dockerデーモンの起動時やホストOSの起動時に自動的にコンテナを開始できる。 alwaysの他にいくつかあるか今はスキップ. environment MYSQL_ROOT_PASSWORD: root_pass_fB3uWvTSMYSQL_DATABASE: wordpress_dbMYSQL_USER: userMYSQL_PASSWORD: user_pass_Ck6uTvrQ コンテナの環境変数を定義する. 環境変数名はコンテナ依存. wordpress wordpressの定義についてパラメタを追っていく. パラメタ 値 定義 image wordpress:latest pull してくるイメージを書く. wordpressという名前のイメージをpullする. バージョンは最新. volumes ./wordpress/html:/var/www/html./php/php.ini:/usr/local/etc/php/conf.d/php.ini コンテナ内のディレクトリをホストのディレクトリにマウントする.マウント先を定義する. restart always 再起動ポリシーをalwaysに設定.内容はdbと同じ. depends_on - db サービスの依存関係を定義する.要は起動する順番を定義する. wordpressのdepends_onにdbを設定することで,wordpressよりも先にdbを起動できる.dbの起動が完了するまで待ってくれるという意味ではない! Control startup and shutdown order in Compose. ports 8080:80 コンテナの80番をホストの8080にマップする. http://localhost:8080 とかするとコンテナの80番が開く. environment WORDPRESS_DB_HOST: db:3306WORDPRESS_DB_NAME: wordpress_dbWORDPRESS_DB_USER: userWORDPRESS_DB_PASSWORD: user_pass_Ck6uTvrQ wordpressコンテナの環境変数を設定する.環境変数はコンテナ依存. docker compose up 以下で定義したdocker-compose.ymlに基づいて構築が始まる. -dはDetachedMode. これによりバックグラウンドで実行される. $ docker-compose up -d ホストで http://localhost:8080 を開くと以下の通り. 永続化の確認 言語を設定しwordpressのインストールを完了させる. db(MySQL)に加えられた変更はホストにマウントされたファイルに反映される. 以下により環境を停止した後, 再度upしたとしても, ホストにマウントされたファイルへの変更が反映され, インストール済みの状態で立ち上がる. $ docker-compose down $ docker-compose up -d まとめ docker-compose を使った WordPress環境構築のデモに超速で入門した. 一緒にコンテナを永続化するデモにも入門した.

default eye-catch image.

(今さら)DockerでWordPress環境を用意する

最小の手数でHello world. とりあえず最小の手数でwordpressを起動してみる。 イメージのダウンロード docker pullでMySQLとWordPressのイメージをダウンロードする。 イメージはサービス単位。 \"MySQL\"を実現するためのOSとミドルウェア。 \"WordPress\"を実現するためのOSとミドルウェア。例えばWebサーバも含んでいる。 まずはMySQL。 $ docker pull mysql:5.7.21 5.7.21: Pulling from library/mysql 2a72cbf407d6: Pull complete 38680a9b47a8: Pull complete 4c732aa0eb1b: Pull complete c5317a34eddd: Pull complete f92be680366c: Pull complete e8ecd8bec5ab: Pull complete 2a650284a6a8: Pull complete 5b5108d08c6d: Pull complete beaff1261757: Pull complete c1a55c6375b5: Pull complete 8181cde51c65: Pull complete Digest: sha256:691c55aabb3c4e3b89b953dd2f022f7ea845e5443954767d321d5f5fa394e28c Status: Downloaded newer image for mysql:5.7.21 docker.io/library/mysql:5.7.21 次にWordPress。何も指定しないと最新が落ちる様子。 $ docker pull wordpress Using default tag: latest latest: Pulling from library/wordpress bb79b6b2107f: Pull complete 80f7a64e4b25: Pull complete da391f3e81f0: Pull complete 8199ae3052e1: Pull complete 284fd0f314b2: Pull complete f38db365cd8a: Pull complete 1416a501db13: Pull complete be0026dad8d5: Pull complete 7bf43186e63e: Pull complete c0d672d8319a: Pull complete 645db540ba24: Pull complete 6f355b8da727: Pull complete aa00daebd81c: Pull complete 98996914108d: Pull complete 69e3e95397b4: Pull complete 5698325d4d72: Pull complete b604b3777675: Pull complete 57c814ef71bc: Pull complete ed1877bc3d14: Pull complete 673ead1d3971: Pull complete Digest: sha256:46fc3c784d5c4fdaa46977debb83261d29e932289a68739f1e34be6b27e04f87 Status: Downloaded newer image for wordpress:latest docker.io/library/wordpress:latest MySQLコンテナを起動 コンテナ(イメージ)を起動する。 $ docker run --name test-mysql -e MYSQL_ROOT_PASSWORD=test-pw -d mysql 013a2eb6b5b1c0b0f61e85cace6540ec036be80c9f85e8c9b5ed3e114a4cc8e8 パラメタは以下の通り。 Option Value 説明 --name test-mysql コンテナに名前を付ける。この例であれば test-mysql -e MYSQL_ROOT_PASSWORD=test-pw コンテナの環境変数を設定する。MYSQL_ROOT_PASSWORDという環境変数としてtest-pwを設定 -d - DetachedMode(Background)を指定する。指定がない場合Foregroud. WordPressコンテナを起動 WordPressコンテナを起動する。 $ docker run --name test-wordpress --link test-mysql:mysql -d -p 8080:80 wordpress a1301075d3667de7eddd9edc0c46edaeb4346a5c46ef444538c9cf9987f31471 パラメタは以下の通り。 Option Value 説明 --link test-mysql:mysql コンテナを連携させる。書式は --link [コンテナ名]:[エイリアス]。test-mysqlがコンテナ名(前述)で、mysqlがそのエイリアス。 -p 8080:80 HostとGuestのポートマッピング。Hostの8080をGuestの80にマッピングする。 Hostの8080にWordPressコンテナ内の80がマップされた。 http://localhost:8080/ でWordPressの言語選択画面が表示される。 非同期で起動したコンテナにアタッチ docker execで非同期に起動したWordPressコンテナ内のディレクトリにアクセスできる。 この例だと/var/www/html。 ゴニョゴニョいじると変更が反映される。 $ docker exec -it test-wordpress /bin/bash root@a1301075d366:/var/www/html# もちろん、コンテナを落とすと変更は失われる。 まとめ DockerでWordPressを動かすデモに超速で入門した。

default eye-catch image.

SNS Count Cache… WP_DEBUG=TRUEでinfoをerrorに吐くのは仕様です

あまりこういうことは書かないのだけれども、あんまりだったので記事にしておく。 FacebookやTwitterのシェア数、フォロー数などをバックグラウンドで取得するWordPressプラグイン \"SNS Count Cache\"。 なんか大量にエラーログを吐くので調べてみたら、 WP_DEBUGが立っているとinfoレベルのログをerror_log()で吐く仕様...。 開発環境でエラーログ確認しないのだろうか...。 class SCC_Logger { /** * Class constarctor * Hook onto all of the actions and filters needed by the plugin. */ protected function __construct() { self::log( \'[\' . __METHOD__ . \'] (line=\' . __LINE__ . \')\' ); } /** * Output log message according to WP_DEBUG setting * * @param string $message Message. * @return void */ public static function log( $message ) { if ( WP_DEBUG === true ) { if ( is_array( $message ) || is_object( $message ) ) { error_log( print_r( $message, true ) ); } else { error_log( $message ); } } } } log()というメソッド名でerror_log()を呼ぶというのは想像の斜め上で、 デバッグ初手のアイデアとして浮かばなかった。 /** * Class constarctor * Hook onto all of the actions and filters needed by the plugin. */ private function __construct() { SCC_Logger::log( \'[\' . __METHOD__ . \'] (line=\' . __LINE__ . \')\' ); load_plugin_textdomain( self::DOMAIN, false, basename( dirname( __FILE__ ) ) . \'/languages\' ); add_action( \'init\', array( $this, \'initialize\' ) ); register_activation_hook( __FILE__, array( $this, \'activate_plugin\' ) ); register_deactivation_hook( __FILE__, array( $this, \'deactivate_plugin\' ) ); add_action( \'admin_menu\', array( $this, \'register_admin_menu\' ) ); add_action( \'admin_print_styles\', array( $this, \'register_admin_styles\' ) ); add_action( \'admin_enqueue_scripts\', array( $this, \'register_admin_scripts\' ) ); // add_action( \'admin_notices\', array( $this, \'notice_page\' ) ); add_action( \'wp_ajax_\' . $this->ajax_action, array( $this, \'get_cache_info\' ) ); add_action( \'wp_dashboard_setup\', array( $this, \'add_wp_dashboard_widget\' ) ); add_action( \'deleted_post\' , array( $this, \'clear_cache_deleted_post\' ) ); add_filter( \'plugin_action_links_\' . plugin_basename( __FILE__ ), array( $this, \'add_plugin_action_links\' ), 10, 4 ); }

default eye-catch image.

PHPで統計アプリを作れるか否か

LaravelをAPIサーバにして同期的にsklearnのPCAを実行するアプリを作ってみました。 jQyery/bootstrap/chart.jsがフロント、APIサーバはLaravel+MySQL。 Laravel製APIがGET/POSTに対してPythonコードを実行します(Shellで...)。 exec()でPythonを起動するため無茶苦茶重いし、 ろくにエラーハンドリングできません。 結論から書けば同期的なアプリをこの構造で作るのは無理があります。 バックエンドが無茶苦茶重くてどうせバッチ実行になるのであれば、 上記の問題は結構問題なくなって、これでも良いかなと思い始めます。 MS系のInteroperabilityで、多言語が動的に結合するやつがありますが、 あんな感じでLL言語をglueできれば楽なのになと思います。 PSRの多言語拡張みたいなやつで、PHPからPythonのクラスを使うとか...

default eye-catch image.

Laravel Accessor/Mutatorを使って透過的にフィールドを暗号化/復号するサンプル

DBに入っているデータを決まった書式/形式に変換して表示したり、 逆に逆変換して保存する例は多いかと思います。 変換,逆変換の実装方法は以下みたいな感じかと..。 いずれも変換/逆変換の存在を忘れて仕様が抜けたり、 同じことを他でも書くコードクローンが発生する原因になる。 Controllerにダラダラと変換/逆変換を書く EloquentにオレオレSetter/Getterを書く Accessor/Mutatorを使うことで上記の原因を無くすことができます。 Accessor/Mutator Eloquentのメンバ変数(つまり、テーブルのフィールド)へのアクセスを ある規則をもってEloquentに定義したSetter/Getterを仲介するように強制できます。 [clink implicit=\"false\" url=\"https://laravel.com/docs/5.8/eloquent-mutators\" imgurl=\"http://laravel.jp/assets/img/logo-head.png\" title=\"Eloquent: Mutators Introduction\" excerpt=\"Accessors and mutators allow you to format Eloquent attribute values when you retrieve or set them on model instances. For example, you may want to use the Laravel encrypter to encrypt a value while it is stored in the database, and then automatically decrypt the attribute when you access it on an Eloquent model.\"] Accessors and mutators allow you to format Eloquent attribute values when you retrieve or set them on model instances. For example, you may want to use the Laravel encrypter to encrypt a value while it is stored in the database, and then automatically decrypt the attribute when you access it on an Eloquent model. In addition to custom accessors and mutators, Eloquent can also automatically cast date fields to Carbon instances or even cast text fields to JSON. 暗号化/復号 サンプル 標題の通りですが、Accessor/Mutatorを使ってフィールドを暗号化/復号してみます。 Cryptファサードを使ってAES-256-CBCの暗号化/復号を行う対です。 secretvalueというフィールドにAES256CBCで暗号化して書き込み、復号して読み込みます。 class User extends Authenticatable { use Notifiable; /** * Get the user\'s secretvalue. * * @param string $value * @return string */ public function getSecretvalueAttribute($value) { return decrypt($value); } /** * Set the user\'s secretvalue. * * @param string $value * @return string */ public function setSecretvalueAttribute($value) { $this->attributes[\'secretvalue\'] = encrypt($value); $this->save(); } } 透過的に呼び出す例です。 Userのsecretvalueフィールドに\"hogehoge\"という値を設定しています。 hogehogeという平文を暗号化してsecretvalueフィールドに書き込む処理は使う側には見えません。 Route::get(\'/sample/setvalue\',function(){ AppUser::find(1)->secretvalue = \'hogehoge\'; }); Userのsecretvalueフィールドを読み込んで出力しています。 暗号化済み文字列を復号する処理は使う側には見えません。 Route::get(\'/sample/getvalue\',function(){ echo AppUser::find(1)->secretvalue; }); より広い用途で使える 暗号化/復号はかなり直球な使い方ですが、ビジネスロジック内の定型処理など 積極的に使おうとするとAccessor/Mutatorに掃き出せるケースがありそうです。

default eye-catch image.

Model Binding と 1枚のBladeで CRUD する

1枚のBladeで確認画面付きCRUDを実現できると、Bladeの枚数が格段に少なくなって良さそう。 その前にまずModelBindingで単なるUserを1枚のBladeでCRUDしてみる。 1枚のBladeが複数の機能で使われることになり、Bladeの中に要素と制御が増えていくため、 実は、Bladeの枚数が増えたとしても1つのBladeを単純にした方が良いのかもしれないが、 1度作っておくとずっと使えるかもしれないので、そこまでやってみる。 やること Laravelに最初から付いてくるUserを使って、name,email,passwordのCRUDをする。 URL(route)は以下。showのパラメタをOptionalにして、あればUpdate、なければCreateする。 Update、Createは、本質的に分けるべきと考えてURLを別にしてある。 firstOrNew()を使うと、あればUserインスタンスを読み込んでくれる。 なければインスタンスを作る。ただしレコードは作らない。新規作成操作時にレコードを作成する。 <?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(\'/user/{user?}\',\'UserController@show\'); Route::post(\'/user/\',\'UserController@add\')->name(\'postAddUser\'); Route::post(\'/user/{user}\',\'UserController@edit\')->name(\'postEditUser\'); コントローラ コントローラは以下。無条件に保存するだけなのでほとんど何も書いてない。 条件が増えてくるとそれなりに行数が増える。 ModelBindingの良さは、タイプヒンティングでEloquentのインスタンスを受けられること。 変数を受けてEloquentインスタンスを探す手間がバッサリ無い。 RequestValidatorは載せていません。 <?php namespace AppHttpControllers; use AppHttpRequestsAddUserRequest; use AppHttpRequestsEditUserRequest; use AppUser; class UserController extends Controller { public function show($id=null) { $user = User::firstOrNew([\'id\'=>$id]); return view(\'user\',compact(\'user\')); } public function add(User $user,AddUserRequest $request) { $user->fill($request->only([\'name\',\'email\',\'password\']))->save(); return view(\'user\',compact(\'user\')); } public function edit(User $user,EditUserRequest $request) { $user->fill($request->only([\'name\',\'email\',\'password\']))->save(); return view(\'user\',compact(\'user\')); } } Blade 肝心のBladeは以下。これだけなのに結構書かないといけない。 laravelcollective/htmlは大分前にLaravelから外れていて、使わない方が良いのかも。 自力でHTMLを書くのと大して労力が変わらない可能性がある。 結構書かないといけないから1枚にしたいのか、複数枚でよければあまり書かなくて良いのか、 微妙なところ。Laravel5.7なのでBootstrap4。validation用のクラスが全然違う。 親Blade(layouts.app)は何でも良いので載せていません。 @extends(\'layouts.app\') @section(\'content\') @if (isset($user->id))編集 @else 追加 @endif @if ($user->wasRecentlyCreated) {!! Form::model($user,[\'route\'=>[\'postEditUser\',$user->id],\'class\'=>\'form-horizontal\'])!!} @else {!! Form::model($user,[\'route\'=>[\'postAddUser\'],\'class\'=>\'form-horizontal\'])!!} @endif {!! Form::label(\'name\', \'名前 :\') !!} @if($errors->has(\'name\')) {!! Form::text(\'name\',$user->name,[\'class\'=>\'form-control is-invalid\']) !!} @else {!! Form::text(\'name\',$user->name,[\'class\'=>\'form-control\']) !!} @endif {!! $errors->first(\'name\') !!} {!! Form::label(\'email\', \'email :\') !!} @if($errors->has(\'email\')) {!! Form::email(\'email\',$user->email,[\'class\'=>\'form-control is-invalid\']) !!} @else {!! Form::email(\'email\',$user->email,[\'class\'=>\'form-control\']) !!} @endif {!! $errors->first(\'email\') !!} {!! Form::label(\'password\', \'password :\') !!} @if($errors->has(\'password\')) {!! Form::password(\'password\',[\'class\'=>\'form-control is-invalid\']) !!} @else {!! Form::password(\'password\',[\'class\'=>\'form-control\']) !!} @endif {!! $errors->first(\'password\') !!} @if($user->wasRecentlyCreated) {!! Form::submit(\'保存\',[\'class\'=>\'btn btn-primary form-control col-sm-2\']) !!} @else {!! Form::submit(\'新規作成\',[\'class\'=>\'btn btn-primary form-control col-sm-2\']) !!} @endif {!! Form::close() !!} @endsection まとめ relationもないし懸案の確認画面もないので、単純。 次回、has a、has many relation版と、確認画面付きの版を試します。

default eye-catch image.

ミドルウエア

この記事は自分の勉強のために書いています。 ソースはLaravel5.7の公式ドキュメントです。 新しいことは何もないので通常はそちらを参照してください。 [clink implicit=\"false\" url=\"https://readouble.com/laravel/5.7/ja/middleware.html\" imgurl=\"http://laravel.jp/assets/img/header.jpg\" title=\"Laravel 5.7 ミドルウェア\" excerpt=\"ミドルウェア\"] [arst_toc tag=\"h4\"] アプリケーションに送られてきたリクエストを途中でフィルタするのがミドルウェア。 認証とかCORSとかCSRFとか、デフォルトでミドルウェアが用意されているけれども、 ミドルウェアを自作することができる。 ミドルウェアの定義 新しいミドルウェアの作成 ミドルウェアを作成するartisanコマンドは以下の通り。 こうすると、app/Http/Middlewareの中にCheckCostクラスが作られる。 $ php artisan make:middleware CheckCost; CheckCostクラスの中でhandle()メソッドを実装する。 $requestを$nextに流す際に$closureを評価するように動作し、 結果として$requestと$nextの間のフィルタ処理を$closureに書くことができる。 <?php namespace AppHttpMiddleware; use Closure; class CheckCost { /** * 送信されたきたリクエストをフィルタする * * @param IlluminateHttpRequest $request * @param Closure $next * @return mixed */ public function handle($request, $closure $next) { if ($request->value < 100) { return redirect('hoge'); } return $next($request); } } ミドルウェアを複数使用するとき、 前の段のミドルウェアを通過した後、次の段のミドルウェアを通過する。 Before Middleware/ After Middleware 公式だと仰々しい名前が付いているのだけれども、 $nextを呼んだ後に処理をするか、呼ぶ前に処理をするか、の違いを表現できる。 つまり、リクエストを完了する前の処理なのか、後の処理なのか。 <?php namespace AppHttpMiddleware; use Closure; class CheckCost { /** * Before Middleware * リクエストを評価する前に処理する * * @param IlluminateHttpRequest $request * @param Closure $next * @return mixed */ public function handle($request, $closure $next) { //アクションを実行 return $next($request); } } namespace AppHttpMiddleware; use Closure; class CheckCost { /** * After Middleware * リクエストを評価した後に処理する * * @param IlluminateHttpRequest $request * @param Closure $next * @return mixed */ public function handle($request, $closure $next) { $response = $next($request); return $response; } } 登録 グローバルミドルウェア 要は全てのHTTPリクエストについてミドルウェアを通すやり方。 App/Http/Kernel.phpに書く。Kernelクラスにある$middlewareという配列に グローバルミドルウェアにしたいやつを並べていく。 <?php namespace AppHttp; use IlluminateFoundationHttpKernel as HttpKernel; class Kernel extends HttpKernel { protected $middleware = [ ... ] } 特定のルートのみにミドルウェアを設定 特定のルートに限定してミドルウェアを設定する場合もApp/Http/Kernel.phpに書く。 まず、$routeMiddlewareにミドルウェアの短縮キーを書く <?php $routeMiddleware = [ \'auth\' => AppHttpMiddlewareAuthenticate::class, \'auth.basic\' => IlluminateAuthMiddlewareAuthenticateWithBasicAuth::class, ... ]; そして、routes.phpでそれぞれのルートに定義した短縮キーを指定する。 短縮キーではなく完全なクラス名を書いても良いみたい。 ミドルウェアを複数書くと、その順番に通すことになる。 Route::get(\'admin/profile\', function() { })->middleware(\'auth\'); Route::get2(\'admin/profile2\', function() { })->middleware(\'CheckCost::class\'); Route::get3(\'admin/profile3\', function() { })->middleware(\'auth\',\'guest\'); ミドルウェアグループ 複数のミドルウェアをグループ化して、そのグループのミドルウェアを一気に当てることができる。 App/Http/Kernel.phpに書く。Kernelクラスにある$middlewareGroupという配列に グループ名をキー、ミドルウェアの配列を値として書いていく。以下のように。 <?php $middlewareGroup = [ \'web\' => [ \'auth\' => AppHttpMiddlewareAuthenticate::class, \'auth.basic\' => IlluminateAuthMiddlewareAuthenticateWithBasicAuth::class, ... ], ... ]; routes.phpの中では、それぞれのルートに定義したグループ名を与える。 例えば、以下だと\'/\'に対して\'web\'ミドルウェアグループを与えている。 つまり\'/\'に\'web\'ミドルウェアグループに定義したミドルウェアが順番にあたる。 Route::get(\'/\',function(){ })->middleware(\'web\'); routes/web.php に書いたルートには自動的に\'web\'ミドルウェアグループがあたる。 ミドルウェアパラメタ ミドルウェアパラメタの書き方 ミドルウェア実行時に引数を与えることができる。 書き方は以下の通り。handle()をオーバーライドする際に、引数として受けるパラメタを追加する。 例えば、パラメタ$paramを受け取るミドルウェアは以下の通り。 クエリパラメタにある$valが$paramである場合に限り処理を書いている。 <?php namespace AppHttpMiddleware; use Closure; class CheckCost { /** * 送信されたきたリクエストをフィルタする * * @param IlluminateHttpRequest $request * @param Closure $next * @return mixed */ public function handle($request, $closure $next, $param) { if ($request->val == $param) { return redirect(\'hoge\'); } return $next($request); } } ミドルウェア指定時に以下のようにパラメタを渡す。 T.B.D. 終了処理ミドルウェア T.B.D. サンプル 実行時間を計測するミドルウェアの試作 こちらを参考に試しに作ってみた。 https://qiita.com/niisan-tokyo/items/663300f8df1c6c89f0ae $ php artisan make:middleware TimerMiddleware Middleware created successfully. TimerMiddleware本体はこちら。 <?php namespace AppHttpMiddleware; use Closure; class TimerMiddleware { /** * Handle an incoming request. * * @param IlluminateHttpRequest $request * @param Closure $next * @return mixed */ public function handle($request, Closure $next) { $before = microtime(true); $res = $next($request); Log::debug(mictotime(true) - $before); return $res; } } ミドルウェアの登録は、app/Http/Kernel.phpの$routeMiddlewareに追加。 protected $routeMiddleware = [ .. AppHttpMiddlewareTimerMiddleware::class, ]; ルートの定義はこちら。 Route::get(\'timer\',function(){ })->middleware(\'timer\'); /timer にアクセスすると、storages/logs/laravel.logにログが残る。 ... [2019-01-29 16:05:07] local.DEBUG: 0.019489049911499