20201215のlaravelに関する記事は17件です。

Laravel de パスワードチェッカー

昨今では辞書型攻撃が......
という前置きはゴミ箱に捨てておいて、漏洩が確認されたパスワードを公開しているHave I Been PwnedのAPIを活用し、脆弱なパスワードで登録できないようなバリデーションを作成していきます。

APIのドキュメントを読む

今回は、送信されたパスワードが漏洩履歴があるのか、またどれだけあるのかどうかを知りたいので、Pwned PasswordsのAPIを活用していきます。

詳細は、ドキュメントを読んでいただきたいのですが、要約すると、

  1. パスワードをSHA1でハッシュ化。
  2. 先頭5文字を送信(大文字小文字問わず)
  3. 先頭5文字までで一致したものすべてを、6文字目以降と漏洩回数ともに返却
  4. なんやかんやしてちょ

です。

実装しようぜ

土台作り

cmd
> php artisan make:rule SafePassword
app\Rules\SafePassword.php
<?php

namespace App\Rules;

use Illuminate\Contracts\Validation\Rule;

class SafePassword implements Rule
{
    /**
     * Create a new rule instance.
     *
     * @return void
     */
    public function __construct()
    {
        //
    }

    /**
     * Determine if the validation rule passes.
     *
     * @param  string  $attribute
     * @param  mixed  $value
     * @return bool
     */
    public function passes($attribute, $value)
    {
        //
    }

    /**
     * Get the validation error message.
     *
     * @return string
     */
    public function message()
    {
        return 'The validation error message.';
    }
}

これで土台部分はできました。

ゴリゴリ書く

app\Rules\SafePassword.phpのpasses()の中
// パスワードをSHA1でハッシュ化
$hash_password  = strtoupper(hash('sha1', $value));

// ハッシュ値の先頭5文字を変数定義
$first_section  = substr($hash_password, 0, 5);

// ハッシュ値の6文字目以降を変数定義
$second_section = substr($hash_password, 5);


// cURLリソースの新規作成
$curl_handle = curl_init('https://api.pwnedpasswords.com/range/'.$first_section);

// cURLの設定
curl_setopt_array($curl_handle, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT        => 3
]);

// 結果を変数に保存
$response = curl_exec($curl_handle)."\n";

// そもそも結果の中に存在するか検索
if (preg_match("/$second_section:\d*\r?\n/", $response) === 1) {

    // 存在した場合、「何回」漏洩したか取得
    $count = (int) preg_replace("/([\s\S]*)$second_section:(\d*)\r?\n([\s\S]*)/", "$2", $response);

    // 漏洩回数の許容値をconfigで取得(直接定義しても可)
    $allow_leak_time = (int) config('auth.allow_password_leak_time');

    // もし、許容以上の漏洩回数ならfalse
    if ($count > $allow_leak_time)
        return false;

    // 許容以下ならtrue
    return true;
}

// そもそも漏洩してなかったらtrue
return true;

実際に指定する

適当なController,Request
'password' => ['required', 'confirmed', 'min:8', \App\Rules\SafePassword()]

コード書いてみて

最初のほうはforeachを使ったりして正規表現を使わないように努力しましたが結局正規表現が楽でした。。
似たような正規表現を二回使っているのはあまり美しくない(と個人的には)思っているのでおいおい修正していきたいと思います。
(強引すぎるという自覚はあるので「こんな方法いいよ!」等ありましたらお教えいただけると幸いです。)

作成後記

23:59作成終了!

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

dockerコンテナでlaravel php artisan serveする時のhost指定

host=0.0.0.0で起動する必要がある。
デフォルトだと127.0.0.1でlistenされるが、これはあくまでコンテナ内でlistenしている。
ホストpcからcurl localhostでアクセスすると、コンテナ内に通信する際に別の内部ipに変換される。
そのため127.0.0.1でlistenしてしまうと応答できない。
そのため0.0.0.0でlistenする。こうするとコンテナ内の全てのインターフェース?でlistenする。
そのため応答できるようになる。

↓コードは/appにマウントされる想定

FROM centos:centos8

RUN dnf module install -y php:7.4

WORKDIR /app
CMD [ "php", "artisan", "serve", "--port=80", "--host=0.0.0.0" ]

参考
https://qiita.com/amuyikam/items/01a8c16e3ddbcc734a46

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

[Laravel]早めのクリスマスviewが真っ白問題

Laravelのviewが真っ白だ。

題名の通り、なぜかLaravelのviewが真っ白になったので、そのことについて残しておきます。ちなみに原因はめちゃめちゃ単純なことでした。

環境

  • windows10
  • Laravel6
  • php(7.4.6)

やろうとしていたこと

TestController.php
class TestController extends Controller
{
  public function index($id)
  {
   $users = Users::find($id);

   return view('test',['users' => $users]);
  }

  public function post()
  {
   return view('test.view');
  }
web.php
Route::get('/test/{id}', 'testController@index');

Route::get('/test/post', 'testController@post');

てきな記述をしていたのですが、真っ白状態です。。。。
ググっていったところ、web.phpの順番が問題そう。

web.php
Route::get('/test/post', 'testController@post');

Route::get('/test/{id}', 'testController@index');

変えてみたら、直った!

原因

結果原因はweb.phpの順番というよりは、{id}の部分が邪魔していたそう。
最初の通り、

web.php
Route::get('/test/{id}', 'testController@index');

Route::get('/test/post', 'testController@post');

このように記載していると、最初のRouteで/test/{id}を読み込んでしまい、test/postと送っても、ルーティングでpostをidと判断してしまい、そのようなidはないため、真っ白になっていた。
そのため、順番を変えると、先にtest/postが先に読み込まれるので、viewを返したのですね。
このままでも動くのですが、何かの間違いでまたidを読み込まれてもめんどくさいので、このように直しました!

web.php
Route::get('/test/{id}', 'TravelController@showDetail')->where('id', '[0-9]+');

こうすると、idは数字しかとってこなくなり、先ほどの順番を変えても、先に読み込まれることはなくなりました。

まとめ

以上がLaravelからのクリスマスプレゼントでした、この先も色々なプレゼント(エラー)をいただくと思いますが、ありがたく受け取ってうまく使いこなしていこうと思います。

参考

ルーティングを書く順番をミスって画面真っ白から抜け出せなくなった話

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

LaravelでリクエストのSTART/ENDログを出してみた

会社のTechBlogでCloudWatchにログファイルを表示させたのですが、その際にLaravelのログで開始、終了ログを出してみようとして、処理が長くなりそうだったので別記事にしてみました。

ログの目的としてはこんな感じです。

  • 開始と終了のログ2つを出す
  • 処理の結果をある程度分かるようにする(success,validate,exceptionとか)
  • 処理時間をミリ秒で出す
  • apacheのログと合わせて見られるようにIPを出す
  • 開始と終了のログを出すので2つを繋げるIDを出す(リクエストID)
  • 同じユーザ(同セッション)の操作は追えるようにする(セッションID)

と結構多い。
でもこれぐらいあれば、操作を追える、遅い処理を追えるので実装してみました。


環境

  • Laravel v8.18.1 (PHP v7.3.20)

ログを出すミドルウェアを実装する

artisanでmiddlewareを作成し、処理を記述

php artisan make:middleware RequestMiddleware
<?php

namespace App\Http\Middleware;

use Closure;
use Exception;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
use Str;

class RequestMiddleware
{
    public function handle(Request $request, Closure $next)
    {
        //開始終了ログを出したくない場合のコード
        //AWS ELBのHealthCheckerとか
        if ($this->isSkipLog($request)) {
            return $next($request);
        }

        $resultType = 'success';
        $start = microtime(true); //開始時間

        //requestのidとsessionのid。hashしてるのは単にログ表示を短くするため
        $rid = hash('crc32', Str::uuid()->toString());
        $sid = hash('crc32', session()->getId());

        $ip = $this->getIp();

        //startのログ. RIDとかSIDはhashが2つもあるのでわかりやすくしてる
        Log::info(implode(' ', [
            'path:', $request->path(), 'START', $request->method(),
            $ip,
            'RID' . $rid,
            'SID' . $sid,
        ]));

        $result = $next($request);

        if ($this->isValidate($request)) {
            //validateメッセージがあれば設定
            $resultType = 'validate';
        } else if ($this->isException($result)) {
            //Controllerでexceptionが発生していれば設定
            $resultType = 'exception';
        }
        //endログ
        Log::info(implode(' ', [
            'path:', $request->path(), 'END',
            $resultType,
            $ip,
            explode('.', ((microtime(true) - $start) * 1000))[0], //ミリ秒表示
            'RID' . $rid,
            'SID' . $sid,
        ]));
        return $result;
    }

    private function getIp(): string
    {
        //ALB経由とかの場合はX_FORWARDED_FORを出す
        if (isset($_SERVER['HTTP_X_FORWARDED_FOR'])) {
            return $_SERVER['HTTP_X_FORWARDED_FOR'];
        }
        return $_SERVER['REMOTE_ADDR'];
    }

    private function isValidate(Request $request): bool
    {
        return !is_null($request->session()->get('errors'));
    }

    private function isException($result): bool
    {
        return !empty($result->exception);
    }

    private function isSkipLog(Request $request): bool
    {
        $isSkip = Str::startsWith($request->header('User-Agent'), 'ELB-HealthChecker');

        return $isSkip;
    }
}

App\Http\Kernel.phpに作ったミドルウェアを設定する

webのミドルウェアグループに追加する

    ...
    protected $middlewareGroups = [
        'web' => [
            \App\Http\Middleware\EncryptCookies::class,
            \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
            \Illuminate\Session\Middleware\StartSession::class,
            // \Illuminate\Session\Middleware\AuthenticateSession::class,
            \Illuminate\View\Middleware\ShareErrorsFromSession::class,
            \App\Http\Middleware\VerifyCsrfToken::class,
            \Illuminate\Routing\Middleware\SubstituteBindings::class,
            \App\Http\Middleware\RequestMiddleware::class, //ADD
        ],
    ...

ログを確認する

適当に作ったHelloControllerで確認

[2020-12-15 09:17:25] local.INFO: path: hello START GET 192.168.33.1 RIDe6fe0217 SID0a1be91a  
[2020-12-15 09:17:25] local.INFO: path: hello END success 192.168.33.1 46 RIDe6fe0217 SID0a1be91a  

[2020-12-15 09:17:48] local.INFO: path: hello/edit START POST 192.168.33.1 RIDc24c6515 SID0a1be91a  
[2020-12-15 09:17:48] local.INFO: path: hello/edit END validate 192.168.33.1 50 RIDc24c6515 SID0a1be91a  

[2020-12-15 09:18:50] local.INFO: path: hello/edit START POST 192.168.33.1 RID8b780f08 SID0a1be91a  
[2020-12-15 09:18:50] local.ERROR: test throw {"exception":"[object] (Exception(code: 0): test throw at /var/www/html/laravel8/app/Http/Controllers/HelloController.php:37)
[stacktrace]
...略
[2020-12-15 09:18:50] local.INFO: path: hello/edit END exception 192.168.33.1 98 RID8b780f08 SID0a1be91a  

出てますね!
ただこのmiddleware通る前に失敗すると当然ながらログ出ません。(csrfエラーとか)

感想

とりあえずこれで目的のログを出す1例にはなったと思います。

結構手間だったのと、もっと綺麗にコードかけないかなー?ってところは所々あります...まだphper歴低いので指摘あれば是非に。

ちなみに失敗談として、最初間違えてRequestMiddlewareをwebのmiddlewareGroupsではなく共通のmiddlewareに追加してしまい、sessionidが毎回変わってました。

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

PHP8.0 + Laravel6.x(LTS)環境をDocker Composeで手早く作る

本記事はAll About Group(株式会社オールアバウト) Advent Calendar 2020 15日目になります。

概要三行

  • LAMP(PHP8.0) + Laravelの簡易開発環境をDocker Composeで作成
  • 基本コピペで誰でも作業できるように記述。
  • {}で囲んでいる箇所は任意に書き換えてください。

概要

PHP8.0が正式リリースされましたね。https://www.php.net/releases/8.0/en.php
実行速度の改善はありがたい限りです。
気になる楽しみ!ということで、今回はPHP8で遊べるように簡易のLAMP+laravel開発環境を構築いたしました。

環境構築_概要

使用技術 / 環境

  • Docker Compose
  • centos7
  • PHP8.0
  • laravel6.x(LTS)
  • MySQL(5.7)
  • phpMyAdmin

構成について

先に、今回作成する開発環境の構成を説明しておきます

{開発環境_命名は自由} /
├── docker-compose.yaml
├── web_php8/
|  ├── Dockerfile
|  └── setup/
|     ├──.bashrc
|     └── httpd/
|        └── app.conf
└── repository/
   └── {laravelアプリ(命名は自由)}
名称 種別 概要
{開発環境_命名は自由}/ ? ディレクトリ 今回の開発環境ファイルを束ねるディレクトリ
docker-compose.yaml ? ファイル docker composeの実行ファイル
web_php8/ ? ディレクトリ WEB系コンテナの関連ファイル置き場です
web_php8/Dockerfile ? ファイル 今回作成するWEB系コンテナのDocker Fileです
web_php8/setup/ ? ディレクトリ WEB系コンテナ内での設定ファイル郡です
web_php8/setup/.bashrc ? ファイル WEB系コンテナ内に適用するbashrcです
web_php8/setup/httpd/ ? ディレクトリ WEB系コンテナ内に適用するApacheの設定ファイル郡です
web_php8/setup/httpd/app.conf ? ファイル WEB系コンテナで摘要するconfファイルです
repository/ ? ディレクトリ WEBサーバーで稼働させるWEBアプリ置き場です
repository/{laravelアプリ(命名は自由)} ? ディレクトリ 今回WEB系コンテナ内で稼働させるLaravelアプリです

本件で立ち上がるコンテナは、WEB、DB、PHP-MY-ADMINの3つです。
最低限の環境なので、自由に変更/追加するといいと思います。

repository ディレクトリに保管したアプリをWEBコンテナ内にマウントしており、
アプリ開発する際には、localのrepository/{laravelアプリ(命名は自由)}を編集すれば、コンテナ内に反映されます。

URL

今回の構築にて作成されるURLを記載します

名称 URL
アプリURL http://localhost/
phpMyAdmin http://localhost:89/

前提

  • 筆者はMac端末で作業しております。
  • {}で囲んでいる箇所は任意に書き換えてください。
  • docker-composeは既に入っているものとします。

環境構築_手順

docker-compose.yamlの作成

複数のdockerコンテナを束ねるDocker Composeのyamlファイルを作成します。
※ {}で囲んでいる箇所は任意に書き換えてください

mkdir {開発環境_命名は自由}; cd {開発環境_命名は自由}
vim docker-compose.yaml

下記をdocker-compose.yamlに記述してください

docker-compose.yaml
version: '3'

services:
  web_php8:
    build:
      context: ./web_php8
    volumes:
      - ./repository/{laravelアプリ(命名は自由)}:/var/www/{laravelアプリ(命名は自由)}
      - ./web_php8/setup/.bashrc:/root/.bashrc
    ports:
      - "80:80"
    working_dir: /var/www/{laravelアプリ(命名は自由)}/
    environment:
      TZ: "Asia/Tokyo"

  db:
    image: mysql:5.7
    ports:
      - "3306:3306"
    environment:
      MYSQL_DATABASE: test_db
      MYSQL_USER: test_user
      MYSQL_PASSWORD: test_pass
      MYSQL_ALLOW_EMPTY_PASSWORD: "yes"
      TZ: "Asia/Tokyo"

  php-my-admin:
    image: phpmyadmin/phpmyadmin
    links:
      - db
    ports:
      - "89:80"
    environment:
      MYSQL_USERNAME: test_user
      MYSQL_ROOT_PASSWORD: test_pass
      PMA_HOST: db
      PMA_PORT: 3306

立ち上げる各コンテナとその設定を定義しています。
WEBコンテナはPHP8.0の導入など、色々やりたいので、個別でDockerFileを作成します。

WEB系コンテナの作成①Docker File編

WEB系のDockerコンテナを立ち上げるためのDocker Fileを作成します。

mkdir web_php8; cd web_php8
vim Dockerfile

下記をDockerfileに記述してください

FROM centos:centos7

# timezone
RUN rm -f /etc/localtime \
    && cp -p /usr/share/zoneinfo/Japan /etc/localtime

# php8.0
    RUN yum -y update \
    && yum -y install https://dl.fedoraproject.org/pub/epel/epel-release-latest-7.noarch.rpm \
    && yum -y install https://rpms.remirepo.net/enterprise/remi-release-7.rpm \
    && yum -y install yum-utils \
    && yum-config-manager --disable 'remi-php*' && yum-config-manager --enable remi-php80 \
    && yum remove php* \
    && yum -y install php php-{cli,fpm,mysqlnd,zip,devel,gd,mbstring,curl,xml,pear,bcmath,json} \
    && php --version

# apache
RUN yum install -y httpd httpd-devel \
    && rm -rf /var/cache/yum/* \
    && yum clean all \
    && mkdir -p /app/ENV /AccessLog.local
COPY ./setup/httpd/app.conf /etc/httpd/conf.d/app.conf

# composer
RUN cd /tmp && curl -sS https://getcomposer.org/installer | php \
    && mv ./composer.phar /usr/bin/composer

CMD ["httpd", "-D", "FOREGROUND"]

WEB系コンテナに必要なツール類や設定はここで定義しています。

WEB系コンテナの作成②関連ファイル編

WEB系コンテナ内にて摘要する各ファイルを作成しておきます

まずは、コンテナ内に適用するbashrcを作成します

mkdir setup; cd setup
vim .bashrc

中身は自由ですので、皆さんのお好きな設定を記述ください。
(マウントしちゃってもいいと思います)

次にApache confファイルを作成しておきます

mkdir httpd; cd httpd
vim app.conf

app.conf に下記を記述してください

app.conf
<VirtualHost *:80>
  ServerName localhost
  DocumentRoot /var/www/{laravelアプリ(命名は自由)}/public
  ErrorLog  "/dev/stdout"
  CustomLog "/dev/stdout" combined

  <Directory /var/www/{laravelアプリ(命名は自由)}/public>
    AllowOverride All
    Options Includes FollowSymLinks
    Order allow,deny
    Allow from all
  </Directory>
</VirtualHost>

Laravelアプリケーション_①アプリのclone

実際に稼働するLaravelアプリを作っていきましょう。

ディレクトリを移動して、laravelをgit cloneします。
ここではWEBアプリはrepositoryディレクトリ配下で管理するようにしています。

cd ../../../
mkdir repository; cd repository
git clone -b 6.x https://github.com/laravel/laravel.git {適当なアプリ名}

環境の立ち上げ

下記にてdocker-composeを実行。

cd で移動しまくってて、わかりにくく申し訳ないですが
docker-compose.yamlと同階層で実行すればOKです。

cd ../
docker-compose up

各環境が立ち上がり、各コンテナのログが流れ始めたことを確認します。

Laravelアプリケーション_②初期設定

laravelをcloneした後に実行するお作法です。
権限とcomposer、そしてenv周り

cd repository/{laravelアプリ(命名は自由)}
chmod -R 777 storage
chmod -R 777 bootstrap/cache
docker-compose exec web_php8 /bin/bash -c “composer install”
cp -p .env.example .env
php artisan key:generate

この時点で http://localhost:/ にアクセスし、Laravelのテストページが表示されていたら、構築成功です。

Laravelアプリケーション_③DB周り初期設定

.envに本環境のDB設定を記述しましょう

sed  -e 's/DB_HOST=127.0.0.1/DB_HOST=db/' .env
sed  -e 's/DB_DATABASE=laravel/DB_DATABASE=test_db/' .env
sed  -e 's/DB_USERNAME=root/DB_USERNAME=test_user/' .env
sed  -e 's/DB_PASSWORD=/DB_PASSWORD=test_pass/' .env

接続テスト兼、DBマイグレーション
下記にてmigrateを実行をし、デフォルトのテーブル郡が作成できれば成功です。

docker-compose exec web_php8 /bin/bash -c “cd /app/operation-cms && php artisan migrate

方法は何でもいいですが一例として。
下記にてphpMyAdminからDB内を確認できます。
http://localhost:89/

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

summernote on laravel の導入備忘録

インストール(npm)

npm install summernoteでnode_modulesに入る
レイアウトがいろいろ用意されている。
通常はrequire('summernote');で行けるが
日本語化とレイアウトをbootstrap4ベースにしたかったけどサンプルなかった。
多分以下で行けるはず。

resources/js/bootstrap.js
require('summernote/dist/summernote-bs4.min');
require('summernote/dist/lang/summernote-ja-JP.min');
resources/js/app.scss
@import 'summernote/dist/summernote-bs4.min';

フロント側(bladeファイルとjavascript)

divタグを用意するだけ。textareaタグでもよい。

edit.blade.php
<div id="summernote"></div>
<textarea id="summernote" class="form-control" name="description"></textarea>
edit.js
$(document).ready(function()
{
  $('#summernote').summernote({
    lang: 'ja-JP',
    height: 300,
  }
}

画像挿入時のサーバ転送

デフォルトはimgタグにbase64エンコードされたデータとして挿入されます。
独自のサーバにアップロードしてそのURLをimgタグに挿入する方法も用意されている。

  $('#summernote').summernote({
......
    callbacks: {
      onImageUpload: (files) => {
        /* ローカルファイルを選んだあと呼ばれるのでサーバーへアップする */
        insertImageFile(files[0]);
      },
      onMediaDelete: (target) => {
      /* 画像を削除するを選んだ時呼ばれるのでサーバーへ削除要求する */
        removeImageFile(target[0].src);
      },  
    },
......

function insertImageFile(file)
{
  let formData = new FormData();
  formData.append('file', file);

  fetch('/insert-image-file', formData,...)
  .then((response) => {
    if (response.ok) {
      // サーバ側で応答に画像のurlを付与する
      let imgNode = document.createElement('img');
      imgNode.src = response.url;
      $('#summernote').summernote('insertNode', imgNode);
    }
  });
}

function removeImageFile(src)
{
  /* 削除のアクションを呼ぶ */
  let formData = new FormData();
  formData.append('src', src);
  fetch('/remove-image-file', formdata, ...)
  .then((response) => {
    if (response.ok) {
    }
  }  
}

サーバ側(laravel/php)ではinsert-image-fileとremove-image-fileに対応する
アクションを用意する。

app/Http/Controller/TestController.php
public function insertImageFile(Request $request)
{
  /* ファイル名をuuidに変換 */
  $uuid = (string) Str::uuid();
  $ext = $request->file('file')->extension();
  $file = $uuid . '.' . $ext;
  $imgpath = $request->file('file')->storeAs('attachments', $file, 'public');
  $url = Storage::disk('public')->url($imgpath);
  return response()->json(['result' => 'OK', 'url' => $url]);
}

public function removeImageFile(Request $request)
{
  /* storageまでのパスを取得 */
  $url = Storage::disk('public')->url('');
  /* 画像のパス(URL)からサーバ内のローカルパスに変換 */
  $delete_file = str_replace($url, '', $request->src);
  $resp = Storage::disk('public')->delete($delete_file);
  return response()->json(['result' => 'OK']);
}

フォームのsubmitアクション

HTMLエディタの入力欄に対応するinputタグがないので
submitのクリックイベントを引っ掛けて隠し属性のinputを追加してsumbmitする
buttonタグにname="submit"があるとformのsubmitが効かないので削除する。
追記(2020/12/15)
普通にtextareaタグにid設定すればsummernoteのレイアウトになるっぽいので
以下の処理は必要ない。

  $("#submit_button").click((e) => {
    /* 一旦submitを無効 */
    e.preventDefault();
    let description = $('#summernote').summernote('code');
    let edit_form = document.getElementById('edit_form');
    if (edit_form != null) {
      inputDescription = document.createElement("input");
      inputDescription.type = 'hidden';
      inputDescription.name = 'description';
      inputDescription.value = description;
      edit_form.appendChild(inputDescription);
      edit_form.submit();
    }
  });
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Method Illuminate\Http\UploadedFile::getClientSize does not exist. と出た場合の対処法

結論

top.php
$request->file('file')->getClientSize()
                //変更する
$request->file('file')->getSize()

新しいバージョンのLaravelだとgetClientSize()は使えないみたい(;´Д`)

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

[PHP]複数配列のループ

複数配列を同時にループさせる方法として以下のようにarray_map()を使うのが便利だったので共有します。

.php
$array1 = [1,2,3];
$array2 = [4,5,6];

foreach (array_map(null, $array1, $array2) as [$arr1, $arr2]) {
    echo ($arr1 + $arr2); //5,7,9
}

array_map(null, $array1, $array2)の結果は [ [1,4], [2,5], [3,6] ] となっていて、
nullを渡すことによって、それぞれの配列の同じインデックスの要素同士をくっつけて、配列の配列にしてくれます。

会社の紹介

私は現在、株式会社ダイアログという物流×ITの会社に勤務しております。
2020年12月現在、エンジニアの募集はしていませんが、他にも様々な職種を募集しているので、Wantedlyのページをご覧ください。

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

LaravelにおけるORMについて

ORMとは?

アプリケーションからデータを操作しやすくするための装置。

アプリ側(オブジェクト指向)はクラス内のプロパティにデータを持たせる。
データベース(リレーショナルデータベース)はテーブルにデータを持たせる。

アプリ側

class User
{
    private $email;
    private $password;
    // 中略
}

データベース側

users テーブル
| email           | password |
|-----------------|----------|
| sample@mail.com | test1234 |

つまり構造上繋がりをもっておかなければならない「モデル」と「データベース」のデータの持ち方がそれぞれバラバラということ。

これによって何が起こるかというと、例えばテーブルから取り出したデータ形式をいちいち変えて改めてモデルに格納しなければならなかったり、逆にモデルのデータの変更した場合、それとデータベースを対応させるために改めてインサート文を書くことになる。つまり、「片方が変更した場合それと対応させるためにもう片方もその形式に準じて改めて変更させなければならない」という手間が生まれてくる。

そこで「アプリ側のデータとデータベース側のデータの紐付けをあらかじめ定義しておこう!」というのがORM。

アプリとDBのデータの紐付け

ではどうやって紐付けるか?

まずモデルを作成する。モデルクラスはPHP側のデータの入れ物になるクラス。
基本的にはモデルひとつにテーブルひとつが対応するように作成する。

$ php artisan make:model Group

するとappフォルダにGroupクラスが作成される。

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class Folder extends Model
{
    //
}

クラスに何も書かれていないが、継承元のModelクラスに様々な設定が書き込まれているためこれだけでモデルとして機能する。

このModelクラスが「アプリ側のデータとデータベース側のデータの紐付け」を実現してくれる。
どういうことかというと、このクラス名の複数形の名前のテーブルと自動的に紐づくように設定してくれる。ここでいうと、クラス名を「Group」にしたので「Group」クラスは「groups」テーブルと自動的に紐づくことになる。

こうしてModelクラスの紐付け(ORM)によって、モデルクラスとデータベースでデータの持ち方に違いがあったが、開発者は今まで通りオブジェクト指向に沿った書き方でデータベース側とデータのやり取りを行うことができる。

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

MVCモデルのメリット

結論

1. 機能ごとに分割されているため分業して作業が進めやすい(効率化)

2. それぞれが独立しているので修正や変更があった場合影響を受けにくい(時間と労力の節約)

MVCモデルとは?

役割ごとに(View, Model, Controller)に分割してコーディングを行う考え方

Modelとは?

実際にデータの処理を行う。

Viewとは?

モデルの状態を表示する。

Controllerとは?

ユーザーの入力に応じてModelとViewに処理をお願いする。

MVCモデルの処理の流れ

図

参照

MVCモデルについて

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

docker環境でlaravel/Duskをインストール(laravel6)

煮詰まっちゃったんで記事に残しておきます

前提条件

simotarooさんの絶対に失敗しないDockerでLaravel+Vueの実行環境(LEMP環境)を構築する方法、もしくはdockerにてlaravel,PHPの環境構築済みの方

環境

mac

docker (LEMP環境)

構築方法

任意のdockerfileに以下を追加しbuildし直す
※僕はsimotarooさんの記事を参考にして構築したためphpのdockerfileに記述しました

dockerfile
RUN apt-get -y install libzip-dev
RUN docker-php-ext-install zip


RUN apt-get -y install libnss3
RUN apt-get -y install libasound2-data  libasound2 xdg-utils
RUN apt install sudo
RUN apt-get install -y wget
RUN apt-get install -y gnupg2

RUN wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | apt-key add -
RUN echo "deb http://dl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/google.list
RUN sudo apt update
RUN sudo apt-get install -y google-chrome-stable
RUN apt-get install -y fonts-ipafont 

.envのAPP_URLを書き換え

.env
APP_URL = http://localhost を

APP_URL=http://127.0.0.1:8000 に書き換え

tests/DuskTestCase.phpを書き換え

tests/DuskTestCase.php
    protected function driver()
    {
        $options = (new ChromeOptions)->addArguments([
            '--no-sandbox',
            '--disable-gpu',
            '--headless',
            '--window-size=1920,1080',
            '--lang=ja_JP',
        ]);

インストール後プロジェクト内で

composer require --dev laravel/dusk:
artisan dusk:install
php artisan serve

そんでテスト実行

php artisan dusk
PHPUnit 8.5.8 by Sebastian Bergmann and contributors.

.                                                                   1 / 1 (100%)

Time: 11.2 seconds, Memory: 24.00 MB

OK (1 test, 1 assertion)

成功したら無事完成です

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

Laravelで簡易的にBasic認証

LaravelでBasic認証をやる方法はいくつかありますが、ライブラリを入れたりせず、データベース使ったりせず、簡易的にに実現する方法です。

  • ミドルウェアを作成する(1ファイル)
  • ミドルウェアをKernelに記述する
  • routes/web.php で認証の対象のページを記述する

の3ステップで実現できます。

ミドルウェアを作成する

今回のサンプルでは、BasicAuthMiddlewareという名前でミドルウェアを作成します。

php artisan make:middleware BasicAuthMiddleware

作成したミドルウェアのhandleメソッドを、以下のように実装します。
ユーザー名とパスワードは、ここに直接書いてます。

App\Http\Middleware\BasicAuthMiddleware.php
    public function handle(Request $request, Closure $next)
    {
        $username = $request->getUser();
        $password = $request->getPassword();

        if ($username == 'sample' && $password = 'sample') {
            return $next($request);
        }

        abort(401, "Enter username and password.", [
            header('WWW-Authenticate: Basic realm="Sample Private Page"'),
            header('Content-Type: text/plain; charset=utf-8')
        ]);
    }

ミドルウェアをKernelに登録

app/Http/Kernel.php の $routeMiddleware に、作成したミドルウェアを記述。
今回は、'basicauth'という名前で登録

app/Http/Kernel.php
'basicauth' => \App\Http\Middleware\BasicAuthMiddleware::class,

バージョン5くらいのLaravelだと、下記の書き方になります。

'basicauth' => 'App\Http\Middleware\BasicAuthMiddleware',

routes/web.php で認証の対象のページを記述する

登録した名前のミドルウェアで、対象のページをグループ化する

routes/web.php
Route::group(['middleware' => 'basicauth'], function() {
    // ここに対象のページを記述
    // 例)
    Route::get('/', [App\Http\Controllers\TopController::class, 'index'])->name('top');
});

以上で完了です。

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

Dockerを使ってLaravelのローカル開発環境を作る(Apache版)

Laravel Advent Calendar 2020 - Qiita の 15日目 の記事です。
昨日は @noel_kuma さんのLinter / FormatterからLaravelへの贈り物の記事でした!
明日は @kyoya0819 さんの記事です!

概要

最強のLaravel開発環境をDockerを使って構築する【新編集版】

この記事の反響が多く、Apache版も作って欲しいという要望もあってので記事を書いてみました。

対象読者

  • Laravelを愛する心の持っている方
  • PHP, Linux, Git, Dockerの知識をある程度持っている方

リポジトリ

https://github.com/ucan-lab/docker-laravel-apache

使い方

A. Laravelプロジェクトの新規作成

$ git clone git@github.com:ucan-lab/docker-laravel-apache.git
$ cd docker-laravel-apache
$ make create-project

http://localhost

以上の3ステップでLaravelの新規プロジェクトの環境構築は完了です。

A'. Laravelプロジェクトをバージョン指定して新規作成

Makefile のLaravelインストール部分を書き換えます。

    docker-compose exec app composer create-project --prefer-dist "laravel/laravel=6.*" .

B. 既存のLaravelプロジェクトの環境を構築する

$ git clone git@github.com:ucan-lab/docker-laravel-apache.git

# Laravelプロジェクトを docker-laravel-apache/backend へクローンする
$ git clone git@github.com:laravel/laravel.git docker-laravel-apache/backend

$ cd docker-laravel-apache
$ make init

http://localhost

これで既存のLaravelプロジェクトの環境構築は完了です。

コンテナ構成

├── web
└── db

web, db の2つのコンテナ構成で進めます。

nginxとの違い

Nginxの場合は、php-fpm(アプリケーションサーバー)とコンテナを分けていました。
Apacheの場合は、mod_phpというモジュールがデフォルトでインストールされており、それがPHPを実行してくれるアプリケーションサーバーを兼ねるウェブサーバーです。

ディレクトリ構成

.
├── backend # Laravelプロジェクトのルートディレクトリ
├── infra
│     └── docker
│          ├── apache
│          │   └── httpd.conf
│          ├── mysql
│          │   ├── Dockerfile
│          │   └── my.cnf
│          └── php
│              ├── Dockerfile
│              └── php.ini
├── .env.example
├── Makefile
└── docker-compose.yml

nginx版との差分

※nginx版との違いはnginxのディレクトリがapacheディレクトリに変わったくらいですね?

  • infra/docker/apache/httpd.conf
  • infra/docker/php/Dockerfile
  • Makefile
  • docker-compose.yml

変更点に関しては、上記の4つのファイルに注目していただければ良いかなと思います。
プルリクエストで詳細な差分を確認できます。

PHP のベースコンテナ

イメージタグに php:<version>-apache が用意されています。

解説など

docker-compose.yml

version: "3.8"
volumes:
  db-store:
services:
  web:
    build:
      context: .
      dockerfile: ./infra/docker/php/Dockerfile
    ports:
      - ${WEB_PORT:-80}:80
    volumes:
      - ./backend:/work/backend
    environment:
      - DB_CONNECTION=mysql
      - DB_HOST=db
      - DB_PORT=3306
      - DB_DATABASE=${DB_NAME:-laravel_local}
      - DB_USERNAME=${DB_USER:-phper}
      - DB_PASSWORD=${DB_PASS:-secret}

  db:
    build:
      context: .
      dockerfile: ./infra/docker/mysql/Dockerfile
    ports:
      - ${DB_PORT:-3306}:3306
    volumes:
      - db-store:/var/lib/mysql
    environment:
      - MYSQL_DATABASE=${DB_NAME:-laravel_local}
      - MYSQL_USER=${DB_USER:-phper}
      - MYSQL_PASSWORD=${DB_PASS:-secret}
      - MYSQL_ROOT_PASSWORD=${DB_PASS:-secret}

appとwebコンテナをまとめて、webコンテナのみになりました。
どっちにするか悩みましたけど、webコンテナにしました。

解説することないので、次へ行きます。

infra/docker/php/Dockerfile

infra/docker/php/Dockerfile
FROM node:14-buster as node
FROM php:7.4-apache-buster
LABEL maintainer="ucan-lab <yes@u-can.pro>"
SHELL ["/bin/bash", "-oeux", "pipefail", "-c"]

# timezone environment
ENV TZ=UTC \
  # locale
  LANG=en_US.UTF-8 \
  LANGUAGE=en_US:en \
  LC_ALL=en_US.UTF-8 \
  # composer environment
  COMPOSER_ALLOW_SUPERUSER=1 \
  COMPOSER_HOME=/composer

# composer command
COPY --from=composer:2.0 /usr/bin/composer /usr/bin/composer
# node command
COPY --from=node /usr/local/bin /usr/local/bin
# npm command
COPY --from=node /usr/local/lib /usr/local/lib
# yarn command
COPY --from=node /opt /opt

RUN apt-get update && \
  apt-get -y install git libicu-dev libonig-dev libzip-dev unzip locales && \
  apt-get clean && \
  rm -rf /var/lib/apt/lists/* && \
  locale-gen en_US.UTF-8 && \
  localedef -f UTF-8 -i en_US en_US.UTF-8 && \
  a2enmod rewrite && \
  docker-php-ext-install intl pdo_mysql zip bcmath && \
  composer config -g process-timeout 3600 && \
  composer config -g repos.packagist composer https://packagist.org

COPY ./infra/docker/php/php.ini /usr/local/etc/php/php.ini
COPY ./infra/docker/apache/httpd.conf /etc/apache2/sites-available/000-default.conf

WORKDIR /work/backend

php, composer, nodeの3つのコンテナをマルチステージビルドして使ってます。
(phpのイメージ内にapacheも入ってます。)

nodeいらないよって場合はDockerfileから削除しちゃってください。
英語圏の方にも使ってもらえるように言語&タイムゾーンのデフォルトは英語にしてます。
実際に使うときは日本に変更してご利用ください。

a2enmod rewrite はApacheのmod_rewriteモジュールを有効化してます。
これがないとLaravelのルーティングがうまく機能しなくなります。

infra/docker/apache/httpd.conf

infra/docker/apache/httpd.conf
<VirtualHost *:80>
    ServerName example.com
    ServerAdmin webmaster@localhost
    DocumentRoot /work/backend/public

    ErrorLog ${APACHE_LOG_DIR}/error.log
    CustomLog ${APACHE_LOG_DIR}/access.log combined

    <Directory /work/backend/public>
        Options Indexes FollowSymLinks
        AllowOverride All
        Require all granted
    </Directory>
</VirtualHost>

最低限設定必要な項目だけ設定してます。

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

閏年じゃない年も2/29生まれの人をお祝いしたい??

これはSOUSEI Technology アドベントカレンダー2020 15日目の記事です。

2/28と3/1どちらで祝うべきなのか

2/29生まれの人、閏年なら当日お祝いしてあげればいいのですが、閏年じゃないときはどうすればいいの?
ググってみました。

例年については、男女共に「2月28に祝う」という声が圧倒的。
https://cocoloni.jp/love/232234/

あとで祝われると「忘れてたでしょ?」ってなりそう
https://teratail.com/questions/145109

ということで2/28にお祝いします!??
あとはデータを取得するだけ!

今日が誕生日の人を取得する

当日が2/28かつ閏年でないときに、追加で2/29のデータを取得してあげればOK!

$today = Carbon::now()->toDateString();
$users = User::whereDay('birthday', date('d', strtotime($today)))
    ->whereMonth('birthday', date('m', strtotime($today)))
    ->get();
if(date('m-d', strtotime($today)) == '02-28' && !date('L', strtotime($today))) {
    $users->merge(
        User::whereDay('birthday', 29)
        ->whereMonth('birthday', 2)
        ->get();
    );
}
return $users;

範囲指定(from〜to)で誕生日の人を取得する

これは指定された日付が閏年かどうか気にする必要はなく、toが2/28のときだけ考慮すればOK!

/**
 * 月日が日付範囲に含まれるスコープ
 *
 * @param Builder $query クエリ
 * @param string $column 対象カラム
 * @param Carbon $from 開始日
 * @param Carbon $to 終了日
 * @return Builder
 */
private function scopeWhereBetweenMonthAndDays($query, $column, $from, $to)
{
    // fromとtoの差が1年以上あるときはすべての日が対象になるのでクエリ追加不要(全データ取得する)
    if (!$to->diffInYears($from)) {
        $from_without_year = $from->format('m-d');
        // 閏年対応。2/28が指定された場合は2/29として、2/29のデータも取得対象とする
        $to_without_year = $to->format('m-d') == '02-28' ? '02-29' : $to->format('m-d');

        if ($from->year == $to->year) {
            $query = $query->whereRaw('DATE_FORMAT(' . $column . ',"%m-%d") >= "' . $from_without_year . '"')
                ->whereRaw('DATE_FORMAT(' . $column . ',"%m-%d") <= "' . $to_without_year . '"');
        } else {
            $query = $query->where(function ($q1) use ($from_without_year, $to_without_year) {
                $q1->orWhereRaw('DATE_FORMAT(' . $column . ',"%m-%d") >= "' . $from_without_year . '"')
                    ->orWhereRaw('DATE_FORMAT(' . $column . ',"%m-%d") <= "' . $to_without_year . '"');
            });
        }
    }
    return $query;
}

さいごに

僕の誕生日は3/3です!
プレゼント待ってます!

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

Laravel Excel から出力した csv のレコードが重複してしまっていた問題を解決した話

はじめに

あるプロジェクトで、 Laravel を使って構築したシステムの DB のデータを csv で出力する要件があり、Laravel Excel を導入して実現していました。

しかしあるとき、出力した csv のレコードが重複しているという報告があり、実際に全く同じレコードが2件重複して出力されていることがわかりました。
さらには、出力されるはずのデータが含まれていないことも判明しました。

この記事では、この問題を解決するまでの過程を紹介し、自らへの戒めと教訓としたいと思います。

Laravel Excel とは

https://laravel-excel.com

Laravel で Excel ファイルの操作を行うためのライブラリです。
PhpSpreadsheet というライブラリが元になっていて、 Laravel での操作に特化しています。
今回のプロジェクトでは、この Laravel Excel を使って、主に csv ファイルの入出力を行っています。

環境

  • PHP : 7.3.5
  • Laravel : 5.8
  • PostgreSQL : 11.3
  • Laravel Excel : 3.1.9

発生した問題

  • 約4万件のデータを抽出した際に、20〜30件ほど重複している
  • CSVを出力するたびに、重複するデータが異なる
  • データが重複した分だけ、本来であれば存在するデータがなくなっている
  • データの総件数は毎回変わらない

犯人探し

PostgreSQL

LEFT JOIN のやり方が間違っている?

とある関係で Eager Loading ではなく LEFT JOIN を使用しているので、これが真っ先に原因かと思ったのですが、単純なクエリだったのでこれは問題なさそうでした。

SELECT 文の書き方が間違っている?

SELECT でテーブル名を指定しておらず、意図しないテーブルの id などのデータが出力されてしまっているのかとも思いましたが、テーブル名を指定する書き方をしているのでこれも大丈夫そうでした。

PostgreSQL の バグ?

こんなバグがあったらもっと叩かれてそう。。

Laravel Excel

Github の issue で同じような問題を探してみる

次にライブラリである Laravel Excel のバグかと思い、 Github で同じような報告がないか探してみました。
が、これは特に見つかりませんでした。

ライブラリの中身をみてみる

中でどのような処理が行われているのかわからなかったので、中のコードを読んでみました。
今回は LaravelQueryBuilder から csv を作成していたので、途中で chunk メソッド(リファレンス) が使用されていました。
これはDBから取得するデータを小分けにすることで、メモリ使用量を抑え、大量のデータでも出力できるようにしているのだと思います。

再度考察

ここまでで具体的な原因はわからなかったので、再度原因を考察してみることにしました(実際は結構頭抱えてました)。

改めて重複しているデータを眺めてみる

よくよく見てみると、何故か重複データは2000行目や5000行目など、1000行目単位の付近で発生していました。
この 1000 という数字が何かのヒントだと思い、調べてみると、Laravel Excel の設定ファイルに chunk_size という項目があり、これが 1000 になっていました。
これを試しに 500 にしてみると、重複が1500行目や4500行目など、500の区切りでも発生するようになりました。

chunk によって発行されているクエリを調べてみる

SELECT * FROM 'テーブル名' LIMIT 1000 OFFSET 0;

そうか、わかったぞ!(cv:高山みなみ)

原因は...

ORDER BY に、ユニークになるカラムを指定していないこと でした。
今回のクエリでは、ユニークでないカラムを ORDER BY に指定していたため、このカラムが同じ値の場合、並び順がDBによって決定されてしまっていました。
これと LIMIT および OFFSET が合わさることにより、クエリを発行するたびに並び順が変更され、境目付近のデータが重複してしまっていたのだと考えられます。

この現象については、下記のブログに詳細が記載されていました。
https://blog.mmmcorp.co.jp/blog/2018/03/22/sql_order_by/

同じように LIMITOFFSET を使用しているペジネーションなんかでも注意が必要ですね。

そして ORDER BY にユニークとなるカラムを設定したところ、見事に重複がなくなりました。
これで解決ですね。

さいごに

というわけで結局犯人は自分だったわけですが。。
原因がわかるとすっきりしますが、わからないうちは辛く苦しかったです。。笑
ライブラリを使っているときに予期せぬことが起きた場合には、ライブラリの中身を見ることが大事ですね。

では最後にまとめ

並び順の権を DB に握らせるな!!

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

【スクショで解説】PHP/Laravelを使ってGoogleドライブにファイルをアップロードしてみる

対象読者

  • PHP/Laravelを使ってGoogleドライブにファイルをアップロードしたい人。

コードの実装以外の部分(認証のための準備)でてこずる部分が結構あったため、メモとして保存。
サンプルコードも用意しているので、手順通りに進めれば必ず同じ結果を得られるようになっています。

下準備

コードを実装する前に、Googleドライブを外部から利用するために事前にやっておかなければならない事があるのでそちらから手をつけていきます。(APIの有効化や認証アカウントの作成など)

Google Cloud Platformの設定

https://console.cloud.google.com/
gcp_censored.jpg

まず最初にGoogle Cloud Platform(GCP)の設定を行います。

これまでGCPを利用した事の無い人はスタートガイドに従って利用を開始してください。(クレジットカードなどの情報を入力する必要がありますが、無料トライアル期間があるのでこの時点で勝手に課金される事はありません。)

参照記事: これから始めるGCP(GCE) 安全に無料枠を使い倒せ

任意のプロジェクトを選択

今回はアカウント作成時にデフォルトで用意された「My First Project」を使用します。

Google Drive APIの有効化

APIとサービス→ライブラリ.png
Google Drive API.png

左サイドバーから「APIとサービス→ライブラリ」を選択。
各種Google APIが出てくるので、その中から「Google Drive API」を見つけ、有効化します。

サービスアカウントを作成

認証情報.png

有効化に成功するとGoogle Drive APIの管理画面に飛ぶので、左サイドバーから「認証情報」を選択。
外部からGoogle Drive APIを使用する際、認証に必要な「サービスアカウント」の作成を行います。

参照記事: GCP Service Accountを理解する

サービスアカウント作成_censored.jpg
役割.png

アカウント名や説明文を入力し、「作成」ボタンをクリック。
その後、アカウントにロール(役割)を付与していきます。(今回はテストなのでProject→オーナーを選択)

③は無視でOKなので、そのまま「完了」ボタンをクリックしてください。

成功_censored.jpg

上手くいくとこんな感じでサービスアカウントが追加されています。

秘密鍵(JSON)を作成

json_censored.jpg

アカウント詳細画面を開くと、下の方に「鍵を追加」というメニューがあるので、そちらから秘密鍵(JSON)を作成します。

jsonを保存_censored.jpg

後にこの秘密鍵を使ってアプリからサービスアカウントへの認証を行うので、ダウンロードして保管しておきましょう。

Googleドライブの設定

https://drive.google.com/drive/u/0/my-drive

次にGoogleドライブの設定を行います。

フォルダを作成

test.png

テスト用に適当なフォルダを用意しましょう。

サービスアカウントを共同編集者に追加

共有.png
メルアド共有_censored.jpg

先ほど作成したサービスアカウントがGoogleドライブ内のフォルダにアクセスできるよう、「共有」を選択してサービスアカウントのメールアドレスを追加します。

メルアド_censored.jpg

(メールアドレスはサービスアカウントの詳細画面から確認可能)

共有が終わったら下準備は完了です。

コードを実装

さて、いよいよコードの実装を行っていきます。

Laravelプロジェクトを作成

誰でも同じ結果になるように、今回はLaravelプロジェクトを作成するところから始めます。

https://github.com/kazama1209/laravel-new

↑サンプルを用意したので、各自クローンして使ってください。

セットアップ

Dockerでちゃちゃっと準備を済ませます。

コンテナを起動
$ docker-compose up -d
コンテナ内に入る
$ docker-compose exec php bash
プロジェクトを作成
$ composer create-project laravel/laravel
ブラウザを確認

スクリーンショット 2020-12-15 1.52.52.png

「localhost」へアクセスしてLaravelの初期画面が表示されていれば成功。

Composerをインストール

https://github.com/googleapis/google-api-php-client

Google Drive APIを使用するために公式のライブラリを準備します。

$ cd laravel
$ composer require google/apiclient:^2.0

...

  - Downloading phpseclib/phpseclib (2.0.29)
  - Downloading psr/cache (1.0.1)
  - Downloading firebase/php-jwt (v5.2.0)
  - Downloading google/auth (v1.14.3)
  - Downloading google/apiclient-services (v0.156)
  - Downloading google/apiclient (v2.8.3)
  - Installing phpseclib/phpseclib (2.0.29): Extracting archive
  - Installing psr/cache (1.0.1): Extracting archive
  - Installing firebase/php-jwt (v5.2.0): Extracting archive
  - Installing google/auth (v1.14.3): Extracting archive
  - Installing google/apiclient-services (v0.156): Extracting archive
  - Installing google/apiclient (v2.8.3): Extracting archive
4 package suggestions were added by new dependencies, use `composer suggest` to see details.
Package phpunit/php-token-stream is abandoned, you should avoid using it. No replacement was suggested.
Generating optimized autoload files
> Illuminate\Foundation\ComposerScripts::postAutoloadDump
> @php artisan package:discover --ansi
Discovered Package: facade/ignition
Discovered Package: fideloper/proxy
Discovered Package: fruitcake/laravel-cors
Discovered Package: laravel/tinker
Discovered Package: nesbot/carbon
Discovered Package: nunomaduro/collision
Package manifest generated successfully.
68 packages you are using are looking for funding.
Use the `composer fund` command to find out more!

先ほど作成したプロジェクト内に移動し、Composerをインストール。

Googleドライブにファイルをアップロードするための関数を作成

$ mkdir app/Libs
$ touch GoogleDrive.php

今回はapp/配下に「Libs」というフォルダを用意し、「GoogleDrive.php」ファイル内に関数を作成していきます。

app/Libs/GoogleDrive.php
<?php

namespace App\Libs;

class GoogleDrive
{
    /**
     * Googleドライブへの認証を行う
     * @return Google_Service_Drive
     */
    public function getDriveClient(): \Google_Service_Drive
    {
        $client = new \Google_Client();

        // サービスアカウント作成時にダウンロードしたJSONファイルの名前を「client_secret」変更し、configフォルダ内に設置
        $client->setAuthConfig(config_path('client_secret.json'));
        $client->setScopes(['https://www.googleapis.com/auth/drive']);

        return new \Google_Service_Drive($client);
    }

    /**
     * ファイルをアップロードする
     *
     * @return GoogleDrive
     */
    public function fileUpload()
    {
        $driveClient = $this->getDriveClient();

        $fileMetadata = new \Google_Service_Drive_DriveFile([
            'name' => 'sample.jpg', // Googleドライブへアップロードされた際のファイル名(今回は「sample.jpg」とする)
            'parents' => ['xxxxxxxxxxxxxxxxxx'], // 保存先のフォルダID(配列で渡さなければならないので注意)
        ]);

        $driveClient->files->create($fileMetadata, [
            'data' => file_get_contents(storage_path('app/public/sample.jpg')), // アップロード対象となるファイルのパス(今回はstorage/app/public配下の「sample.jpg」を指定)
            'mimeType' => ' image/jpeg',
            'uploadType' => 'media',
            'fields' => 'id',
        ]);
    }
}

全体的にかなりシンプルな記述にしてあるので、細かい説明は割愛しても大丈夫だと思います。

※アップロードに使用する画像は各自用意してapp/Storage/app/public配下に設置してください。

Artisanコマンドを作成

動作確認しやすいようにArtisanコマンドを作成していきます。

$ php artisan make:command GoogleDriveFileUpload

ターミナルで↑を打ち込むとapp/Console/Commands/配下に「GoogleDriveFileUpload.php」というファイルが作成されているはずなので、次のように変更してください。

app/Console/Commands/GoogleDriveFileUpload.php
<?php

namespace App\Console\Commands;

use Illuminate\Console\Command;
use App\Libs\GoogleDrive;

class GoogleDriveFileUpload extends Command
{
    /**
     * The name and signature of the console command.
     *
     * @var string
     */
    protected $signature = 'google_drive:file_upload';

    /**
     * The console command description.
     *
     * @var string
     */
    protected $description = 'Upload file to Google Drive';

    /**
     * @var GoogleDrive
     */
    public $googleDrive;

    /**
     * Create a new command instance.
     *
     * @return void
     */
    public function __construct(GoogleDrive $googleDrive)
    {
        parent::__construct();
        $this->googleDrive = $googleDrive;
    }

    /**
     * Execute the console command.
     *
     * @return int
     */
    public function handle()
    {
        $this->googleDrive->fileUpload();
    }
}

Artisanコマンドを実行

$ php artisan google_drive:file_upload

アップロード成功.png

先ほど作成したArtisanコマンドを実行し、Googleドライブ内に「sample.jpg」というファイルがアップロードされていれば成功です。

あとがき

お疲れ様でした。基本的な流れは以上になります。あとは各自のプロジェクトに合わせてカスタマイズしてみてください。

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

Laravelのmodelでphp-enumをいい感じに使う

はじめに

Laravelでenumを扱おうとすると、よく出てくるのがこの三つのライブラリです。

このうち、下二つはLaravel用のライブラリなので、親和性は高いと思うのですが、他PHPフレームワークを使う時に応用が効かないかなと思って、個人的にはphp-enumを推しています。(そもそもphp-enum以外は使ったことがないので、こっちのがいいぜ!という時は教えていただけるとありがたいです)

そういう訳で、laravelでphp-enumを使う時の個人的なベストプラクティスを紹介します。

ライブラリ バージョン
laravel 6.18
php-enum 1.7

やり方

enumの定義

php-enumでは、MyCLabs\Enum\Enumクラスを継承したクラスに、定数を定義することで、num値を定義します。このような感じです。

<?php

declare(strict_types=1);

namespace App\Enums;

use MyCLabs\Enum\Enum;

class RoleType extends Enum
{
    public const SUPER_ADMIN = 'SUPER_ADMIN';

    public const ADMIN = 'ADMIN';

    public const USER = 'USER';
}

マイグレーションの作成

マイグレーションファイルを作る時は、Blueprintenum()メソッドを使います。第二引数にはenumとして許可する値の配列を渡します。php-enumでは、values()メソッドでそのenum内で定義されている値の配列を得ることができるので、この値を渡します。

class CreateUsersTable extends Migration
{
    /**
     * Run the migrations.
     */
    public function up(): void
    {
        Schema::create('users', function (Blueprint $table): void {
            $table->enum('role', RoleType::values());
        });
    }

    /**
     * Reverse the migrations.
     */
    public function down(): void
    {
        Schema::dropIfExists('users');
    }
}

モデルの定義

UserモデルにRoleTypeを持たせる時、今の状態では$user->roleとしてもRoleTypeのインスタンスは取れず、DB上にあるスカラー値しか取れません。また、$user->roleRoleTypeをそのままセットすることもできず、スカラー値に変換してからセットする必要があります。

$role = RoleType::ADMIN();
$user = new User();

$user->role = $role->getValue();
$role = new Role($user->role);

// NG
// $user->role = $role;

プロパティの値を使う度に変換を行うのは手間なので、Laravelのミューテタ1を使います。
以下のようにgetRoleAttribute〇〇(), setRoleAttribute〇〇()メソッドを定義しておくことで、laravelがよしなに変換をしてくれます。

<?php

declare(strict_types=1);

namespace App\Models;

use App\Enums\AnaUserRole;
use Illuminate\Foundation\Auth\User as Authenticatable;

class User extends Authenticatable
{
    /**
     * The attributes that are mass assignable.
     *
     * @var array
     */
    protected $fillable = ['role'];

    public function getRoleAttribute(string $value): RoleType
    {
        return new Role($value);
    }

    public function setRoleAttribute(RoleType $role): void
    {
        $this->attributes['role'] = $role->getValue();
    }

}

このようにかけます。

$role = RoleType::ADMIN();
$user = new User();

$user->role = $role;
// $roleはRoleTypeのインスタンス
$role = $user->role;

以上!

参考文献

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