20210108のPHPに関する記事は19件です。

PHP基礎 -文字列の出力-

はじめに

これまでRubyを使ってきましたが、web系で使われる言語を幅広く扱えるようにするため、PHPも基礎から学習していきます。Rubyと似ている部分もありますが、細かい文法の違いや独自の概念が多いので、学習内容を記事にまとめていきます。

文字列の出力

#文字列を出力バッファに書き出す
echo "foo";

#返り値として1を持つ
print("bar");

#フォーマット(書式)を整えて出力
printf(%s, "foobar");

echo、printは基本的にどちらを使っても文字列を出力できますが、printは返り値として1を持つ、echoは文字列を出力バッファに書き出すだけのという違いがあります。

printfは上記2つとは異なり、PHPの組み込み関数で、指定したフォーマットに合わせて出力します。使いこなせれば便利ですが、その分仕様を意識して活用しないとエラーや予期しない動作の原因となりやすいです。

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

【PHP】mb_substr()はなぜ遅いのか

簡単なまとめ

  • PHPの mb_substr() はあんまり速くない
  • 長い文字列を与えて末尾の方を取り出そうとするとすげー遅い
    • 先頭部分を取り出すなら別に問題ない
  • どうしても長い文字列をカットしたければ別の方法を使おう
  • 無理やり分割するときは文字の境界に気をつけてね

検証環境: PHP 8.0.1

検証

65万文字(ASCII文字のみ)の文字列を用意して、 mb_substr($文字列, 開始位置, 1) が要する処理時間を計測しました。(使ったコードはGistに置いてあります)
ところどころに怪しいぴょこぴょこが見えますが、見事に線形になっています。終わりの方ではたかが 文字列から1文字取り出すだけ の処理1回が1msを超えてしまいました。

image.png

つまり「任意の文字列を先頭から末尾まで1文字ずつ切り出して文字種をチェックする」という処理に mb_substr() を使うと、かかる時間は 文字数の2乗 に比例します。10,000文字の文字列を処理するには1,000文字のときの100倍の時間がかかるということです。

なんで?

PHPは文字列をただのバイト配列として保持します。そしてUTF-8では1文字のバイト長が可変なのでした。そこで次の例題を考えます。

【Q】 ABCあいうαβγかきく という文字列から「10 文字目以降(かきく)」が欲しいとします。何バイト目から切り出せばいいでしょうか? 文字列のエンコーディングはUTF-8だとします。
【A】 先頭から順に数えていくしかありません。ABCは1文字1バイト、あいうは1文字3バイト、αβγは1文字2バイトですから、先頭18バイトを除いて19バイト目から取り出します。これは面倒くさい!

したがって、 mb_substr() に「50万文字目」という無茶な値を突っ込むと、 mb_substr() は内部1mbfl_string() という関数23を呼び出して愚直に先頭から文字数とバイト数のカウントを始めます。 mb_substr() を1,000回呼ぶと同じ処理が1,000回走ります。そりゃー遅いよね、という話でした。

どうすればいいのか

文字数を制限する、文字列を分割する

長さ制限を加えてしまうというのが一つの手です。
改行もデリミタもない、ながーーーーーい文字列をPHPで処理しなくてはならないという状況は割とレアでしょうから(Webアプリの場合それはDoSアタックの可能性があります)、適当に制限をかけるなり、 explode() するなりでだいたいの問題は解決するはずです。

改行やデリミタのような手がかりがないなら一定サイズごとに無理やり分割することになりますが、たとえば「あ」(UTF-8では 0xE3 0x81 0x82)の真ん中で切ってしまったりすると困ります。切り方には気をつけないといけません。

特定の文字種・エンコーディングを前提に処理を書く

処理対象の文字列がASCII文字だけで構成されていることが確実なのであれば(正規表現を使ってチェックしましょう) substr() で同じ結果を得られます。

preg_splitで分解する

「任意の文字列を先頭から末尾まで1文字ずつ切り出す」をやりたいなら、

$arr = preg_split('//u', $str, -1, PREG_SPLIT_NO_EMPTY);

で文字列を1文字ずつにバラして配列にできます。こちらの方が圧倒的に高速です。

長さ固定のエンコーディングを使う

ここまでで分かった通り、1文字の長さが可変であるUTF-8を使うから時間がかかるわけです。1文字の長さが固定4のエンコーディング、たとえばUTF-32を使えば爆速になります(文字種をチェックしなくても 文字数×定数 でバイト長が直接出せるため)。

$longString_utf8 = "...."; // なんか長い文字列
$longString_utf32 = mb_convert_encoding($longString_utf8, "UTF-32");
$len = mb_strlen($longString_utf8); // mb_strlen($longString_utf32, "UTF-32") でも同じ
for($i = 0; $i < $len; $i++) {
    $char_utf32 = mb_substr($longString_utf32, $i, 1, "UTF-32"); // エンコーディングを明示的に指定する
    $char_utf8 = mb_convert_encoding($char_utf32, "UTF-8", "UTF-32"); // これで1文字ずつが取れる
    // あとはchar_utf8 を使って好きなことをやる
}

なお、こうするとお手軽に高速になりますがおそらく地獄のような状況を招きます(やめましょう)

mb_internal_encoding("UTF-32");

補足

何か間違いなどあればコメントで教えてください。よろしくお願いします。


  1. https://github.com/php/php-src/blob/PHP-8.0.1/ext/mbstring/mbstring.c#L2123 

  2. https://github.com/php/php-src/blob/PHP-8.0.1/ext/mbstring/libmbfl/mbfl/mbfilter.c#L919 

  3. この関数は mb_strlen() も使っています 

  4. 結合文字や異体字セレクタを考えると全然固定ではないのですが、ここでは説明を省きます…… 

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

【正規表現】はじめの数文字は英字、それ以降は数字で制限する

1/8 19:30
内容に不備があるので後ほど修正します?

たとえば

A012345

みたいな形式の文字列で制限したい。
今回は『最初1文字は英大文字、それに続く数字は6文字』というルールを作る。
たぶん他の言語でもいけると思いますが、PHPで書くことを想定します。

正規表現を書く

/^[A-Z{1}]+[0-9{6}]+$/

英大文字(1文字)+数字(6文字)』と指定しています。単純ですね。
こちらで検証できます。↓

PHP: preg_match() / JavaScript: match() 正規表現チェッカー ver3.1
http://okumocchi.jp/php/re.php

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

【正規表現】はじめの数文字は英字、それ以降は数字で制限する

たとえば

A012345

みたいな形式で制限したい。
最初1文字は英大文字、それに続く数字は6文字』というルールを作る。
たぶん他の言語でもいけると思いますが、PHPで書くことを想定します。

正規表現

/^[A-Z{1}]+[0-9{6}]+$/

『英大文字(1文字)+数字(6文字)』と指定しています。単純ですね。
こちらで検証できます。↓

PHP: preg_match() / JavaScript: match() 正規表現チェッカー ver3.1
http://okumocchi.jp/php/re.php

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

プログラムを少しかじった私がバックエンドエンジニアとして働くためにECサイトを作り始めてみた その④

対象者

・バックエンドエンジニアを志す人
・web開発初学者
・ECサイトを作成しようと思っている人

はじめに

こんにちは!2021年がはじまりました。
この投稿はその③の続きです。

その③では一通りのざっくりとしたhtmlとcssを書いたところまででした。
今回はDB設計とバックエンド機能の設計を進めていきます。

目次

1.今回の作業報告
2.反省点
3.次回までの目標

1.今回の作業報告

年末から年始12月26日から1月8日までざっくりまとめます。
意気揚々とhtml、cssを書きましたが・・・・

年末にDBを取り組んで撃沈。
そこから別件でも追われ気持ち的に着手できませんでした(反省)

テーブルを作成しました。
データ型についてこだわるべきなのかもしれませんが動けば良しでいきます。

スクリーンショット 2021-01-08 17.31.09.png

会員登録するところでエラー。ググルもなかなか解決することができず(,が抜けてただけだった・・)ここでかなり時間を溶かしてしまいました。
スクリーンショット 2021-01-08 17.29.46.png

商品で検索の機能を実装することができました。(いつかいい感じの画像にしますw)
スクリーンショット 2021-01-08 17.40.47.png

2.反省点

1.エラーを放置してしまった。
2.有識者がいるなかで聞けずにいたところ

とりあえず大きく2点ですね。
ググりはしたものの3日ぐらい溶かしてそのまま放置してしまいました。
自分で解決できないのであれば有識者にヒントもらうなりする必要があったと思いました。
まあ、自分で解決できればそれに越したことないと思いますが・・・

3.次のステップ:機能の優先順位洗い出し。引き続き実装!

幸いにも前工程で予定していたスケジュールより早かったために大きくスケジュール変更をしなくてよさそうです。別でプロダクトの開発選手権があるのでそちらのスケジュールと両立をやりきります。

ものすごく簡単ですが今回はこのへんで!(プロダクト開発に時間をさきます)

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

[php] __constructについて

今回の記事

phpを学んでいる中で、躓いた__constructについて初心者なりに解説していきたいと思います。

__constructとは?

クラスから新しくインスタンスを生成する時に最初に実行される関数が__constructです。これを使用することにより、インスタンスを生成する際の初期化に用いることができます。

インスタンス

上記で出てきたインスタンスとは、生成したクラスから値を作成するときに作るものです。クラスで定義したプロパティ(変数)を受け継いでいるものとして考えていただければ大丈夫です。

使い方

まずは比べた方がわかりやすいと思うので__constructを使わない場合を見てみたいと思います。

  • 書いたコード
<?php

class Post
{
  public $text;
  public $likes;

  public function show()
  {
    printf('%s (%d)' . PHP_EOL, $this->text, $this->likes);
  }
}

$posts = [];
$posts[0] = new Post;
$posts[0]->text = "hello";
$posts[0]->likes = 0;

$posts[1] = new Post;
$posts[1]->text = "hello again";
$posts[1]->likes = 0;

$posts[0]->show();
$posts[1]->show();
  • 実行結果
hello (0)
hello again (0)
~ $

次に同じ内容で__constructを使った場合を見ていきます。

  • 書いたコード
<?php

class Post
{
  public $text;
  public $likes;

  public function __construct($text, $likes) //本題のコンストラクトの部分
  {
    $this->text = $text; 
//ここで記入している$textや$likesはプロパティとは異なる変数ということに注意!
    $this->likes = $likes;
  }

  public function show()
  {
    printf('%s (%d)' . PHP_EOL, $this->text, $this->likes);
  }
}

$posts = [];
$posts[0] = new Post('hello', 0); //上記とは異なり、インスタンスの引数に設定

$posts[1] = new Post('hello again', 0);

$posts[0]->show();
$posts[1]->show();
  • 実行結果
hello (0)
hello again (0)
~ $

__constructはインスタンス生成時に実行される関数なので引数として渡します。
そして使い方としてpublic function __construct(変数)と記入して使います。また、 construct内で使っている$textや$likesはクラスのプロパティではないということに注意してください。インスタンスの引数で渡した値が__constructの$textや$likesに入り、プロパティに格納される仕組みになっています。

まとめ

__constructを定義することにより、newを使い呼び出すたびに初期化処理を確実に行っていくれるので記入漏れや呼び出いてから毎回定義しなくて済むのでとても便利です。クラスを使う時は必ず使うので使えるようになっておいて間違いはないです。

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

Docker コンテナ内で composer install/require したら `Could not find package` となった

起こったこと

個人作成のアプリを作成中 docker のコンテナ内に入って、composer require laravel/laravel を実行したら以下のメッセージが出てきた。

[InvalidArgumentException]
Could not find package laravel/laravel with stability stable.

ちなみにローカルからだとうまくいく。
なんでやねーーーん!!

紆余曲折を経て最終的にやったこと

Dockerfile から composer をインストールしている以下の記述を削除

RUN curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/bin --filename=composer
RUN composer config -g repos.packagist composer https://packagist.jp

マルチステージビルドを使用して composer を install する

COPY --from=composer:2.0.8 /usr/bin/composer /usr/bin/composer

終わったら docker image を ビルドして完了

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

Docker コンテナ内で composer install/require で `Could not find package` となった

起こったこと

個人作成のアプリを作成中 docker のコンテナ内に入って、composer require laravel/laravel を実行したら以下のメッセージが出てきた。

[InvalidArgumentException]
Could not find package laravel/laravel with stability stable.

ファッ!?

やったこと

Dockerfile から composer をインストールしている以下の記述を削除

RUN curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/bin --filename=composer
RUN composer config -g repos.packagist composer https://packagist.jp

マルチステージビルドを使用して composer を install する

COPY --from=composer:2.0.8 /usr/bin/composer /usr/bin/composer

終わったら docker image を ビルドして完了

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

Laravelでマイグレーション時にSQLSTATE[42000]: Syntax error or access violation: 1067 Invalid default valueとエラー時の解消法

はじめに

今回はわたしが遭遇したエラーと解消法についてご紹介したいと思います。
なかなか解決法が見つからなかったのですが、案外あっさりとした内容でした。備忘録の意味も含めて書きたいと思います。

-各バージョン
-laravel 6.x
-PHP 7.4.9
-mySQL 5.7.30

どのようなエラーが出たか

Laravelでマグレーションをした時に、発生したエラーです。
以下がエラー内容

Illuminate\Database\QueryException  : SQLSTATE[42000]: Syntax error or access violation: 1075 Incorrect table definition; there can be only one auto column and it must be defined as a key (SQL: create table `stock_cosmes` (`stock_id` bigint unsigned not null auto_increment primary key, `product` varchar(100) not null, `color` varchar(100) not null, `brand` varchar(100) not null, `price` int not null auto_increment primary key, `purchaseDate` date not null, `category` varchar(255) not null, `created_at` timestamp null, `updated_at` timestamp null) default character set utf8mb4 collate 'utf8mb4_unicode_ci')

  at /Users/yuzu/Desktop/jewelry/vendor/laravel/framework/src/Illuminate/Database/Connection.php:669
    665|         // If an exception occurs when attempting to run a query, we'll format the error
    666|         // message to include the bindings with SQL, which will make this exception a
    667|         // lot more helpful to the developer instead of just the database's errors.
    668|         catch (Exception $e) {
  > 669|             throw new QueryException(
    670|                 $query, $this->prepareBindings($bindings), $e
    671|             );
    672|         }
    673|

  Exception trace:

  1   PDOException::("SQLSTATE[42000]: Syntax error or access violation: 1075 Incorrect table definition; there can be only one auto column and it must be defined as a key")
      /Users/yuzu/Desktop/jewelry/vendor/laravel/framework/src/Illuminate/Database/Connection.php:463

  2   PDOStatement::execute()
      /Users/yuzu/Desktop/jewelry/vendor/laravel/framework/src/Illuminate/Database/Connection.php:463

  Please use the argument -v to see more details.
yuzunoMacBook-Air:jewelry yuzu$ -v
-bash: -v: command not found

そして、こちらが作成しようとしたテーブルです。

public function up()
    {
        Schema::create('stock_cosmes', function (Blueprint $table) {
            $table->bigIncrements('id');
            $table->string('product', 100);
            $table->string('color', 100)->nullable();
            $table->string('brand', 100)->nullable();
            $table->integer('price', 6)->nullable();
            $table->date('purchaseDate')->nullable();
            $table->string('category');
            $table->timestamps();
        });
    }

題名にあるエラー文でググってみたのですが、bigIncrementsは主キーにしなくてはいけないや、nullにできないといった解説が多く、わたしのエラー原因に一致するものが見つかりませんでした。
そこで、Laravelの公式サイトを読み直してみると、解決することができました!

解決法

とても初歩的な内容で恥ずかしいのですが、指定しているintegerには、第二引数を設定することができないというのが、今回のエラー原因でした。
integerにも字数制限をする場合には、length()で指定できるようです。

Laravel 6.x データベース:マイグレーション

以下、修正したテーブルです。

public function up()
    {
        Schema::create('stock_cosmes', function (Blueprint $table) {
            $table->bigIncrements('id');
            $table->string('product', 100);
            $table->string('color', 100)->nullable();
            $table->string('brand', 100)->nullable();
            $table->integer('price')->length(6)->nullable();
            $table->date('purchaseDate')->nullable();
            $table->string('category');
            $table->timestamps();
        });
    }

こちらで試したところ、問題なくエラーが解消されました!

おわりに

integerは第二引数を使用できない。とは書かれておらず、stringと同じ要領で使用してしまいましたが、改めて、公式サイトをきちんと読むことの大切さを思い知らされました。

同じようなエラーに悩まされている方のお力になれたら嬉しいです!

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

twigファイルで変数にHTMLタグを入れる方法

.twig
{{ link }}
.php
 //省略
 $this->view->var['link'] = '<a href="">リンク</a>';

こんな感じでtwigファイルにlinkと記述した変数にHTMLタグを入れてリンクを出現させたかったのだが、実際にはHTMLタグがそのままテキストとして表示された。

解決法

.twig
{{ link|raw }}

これでエスケープされずにすみました。

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

ロリポップ!からケチケチバックアップする話

0.初めに

私は、非エンジニアのドシロウトです。しかも貧乏(汗)です。

元々、無料レンタルサーバーのXFREEで、とあるPHP製のオンラインメモの保管ツールで個人的なメモを保存し、テスト利用していました。

無料レンタルサーバー【XFREE(エックスフリー)】

自分専用のオンラインメモツールは、意外と便利だったので、本格的に使って行こうと考えました。

XFREEの無料版は容量が1GB、SSL非対応で少しレスポンスも遅いときがあり、 格安のレンタルサーバーを探しました。

探した結果、結局、有名なロリポップ!レンタルサーバーで一番安いエコノミープランを試すことにしました。

ご利用料金 - レンタルサーバーならロリポップ!

エコノミーでも初期費用が1500円かかりますが、月額は100円で格安です。

エコノミープランは、容量20GB、SSL対応、PHPも使えますが、バックアップをするオプションを入れると月300円追加で必要です。

バックアップ / 機能一覧 / サービス - レンタルサーバーならロリポップ!

貧乏な私は、Google Apps Script(GAS)を使って、なんとかバックアップできないか、試すことにしました。

1.試したこと

試したことをまとめると以下です。

  • ダウンロード用PHPツール

    ロリポップ!にあるメモ保存ディレクトリを丸ごとZIPにしてダウンロードするPHPツールの設置

  • 起動/保存用GASツール

    PHPツールをHTTPベースで起動し、ダウンロードしたZIPを、Google Driveに1世代保存するGASツールの作成、トリガー設定

2.ダウンロード用PHPツール

以下の記事の「PHPでzipコマンドを使ってディレクトリを丸ごと圧縮する」のPHPプログラムを丸パクリしています。

PHPでディレクトリを丸ごとZIP圧縮する方法の速度比較

ZIPを一時保存するディレクトリを新たにロリポップ!に作ったのと以下のPHPプログラムを設置しただけです。

注)getZipのパラメタは、フルパスでの指定が必要ですが、ロリポップ!の管理画面の「ユーザー設定>アカウント情報」で分かります。

down.php
<?php
ini_set('display_errors', "On");
//$dir 取得したいフォルダパス
//$zipFileSavePath 一時、zipを保存しておくフォルダパス
function getZip($dir,$zipFileSavePath){

 // zipファイル名
    $fileName = "zipFile".time();
// 圧縮対象フォルダ
    $compressDir = $dir;

// コマンド
// cd:ディレクトリの移動
// zip:zipファイルの作成
    $command =  "cd ". $compressDir .";".
        "zip -r ". $zipFileSavePath . $fileName .".zip .";

// Linuxコマンドの実行
    exec($command);

// 圧縮したファイルをダウンロードさせる。
    header('Pragma: public');
    header("Content-Type: application/octet-stream");
    header("Content-Disposition: attachment; filename=".$fileName.".zip");
    readfile($zipFileSavePath.$fileName.".zip");

//    消す
    unlink($zipFileSavePath.$fileName.".zip");
}

getZip('/home/users/1/aaaa.jp-bbbbb/web/(保存ディレクトリ)','/home/users/1/aaaa.jp-bbbbb/web/(ZIP一時ディレクトリ)');

?> 

3.起動/保存用GASツール

以下の記事を参考にGASを作成しました。

GASを使ってGoogle Drive内にあるファイルのダウンロードURLからコンテンツを取得したい - Qiita

gasのプログラムが以下でこれを1日1回実行するようにトリガー設定しています。

zipdown.gs
function myFunction() {

    // バックアップファイル名 ※lolipop-down.zipを使ってます
    var svrfilenamearray = [ "lolipop-down.zip",
                             "lolipop-downtest.zip" ];

    // ドライブのファイル名取得        
    var filesdel = DriveApp.getFiles();


    // ファイルがあるだけ繰り返す
    while (filesdel.hasNext()) {
      var filed = filesdel.next();
      // バックアップファイル名かチェック
      for(var i = 0; i < svrfilenamearray.length; i++) {
        if (filed.getName() == svrfilenamearray[i]) { 
          // 古いバックアップファイル削除処理
          filed.setTrashed(true);
          Logger.log(filed.getName()+" バックアップファイル削除終了");
        };
      };
    };


  // ロリポップ!のダウンロード用PHPツールのURLを発行して受信
  var blob = UrlFetchApp.fetch('https://(ロリポップ!のサイト)/down.php', {muteHttpExceptions: true}).getBlob();
  // Google Driveに保存
  DriveApp.createFile(blob.setName('lolipop-down.zip'));
}

4.終わりに

貧乏でなければ、普通にバックアップオプションを使った方が良いと思います。

牛丼一杯の値段ですので…

以 上

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

【Laravel】リクエストバリデーションをコントローラから分離させる!

コントローラーでのバリデーション処理の書き方

laravelでは以下のようにバリデーションを設定することができます。

SampleController.php
public function store(Request $request) 
{
    // validation ruleを作成
    $rules = [
        'name' => ['required', 'string'],
        'user_id' => ['required', 'integer'],
        'content_id' => ['sometimes', 'integer'] 
    ]

    $this->validate($request, $rules);

    // メイン処理へと続く
}

みたいな感じで書くことでバリデーションを行ってくれます。
各パラメータに対応する形でルールを書いていくことで一つ一つ細かくバリデーションすることができます。
細かいルールに関しては公式ドキュメント参照→https://readouble.com/laravel/5.8/ja/validation.html

バリデーションを通過すれば後続処理へと渡し、通過できなかった時は適切なレスポンスを生成して前のページへと戻してくれます。

バリデーション処理はコントローラーから分離させたい

上記のようにコントローラーやサービスクラスの中でバリデーションを記述しても良いですが、なるべくコントローラに書くコードは少なくした方がコード的にはイケてますよね。

また、バリデーションだけ分離すれば可読性が上がり、使い回しもしやすくなります。
具体的には下記のように書くことでバリデーション処理をコントローラーから分離させることができます。

まずはRequestClassを作成しましょう。下記のarstisanコマンドを打つとRequestクラスを継承したクラスを作成することができます。

php artisan make:request SampleRequest
SampleRequest.php
namespace App\Http\Requests;

use Illuminate\Http\Request;
use Illuminate\Foundation\Http\FormRequest;

class SampleRequest extends FormRequest
{
     /**
     * Determine if the user is authorized to make this request.
     *
     * @return bool
     */
    public function authorize()
    {
        // 初期の状態ではfalseを返すようになっているため、機能させたい場合はtrueに変更してください
        return true;
    }

    /**
     * Get the validation rules that apply to the request.
     *
     * @return array
     */
    public function rules()
    {
        return [
          'name' => ['required', 'string'],
          'user_id' => ['required', 'integer'],
          'content_id' => ['sometimes', 'integer'] 
        ];
    }
}

コントローラー側ではこのSampleRequestクラスを通過するように書いてあげれば元のコードと同じようにバリデーションをかけることができます。

SampleController.php
public function store(SampleRequest $request) 
{
    // メイン処理へと続く
}

これでコントローラーからリクエストバリデーションのロジックを分離させることができました。

複雑なバリデーション処理の指定の仕方

リクエストバリデーションクラスを作ったことで、複雑なバリデーションを書いてもコントローラの幅をとることはありません。

バリデーションクラスでの複雑な処理の書き方を紹介していきます。

SampleRequest.php
namespace App\Http\Requests;

use Illuminate\Http\Request;
use Illuminate\Foundation\Http\FormRequest;

class SampleRequest extends FormRequest
{
     /**
     * Determine if the user is authorized to make this request.
     *
     * @return bool
     */
    public function authorize()
    {
        // 初期の状態ではfalseを返すようになっているため、機能させたい場合はtrueに変更してください
        return true;
    }

    /**
     * Get the validation rules that apply to the request.
     *
     * @return array
     */
    public function rules()
    {
        return [
          'name' => ['required', 'string'],
          'user_id' => ['required', 'integer'],
          'content_id' => ['sometimes', 'integer'] 
        ];
    }

   // 下記のような書き方で、それぞれのバリデーションに対するメッセージのカスタマイズが設定できます。
    public function message()
    {
        return [
            'name.required' => '名前を入力してください',
            'name.string' => '名前は文字列で入力してください',
            'user_id.required' => 'ユーザーIDを入力してください'
        ]
    }
}

   // 下記のように書くことでバリデーションに通す前のデータを加工することができます。
    protected function validationData()
    {
        $data = $this->all();

        // 文字を切り取るなどの前処理
        $data['name'] = substr($data['name'], 0, -1);

        return data;
    }

より複雑なルールはRulesクラスに切り出そう

もっと細かなオリジナルルールを適用したい場合はRulesクラスを作成するとすっきり書くことができます。

SampleRules.php
<?php

namespace App\Rules;

use Illuminate\Contracts\Validation\Rule;

class SampleRule implements Rule
{
    /**
     * Determine if the validation rule passes.
     *
     * @param  string  $attribute
     * @param  mixed  $value
     * @return bool
     */
    public function passes($attribute, $value)
    {
        // オリジナルルール
        // $valueとすることで引数で渡した値を参照することができます。
    }

Rulesは以下のようにリクエストクラスから呼び出します。

SampleRequest.php
 public function rules(Request $request)
    {
        $useId = $request->input('user_id');

        return [
          'name' => ['required', 'string'],
          'user_id' => ['required', 'integer', new SampleRule($userId)],
          'content_id' => ['sometimes', 'integer'] 
        ];
    }

rulesクラスに渡す値は$useIdのように、一度変数に持たせてから、渡しています。

とはいえlaravelには様々なバリデーションを指定できるようになっているため、オリジナルのRuleを使う場面はなかなかないのかなと思っています。
実装したいルールは公式ドキュメントを読めば意外と用意されていたりするかもしれません。

読んでいただきありがとうございました。

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

FormRequestで自作のファイル拡張子バリデーションを作ってみた

環境

Laravel 6.20.8
PHP7.4

はじめに

コントローラーにもバリデーションは書けますが、複雑なバリデーションになればなるほど、コントローラーが肥大化してしまうので、FormRequestクラスを活用します。
FormRequestに自作でバリデーションをどのように書くのか、画像ファイルの拡張子チェックを例にします。

Laravelのバリデーションルールは豊富なので、rulesメソッドにmimesを指定してしまえば、アップロードしようとしているファイルのMIMEタイプはチェックしてくれます。ですが、MIMEタイプも改ざんできてしまうので、同時に拡張子のチェックもしたいと思います。

準備

下記コマンドで、App\Http\Requests配下にFormRequestクラスを生成します。

php artisan make:request CreateItemRequest

すると、下記のようなファイルが作られます。authorize()は、認証関係の判定の処理を書きますが、今回は不要なのでtrueを返すようにします。バリデーションはrules()に書いていき、前述の通りmimesを指定します。(今回はjpgとpngのみにします)

app/Http/Requests/CreateItemRequest.php
<?php

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;

class CreateItemRequest extends FormRequest
{
    /**
     * Determine if the user is authorized to make this request.
     *
     * @return bool
     */
    public function authorize()
    {
        return true;
    }

    /**
     * Get the validation rules that apply to the request.
     *
     * @return array
     */
    public function rules() 
    {
        'name' => 'required|string|max:255',
        'picture_name' => 'required|file|mimes:jpg,png', 
    }

}

コントローラーの変更点は2点です。作成したFormRequestクラスをuseして、store()などPOST送信を受けるメソッドの引数に、作成したcreateItemクラスを渡すだけです。

ItemController.php
//中略

use App\Http\Requests\CreateItemRequest;

//中略

public function store(CreateItemRequest $request)
{
    //
}

自作で拡張子バリデーションを作成する

下記にバリデーションを書いていきます。withValidatorメソッドは、rules()で指定したバリデーション評価前に実行されるようです。

app/Http/Requests/CreateItemRequest.php
public function withValidator($validator)
{
    //ここにバリデーションを書いていきます
}

app/Http/Requests/CreateItemRequest.php
public function withValidator($validator)
{
    $validator->after(function ($validator)
    {
        $file_data = $this->file('picture_name');
        $file_extension = $file_data->getClientOriginalExtension(); //拡張子取得
        $lower_case_extension = strtolower($file_extension); //拡張子を小文字に変換

        if($lower_case_extension !== 'jpg' && $lower_case_extension !== 'png'){
            $validator->errors()->add('', 'アップロードされたファイルは画像ファイルではありません。');
        }
    });
}

拡張子は、大文字でも小文字でもチェックできるよう、一度拡張子を小文字に変換します。エラー時のメッセージは直接書きます。

最終的なファイル

app/Http/Requests/CreateItemRequest.php
<?php

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;

class CreateItemRequest extends FormRequest
{
    /**
     * Determine if the user is authorized to make this request.
     *
     * @return bool
     */
    public function authorize()
    {
        return true;
    }

    /**
     * Get the validation rules that apply to the request.
     *
     * @return array
     */
    public function rules() 
    {
        'name' => 'required|string|max:255',
        'picture_name' => 'required|file|mimes:jpg,png', 
    }

    public function withValidator($validator)
   {
        $validator->after(function ($validator)
        {
            $file_data = $this->file('picture_name');
            $file_extension = $file_data->getClientOriginalExtension(); //拡張子取得
            $lower_case_extension = strtolower($file_extension); //拡張子を小文字に変換

            if($lower_case_extension !== 'jpg' && $lower_case_extension !== 'png'){
                $validator->errors()->add('', 'アップロードされたファイルは画像ファイルではありません。');
            }
        });
    }

}

おわりに

もともとLaravelが用意してくれているバリデーションルールを使えば、かなりのバリデーションができると思いますが、独自のバリデーションが必要なこともあると思うので、withValidatorメソッドの使い方のメモを残しました。

参考

https://readouble.com/laravel/6.x/ja/validation.html
https://www.ritolab.com/entry/41

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

個人的phpenv installの備忘録

システムにPHPを入れたくない!

開発機というかいつも使う環境にシステムのパッケージマネージャでPHPをインストールして、システムを汚染するのは愚の骨頂だと考えているので、できるだけphpenvを使ってユーザランドだけで完結したい。

phpenvの導入

anyenvで楽をしましょう
https://github.com/anyenv/anyenv

phpのビルド

開発環境なのでとりあえずdevelopmentで入れる。
あとビルドするときに並列処理をしてもらいたいのでmakeの変数も指定しておく。

export PHP_BUILD_EXTRA_MAKE_ARGUMENTS="-j $(nproc)"
phpenv install -i development -v [任意のバージョン]

以上!

いかがでしたか?(←なんか入れなければならない気がした)

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

【PHP】mb_substr()は遅い

環境

PHP:7.1.21

内容

下記のような処理がありました(簡素化してます)。

入力された文字列を、mb_substr() で1文字ずつ切り取ってチェックしていました。
処理速度に問題はありませんでした。

//住所・氏名など
$src = '入力された文字列';

$count = mb_strlen($src);

//1文字ずつチェック
for ($i=0; $i<$count; $i++) {
    $moji = mb_substr($src,$i,1);
}

ある日、上記処理に、100KB以上の文字列を渡しました。
するとタイムアウトが発生しました。

Fatal error: Maximum execution time of xx seconds exceeded in /foo/bar/xxxx.php on line xxx

何でそんなに時間かかるの? と思いましたが、
原因を推測すると、こういうことなんじゃないかと。

mb_substr() は何かしら文字コード変換をおこなうのですが、
「1文字 切り取って文字コード変換」ではなくて、
「文字列全部をコード変換して、1文字 切り取る」なのでしょう。

つまり、上記処理にマルチバイトの 100,000文字が渡されたら、

100,000文字 x 100,000ループ = 10,000,000,000文字

100億文字のコード変換をおこなうことになります(推測ですが)。
(100億文字は Shift_JIS だと約20GBかな?)

文字数の2乗に比例して遅くなる計算です。
そりゃタイムアウトになります :sob:

ちなみに上記処理の mb_substr() を substr() にすると、一瞬で完了します。
やはりマルチバイトの処理で時間がかかっているようです。

さいごに

mb_substr() が遅いというより、私の使い方に問題があるとも言えます。

ちなみに100KB以上の文字列を、分割して数回に分けて渡したら、
タイムアウトはしなくなりました。

参考

mb_substrが遅い

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

3分で実装できる Laravelでユーザ編集、退会機能(忙しい人向け)

前書き

useredit/laraveluiのレポジトリ
当ライブラリはMITライセンスです
laravel6系にて動作確認しております。
機能を追加したい場合はご自身でソースコード読んでください

laravel6 パッケージ開発ハンズオン(パッケージを作ったことない人向け)で同様の機能パッケージ開発ハンズオンあるので時間があれば是非

前提条件

laravel/uiをインストール
php artisan ui vue --authでUIを実装

インストール方法

プロジェクト直下で以下コマンドを実行

composer require useredit/laravelui

インストール完了後下記コマンドで各種controllerやRequestを実装

php artisan useredit

http://ドメイン名(localhost等)/userにアクセスし以下画面が出力できれば完成です

スクリーンショット 2021-01-08 10.39.24.png

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

Laravel6 独自のリクエストクラスを作成してpostしたらエラーが出た

目的

  • Laravel6のアプリでバリデーションをリクエストクラスに記載するために独自のリクエストクラスを作成してデータをpostしたらエラーが出たので回避方法をまとめておく

環境

  • ハードウェア環境
項目 情報
OS macOS Catalina(10.15.5)
ハードウェア MacBook Pro (13-inch, 2020, Four Thunderbolt 3 ports)
プロセッサ 2 GHz クアッドコアIntel Core i5
メモリ 32 GB 3733 MHz LPDDR4
グラフィックス Intel Iris Plus Graphics 1536 MB
  • ソフトウェア環境
項目 情報 備考
PHP バージョン 7.4.11 Homebrewを用いてこちらの方法で導入→Mac HomebrewでPHPをインストールする
Laravel バージョン 6.X commposerを用いてこちらの方法で導入→Mac Laravelの環境構築を行う
MySQLバージョン 8.0.21 for osx10.15 on x86_64 Homwbrewを用いてこちらの方法で導入→Mac HomebrewでMySQLをインストールする

情報

  • 本エラーはAuthの認証機能を付与しているLaravelアプリだと発生するらしい。例にもれずエラーが出た筆者のアプリもAuthによる認証機能が付与されている。

経緯

  1. 下記コマンドを実行してリクエストクラスを作成した。

    $ php artisan make:request ContentRequest
    
  2. 作成されたリクエストクラスのファイルを下記のように記載した。

    アプリ名ディレクトリ/app/Http/Requests/ContentRequest.php
    <?php
    
    namespace App\Http\Requests;
    
    use Illuminate\Foundation\Http\FormRequest;
    
    class ContentRequest extends FormRequest
    {
        /**
         * Determine if the user is authorized to make this request.
         *
         * @return bool
         */
        public function authorize()
        {
            return false;
        }
    
        /**
         * Get the validation rules that apply to the request.
         *
         * @return array
         */
        public function rules()
        {
            return [
                'content' => 'required',
            ];
        }
    }
    
  3. データがpostされるコントローラのメソッド(アクション)の引数にContentRequest $contentRequestを追加してデータをpostした。

エラー

  • 「403 This action is unauthorized.」というエラーが発生した。

    Forbidden.png

解決策

  1. 作成したリクエストクラスのauthorize()メソッド内部を下記のように書き換える。

    アプリ名ディレクトリ/app/Http/Requests/ContentRequest.php
    <?php
    
    namespace App\Http\Requests;
    
    use Illuminate\Foundation\Http\FormRequest;
    
    class ContentRequest extends FormRequest
    {
        /**
         * Determine if the user is authorized to make this request.
         *
         * @return bool
         */
        public function authorize()
        {
            //下記を修正する
            return true;
        }
    
        /**
         * Get the validation rules that apply to the request.
         *
         * @return array
         */
        public function rules()
        {
            return [
                'content' => 'required',
            ];
        }
    }
    
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【PHP】Log出力 in GKE

GKEでログ出力をしようとしてはまったのでメモを残しておく。
なお、標準出力ではなく、ログエクスプローラでの閲覧を前提としています。

構成

k8s on gke
nginx → php(php7.4.3)

実装

前段階:Monologで標準出力

*こちらの方法は問題がありましたので前段階とします。

use Monolog\Formatter\LineFormatter;
use Monolog\Handler\StreamHandler;
use Monolog\Logger;

// --- 省略

        $logger = new Logger($name);
        $formatter = new LineFormatter(null, null, true, true);

        $sh = new StreamHandler('php://stdout', Logger::DEBUG);
        $sh->setFormatter($formatter);
        $logger->pushHandler($sh);

        $sh = new StreamHandler('php://stderr', Logger::ERROR);
        $sh->setFormatter($formatter);
        $logger->pushHandler($sh);

        $logger->debug('なんか内容');

// --- 省略

こちらの方法では、ログ出力はできたものの、すべてのログが severity:ERROR になってしまった。本来infoのログもERRORででるので、もはや訳が分からない状態。

{
textPayload: "なんか内容"
insertId: "省略"
resource: {
type: "k8s_container"
labels: {6}
  # pod名とか入ってる
}
timestamp: "省略"
severity: "ERROR"
labels: {3}
logName: "projects/${PROJECT_id}/logs/stderr"
receiveTimestamp: "省略"
}

標準ライブラリ使用

上記の方法では適切に表示ができなかったので調べたところ、ログのformatをがっちがちに設定するか、標準ライブラリを使えとのこと。
今回は後者の方法でやってみることとした。

use Google\Cloud\Logging\LoggingClient;
use Google\Cloud\Logging\PsrLogger;

// --- 省略
  $name = 'test';
  $logger = LoggingClient::psrBatchLogger($name);

  $logger->info('なんか内容'); 
// --- 省略

なんやこれ、くっそかんたんやんけ、と思ったのものの、ロギングに出ない。
ちなみにログエクスプローラ上では以下のクエリで検索をかけていた。

resource.type="k8s_container"
resource.labels.namespace_name="namespace_name"
resource.labels.cluster_name="cluster_name"
resource.labels.container_name="container_name"

よくよく調べてみたところ、psrBatchLoggerは名前だけ指定すると、resource.type="global"
に設定されるようだった。

https://stackoverflow.com/questions/45022651/logging-using-stackdriver-api-on-kubernetes-google-container-engine-gke/45085569#45085569

クラウドコンソール上で以下のコマンドを試すと目的のログがいた。
```sh
gcloud beta logging logs list

--- 省略
projects/${PROJECT_ID}/logs/test
--- 省略
```

gcloud beta logging read projects/<PROJECT>/logs/<LOG_NAME>
こちらで内容を確認すると

insertId: "省略"  
 jsonPayload: {}  # 内容入ってる
 logName: "projects/${PROJECT_ID}/logs/test"  
 receiveTimestamp: "省略"  
 resource: {
  labels: {
   project_id: ${PROJECT_ID}    
  }
  type: "global"   
 }
 severity: "INFO"  
 timestamp: 省略  
}

severity:INFOになっているものの、typeがglobalなのはちょといただけない。

設定をきちんとしてみる

use Google\Cloud\Logging\LoggingClient;
use Google\Cloud\Logging\PsrLogger;

// --- 省略
        $logger = LoggingClient::psrBatchLogger(
            $name,
            [
                'resource' => [
                    'type' => 'k8s_container',
                    'labels' => [
                        'project_id' => ${PROJECT_ID},
                        'cluster_name' => ${CLUSTER_NAME},
                        'container_name' => ${CONTAINER_NAME},
                        'namespace_name' => ${NAMESPACE_NAME}
                    ]
                ]
            ]
        );

        $logger->info('なんか内容');
// ---省略

こちらの設定方法でログエクスプローラに以下のクエリを指定したところ

resource.type="k8s_container"
resource.labels.namespace_name="namespace_name"
resource.labels.cluster_name="cluster_name"
resource.labels.container_name="container_name"

以下のログが出力できた。

{
insertId: "省略"
jsonPayload: {
message: "なんか内容"
}
resource: {
type: "k8s_container"
labels: {
namespace_name: "namespace_name"
container_name: "conteainer_name"
project_id: "${PROJECT_ID}"
pod_name: ""
location: ""
cluster_name: "cluster_name"
}
}
timestamp: "省略"
severity: "INFO"
logName: "projects/${PROJECT_ID}/logs/name"
receiveTimestamp: "省略"
}

と狙い通りに出力することができた。

*標準出力に出していた際には、pod_nameやlocationなども自動で入力されていたが、今回は不要と判断し、設定していない。
*logNameに / をもちいて階層化を狙ったが、そうすると全く出力されなくなってしまった
. で区切るのが今のところベターと思われる。

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

�【未経験・転職活動中】PHPでインスタ風Webアプリを作りました(ポートフォリオ)

初めまして。たけもとまりなと申します。

私は現在未経験からのWebエンジニア転職を目指して活動中であり、
自身の学習成果を公開することを目的にこの記事を書いています。
企業の採用担当者の方にこの記事をご覧頂きたいのはもちろん、
同じ境遇の方にもご参考頂けましたら大変嬉しく思います。

今回は私が初めて作成したWebアプリを紹介したいと思います。
開発したのは、ペットに特化したインスタグラム風の写真投稿・閲覧アプリです。

ここをクリックすると実際のアプリのページにジャンプします

開発のきっかけ

私は動物が好きで、ツイッターやyoutubeで動物の画像や動画を閲覧することが多いのですが、以下の不満がありました。

  • 動物の投稿だけをまとめて見たい
  • お気に入りのペットの投稿はまとめてチェックしたい
  • できれば苦手な動物の画像は避けたい(個人的に爬虫類が苦手なので)

そこで、これらの要件を満たすWebアプリを開発することにしました。

使用言語・データベース・ツール・プラグインなど

  • HTML/CSS
  • PHP
  • Javascript
  • jQuery(いいねボタンをAjaxで実装するために使用)
  • MySQL
  • Git/GitHub
  • phpMyAdmin
  • VSCode
  • MAMP(Mac)
  • XAMPP(Windows)

※GitHubに慣れたかったため、所有しているPC2台を使ってpushしたりpullしたり、いろいろやっていました。WindowsとMacだったため、MAMPとXAMPPの両方を駆使しました。

開発したアプリの動作

1.会員登録

最初にe-mailアドレスとパスワードを入力

ニックネームや好きな動物の種類を登録(ここで登録した種類の動物の投稿のみが表示されるようになります。)

自分のペットを最大5匹まで登録(ペットがいなくても会員登録はできるようになっています。ペット登録していない動物の写真の投稿も可能ですが、ペットの登録をしないと他のユーザーからお気に入り登録はして貰えません。)
※登録内容はいつでも変更可能です(e-mailアドレス以外)

2.ログイン/ログインアウト時の表示の違い

ログインされていない状態の時は以下の画面表示になります。
好きな動物の種類が登録されていないので、全ての投稿が表示されます。
unlogined.jpeg
ログインした状態では、メニューバー(NEW/LIKE/MYPAGE)が表示されます。NEWでは、予め登録した種類の動物の投稿のみが表示されます。
logined.jpeg
また、ログイン/ログアウトいずれの場合でも、投稿をクリックすると「飼い主・投稿年月日・コメント」などの投稿に対する詳細を見ることができます。また、同じペットの他の投稿や同じ飼い主の他のペットの投稿もまとめて楽しむことができます。

3.新規投稿機能(※ログイン時)

新しくペットの写真を投稿することができます。特に写真の投稿・閲覧を目的としたアプリのため、写真は必須、コメントは任意入力項目にしています。投稿時には、どのペットの投稿か(もしくはペット以外か)を選択できるようになっています。

4.いいね機能(※ログイン時のみ)

それぞれの投稿に対して「いいね」することができます。「いいね」した投稿は、別ページでまとめて閲覧可能です(後述)。

5.お気に入り登録機能(※ログイン時のみ)

それぞれのペットをお気に入り登録することができます。お気に入り登録したペットの投稿は、別ページでまとめて閲覧可能です(後述)。

6.LIKEページ(※ログイン時のみ)

「いいね」した投稿とお気に入り登録したペットの投稿のみがまとめて表示されます。お気に入り登録したペットはそれぞれ最新の投稿が4つ表示され、最新の投稿は吹き出し内にコメントや投稿日時も表示されます。また、複数のペットをお気に入り登録した場合には、最新投稿日が新しいペットの順に上から表示されるようになっています。

favorite.gif

7.MYPAGEページ(※ログイン時のみ)

自分の投稿がペット毎に分かれて表示されます。最新の投稿は吹き出し内にコメントや投稿日時も表示されます。また、他のユーザーからのお気に入り登録数も分かるようになっています。

8.ログアウト機能・会員情報変更など(※ログイン時のみ)

ログイン状態ではハンバーガーメニューが表示され、ここをクリックするとログアウトや会員情報の変更などの処理をすることができます。

要件は満たしたか

私がこのアプリを開発するに当たって、必ず実装したかった機能は以下の通りでした。

  • 動物の投稿だけをまとめて見たい →動物の画像投稿の専用アプリを開発した!
  • お気に入りのペットの投稿はまとめてチェックしたい →気に入った投稿やお気に入りのペットを登録し、それらをまとめて表示するページを作成した!
  • できれば苦手な動物の画像は避けたい →見たい動物の種類を登録できるようにした。また、画像の新規投稿時にどのペットか選択して貰うことで、投稿された画像がどの種類の動物かを認識できるようにした。結果的に、見たい動物の投稿のみを閲覧可能にした!

また、開発するうちに以下の機能を追加で実装しました。

  • ペット以外の画像も投稿できる ←ペットは飼っていないけど動物好きのユーザーにも楽しんで貰えるように。また、自分のペット以外でも投稿したい動物の画像があると思ったため。
  • 自分のペットの投稿をペット毎にまとめて閲覧できる ←複数のペットを飼っているユーザーはペット毎に投稿が分けられた方が便利だと思ったため。
今後実装したい機能

投稿の「いいね」の数やペットのお気に入り登録数を期間毎に集計し、「今話題の投稿」や「今話題のペット」をランキング形式で表示する機能を実装したら面白いかなと思っています。

開発期間

約2か月半
平均すると平日は就業前後の約2~3時間程度、休日は5~8時間程度取り組んでいました。初めての開発ということもあり、手探り状態でいろいろ調べながらの開発だったのでかなり時間がかかってしまった印象ですが、大変実りの多い2か月半だったと感じています。データベースの設計では、正規化や主キー・外部キーなど応用情報の勉強で得た知識が大いに役立ちました。

最後に

後日、難しかった部分の実装方法や本番サーバー(ロリポップサーバーを使用しています)への移行方法について、別記事をアップしたいと思います。同じ志をもって学習に取り組んでいる皆様に、少しでもご参考頂けましたら大変嬉しく思います。
初めてのQiita投稿でおかしな表現等あるかもしれませんが、その際はご指摘頂けましたら幸いです。
以上、たけもとまりなでした。

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