- 投稿日:2019-02-26T23:29:18+09:00
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
を実行すると$user
にposts
情報がキャッシュされる。
これ以降、$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パターン) }このようにリレーションメソッドでアクセスすると都度クエリが発行されてしまうため駄目ですよ、遅延ロードの際は動的プロパティを使いましょうという話。
何か誤解があれば指摘頂きたいと思います。
- 投稿日:2019-02-26T23:28:34+09:00
stdClassのオブジェクトを作る方法
- データベースから取ってくるデータがstdClassの配列だったりします。
- そのstdClassの配列を処理するコードを作ったりします。
- コードを作るとPHPUnitでテストしたりします。
- そうすると期待値や引数がstdClassだったりします。
- するとstdClassのオブジェクトを作らねばなりません。
- 環境
- macOS Mojave バージョン10.14.3
- PHP 7.3.1
- Laravel Framework 5.7.26
方法1. 配列をオブジェクトでキャストする
controlStdClassArray->createStdClassArrayCastpublic 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->createStdClassArrayNewpublic 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());参考
- 投稿日:2019-02-26T23:08:06+09:00
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) "パースレー" }
- 投稿日:2019-02-26T22:05:12+09:00
クラスの継承
未来電子テクノロジー(https://www.miraidenshi-tech.jp/intern-content/program/ )でインターンをしている@hisayamaです。
今回はクラスの継承についてアウトプットします。クラスの継承
新しいクラスを作る上で、もともとあるメインのクラスのプロパティやメソッドを継承して、それに色々付け加えたい時はクラスの継承を行います。
新しい子クラスはこのように定義します。
class 子クラス extends 親クラス { }この親クラスはスーパークラスとも呼び、継承したクラスのことを指します。
このように定義することで、親クラスのコンストラクタやメソッドが呼び出せるようになります。
さらに子クラスでは子クラス独自のプロパティやメソッドを追加できます!ちなみに子クラスは複数の親クラスを継承できません。1つだけです。
反対に親クラスは複数の子クラスに継承できます。子クラスのメソッドを呼び出すとします。
もし、子クラスにメソッドが定義されている場合は、子クラスのメソッドが呼び出されます。
一方で定義されていない場合、親クラスのメソッドが呼び出されます。反対に、子クラスで設定した独自プロパティやメソッドは親クラスからは呼び出せないので注意が必要です!
- 投稿日:2019-02-26T20:30:21+09:00
【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) );とかのあたりね、そのね、全く意味がわからんのじゃけど。
実際は
strtolower(env('APP_NAME')).'_session'
のような名前になる ↩
- 投稿日:2019-02-26T20:14:59+09:00
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"; }
- 投稿日:2019-02-26T20:05:37+09:00
CakePHPでのDB更新処理の2つの場合(idで指定 or whereで指定)の方法について
バージョンはCakePHP3.2です。
DBはMySQLを使ってます。下記の条件についてそれぞれの方法を書いていきます
- idで更新するrowを指定する場合
- 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(); }以上です。
- 投稿日:2019-02-26T20:01:52+09:00
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 が出力される。
- 投稿日:2019-02-26T19:53:50+09:00
複数のフォームデータを、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に値セット→アクション実行
という処理の流れなので、
コントローラ側に値が渡される。
- 投稿日:2019-02-26T18:20:07+09:00
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
- 投稿日:2019-02-26T17:11:20+09:00
[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
のミューテターを定義してあげる
- 投稿日:2019-02-26T13:10:23+09:00
Laravel5.7で前回のTwitterOAuthを使ってツイートやらハッシュタグやらユーザーやらの取得方法一覧(投稿あり)
TwitterOAuthでツイートやらを取得する記事はいくらでもあるが、中々取得する一覧みたいのがなかったので、
完全自分用に書いていく。前回で設定したTwitterOAuthを使って色々取得してみる。
以下任意のコントローラーで試してください
この記事ではSampleController.phpというファイル名で使用します。
※全てdump()でjsonデータを表示するだけの処理任意のキーワードを検索する
キーワードを複数指定する場合は半角スペース区切りで入力する
※ツイートしたばかりだと少し時間を置かないとツイートを取得できない(約3分ほど?)SampleController.phppublic function sample() { //"TwitterOAuthを使って検索するよ"というツイートを10件取得する $search_word = \Twitter::get("search/tweets", array("q" => "TwitterOAuthを使って検索するよ", 'count' => 10)); dump($search_word); }任意のハッシュタグを検索する
上記のキーワード検索に
#ハッシュタグ
をつけるだけSampleController.phppublic function sample() { //"TwitterOAuthを使って検索するよ"というツイートを10件取得する $hash_tag = \Twitter::get("search/tweets", array("q" => "#TwitterOAuthを使ってハッシュタグを検索するよ", 'count' => 10)); dump($hash_tag); }自分のタイムラインを取得
SampleController.phppublic function sample() { // 自分のタイムラインを10件取得 $time_line = \Twitter::get('statuses/home_timeline', ['count' => 10]); dump($time_line); }フォロワーを取得
自分のフォロワーを取得する
SampleController.phppublic function sample() { // フォロワーを10件取得 $follower = \Twitter::get('followers/list', ['count' => 10]); dump($follower); }任意のユーザをIDで検索する
とりあえず僕のTwitterIDを取得する(フォローしてほしいだけ)
SampleController.phppublic function sample() { // @以降のuser_idを指定してください $search_user = \Twitter::get('users/show', ['screen_name'=> '@namizatop']); dump($search_user); }ツイートする
TwitterOAuthを使ってツイート
とツイートしてみる
※2回投稿するとStatus is a duplicate.
というerrorが吐かれるので注意。SampleController.phppublic function sample() { // "TwitterOAuthを使ってツイート"とツイート $tweet = \Twitter::post('statuses/update', ['status'=> 'TwitterOAuthを使ってツイート']); dump($tweet); }任意のツイートをリツイートする
僕の投稿の
TwitterOAuthを使ってリツイートするよ
をリツイートしてみるSampleController.phppublic function sample() { // 僕の投稿の"TwitterOAuthを使ってリツイートするよ"の投稿をリツイート $retweet = \Twitter::post("statuses/retweet/1100242514680807424"); dump($retweet); }任意のツイートをいいね!する
僕の投稿の
TwitterOAuthを使っていいね!するよ
をいいね!してみるSampleController.phppublic function sample() { // 僕の投稿の"TwitterOAuthを使っていいね!するよ"の投稿をいいね! $favorite = \Twitter::post("favorites/create", ['id' => '1100245496159821825']); dump($favorite); }以上!
僕のTwitterのステマでした!参考リンク
参考にさせていただきましたm(_ _)m
Twitter APIでつぶやきを取得する
- 投稿日:2019-02-26T12:21:31+09:00
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.2Mecabのインストール方法
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 -ymecab本体の用意
sudo apt install mecab -y sudo apt install mecab-ipadic -y sudo apt install libmecab-dev -yphp-mecabの用意
(展開場所はお好きに)
sudo mkdir -p /opt/php-mecab sudo git clone https://github.com/rsky/php-mecab.git phpize sudo ./configure make make installmecab.iniを作成
/etc/php/7.2/mods-available/mecab.iniextension=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
- 投稿日:2019-02-26T09:05:17+09:00
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)
- https://github.com/php/php-src/commit/4a72dd782df3089a0d944a7e51eabebdf1f1abc3
- [7.2~]
- 未定義定数を持つクラスをストリームラッパーに登録して利用しようとすると SEGV を起こす問題の修正
laruence: Update NEWS
- https://github.com/php/php-src/commit/3b5475e9ee48c27f7164eb1d9cbe873d2704b571
- [7.3~]
- ↑の 7.3 用の NEWS エントリの追記
laruence: Fixed bug #77660 (Segmentation fault on break 2147483648)
- https://github.com/php/php-src/commit/1c22ace0582fb0a2ec581237fcf1c5b9c41edd04
- [7.2~]
- PHP の break はネストレベルを数値指定することでネストしたループを抜けられるが、
break 2147483648
を指定すると SEGV が起きる問題の修正- 過剰ボケとかオーバーリアクションに通じる系の笑いでなんかクスっときた
laruence: Update NEWS
- https://github.com/php/php-src/commit/fb3f078eeb4ecb2de783b7ee936dd583e6285d3e
- [7.3~]
- ↑の 7.3 用の NEWS エントリの追記
laruence: Fixed compiler warning
- https://github.com/php/php-src/commit/4ac954ac3e1217864e592ebb1531b4f0f31e2264
- [7.3~]
- continue が switch に適用される際の警告メッセージで、フォーマット文字列 "%d" を ZEND_LONG_FMT にしてコンパイラ警告を修正
nikic: Fix some port collisions in sockets tests
- https://github.com/php/php-src/commit/c937c55d7553a7fa99877998cd6617d9e82c7dd1
- ext/sockets のテストで、テストケース間のポート衝突の修正
- [7.4~]
- 並列実行対応
nikic: Fix some directory collisions in dir tests
- https://github.com/php/php-src/commit/251e94894694ae8ccaa75a43b574f544f3a8c203
- [7.4~]
- ディレクトリ操作系のテストで、テストケース間のディレクトリ衝突の修正
- 並列実行対応
nikic: Fix assertion in Exception::getMessage() if $message is a ref
- https://github.com/php/php-src/commit/af37d58cf7b77814b93ea97a8dcd2afb46c4424e
- [7.2~]
- Exception::getMessage() 等での例外の各プロパティへのアクセス時、プロパティが参照である可能性を考慮したコードに修正
- 参照ならデリファレンスするよう ZVAL_DEREF() を足してる
- 投稿日:2019-02-26T00:42:16+09:00
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#tinkerlaravelのバリデーションって豊富だ。
ルールがアレコレあってチョコっとだけ試したい時があるなぁ。
https://readouble.com/laravel/5.5/ja/validation.html#available-validation-rulestinkerを起動したら以下の様にかく
# こうかくと >>> 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.", ], ]