20190226のPHPに関する記事は15件です。

Laravel $user->posts()->get() と $user->posts の違いを理解する

環境

  • PHP 7.2
  • Laravel 5.6

前提

  • ブログサービスを想定、Userが複数のPost(投稿)を持つ(1対多関係)
  • Userテーブルはid, name, email, password, created_at, updated_atカラムを持つ
  • Postテーブルはid, content, created_at, updated_atカラムを持つ

リレーションメソッドと動的プロパティ

$user = User::find($id); // ユーザを取得
$posts = $user->posts()->get(); // ユーザが持つ投稿一覧を取得

2行目は user->posts と書いても同じ結果が得られる。

https://readouble.com/laravel/5.6/ja/eloquent-relationships.html

リレーションが定義できたらEloquentの動的プロパティを使って、関係したレコードを取得できます。動的プロパティによりモデル上のプロパティのようにリレーションメソッドにアクセスできます。

ドキュメントにならって $user->posts()->get() をリレーションメソッドを扱ったアクセス、 $user->posts を動的プロパティを扱ったアクセスと呼ぶことにする。

筆者はてっきり動的プロパティの記法はリレーションメソッドの省略形と思っていた。

しかしこの後の検証で動作上の違いを認識することができた。

LaravelのREPL環境である php artisan tinker を実行する。

>>> $user = User::find(5)
=> App\User {#2952
     id: 5,
     name: "test",
     created_at: "2019-02-26 20:12:50",
     updated_at: "2019-02-26 20:12:50",
   }
>>> $user
=> App\User {#2952
     id: 5,
     name: "test",
     created_at: "2019-02-26 20:12:50",
     updated_at: "2019-02-26 20:12:50",
   }
>>> $user->posts()->get()
=> Illuminate\Database\Eloquent\Collection {#2958
     all: [
       App\Post {#2950
         id: 67,
         content: "test",
         user_id: 5,
         created_at: "2019-02-26 20:13:14",
         updated_at: "2019-02-26 20:13:14",
       },
     ],
   }
>>> $user
=> App\User {#2952
     id: 5,
     name: "test",
     created_at: "2019-02-26 20:12:50",
     updated_at: "2019-02-26 20:12:50",
   }
>>> $user->posts
=> Illuminate\Database\Eloquent\Collection {#2955
     all: [
       App\Post {#2942
         id: 67,
         content: "test",
         user_id: 5,
         created_at: "2019-02-26 20:13:14",
         updated_at: "2019-02-26 20:13:14",
       },
     ],
   }
>>> $user
=> App\User {#2952
     id: 5,
     name: "test",
     created_at: "2019-02-26 20:12:50",
     updated_at: "2019-02-26 20:12:50",
     posts: Illuminate\Database\Eloquent\Collection {#2955
       all: [
         App\Post {#2942
           id: 67,
           content: "test",
           user_id: 5,
           created_at: "2019-02-26 20:13:14",
           updated_at: "2019-02-26 20:13:14",
         },
       ],
     },
   }

リレーションメソッドはレコードの結果を返す。
動的プロパティもレコードの結果を返す。
違いは、動的プロパティを使った場合はレシーバである $user インスタンスに posts プロパティが追加される点。

つまり、 $user->posts を実行すると $userposts 情報がキャッシュされる。
これ以降、 $user->posts を実行した場合は、クエリを発行せずにキャッシュされたデータを返すようだ。

遅延ロード

この挙動は遅延ローディングとその目的であるN+1問題の解消のために存在しているようだ。

動的プロパティは「遅延ロード」されます。つまり実際にアクセスされた時にだけそのリレーションのデータはロードされます。そのため開発者は多くの場合にEagerローディングを使い、モデルをロードした後にアクセスするリレーションを前もってロードしておきます。Eagerロードはモデルのリレーションをロードするため実行されるSQLクエリを大幅に減らしてくれます。

ドキュメントではAuthorとBookの例で説明されている。

$books = App\Book::with('author')->get();

foreach ($books as $book) {
    echo $book->author->name;
}

with でN+1を防止できることは知っていたが、そちらばかりに注目がいっており動的プロパティは雑な理解に留まってしまっていた。

結論

まとめると下記のようになる。

  • リレーションメソッドによるアクセスは常にクエリ発行する
  • 動的プロパティによるアクセスは、キャッシュデータが存在しなければクエリ発行し、レシーバインスタンスにキャッシュとなるプロパティを生成する。キャッシュが存在すればそれを返すためクエリ発行しないという挙動をする

App\Book::with('author')->get(); で返ってくるBooksコレクションにはその一つ一つにAuthorのデータがキャッシュされている状態になっており、キャッシュデータを利用するには動的プロパティでアクセスしなければならない。

すなわち、

$books = App\Book::with('author')->get();

foreach ($books as $book) {
    echo $book->author()->first()->name; // リレーションメソッドによるアクセス(NGパターン)
}

このようにリレーションメソッドでアクセスすると都度クエリが発行されてしまうため駄目ですよ、遅延ロードの際は動的プロパティを使いましょうという話。

何か誤解があれば指摘頂きたいと思います。

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

stdClassのオブジェクトを作る方法

  1. データベースから取ってくるデータがstdClassの配列だったりします。
  2. そのstdClassの配列を処理するコードを作ったりします。
  3. コードを作るとPHPUnitでテストしたりします。
  4. そうすると期待値や引数がstdClassだったりします。
  5. するとstdClassのオブジェクトを作らねばなりません。
  • 環境
    • macOS Mojave バージョン10.14.3
    • PHP 7.3.1
    • Laravel Framework 5.7.26

方法1. 配列をオブジェクトでキャストする

controlStdClassArray->createStdClassArrayCast
public function createStdClassArrayCast(): array
{
    $stdArray = [
        (object)['name' => 'イタリアンパセリ', 'otherName' => 'パースレー', 'gakumei' => 'Petroselinum neapolitanum', 'bunrui' => 'セリ科オランダゼリ属'],
        (object)['name' => 'グラジオラス', 'otherName' => '唐菖蒲', 'gakumei' => 'Gladiolus', 'bunrui' => 'アヤメ科グラジオラス属'],
        (object)['name' => 'アボカド', 'otherName' => 'ワニナシ', 'gakumei' => 'Persea americana', 'bunrui' => 'クスノキ科ワニナシ属'],
        (object)['name' => 'マロウ', 'otherName' => 'ウスベニアオイ', 'gakumei' => 'Malva sylvestris', 'bunrui' => 'アオイ科ゼニアオイ属'],
    ];
    return $stdArray;
}
$ php controlStdClassArray.php 
array(4) {
  [0]=>
  object(stdClass)#2 (4) {
    ["name"]=>
    string(24) "イタリアンパセリ"
    ["otherName"]=>
    string(15) "パースレー"
    ["gakumei"]=>
    string(25) "Petroselinum neapolitanum"
    ["bunrui"]=>
    string(30) "セリ科オランダゼリ属"
  }
  [1]=>
  object(stdClass)#3 (4) {
    ["name"]=>
    string(18) "グラジオラス"
    ["otherName"]=>
    string(9) "唐菖蒲"
    ["gakumei"]=>
    string(9) "Gladiolus"
    ["bunrui"]=>
    string(33) "アヤメ科グラジオラス属"
  }
  [2]=>
  object(stdClass)#4 (4) {
    ["name"]=>
    string(12) "アボカド"
    ["otherName"]=>
    string(12) "ワニナシ"
    ["gakumei"]=>
    string(16) "Persea americana"
    ["bunrui"]=>
    string(30) "クスノキ科ワニナシ属"
  }
  [3]=>
  object(stdClass)#5 (4) {
    ["name"]=>
    string(9) "マロウ"
    ["otherName"]=>
    string(21) "ウスベニアオイ"
    ["gakumei"]=>
    string(16) "Malva sylvestris"
    ["bunrui"]=>
    string(30) "アオイ科ゼニアオイ属"
  }
}

方法2. new \stdClassをして1つ1つ設定する

controlStdClassArray->createStdClassArrayNew
public function createStdClassArrayNew()
{
    $stdArray = [];

    $stdObj = new \stdClass();
    $stdObj->name = 'イタリアンパセリ';
    $stdObj->otherName = 'パースレー';
    $stdObj->gakumei = 'Petroselinum neapolitanum';
    $stdObj->bunrui = 'セリ科オランダゼリ属';
    $stdArray[] = $stdObj;

    $stdObj = new \stdClass();
    $stdObj->name = 'グラジオラス';
    $stdObj->otherName = '唐菖蒲';
    $stdObj->gakumei = 'Gladiolus';
    $stdObj->bunrui = 'アヤメ科グラジオラス属';
    $stdArray[] = $stdObj;

    $stdObj = new \stdClass();
    $stdObj->name = 'アボカド';
    $stdObj->otherName = 'ワニナシ';
    $stdObj->gakumei = 'Persea americana';
    $stdObj->bunrui = 'クスノキ科ワニナシ属';
    $stdArray[] = $stdObj;

    $stdObj = new \stdClass();
    $stdObj->name = 'マロウ';
    $stdObj->otherName = 'ウスベニアオイ';
    $stdObj->gakumei = 'Malva sylvestris';
    $stdObj->bunrui = 'アオイ科ゼニアオイ属';
    $stdArray[] = $stdObj;

    return $stdArray;
}
$ php controlStdClassArray.php
array(4) {
  [0]=>
  object(stdClass)#2 (4) {
    ["name"]=>
    string(24) "イタリアンパセリ"
    ["otherName"]=>
    string(15) "パースレー"
    ["gakumei"]=>
    string(25) "Petroselinum neapolitanum"
    ["bunrui"]=>
    string(30) "セリ科オランダゼリ属"
  }
  [1]=>
  object(stdClass)#3 (4) {
    ["name"]=>
    string(18) "グラジオラス"
    ["otherName"]=>
    string(9) "唐菖蒲"
    ["gakumei"]=>
    string(9) "Gladiolus"
    ["bunrui"]=>
    string(33) "アヤメ科グラジオラス属"
  }
  [2]=>
  object(stdClass)#4 (4) {
    ["name"]=>
    string(12) "アボカド"
    ["otherName"]=>
    string(12) "ワニナシ"
    ["gakumei"]=>
    string(16) "Persea americana"
    ["bunrui"]=>
    string(30) "クスノキ科ワニナシ属"
  }
  [3]=>
  object(stdClass)#5 (4) {
    ["name"]=>
    string(9) "マロウ"
    ["otherName"]=>
    string(21) "ウスベニアオイ"
    ["gakumei"]=>
    string(16) "Malva sylvestris"
    ["bunrui"]=>
    string(30) "アオイ科ゼニアオイ属"
  }
}

全貌

controlStdClassArray.php
<?php
namespace App;


class controlStdClassArray
{
    public function createStdClassArrayCast(): array
    {
        $stdArray = [
            (object)['name' => 'イタリアンパセリ', 'otherName' => 'パースレー', 'gakumei' => 'Petroselinum neapolitanum', 'bunrui' => 'セリ科オランダゼリ属'],
            (object)['name' => 'グラジオラス', 'otherName' => '唐菖蒲', 'gakumei' => 'Gladiolus', 'bunrui' => 'アヤメ科グラジオラス属'],
            (object)['name' => 'アボカド', 'otherName' => 'ワニナシ', 'gakumei' => 'Persea americana', 'bunrui' => 'クスノキ科ワニナシ属'],
            (object)['name' => 'マロウ', 'otherName' => 'ウスベニアオイ', 'gakumei' => 'Malva sylvestris', 'bunrui' => 'アオイ科ゼニアオイ属'],
        ];
        return $stdArray;
    }

    public function createStdClassArrayNew()
    {
        $stdArray = [];

        $stdObj = new \stdClass();
        $stdObj->name = 'イタリアンパセリ';
        $stdObj->otherName = 'パースレー';
        $stdObj->gakumei = 'Petroselinum neapolitanum';
        $stdObj->bunrui = 'セリ科オランダゼリ属';
        $stdArray[] = $stdObj;

        $stdObj = new \stdClass();
        $stdObj->name = 'グラジオラス';
        $stdObj->otherName = '唐菖蒲';
        $stdObj->gakumei = 'Gladiolus';
        $stdObj->bunrui = 'アヤメ科グラジオラス属';
        $stdArray[] = $stdObj;

        $stdObj = new \stdClass();
        $stdObj->name = 'アボカド';
        $stdObj->otherName = 'ワニナシ';
        $stdObj->gakumei = 'Persea americana';
        $stdObj->bunrui = 'クスノキ科ワニナシ属';
        $stdArray[] = $stdObj;

        $stdObj = new \stdClass();
        $stdObj->name = 'マロウ';
        $stdObj->otherName = 'ウスベニアオイ';
        $stdObj->gakumei = 'Malva sylvestris';
        $stdObj->bunrui = 'アオイ科ゼニアオイ属';
        $stdArray[] = $stdObj;

        return $stdArray;
    }
}

$clas = new controlStdClassArray();
var_dump($clas->createStdClassArrayCast());
var_dump($clas->createStdClassArrayNew());

参考

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

stdClassをnewしたらPHP Warning: Creating default object from empty value inとなったときの対応方法

  • 環境
    • macOS Mojave バージョン10.14.3
    • PHP 7.3.1
    • Laravel Framework 5.7.26

事象 : stdClassをnewしたら警告が出た

controlStdClassArray.php
<?php
namespace App;

class controlStdClassArray
{
    public function createStdClassArrayNew(): array
    {
        $stdobj = new \stdClass();
        $stdobj->name = 'イタリアンパセリ';
        $stdObj->otherName = 'パースレー';
        return $stdobj;
    }
}

$clas = new controlStdClassArray();
var_dump($clas->createStdClassArrayNew());
$ php controlStdClassArray.php 
PHP Warning:  Creating default object from empty value in /path/to/tryPhp/app/controlStdClassArray.php on line 24

Warning: Creating default object from empty value in /path/to/tryPhp/app/controlStdClassArray.php on line 24
object(stdClass)#2 (1) {
  ["name"]=>
  string(24) "イタリアンパセリ"
}

原因 : 変数名の大文字と小文字が間違っているから

恥ずかしい原因ですがかなり気が付かずに調べたので。

PHP: 基本的な事 - Manual
変数名は大文字小文字を区別します。

よって、$stdObj->otherName = 'パースレー';は初期化しないで実行されることとなります。
PHP5.3以降ではstdClassをnew(初期化)しないで実行すると怒られるようになりました。

  • PHP5.3 : PHP Strict Standards: Creating default object from empty value
  • PHP5.4以降 : PHP Warning: Creating default object from empty value

PHP5.4からは即時stdClass生成がWarningエラーを吐くようになった話 | ブログ :: Web notes.log

controlStdClassArray.php
// 省略
        $stdobj = new \stdClass(); // $stdobjの「o」が小文字
        $stdobj->name = 'イタリアンパセリ';
        $stdObj->otherName = 'パースレー'; // $stdobjの「o」が大文字
        return $stdobj;
// 省略

対応 : 同じ変数は大文字小文字を合わせる

controlStdClassArray.php
// 省略
        $stdObj = new \stdClass();
        $stdObj->name = 'イタリアンパセリ';
        $stdObj->otherName = 'パースレー';
        return $stdobj;
// 省略
$ php controlStdClassArray.php 
object(stdClass)#2 (2) {
  ["name"]=>
  string(24) "イタリアンパセリ"
  ["otherName"]=>
  string(15) "パースレー"
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

クラスの継承

未来電子テクノロジー(https://www.miraidenshi-tech.jp/intern-content/program/ )でインターンをしている@hisayamaです。
今回はクラスの継承についてアウトプットします。

クラスの継承

新しいクラスを作る上で、もともとあるメインのクラスのプロパティやメソッドを継承して、それに色々付け加えたい時はクラスの継承を行います。

新しい子クラスはこのように定義します。

class 子クラス extends 親クラス {          }

この親クラスはスーパークラスとも呼び、継承したクラスのことを指します。
このように定義することで、親クラスのコンストラクタやメソッドが呼び出せるようになります。
さらに子クラスでは子クラス独自のプロパティやメソッドを追加できます!

ちなみに子クラスは複数の親クラスを継承できません。1つだけです。
反対に親クラスは複数の子クラスに継承できます。

子クラスのメソッドを呼び出すとします。
もし、子クラスにメソッドが定義されている場合は、子クラスのメソッドが呼び出されます。
一方で定義されていない場合、親クラスのメソッドが呼び出されます。

反対に、子クラスで設定した独自プロパティやメソッドは親クラスからは呼び出せないので注意が必要です!

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

【Laravel5.7】テスト時にHTTPレスポンスからlaravel_sessionが取れず死んだ

laravel_sessionが取れない

Laravelでは、特に設定変更しなければlaravel_sessionというCookie1が発行され、それを使ってセッションを管理します。

テストからlaravel_sessionをチェックしたかったのだけれども、できなかったのでその記録をメモ。
なお解決はしていない。

class FooTest extends TestCase{
    /**
     * なんかリクエストするテスト
     * @return void
     */
    public function testFoobar(){
        $response = $this->get('foo/bar');
        $response->assertStatus(200)->assertCookieNotExpired('laravel_session');
    }
}

なにひとつ失敗する要素がない。

1) Tests\Feature\FooTest::testFoobar
Cookie [laravel_session] not present on response.
Failed asserting that null is not null.

FAILURES!
Tests: 1, Assertions: 2, Failures: 1.

はい。

リクエストは成功しレスポンスコード200が返ってきている。
getContent()でレスポンス本体が取得できるが、それも想定通りの内容で、別のAPIをリクエストしていたということもない。

    var_dump($response->headers->getCookies());
array(0) {}

何も取得できていない。
どういうことなの。

なんか別のCookie出してみる

コントローラ

class FooController extends Controller{
    public function barAction(Request $request){
        return response()->cookie('hoge', 'fuga', -1);
    }
}

ブラウザから確認。

Set-Cookie: hoge=xxx; expires=xxx; Max-Age=0; path=/; httponly
Set-Cookie: laravel_session=xxx; expires=xxx; Max-Age=7200; path=/; httponly

中身は暗号化されているため値が正しいかはわからないが、Cookie自体はきちんと発行されている。

テスト。

class FooTest extends TestCase{
    public function testFoobar(){
        $response = $this->get('foo/bar');
        var_dump($response->headers->getCookies());
    }
}

実行結果。

array(1) {
  [0]=>
  object(Symfony\Component\HttpFoundation\Cookie)#396 (10) {
    ["name":protected]=>
    string(4) "hoge"
    ["value":protected]=>
    string(192) "xxx"
    ["domain":protected]=>
    NULL
    ["expire":protected]=>
    int(9999999999)
    ["path":protected]=>
    string(1) "/"
    ["secure":protected]=>
    bool(false)
    ["httpOnly":protected]=>
    bool(true)
    ["raw":"Symfony\Component\HttpFoundation\Cookie":private]=>
    bool(false)
    ["sameSite":"Symfony\Component\HttpFoundation\Cookie":private]=>
    NULL
    ["secureDefault":"Symfony\Component\HttpFoundation\Cookie":private]=>
    bool(false)
  }
}

追加で出力したhogeだけが取得できた。

つまり、laravel_sessionは自動的に隠蔽されるということなのか?

でもセッションは継続してる

テストを1項目追加。

class FooTest extends TestCase
{
    /**
     * テストを追加
     * @return void
     */
    public function testFoobar(){
        $response = $this->get('foo/bar');

        // foo/barが前提のリクエスト
        $response2 = $this->get('foo/baz');
        $response2->assertStatus(200);
    }
}

コントローラ。

class FooController extends Controller{

    public function barAction(Request $request){
        $request->session()->put('hoge', 'fuga');
    }

    public function bazAction(Request $request){
        if($request->session()->get('hoge') === 'fuga'){
            return [];
        }
        abort(404);
    }
}

foo/bazに来たときに、foo/barを経由していれば200、していなければ404が出力されることになる。
さて結果は?

OK (1 test, 1 assertion)

はい。

なぜなのか。

セッションデータは何処にあるの?

ここ。

class FooTest extends TestCase
{
    public function testFoobar(){
        $response = $this->get('foo/bar');
        $response2 = $this->get('foo/baz');

        // セッションデータはここ
        var_dump($this->app['session']->all());
    }
}

セッションIDではなく、["hoge"]=>"fuga"というデータそのものがここに入ってる。

laravel_sessionはどこにあるの?

どこでしょうね?

どうにかした

laravel_sessionではなく別のCookieを発行して、そちらに対してassertCookieXXXを行うことでお茶を濁した。

なお、"Cookieが発行されていること"そのものを見る必要がなく、セッションの中身だけ検証すればよいのであれば、最初からassertSessionXXXを使うといい。

感想

結局laravel_sessionが何処で消されているのかはわからなかった。

$response = $kernel->handle(
    $request = Request::createFromBase($symfonyRequest)
);

とかのあたりね、そのね、全く意味がわからんのじゃけど。


  1. 実際はstrtolower(env('APP_NAME')).'_session'のような名前になる 

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

PHP: Generatorの関数合成

本稿ではGeneratorを関数合成してひとつのGeneratorを作る方法を紹介する。

関数合成関数を実装する方法は、「PHP: 関数合成する関数を作る方法 - Qiita」で紹介した。ここで紹介した関数合成関数を使えば、Generatorも合成することができる。

どんな合成をしたいか?

3つのジェネレーターがある。これを合成して、ひとつのジェネレーターを作りたい。

function generator(string $name): callable
{
    return function (iterable $items) use ($name): iterable {
        foreach ($items as $item) {
            echo "generator('$name') yields $item\n";
            yield $item;
        }
    };
}

// 3つのジェネレーター
$a = generator('a');
$b = generator('b');
$c = generator('c');

手続き型で書くとこんな感じ:

$composedGenerator = function (iterable $items): iterable {
    $a = generator('a');
    $b = generator('b');
    $c = generator('c');
    yield from $c($b($a($items)));
};

foreach ($composedGenerator([1, 2, 3]) as $item) {
    echo "item = $item\n";
}

これを実行すると、次のような出力が出る:

generator('a') yields 1
generator('b') yields 1
generator('c') yields 1
item = 1
generator('a') yields 2
generator('b') yields 2
generator('c') yields 2
item = 2
generator('a') yields 3
generator('b') yields 3
generator('c') yields 3
item = 3

関数合成関数で合成する

関数合成関数はこれを使う:

$composeAll = function (callable $function, callable ...$functions): callable {
    return array_reduce(
        $functions,
        function (callable $f, callable $g): callable {
            return function (...$arguments) use ($f, $g) {
                return $g($f(...$arguments));
            };
        },
        $function
    );
};

合成のしかた:

$composedGenerator = $composeAll(generator('x'), generator('y'), generator('z'));

実行してみる:

foreach ($composedGenerator([1, 2, 3]) as $item) {
    echo "item = $item\n";
}

実行結果:

generator('x') yields 1
generator('y') yields 1
generator('z') yields 1
item = 1
generator('x') yields 2
generator('y') yields 2
generator('z') yields 2
item = 2
generator('x') yields 3
generator('y') yields 3
generator('z') yields 3
item = 3

合成できた。

別解

foreachする方法もある。

function compose(callable ...$functions): callable
{
    return function (iterable $items) use ($functions): iterable {
        foreach ($functions as $function) {
            $items = $function($items);
        }
        return $items;
    };
}

$composedGenerator = compose($a, $b, $c);

foreach ($composedGenerator([1, 2, 3]) as $item) {
    echo "item = $item\n";
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

CakePHPでのDB更新処理の2つの場合(idで指定 or whereで指定)の方法について

バージョンはCakePHP3.2です。
DBはMySQLを使ってます。

下記の条件についてそれぞれの方法を書いていきます

  1. idで更新するrowを指定する場合
  2. whereで更新するrowを指定する場合


1. idでupdateするrowを指定する場合

<?php
use Cake\ORM\TableRegistry;
// ~ 略 ~ 
// MembersTableの読み込み
$this->loadModel('Members');
// DBとの通信開始
$this->Members->connection()->begin();
// updateするrowをidで指定
$qurey = $this->Members->get('10');
// updateするcolumnの値を代入
$qurey->address = 'Kanagawa';
$qurey->age = '21';
// dataがあれば更新,無ければ挿入を行いつつ、処理結果をresultに代入。
$result = $this->Members->save($query);

if ( $result === false ) {
    // もし失敗したらロールバック
    $this->Members->connection()->rollback();
} else {
    // 失敗していなければコミット
    $this->Members->connection()->commit();
}


2. whereでupdateするrowを指定する場合

<?php
use Cake\Datasource\ConnectionManager;
use Cake\ORM\TableRegistry;
// ~ 略 ~
// MembersTableの読み込み
$this->loadModel('Members');
$connection = ConnectionManager::get('default');
// updateするcolumnの値を代入
$membernew = array(
    'address' => 'Kanagawa',
    'age' => '21'
);
// DBとの通信開始
$this->Members->connection()->begin();
// 更新するrowをwhereで探して取得する。
$query = $this->Members->find()
                  ->where(['name' => 'Gaku'])
                  ->where(['address' => 'Tokyo'])
                  ->where(['age' => '20'])
                  ->first();

if(isset($query)) {  
    // queryにデータがあれば更新処理にする。
    $member = $this->Members->patchEntity($query, $membernew);
} else {
    // データが無ければ挿入処理にする。
    $member = $this->Members->newEntity($membernew);
} 
// 上で指定した処理を実行しつつ結果を代入する
$result = $this->Members->save($member);

if ( $resource_result === false ) {
    // もし失敗したらロールバック
    $this->Members->connection()->rollback();
} else {
    // 失敗していなければコミット
    $this->Members->connection()->commit();
}

以上です。

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

PHP: 関数合成する関数を作る方法

本稿ではPHPで関数合成する方法を紹介する。

関数合成とは

関数合成とは2つの関数を組み合わせて1つの関数を作ることだ。この手法は、シンプルで小さく凝集性と再利用性の高い関数を組み合わせることで、保守性を保ちつつ多様な要求に応える処理を実現するために使われたりする。

関数合成してみる

ここに2つの関数がある。

$addOne = function (int $value): int {
    return $value + 1;
};

$timesTwo = function (int $value): int {
    return $value * 2;
};

これを合成してひとつの関数を作ってみる。

$addOneTimesTwo = function (int $value) use ($addOne, $timesTwo): int {
    $value = $addOne($value);
    return $timesTwo($value);
};

echo $addOneTimesTwo(4); //=> 10

これで関数合成できたわけだが、関数合成するためにその都度クロージャを書くのは手間になりそうだ。

関数合成する関数を作る

もっといい合成のしかたがある。それは、関数合成する関数を作ることだ。例えば、$composeという関数合成関数を作って、引数に関数を渡したら、さっきの$addOneTimesTwoと同じロジックを持つ関数が作られるようにしたい:

$addOneTimesTwo = $compose($addOne, $timesTwo);

これは命令型の関数合成と違って、処理の順と記述の順が一致していて読みやすい。

関数合成関数$composeを実装してみよう。

$compose = function (callable $f, callable $g): callable {
    return function (...$arguments) use ($f, $g) {
        return $g($f(...$arguments));
    };
};

$compose関数は2つの関数を受け取って、その2つを呼び出せる新しい関数(接着剤的な役割をする関数)を作って返す。

クライアントコードの読みやすさのために$compose$f$gの順で受け取るようにしつつ、合成される関数の呼び出し順序は後(先($引数))ではならないので、合成の式は$g$fの順で記述する必要がある。これを逆にしてしまうとバグるので注意。

function (...$arguments)...は可変長引数。つまり、あらゆる値をいくつでも受け取れるようにしている。$g($f(...$arguments))...はsplat演算子と呼ばれるもので、配列の$argumentsを関数に渡す際に一個一個の引数に展開する効果がある。

それでは関数合成して動かしてみよう:

$addOneTimesTwo = $compose($addOne, $timesTwo);
echo $addOneTimesTwo(4); //=> 10

一度にたくさんの関数を合成できる関数を作る

上で作った関数合成関数は2つの関数を受け取り、関数合成してくれる実装になっていたが、一度に3つ以上の関数を合成したいこともあるだろう。

ということで、たくさんの関数を合成できる関数を作ってみよう。

// さきほど作った関数合成関数
$compose = function (callable $f, callable $g): callable {
    return function (...$arguments) use ($f, $g) {
        return $g($f(...$arguments));
    };
};

// たくさんの関数を合成できる関数
$composeAll = function (callable $function, callable ...$functions) use ($compose): callable {
    return array_reduce($functions, $compose, $function);
};

上の$composeAll$composeを再利用する形になっているが、次のようにべた書きしても同じ挙動になる。

$composeAll = function (callable $function, callable ...$functions): callable {
    return array_reduce(
        $functions,
        function (callable $f, callable $g): callable {
            return function (...$arguments) use ($f, $g) {
                return $g($f(...$arguments));
            };
        },
        $function
    );
};

動かしてみよう:

$addOne = function (int $value): int {
    return $value + 1;
};

$timesTwo = function (int $value): int {
    return $value * 2;
};

$minusTwo = function (int $value): int {
    return $value - 2;
};

$addOneTimesTwoMinusTwo = $composeAll($addOne, $timesTwo, $minusTwo);

echo $addOneTimesTwoMinusTwo(1); //=> 2

((1 + 1) * 2) - 2 が計算されて 2 が出力される。

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

複数のフォームデータを、javascriptを使って1つのボタンで送信する方法

複数フォームの内容をワンクリックで送信する方法を書きます。

結論
submitボタンのフォームにhiddenでフォームの数だけinputを作り、
ボタンを押した時にjavascriptを呼び出して、
入力されているフォームの内容を、json形式にして、
hiddenのinputのvalueにセットする。

そうすることで、複数フォームの内容をワンクリックで送信することができる。

例えば、

<select id="select">
  <option value="0" >0</option>
  <option value="1" >1</option>
  <option value="2" >2</option>
  <option value="3" >3</option>
  <option value="4" >4</option>
</select>

<textarea id="textarea"></textarea>

<form method="POST" action="/worktimes/edit/" id="post_form">
    <input type="hidden" name="form1" value="" />
    <input type="hidden" name="form2" value="" />
    <button type="submit" >送信</button>
</form>

という2つのフォームがあったら、
呼び出したjsファイル内で下記の処理を書く

// 送信ボタンが押された時に呼び出されるjsファイル内の関数
$('#post_form').submit(function () {
        var selectJsonData = get_select_data_by_json(); // selectの内容をjsonで取得
        $("input[name='form1']").val(selectJsonData); // form1のvalueにselectの内容をセット
        var textareaJsonData = get_textarea_data_by_json(); // textareaの内容をjsonで取得
        $("input[name='form2']").val(textareaJsonData); // form2のvalueにtextareaの内容をセット
        return true;
});

function get_select_data_by_json() {
    var select = $( '#select' + i ).val(); // idを元にselectのvalueの内容を取得
    return JSON.stringify(select); // JSON形式でリターン
}

function get_textarea_data_by_json() {
    var element = document.getElementById( 'textarea' ); // idを元にselectの内容を取得 
    var textarea = element.value; // selectのvlueを取得
    return JSON.stringify(textarea); // JSON形式でリターン
}

保存ボタンクリック→js関数実行→フォームのvalueに値セット→アクション実行
という処理の流れなので、
コントローラ側に値が渡される。

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

PHPで容量の大きいデータをCSV出力するときに工夫したこと

前にもPHPでSJISのデカイCSVデータを扱った時に困ったことという記事を書いたけど、やっぱりCSVを扱うのって少し難しい。
今回は 「ログのデータをCSV出力してほしい」 という依頼があったときの話です。

検索をかければ、スニペットコードはたくさん見つかるのでなんとなく組み合わせて動くコードを書くところまではすんなりいったけれど、それだと容量の大きいデータを出力するときにうまくいかなかったりと手こずりました。

この記事では「容量の大きいデータだとCSV出力できないコード」をどうやって「最大20000件のデータまで出力できるように修正」したときのポイントなどについてまとめます。

備考

自分なりに調べて書いた記事なので、解釈が間違っている箇所もあるかもしれません。
間違っているところがあればコメントでご指摘いただけると幸いです!

仕様

  • Laravel5.4
  • t_logs というテーブルに入ってくるログをCSV出力してほしいとのこと
  • アプリケーションは Heroku にデプロイされている

いちばん最初に書いたコード(容量が大きくなければ動くコード)

Route::get('/log', function(){
    $stream = fopen('php://temp', 'w'); //1.一時的なファイルポインタを作成
    $result = DB::table("t_logs")->get();
    $data = [];
    foreach ($result as $log) {
        fputcsv($stream, array_values((array)$log))); //2.ファイルポインタに書き込む
    }
    rewind($stream); //3.ファイルポインタの位置を先頭に戻す
    $csv = str_replace(PHP_EOL, "\r\n", stream_get_contents($stream)); //4.改行コードの置き換え

    return Response::make($csv, 200, [ //5.CSV出力
        'Content-Type' => 'text/csv',
        'Content-Disposition' => "attachment; filename=t_logs.csv"
    ]);
});

1. 一時的なファイルポインタを作成

fopen() はファイルまたは URL をオープンする関数で、これを利用して一時的なファイルポインタを作成できる。
fopen() は第二引数にモードを指定することができ、これを変えれば「読み出しのみ」や「書き込み&読み出し可」のような指定が可能。

今回は

書き出しのみでオープンします。ファイルポインタをファイルの先頭に置き、 ファイルサイズをゼロにします。ファイルが存在しない場合には、 作成を試みます。

というモード w を使ってファイルポインタを作成した。

補足:ファイルポインタってなに?

ファイルの操作でよく出てくる「ファイルポインタ」というものは、サーバーがファイルを扱うための専用の"しおり"のようなもの。(どこまで読み取ったか、など)

補足: php://temp って?

php://memory および php://temp は読み書き可能なストリームで、一時データをファイルのように保存できるラッパーです。
両者の唯一の違いは、php://memory が常にデータをメモリに格納するのに対して php://temp は定義済みの上限 (デフォルトは 2 MB) に達するとテンポラリファイルを使うという点です。
このテンポラリファイルの場所は、 sys_get_temp_dir() 関数と同じ方法で決めます。

どちらもメモリ上に領域を確保するという点では同じだが、php://memory だとメモリリークしてしまうことがあるため、
2MBを超えた場合はテンポラリファイル(自動削除される一時ファイル)を作ってくれる php://temp を使うことにした。

補足:メモリリークってなに?

プログラムが確保したメモリの一部、または全部を解放するのを忘れ、確保したままになってしまうこと。

2. ファイルポインタに書き込む

fputcsv() を使って、列をCSV 形式にフォーマットしてファイルポインタに書き込んでいく。
このとき、 いちばん最後に改行が追加 される。

3. ファイルポインタを先頭に戻す

fputcsv() ファイルの書き込みが終わると、ファイルポインタの位置は最後に書き込みが終えた時点になっている。
次に行う処理は書き込んだテンポラリファイルの先頭から開始する必要があるため、 rewind() を使ってファイルポインタを先頭に戻す。

4. 改行コードを置き換え

fputcsv でファイルポインタを書き込んでいくときに改行コード(PHP_EOL)が追加されるが、この改行コードはOSに依存する。

OS 改行コード 改行コード文字
Windows CRLF \r\n
Linux,Unix,MACOS10 LF \n

(引用: https://qiita.com/kazu56/items/bc77582313918fe2a3b1

そのため、CSV出力したサーバーのOSと、実際にCSVを使うOSが異なる場合(LinuxとWindows)にうまいこと改行してくれない...ということが起こってしまう。

これを防ぐため、str_replace() を使って \r\n に置き換えた。(今回CSVを使うOSはWindows)

補足: stream_get_contents

file_get_contents() と似ていますが、 stream_get_contents() は既にオープンしているストリームリソースに対して操作を行います。
そして、指定した offset から始まる最大 maxlength バイトのデータを取得して文字列に 保存します。

今回はすでに fopen() でファイルはオープンしているため stream_get_contents() を使用し、文字列として読み込んだ。

5. CSV出力

文字列にしたデータを Response::make() を使ってCSV出力。

容量が大きすぎてエラーになった

実際にテスト環境で出力しようとしたところ、エラーとなってしまった。

Fatal error: Allowed memory size of ...

データが大きい時はどうやらメモリのことを考えないといけないらしい...。

原因となった部分

想定していたよりもデータの容量が大きく(Maxで20000件)、 stream_get_contents で文字列にするときにメモリに乗り切らなかった。

修正後

Route::get('/log', function(){
    $stream = fopen('php://temp', 'w');
    $result = DB::table("t_logs")->get();
    $data = [];
    foreach ($result as $log) {
        fputcsv($stream, str_replace(PHP_EOL, "\r\n", array_values((array)$log))); //修正 1.改行コードの置き換え時に文字列変換しない
    }
    rewind($stream); //注意:fpassthru() する前にもファイルポインタは戻しておく

    return response()->stream(function () use ($stream) { //修正 2. ストリームのままCSV出力できるようにする
        fpassthru($stream);
        fclose($stream);
    }, 200, [
        'Content-Type' => 'text/csv',
        'Content-Disposition' => "attachment; filename=t_logs.csv"
    ]);
});

修正 1. 改行コードの置き換え時に文字列変換しない

stream_get_contents() でストリームが文字列に変換されてしまうと、メモリが必要となってしまうため
fputcsv() で書き出すと同時に改行コードの処理をしてしまうことにした。

修正 2. ストリームのままCSV出力できるようにする

文字列に変換しないことになったので、出力方法にも工夫が必要。
Response::make() では文字列のみを許容するため、ストリームのまま出力できるるよう response()->stream() を使う。

この時、fpassthru() を使って、ファイルポインタ上に残っているすべてのデータを出力する。
テンポラリファイルはスクリプト終了時にも自動削除されるようだが、念のため fclose() で終了を明示した。

注意: fpassthru() する前にもファイルポインタは戻しておく

「改行の置き換えが済んだなら、ファイルポインタを戻す必要はないんじゃないの?」と思いきや、
ポインタが一番後ろに位置したままだと fpassthru() しても読み取るものがなく、空っぽのCSVが出力されてしまうので注意。

まとめ

容量の大きくないデータをCSV出力するのであれば最初のコードでも問題はないけれど、

  • どの関数がなんの役割をしているのか
  • どのような形式でデータを保持しているのか
  • ファイルポインタの挙動 etc...

といったことを理解していないと、今回のログデータのように容量の大きいデータを出力する際にはうまくいきませんでした。

CSV出力に限らず、何かを実現するために手法は何通りもあることが多いけれど、
状況に応じて工夫する必要は必ずでてくるので、関数を使ったりするときは公式ドキュメントなどもよんで理解することは大事だなぁと思いました。

参考

PHP関数・クラス

fopen
fputcsv
rewind
file_get_contents
stream_get_contents
fclose
fpassthru

その他

http://php-beginner.com/practice/file_ope/file_ope6.html
https://www.muchacolla.com/work/php/416/
https://qiita.com/mpyw/items/f24d3764fe3eedf132ff
http://www.standpower.com/php_analyz.html
http://php.net/manual/ja/wrappers.php.php
https://lab.flama.co.jp/archives/1139/
https://qiita.com/ma_me/items/fae63108dbce03290efb

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

[Laravel] 5.4 => 5.5 のバージョンアップでハマった点まとめ

Model

「updated_at」および「created_at」がテーブルに存在しない時に save() でデータ保存しようとするとエラーになる

エラー内容

ArgumentCountError: Too few arguments to function Illuminate\Database\Eloquent\Model::setAttribute(), 1 passed in /home/livede55.com/vendor/laravel/framework/src/Illuminate/Database/Eloquent/Concerns/HasAttributes.php on line 525 and exactly 2 expected

setAttribute() が呼ばれているが引数が足りないよ
と怒られてしまう

原因

updated_at および created_at が存在しないテーブルなのでEloquentの設定は以下にしている

const CREATED_AT = null;
const UPDATED_AT = null;

この状態で何故か 5.5 になると updated_at および created_at に対しての setAttribute() が動作し、
カラム名が null なので 引数エラーが起こる

本家LaravelのGitHubにもIssueが作成されている

https://github.com/laravel/framework/issues/20901

対応

Issueにハックが記載されている対応を行う

public function setUpdatedAt($value)
{
    return $this;
}

public function setCreatedAt($value)
{
    return $this;
}

setAttribute() 呼び出しされる前提で updated_at および created_at のミューテターを定義してあげる

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

Laravel5.7で前回のTwitterOAuthを使ってツイートやらハッシュタグやらユーザーやらの取得方法一覧(投稿あり)

TwitterOAuthでツイートやらを取得する記事はいくらでもあるが、中々取得する一覧みたいのがなかったので、完全自分用に書いていく。

前回で設定したTwitterOAuthを使って色々取得してみる。

参考リンク
【前回】Laravel5.7でTwitterOAuthを使って認証やらツイート取得やら

以下任意のコントローラーで試してください
この記事ではSampleController.phpというファイル名で使用します。
※全てdump()でjsonデータを表示するだけの処理

任意のキーワードを検索する

キーワードを複数指定する場合は半角スペース区切りで入力する
※ツイートしたばかりだと少し時間を置かないとツイートを取得できない(約3分ほど?)

SampleController.php
    public function sample()
    {
        //"TwitterOAuthを使って検索するよ"というツイートを10件取得する
        $search_word = \Twitter::get("search/tweets", array("q" => "TwitterOAuthを使って検索するよ", 'count' => 10));

        dump($search_word);
    }

任意のハッシュタグを検索する

上記のキーワード検索に#ハッシュタグをつけるだけ

SampleController.php
    public function sample()
    {
        //"TwitterOAuthを使って検索するよ"というツイートを10件取得する
        $hash_tag = \Twitter::get("search/tweets", array("q" => "#TwitterOAuthを使ってハッシュタグを検索するよ", 'count' => 10));

        dump($hash_tag);
    }

自分のタイムラインを取得

SampleController.php
    public function sample()
    {
        // 自分のタイムラインを10件取得
        $time_line = \Twitter::get('statuses/home_timeline', ['count' => 10]);

        dump($time_line);
    }

フォロワーを取得

自分のフォロワーを取得する

SampleController.php
    public function sample()
    {
        // フォロワーを10件取得
        $follower = \Twitter::get('followers/list', ['count' => 10]);

        dump($follower);
    }

任意のユーザをIDで検索する

とりあえず僕のTwitterIDを取得する(フォローしてほしいだけ)

SampleController.php
    public function sample()
    {
        // @以降のuser_idを指定してください
        $search_user = \Twitter::get('users/show', ['screen_name'=> '@namizatop']);

        dump($search_user);
    }

ツイートする

TwitterOAuthを使ってツイートとツイートしてみる
※2回投稿するとStatus is a duplicate.というerrorが吐かれるので注意。

SampleController.php
    public function sample()
    {
        // "TwitterOAuthを使ってツイート"とツイート
        $tweet = \Twitter::post('statuses/update', ['status'=> 'TwitterOAuthを使ってツイート']);

        dump($tweet);
    }

任意のツイートをリツイートする

僕の投稿のTwitterOAuthを使ってリツイートするよをリツイートしてみる

SampleController.php
    public function sample()
    {
        // 僕の投稿の"TwitterOAuthを使ってリツイートするよ"の投稿をリツイート
        $retweet = \Twitter::post("statuses/retweet/1100242514680807424");

        dump($retweet);
    }

任意のツイートをいいね!する

僕の投稿のTwitterOAuthを使っていいね!するよをいいね!してみる

SampleController.php
    public function sample()
    {
        // 僕の投稿の"TwitterOAuthを使っていいね!するよ"の投稿をいいね!
        $favorite = \Twitter::post("favorites/create", ['id' => '1100245496159821825']);

        dump($favorite);
    }

以上!僕のTwitterのステマでした!

参考リンク

参考にさせていただきましたm(_ _)m
Twitter APIでつぶやきを取得する

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

Mecabインストールメモ (Ubuntu + Apache + PHP)

環境

Host OS: macOS High Sierra 10.13.6
Virtual Machine: VirtualBox 5.2.26 r128414
Guest OS: Ubuntu 18.04.1.0 LTS amd64
PHP: PHP 7.2

Mecabのインストール方法

VM, Apache, PHPの用意

今回は、VM, Apache, PHP設定の詳細は省きます。
気が向いたら追記したいと思います。
とりあえず、導入したパッケージは記載しておきます。

sudo apt install apache2 -y
sudo apt install php7.2 -y
sudo apt install php7.2-dev -y
sudo apt install php-mbstring -y

mecab本体の用意

sudo apt install mecab -y         
sudo apt install mecab-ipadic -y
sudo apt install libmecab-dev -y 

php-mecabの用意

(展開場所はお好きに)

sudo mkdir -p /opt/php-mecab
sudo git clone https://github.com/rsky/php-mecab.git
phpize
sudo ./configure
make
make install

mecab.iniを作成

/etc/php/7.2/mods-available/mecab.ini
extension=mecab.so
  • phpコマンド単体でMecabが使えるようにさっきのファイルにシンボリックリンクを貼る
ln -s /etc/php/7.2/mods-available/mecab.ini /etc/php/7.2/cli/conf.d/20-mecab.ini
  • apache2で利用できるようにする
ln -s /etc/php/7.2/mods-available/mecab.ini /etc/php/7.2/apache2/conf.d/20-mecab.ini
sudo service apache2 restart
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

php-master-changes 2019-02-25

今日はストリームラッパーでの未定義定数による SEGV の修正、break 2147483648 で SEGV を起こす問題の修正、コンパイラ警告の修正、一部テストケースの並列実行対応、getMessage() 等の getter を通して例外のプロパティへアクセスする際に参照だと正常動作しなかった問題の修正があった!

2019-02-25

laruence: Fixed bug #77664 (Segmentation fault when using undefined constant in custom wrapper)

laruence: Update NEWS

laruence: Fixed bug #77660 (Segmentation fault on break 2147483648)

laruence: Update NEWS

laruence: Fixed compiler warning

nikic: Fix some port collisions in sockets tests

nikic: Fix some directory collisions in dir tests

nikic: Fix assertion in Exception::getMessage() if $message is a ref

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

laravelのvalidatorをtinkerでちょこっとだけ試す

概要

laravelのvalidatorをチョコっと試す
忘れてしまうのでメモしておく

tinkerて何。

コンソールでlaravelとかPHPをチョコっと試せる機能だ
詳しくは・・・⬇️
https://readouble.com/laravel/5.7/ja/artisan.html#tinker
(怖い人よけの為に公式も貼っておく)
https://laravel.com/docs/5.7/artisan#tinker

laravelのバリデーションって豊富だ。

ルールがアレコレあってチョコっとだけ試したい時があるなぁ。
https://readouble.com/laravel/5.5/ja/validation.html#available-validation-rules

tinkerを起動したら以下の様にかく

# こうかくと
>>> Validator::make([''], ['data' => 'required'])->errors()->toArray()
# こう返ってくるよ
=> [
     "data" => [
       "The data field is required.",
     ],
   ]

# あとは好きに試す
>>> Validator::make(['data' => 'yeah'], ['data' => 'numeric'])->errors()->toArray()
=> [
     "data" => [
       "The data must be a number.",
     ],
   ]

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