- 投稿日:2019-12-19T21:05:58+09:00
【 Eagerロード】コメントと、コメントに対する返信について、それぞれユーザー名を取得したい
【 Eagerロード 】コメント&コメントに対する返信について、それぞれユーザー名を別テーブルから取得したい
2019年11月から、Laravel歴数ヶ月の初心者が投稿型ナレッジベースのコミュニティサイトを作るというチャレンジ中。作りたいアプリケーション→機能を因数分解→ググる→先人の轍をたどる(写経する)→ぬかるみにはまる→エラー解消の神を探す→解決を繰り返す日々( ·ㅂ·)و 。備忘録として、Qiitaに投稿しています。
今回はハマったポイントを共有
投稿に対し、コメントがつけられ、さらにそのコメントに対して返信ができるような機能を実装しようとしていた時のこと。
一つの投稿に紐づいているコメントとそれに対する返信を全て取得し、viewファイルへ渡してあげたい。その際、コメントしたユーザーと返信したユーザーについて、それぞれ名前をusers_tableから引っ張ってきたいと思ったが、コメントのユーザー名まではうまくいったけど、返信したユーザー名を取得するところで詰まった。
前提
テーブルは以下のような設計になっており、投稿にコメントを書くことができ、一つ一つのコメントに対して返信ができるような構造。コメント、返信のいずれもuser_idでusers_tableと紐づいています。
posts_table
- id
- title
- body
- user_id
comments_table
- id
- post_id(posts_tableのidに紐付き)
- body
- user_id(users_tableのidに紐付き)
replies_table
- id
- comment_id(comments_tableのidに紐付き)
- body
- user_id(users_tableのidに紐付き)
users_table
- id
- name
リレーションの設定
リレーションは、以下のような関係性になっています。
- 投稿は複数のコメントを持ち、コメントは一つの投稿に従属
- コメントは複数のリプライを持ち、リプライは一つのコメントに従属
- ユーザーは複数の投稿・コメント・リプライを持ち、投稿・コメント・リプライのいずれも一人の投稿者に従属する
それぞれ、モデルに以下のように設定しています。
*余談・・・テーブルデザイン・リレーションについては、きっともっと賢い書き方があると思います。どなたかいい方法をご存知でしたら教えてくれると嬉しいです(、._. )、#Post.php class Post extends Model { // 投稿は複数のコメントを持ち、コメントは一つの投稿に従属 public function comments() { return $this->hasMany('App\Comment'); } // 投稿は一人の投稿者に従属 public function user() { return $this->belongsTo('App\User'); } } #Comment.php class Comment extends Model { // 投稿は複数のコメントを持ち、コメントは一つの投稿に従属 public function post() { return $this->belongsTo('App\Post'); } // コメントは一人の投稿者に従属 public function user() { return $this->belongsTo('App\User'); } // コメントは複数のリプライを持ち、リプライは一つのコメントに従属 public function replies() { return $this->hasMany('App\Reply'); } } #Reply.php class Reply extends Model { // リプライは一人の投稿者に従属 public function user() { return $this->belongsTo('App\User'); } // コメントは複数のリプライを持ち、リプライは一つのコメントに従属 public function comment() { return $this->belongsTo('App\Comment'); } } #User.php class User extends Model { // 投稿者は複数の投稿を持つ。 public function posts() { return $this->hasMany('App\Post'); } // 投稿者は複数のコメントを持つ。 public function comments() { return $this->hasMany('App\Comment'); } // 投稿者は複数のリプライを持つ。 public function replies() { return $this->hasMany('App\Reply'); } }コントローラー
投稿のid($post_id)を指定し、DBからデータを取得して行きましょう。
親のモデルに対するクエリと同時にリレーションを取得してしまう「Eagerロード」という、スッキリした書き方があるそうです。
Eagerロードについては、laravelでwithを使ってSQLの読み込み回数を減らすに詳しく書いてあります。
#PostsController.php public function show($post_id) { $comments = Comment::with(['user', 'replies', 'replies.user']) ->where('comments.post_id', $post_id) ->get(); return view('posts.show', [ 'comments' => $comments, ]); }with後の丸括弧内に入っている3つが、取得したいリレーション先(テーブル)になります。
'user'(←Comment.phpモデルの"public function user()"で指定したメソッド名)
'replies'(←Comment.phpモデルの"public function replies()"で指定したメソッド名)
'replies.user'(←Reply.phpモデルの"public function user()"で指定したメソッド名)comment->userとcomment->repliesは直接の従属関係にありますが、comment->replies->userへと、祖孫関係にあるところまでリーチできるという便利さに感動!!٩(๑′∀ ‵๑)۶•¨•.¸¸♪
参照
こちらに答えがありました〜!!ありがとうございます٩(ˊᗜˋ*)و
[Laravel] Eloquent リレーションと Eager Loading
Laravel with(Eagerロード)の使い方・サンプル付き反省
そもそも、Eagerロードという名前・書き方にたどり着くまでにかなり時間がかかってしまった。一つ一つ経験を積み上げないとダメだなぁ・・・(´;ㅿ;`)
- 投稿日:2019-12-19T20:52:36+09:00
オペレーションミスを防ぐWordMoveのforbid設定
以前に「WordPressを使ったサイト制作標準フロー」の中でWordMoveを紹介しましたが、コマンド操作ミスで大事故になる危険性があります。
例えば、本番環境に修正したテーマを適用する際に下記のように行いますが
wordmove push -e production -t打ち間違えで、
wordmove push -e production -uだったり、
wordmove push -e production -dのように一文字間違えるだけでアップロードファイルを上書きしたりデータベースをまるっと上書きしてしまう可能性があります。
そのような操作ミスを防ぐためにWordMoveバージョン3から
forbidという設定項目が追加されました。
例えば設定ファイルで下記のように記述すると、production環境へのデータベース、プラグイン、アップロードファイルのpushを禁止できます。(禁止したい項目をtrueとする)production: forbid: push: db: true plugins: true themes: false languages: false uploads: true mu-plugins: false pull: db: false plugins: false themes: false languages: false uploads: false mu-plugins: falseこれでwordmoveコマンドを実行するときのストレスが少し緩和されますね。
ただし、2019年12月時点の
vccw-3.21.1ではWordMove2.4.0がインストールされますのでZIPファイルのダウンロードと解凍後にprovision/playbooks/commands.yml
のバージョンを4.0.1に書き換える必要があります。- name: Install Wordmove become: no gem: name: "wordmove" user_install: yes version: "4.0.1" ignore_errors: yesこのあとに、
vagrant upすれば WordMove 4.0.1 がインストールされたWordPressの環境が構築できます。
- 投稿日:2019-12-19T20:45:35+09:00
PHPの便利なモックオブジェクトフレームワーク「Mockery」のケーススタディ
はじめに
Mockeryのケーススタディをつらつらと書いていきます。
こういうケースでは、このようにコードを書いたら良さげ というように書いていきます。
日本語で書かれてるドキュメントがあります。感謝しかない。ケーススタディ
クラスAのテストをしたいけど、クラスAはクラスBに依存してて、クラスBの影響を受けたくない
よくあるパターンですね。
クラスAはクラスBに依存している。(クラスAはクラスBが無いと動かない)
クラスAの単体テストを書きたいので、クラスBがうまく動いてなくてもクラスAのテストに影響を与えたくない。サンプルコード
class A { private $classB; public function __construct(B $classB) { $this->classB = $classB; } /** * foobarを取得する * * @return string */ public function fooBar(): string { $bar = $this->classB->methodBar(); return 'foo' . $bar; } } class B { public function methodBar(): string { // ここにバグがあったらクラスAのテストが失敗する。 return 'bar'; } }Mockeryを使ったテストコード
class Test_A extends TestCase { public function testFooBar() { /** @var B $stubB クラスBの代替オブジェクト */ $stubB = Mockery::mock(B::class); // 実際のクラスBのmethodBarメソッドにバグがあっても、クラスAのテストに影響しない。 // クラスAのテストはクラスBに依存しなくなった! $stubB->shouldReceive('methodBar')->andReturn('bar'); $classA = new A($stubB); $this->assertEquals('foobar', $classA->fooBar()); } }クラスAのテストをしたいけど、クラスBのメソッドをstaticに呼んでいてモックに入れ替えられない
メソッドの中で別のクラスのメソッドをstaticに呼んでるからモックに置き換えられない!!!
強い依存が発生してますね。
Facadeや依存注入で置き換えられたらいいんですが、そのような仕組みなってないこともあると思います。どうしたらいいんだ・・・サンプルコード
class A { /** * foobarを取得する * * @return string */ public function fooBar(): string { // 強い依存が発生!モックに置き換えられない $bar = B::methodBar(); return 'foo' . $bar; } } class B { public static function methodBar(): string { return 'bar'; } }Mockeryを使ったテストコード
そんなときはaliasの出番です。
制約1はありますが、置き換えられます。class Test_A extends TestCase { public function testFooBar() { // Bクラスの定義自体をスタブに置き換える $stubB = Mockery::mock('alias:'. B::class); $stubB->shouldReceive('methodBar')->andReturn('bar'); $classA = new A($stubB); $this->assertEquals('foobar', $classA->fooBar()); } }※クラスの定義自体をMockeryで作成したものに置き換えるので、オートロードが機能していて、かつ本当のクラスBをロードしていない状態でないと利用できません。require_once('classes/classB.php') とか手動でロードしてるとダメです。
クラスAのテストをしたいけど、クラスAはクラスBに強く依存している(密結合になっている)
メソッドの中で依存クラスをnewしてて、差し替え不能。強い依存が発生している・・・
上のstaticを呼んでるパターンと似ていますね。
うーん、つらい。
こんなときもなんとか置き換えられます。制約はstaticの時と同じです。サンプルコード
class A { /** * foobarを取得する * * @return string */ public function fooBar(): string { // 強い依存発生! $classB = new B(); $bar = $classB->methodBar(); return 'foo' . $bar; } } class B { public function methodBar(): string { return 'bar'; } }Mockeryを使ったテストコード
class Test_A extends TestCase { public function testFooBar() { // こうすることで、new B()で生成されるインスタンスがMockeryのオブジェクトになります $stubB = Mockery::mock('overload:'. B::class); $stubB->shouldReceive('methodBar')->andReturn('bar'); $classA = new A($stubB); $this->assertEquals('foobar', $classA->fooBar()); } }※これもstaticの時と同様に、先に実際のクラスをロードしていたらできません。オートロードするように修正しましょう。
クラスAのテストをしたいけど、クラスAはインターフェースZに依存してる
サンプルコード
いいですね!抽象に依存しているので差し替えが楽です。
class A { /** @var InterfaceZ */ private $interfaceZ; public function __construct(InterfaceZ $classB) { $this->interfaceZ = $classB; } /** * foobarを取得する * * @return string */ public function fooBar(): string { $bar = $this->interfaceZ->methodBar(); return 'foo' . $bar; } } class B implements InterfaceZ { public function methodBar(): string { return 'bar'; } } interface InterfaceZ { public function methodBar(): string; }Mockeryを使ったテストコード
この場合は、mockメソッドにインターフェース名を渡してあげればいい感じになります。
class Test_A extends TestCase { public function testFooBar() { // ここでインターフェース名を指定 $stubB = Mockery::mock(InterfaceZ::class); $stubB->shouldReceive('methodBar')->andReturn('bar'); $classA = new A($stubB); $this->assertEquals('foobar', $classA->fooBar()); } }クラスAのテストをしたいけど、中でメソッドチェーンが発生していて色々な別のクラスのメソッドを利用している(デメテルチェーンがある)
サンプルコード
クラスが依存しているクラスが所持している別のクラスのメソッドを呼ぶこともあるでしょう。
あまり良くない構造ではありますが・・・(クラスAが直接依存しているクラスB以外の関心事を知ってしまっている)
クラスCのモックも作ってがんばる方法もありますが、面倒ですね。class A { private $classB; public function __construct(B $classB) { $this->classB = $classB; } /** * foobazを取得する * * @return string */ public function fooBaz(): string { // デメテルチェーン発生。もしクラスD、クラスEがあったらゾッとしますね $bar = $this->classB->getC()->methodBaz(); return 'foo' . $bar; } } class B { private $classC; public function __construct(C $classC) { $this->classC = $classC; } public function getC() { return $this->classC; } } class C { public function methodBaz() { return 'baz'; } }Mockeryを使ったテストコード
shouldReceiveには
method1->method2という書き方ができます。
クラスCのモックまで作らなくても良くなります。class Test_A extends TestCase { public function testFooBaz() { /** @var B $stubB クラスBの代替オブジェクト */ $stubB = Mockery::mock(B::class); // デメテルチェーンが発生していても、モックオブジェクト沢山作らなくて済む $stubB->shouldReceive('getC->methodBaz')->andReturn('baz'); $classA = new A($stubB); $this->assertEquals('foobaz', $classA->fooBaz()); } }クラスAのテストで、依存しているクラスBを期待した条件で利用していることを確認したい
サンプルコード
イメージしやすくなると思って、クラスBをMailerとしています。
クラスAのsendMailWithSignature()って、正しくMailerを使ってるの? を確かめるテスト書きたい。class A { private $mailer; public function __construct(Mailer $mailer) { $this->mailer = $mailer; } public function sendMailWithSignature($mailaddress, $body): void { $body .= ' signature!!!'; $this->mailer->send($mailaddress, $body); } } class Mailer { public function send(string $mailaddress, string $body): string { // 実際にはメールを送信する処理 return 'メール送ったよ'; } }Mockeryを使ったテストコード
with()メソッドやonce()メソッドで、期待している引数や呼ばれる回数を指定できます。
メール1通送るつもりがバグで100通送っちゃった!のようなことが無くなりますね。class Test_A extends TestCase { public function tearDown() { parent::tearDown(); Mockery::close(); } public function testFooBar() { $mailaddress = 'foo@bar.baz'; $body = 'mail body'; $mockMailer = Mockery::mock(Mailer::class); // sendメソッドに、第1引数が「foo@bar.baz」、第2引数が「mail body signature!!!」で // 1回だけ実行されるはず $mockMailer->shouldReceive('send') ->with($mailaddress, $body . ' signature!!!') ->once(); $classA = new A($mockMailer); $classA->sendMailWithSignature($mailaddress, $body); } }おわりに
これらのようなテスト対象が依存しているものの代替品のことをテストダブルといいます。
テスト対象以外の影響を受けなくなり、本当にテストしたいものに集中できる利点があります。
また、テストダブルを念頭に置いてクラス設計することで、クラスの責務を意識するようになり、神クラスを作りづらいという利点もあると思っています。
なんでもできる神クラスは作りたくないものですね。
- 投稿日:2019-12-19T20:13:39+09:00
Laravel で Rails の抽象 ActiveModel みたいなやつが欲しくなった
TL;DR
車輪の再発明してしまったのでソースコードの墓場にする記事
内容
データベースを使わないけど,
app/Services配下に置くような外部 API サービスのレスポンスエンティティを Eloquent ライクに使いたいニーズがあった。最初 Eloquent Model を継承することも考えたが,意味のないメソッドがごちゃごちゃ入ってくるのが鬱陶しいので
HasAttributesHidesAttributesGuardsAttributesの3つのトレイトをベースに,足りないものや動作修正の必要があるものを適宜ケアして,最小限の記述で作ってみたのでここにぶん投げます。IDE でチェックしながらメソッド埋めやったのでエラーとかは起こらないはず…
<?php namespace App\AbstractEntities; use Illuminate\Contracts\Support\Arrayable; use Illuminate\Database\Eloquent\Concerns\GuardsAttributes; use Illuminate\Database\Eloquent\Concerns\HasAttributes; use Illuminate\Database\Eloquent\Concerns\HidesAttributes; use Illuminate\Database\Eloquent\MassAssignmentException; use Illuminate\Support\Collection; use LogicException; abstract class Entity implements Arrayable { use HasAttributes, HidesAttributes, GuardsAttributes; public const CREATED_AT = 'created_at'; public const UPDATED_AT = 'updated_at'; /** * @var bool */ public $timestamps = true; /** * @var bool */ public $incrementing = true; /** * @var string */ protected $primaryKey = 'id'; /** * @var string */ protected $keyType = 'int'; /** * @var array */ protected $relations = []; /** * Create an existing Entity instance. * * @param array $attributes * @return static */ public static function hydrate(array $attributes = []) { return (new static())->forceFill($attributes)->syncOriginal(); } /** * Create existing Entity instances. * * @param array $attributesArray * @return \Illuminate\Support\Collection|static[] */ public static function hydrateMany(array $attributesArray = []): Collection { $collection = new Collection(); foreach ($attributesArray as $key => $attributes) { $collection[$key] = static::hydrate($attributes); } return $collection; } /** * Create a new Entity instance. * * @param array $attributes */ public function __construct(array $attributes = []) { $this->fill($attributes); } /** * Apply updates to an existing Entity instance. * * @param array $attributes * @return $this */ public function apply(array $attributes) { return $this->forceFill($attributes)->syncOriginal(); } /** * Fill the model with an array of attributes. * * @param array $attributes * @throws \Illuminate\Database\Eloquent\MassAssignmentException * @return $this */ public function fill(array $attributes) { $totallyGuarded = $this->totallyGuarded(); foreach ($this->fillableFromArray($attributes) as $key => $value) { if ($this->isFillable($key)) { $this->setAttribute($key, $value); } elseif ($totallyGuarded) { throw new MassAssignmentException(sprintf( 'Add [%s] to fillable property to allow mass assignment on [%s].', $key, get_class($this) )); } } return $this; } /** * Fill the model with an array of attributes. Force mass assignment. * * @param array $attributes * @return $this */ public function forceFill(array $attributes) { return static::unguarded(function () use ($attributes) { return $this->fill($attributes); }); } /** * Determine if the given relation is loaded. * * @param string $key * @return bool */ public function relationLoaded(string $key): bool { return array_key_exists($key, $this->relations); } /** * Set the given relationship on the model. * * @param string $key * @param mixed $value * @return $this */ public function setRelation(string $key, $value) { $this->relations[$key] = $value; return $this; } /** * Determine if the model uses timestamps. * * @return bool */ public function usesTimestamps(): bool { return $this->timestamps; } /** * Get the value indicating whether the IDs are incrementing. * * @return bool */ public function getIncrementing(): bool { return $this->incrementing; } /** * Get the primary key for the model. * * @return string */ public function getKeyName(): string { return $this->primaryKey; } /** * Get the auto-incrementing key type. * * @return string */ public function getKeyType(): string { return $this->keyType; } /** * Get the format for database stored dates. * * @return string */ public function getDateFormat(): string { return $this->dateFormat ?? 'Y-m-d H:i:s'; } /** * Get the database connection for the model. */ public function getConnection(): void { throw new LogicException(static::class . ' is not an Eloquent Model; Database Connection is not available.'); } /** * Dynamically retrieve attributes on the model. * * @param string $key * @return mixed */ public function __get(string $key) { return $this->getAttribute($key); } /** * Dynamically set attributes on the model. * * @param string $key * @param mixed $value */ public function __set(string $key, $value): void { $this->setAttribute($key, $value); } /** * Determine if an attribute or relation exists on the model. * * @param string $key * @return bool */ public function __isset(string $key): bool { return $this->getAttribute($key) !== null; } /** * Unset an attribute on the model. * * @param string $key */ public function __unset(string $key): void { unset($this->attributes[$key]); } /** * Convert the model instance to an array. * * @return array */ public function toArray(): array { return array_replace($this->attributesToArray(), $this->relationsToArray()); } }機能制約
- データベース絡む系は全部無し
- リレーションは手動で
setRelation()するのみ- APIレスポンスとしてそのまま返すことも無いと思うのでインタフェースは
Jsonableとかは実装せずArrayableのみ
ArrayAccessも基本使わないので無しexisting()で外部 API からの取得結果レスポンスからのインスタンス生成を想定apply()で外部 API からの更新結果レスポンスをインスタンスに反映することを想定既存のライブラリ
- 投稿日:2019-12-19T19:48:17+09:00
phpのenctype= multipart/form-data での文字化け
画像を載せるのに
formにenctype= multipart/form-dataを追加したところ
他の文字が文字化けしてしまいます。
print_r(mb_get_info());で確認したところArray ( [internal_encoding] => UTF-8 [http_output] => UTF-8 [http_output_conv_mimetypes] => ^(text/|application/xhtml\+xml) [func_overload] => 0 [func_overload_list] => no overload [mail_charset] => ISO-2022-JP [mail_header_encoding] => BASE64 [mail_body_encoding] => 7bit [illegal_chars] => 0 [encoding_translation] => On [language] => Japanese [detect_order] => Array ( [0] => ASCII [1] => JIS [2] => UTF-8 [3] => EUC-JP [4] => SJIS ) [substitute_character] => 63 [strict_detection] => Off )で問題なさそうです。encoding_translationをOffなどにしても直りませんでした。
mb_convert_encoding($_POST["name"], "UTF-8", "HTML-ENTITIES")
やASCIIからの変換を試して見ても直りませんでした。
どうやったら解決できそうですかね。
- 投稿日:2019-12-19T18:00:25+09:00
BEAR.Sundayでカスタムhostを利用する
はじめに
BEAR.Sundayアドベントカレンダーの天重さんの記事ではカスタムスキームの登録でしたが、この記事ではhostの登録です。
HTTPのURLでいえばプロトコルは変わらずホストが違う場合ということです。やりたいこと
例えば社内で共通の処理を独立した形で公開し、複数のアプリケーションにまたがって利用したいことなどないでしょうか?
最終的にアプリケーションからこんな感じで社内ライブラリのResourceを利用できたら便利そうですね。$article = $this->resource->get('app://acme/article');ライブラリ(AcmeFramework)を作る
各アプリケーションから共通機能を利用するためのブリッジになるような基盤の仕組みを作成します。
このAcmeFrameworkはcomposerのパッケージとして各アプリケーションにインストールされる想定です。SchemeCollectionProvider
標準ではResourceアクセスの際のスキーマやホストの解決のために、BEAR\Resource\Module\SchemeCollectionProviderが用意されていますので、これを参考にします。
<?php declare(strict_types=1); namespace Acme\Framework\Provider; use BEAR\Resource\AppAdapter; use BEAR\Resource\HttpAdapter; use BEAR\Resource\SchemeCollection; use BEAR\Resource\SchemeCollectionInterface; use Ray\Di\Di\Named; use Ray\Di\InjectorInterface; use Ray\Di\ProviderInterface; class SchemeCollectionProvider implements ProviderInterface { /** * @var string */ private $appName; /** * @var InjectorInterface */ private $appInjector; /** * @var InjectorInterface */ private $appInjectorAcme; /** * @var string */ private $appNameAcme; /** * @param string $appName * @param string $appNameAcme * @param InjectorInterface $appInjector * @param InjectorInterface $appInjectorAcme * * @Named("appName=app_name, appNameAcme=app_name_acme, appInjectorAcme=acme") */ public function __construct( string $appName, string $appNameAcme, InjectorInterface $appInjector, InjectorInterface $appInjectorAcme ) { $this->appName = $appName; $this->appNameAcme = $appNameAcme; $this->appInjector = $appInjector; $this->appInjectorAcme = $appInjectorAcme; } /** * Return instance * * @return SchemeCollectionInterface */ public function get() : SchemeCollectionInterface { $schemeCollection = new SchemeCollection; $pageAdapter = new AppAdapter($this->appInjector, $this->appName); $appAdapter = new AppAdapter($this->appInjector, $this->appName); $acmeFrameworkAdapter = new AppAdapter($this->appInjectorAcme, $this->appNameAcme); $schemeCollection->scheme('page')->host('self')->toAdapter($pageAdapter) ->scheme('app')->host('self')->toAdapter($appAdapter) ->scheme('http')->host('self')->toAdapter(new HttpAdapter($this->appInjector)) ->scheme('app')->host('acme')->toAdapter($acmeFrameworkAdapter); return $schemeCollection; } }コストラクタで
$appNameと$appNameAcme、$appInjectorと$appInjectorAcmeとそれぞれ2種類扱っているのは、ライブラリを利用するアプリケーションとライブラリ自体、それぞれ2つのBEAR.Sundayアプリケーションを扱うためです。AcmeFrameworkModule
上記のSchemeCollectionProviderが実際に適用されるようにしたいと思います。アプリケーションからはModuleをインストールすることで利用できます。
<?php namespace Acme\Framework\Module; use BEAR\Package\AppInjector; use BEAR\Resource\SchemeCollectionInterface; use Acme\Framework\Provider\SchemeCollectionProvider; use Acme\Framework\Query\PrioritizedSqlQueryModule; use Ray\Di\AbstractModule; use Ray\Di\InjectorInterface; class AcmeModule extends AbstractModule { /** * @var AbstractAppMeta */ private $appMeta; public function __construct(AbstractAppMeta $appMeta) { $appName = str_replace('\\Module', '', __NAMESPACE__); $rootDir = dirname(__DIR__, 2); $this->appMeta = new Meta($appName, 'appacme', $appMeta->appDir); $this->appMeta->appDir = $rootDir; parent::__construct(); } /** * @throws \ReflectionException */ public function configure() { $rootDir = dirname(__DIR__, 2); $appName = str_replace('\\Module', '', __NAMESPACE__); $this->bind(InjectorInterface::class)->annotatedWith('acme') ->toInstance(new AppInjector($appName, 'app')); $this->bind()->annotatedWith('app_name_acme')->toInstance($appName); $this->bind(SchemeCollectionInterface::class)->toProvider(SchemeCollectionProvider::class); } }コンストラクタで
AppMetaを色々操作しているのは、以下のような設定を実現するためです。
- appDir(アプリケーションルートディレクトリ)
- パッケージがインストールされたディレクトリ
vendor/acme-framework- コンテキスト
appacmeに設定。何でも良いのですが、アプリケーションとバッティングしないように。- tmpDir(キャッシュなどの一時ファイル置き場)
- アプリケーションに用意されたディレクトリ。通常は
{$appDir}/var/tmp/{$context}- logDir(ログ置き場)
- アプリケーションに用意されたディレクトリ。通常は
{$appDir}/var/log/{$context}アプリケーションでの利用
上記Moduleをinstallするだけです。
BEAR\Package\PackageModuleよりも先にinstallする必要があります。<?php namespace Acme\Application\Module; use BEAR\Package\AbstractAppModule; use BEAR\Package\PackageModule; use Acme\Framework\Module\AcmeFrameworkModule; use ReflectionException; class AppModule extends AbstractAppModule { /** * {@inheritdoc} * * @throws ReflectionException */ protected function configure() { $this->install(new AcmeFrameworkModule($this->appMeta)); $this->install(new PackageModule); } }これで以下のようにアプリケーション内でライブラリのリソースにアクセスできるようになっていると思います。
$article = $this->resource->get('app://acme/article');おまけ
Resourceの中でSQLを扱う場合、コードからインフラに関わるコード(MySQLやKVS、APIへのアクセスなど)を排除するためのRay.QueryModuleというものがあり、よくSQLのInjectで利用されていると思います。
例えばこれが以下のようなルールで運用できたら更に処理の共通化がしやすいのではないでしょうか。
- アプリケーションに該当のSQLファイルが用意されていたらアプリケーションのSQLファイルを使う
- アプリケーションに該当のSQLファイルが用意されていなかったらライブラリのSQLファイルを使う
つまり、sqlファイルの探索を優先順位がつけられた二箇所から行うようにするということです。
PrioritizedSqlQueryModule
<?php namespace Acme\Framework\Query; use Ray\Di\AbstractModule; use Ray\Query\Annotation\AliasQuery; use Ray\Query\QueryInterface; use Ray\Query\RowInterface; use Ray\Query\RowListInterface; use Ray\Query\SqlAliasInterceptor; use Ray\Query\SqlQueryRow; use Ray\Query\SqlQueryRowList; class PrioritizedSqlQueryModule extends AbstractModule { /** * @var string */ private $sqlDirPrimary; /** * @var string */ private $sqlDirSecondary; public function __construct(string $sqlDirPrimary, string $sqlDirSecondary, AbstractModule $module = null) { $this->sqlDirPrimary = $sqlDirPrimary; $this->sqlDirSecondary = $sqlDirSecondary; parent::__construct($module); } /** * {@inheritdoc} */ protected function configure() : void { $this->bindSql($this->sqlDirSecondary); $this->bindSql($this->sqlDirPrimary); $this->bindInterceptor( $this->matcher->any(), $this->matcher->annotatedWith(AliasQuery::class), [SqlAliasInterceptor::class] ); } private function bindSql(string $sqlDir) : void { foreach ($this->files($sqlDir) as $fileInfo) { /* @var \SplFileInfo $fileInfo */ $fullPath = $fileInfo->getPathname(); $name = pathinfo($fileInfo->getRealPath())['filename']; $sqlId = 'sql-' . $name; $this->bind(QueryInterface::class)->annotatedWith($name)->toConstructor( SqlQueryRowList::class, "sql={$sqlId}" ); $this->bindCallableItem($name, $sqlId); $this->bindCallableList($name, $sqlId); $sql = trim(file_get_contents($fullPath)); $this->bind('')->annotatedWith($sqlId)->toInstance($sql); } } protected function bindCallableItem(string $name, string $sqlId) : void { $this->bind(RowInterface::class)->annotatedWith($name)->toConstructor( SqlQueryRow::class, "sql={$sqlId}" ); } protected function bindCallableList(string $name, string $sqlId) : void { $this->bind()->annotatedWith($name)->toConstructor( SqlQueryRowList::class, "sql={$sqlId}" ); $this->bind(RowListInterface::class)->annotatedWith($name)->toConstructor( SqlQueryRowList::class, "sql={$sqlId}" ); } private function files($dir) : \RegexIterator { return new \RegexIterator( new \RecursiveIteratorIterator( new \RecursiveDirectoryIterator( $dir, \FilesystemIterator::CURRENT_AS_FILEINFO | \FilesystemIterator::KEY_AS_PATHNAME | \FilesystemIterator::SKIP_DOTS ), \RecursiveIteratorIterator::LEAVES_ONLY ), '/^.+\.sql$/', \RecursiveRegexIterator::MATCH ); } }利用
以下のようにインストールします。引数で優先して探索するsqlディレクトリを指定します。
$this->install(new PrioritizedSqlQueryModule($primarySqlDir, $baseDir . '/var/db/sql'));
- 投稿日:2019-12-19T16:45:13+09:00
FuelPHPの詰まったところやらルールの備忘録
FuelPHPを使った際に詰まった点があったのでメモ。適宜更新されるかもしれない。
ファイル名は全部小文字
HelloWorld.phpをコントローラに突っ込んでたけどなんでかわからんけど全く動かない。
調べたらコーディング標準でファイル名はすべて小文字にするように定められているらしい。PHPのコードしかないときは?>を省略する
見つけにくい「死の白い画面」を避けるためにこのようなルールになっているとのこと。
文字コードはUTF-8
まあそりゃそうだって感じですけどShift-JISで書いた日本語の文字列をビューに持っていったら無かったことにされた。
ClassやFunctionの単語区切りはアンダースコア(_)
キャメルケースでの単語区切りはダメ(例:ControllerHelloはダメ、Controller_Helloと書く)。
これはアンダースコアをオートロードの際のディレクトリの区切り記号に変えているかららしい。参考
- 投稿日:2019-12-19T14:00:54+09:00
【Laravel】イベントとリスナーの関係について整理する
Laravelには、イベントとリスナーという機能があります。
Laravelの公式サイトには、イベントとリスナーの作成方法は書かれているものの、イベントとはなにか、リスナーとは何かについては書かれていないようです。
エンジニアとして基礎中の基礎だから教えるまでもないということなのでしょうか。
残念ながら僕はその基礎ができていないようなのでイベントとリスナーについて調べてみました。イベントとリスナーの関係性
https://www.ritolab.com/entry/35
こちらの記事を参考にすると、こう説明されています。オブザーバパターンとは、デザインパターンの一つで、簡単に言うと監視される側と監視する側の関係を持つプログラム構造で、監視される側の変化(イベント=発行)を、監視する側(リスナー=購読)がキャッチして処理を行う。
イベントとリスナーの関係のことをオブザーバーパターンというらしいです。
そして、毎回混乱するのは、監視される・監視するの意味です。監視の定義が曖昧すぎて、もっと噛み砕いてもらわないとわかりません。
なので、僕なりに噛み砕いてみました。イベントとリスナーの書き方
イベントは監視される側らしいのですが、まず誰に監視されるのかというと、リスナーですね。ここまではわかります。
そして、何を監視されるのか、これは、イベントの中身です。イベントは、以下のようなeventヘルパを利用することで呼び出すことができます。
event(new AccessDetection(str_random(100)));そして、
EventServiceProvider.phpにイベントが起こった時にリスナーを発動させるよう登録をしておきます。EventServiceProvider.php<?php namespace App\Providers; use Illuminate\Support\Facades\Event; use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider; class EventServiceProvider extends ServiceProvider { /** * The event listener mappings for the application. * * @var array */ protected $listen = [ 'App\Events\Event' => [ 'App\Listeners\EventListener', ], // アクセス時にイベントを発行する側 'App\Events\AccessDetection' => [ // テキストを生成&書き込みを行うリスナー側 'App\Listeners\MakeTextListener', ], ]; /** * Register any events for your application. * * @return void */ public function boot() { parent::boot(); // } }
app/Events/AccessDetection.phpの中身に、値の受け渡しなどを記述しておきます。
そうすると、イベントが呼び出されたときに、リスナーで値を使うことができます。最後に、リスナーの内容を記述します。
app/Listeners/MakeTextListener.php<?php namespace App\Listeners; use App\Events\AccessDetection; use Illuminate\Queue\InteractsWithQueue; use Illuminate\Contracts\Queue\ShouldQueue; class MakeTextListener { /** * Create the event listener. * * @return void */ public function __construct() { // } /** * Handle the event. * * @param AccessDetection $event * @return void */ public function handle(AccessDetection $event) { // テキストファイル作成 $file = sprintf('%s/%s.txt', storage_path('texts'), date('Ymd-His')); touch($file); // 書き込み $current = file_get_contents($file); $current .= $event->param; file_put_contents($file, $current); } }イベントからリスナーが発動するまでの流れ
イベントが起こって、リスナーが発動するまでの流れを整理しておきます。
- コントローラーでeventヘルパを使って、イベントを起こす
- プロバイダーに登録してあるイベントが呼び出される
- イベントに記述した値がリスナーに受け渡される
- リスナーが発動する
このような流れになります。
イベントとリスナーの使いみち
一般的には、コントローラーをコンパクトにするために使われます。
例えばメール送信機能は、ユーザ登録時、購入時、更新時などで共通して使われますが、それをコントローラーに書いていたら内容が重複してしまいます。
なので、イベントとリスナーに記述しておいて、それぞれのコントローラからイベントを起こすだけで、メール送信ができるようにしておくのです。
使いみちはいろいろありますが、メールやslackへの通知の際に使うのが一般的かなと思います。
- 投稿日:2019-12-19T13:53:26+09:00
参照渡しでのエラー「Only variables should be passed by reference」について
最近コード書いてたら出くわしたエラーに関して忘備録として残しておきます。
間違えてる箇所がありましたら優しくご指摘お願いします。「Only variables should be passed by reference」
ざっくりというと「参照渡しは変数しか使えませんよ〜」っていうエラーです。下のコードのように、storeContactInfoの引数の中に直接「$id = null」を入れていたのですが、
このやり方ではダメっぽいです。foreach ($infoList as $info) { // 登録処理 $this->storeContactInfo($name, $id = null); } private function saveContactInfo(string $name, ?int &$id) { (省略) // DBに登録 $contact->save(); }そのため、一旦変数に入れてあげて、その変数を引数で使うようにしてあげたら無事に解決しました。
foreach ($infoList as $info) { $id = null; // 登録処理 $this->storeContactInfo($name, $id); }一見おかしくはなさそうなんですけど、saveContactInfo()側は参照渡しを要求しているのに、
呼び出し側で値渡ししているのが原因でおきてるそうです。解決できたので、めでたしめでたし。
- 投稿日:2019-12-19T13:42:12+09:00
composer経由でGuzzleをインストールしたらメモリエラーになったときの対処法
Guzzle https://github.com/guzzle/guzzle
をインストールしようとしたら以下のようなエラーが出た。Installation failed, reverting ./composer.json to its original content. The following exception is caused by a lack of memory or swap, or not having swap configured Check https://getcomposer.org/doc/articles/troubleshooting.md#proc-open-fork-failed-errors for details PHP Warning: proc_open(): fork failed - Cannot allocate memory in phar:///usr/local/bin/composer/vendor/symfony/console/Application.php on line 952 Warning: proc_open(): fork failed - Cannot allocate memory in phar:///usr/local/bin/composer/vendor/symfony/console/Application.php on line 952 [ErrorException] proc_open(): fork failed - Cannot allocate memoryメモリが足りないらしい。
対処法
↑のエラーに表示される
https://getcomposer.org/doc/articles/troubleshooting.md#proc-open-fork-failed-errors
に書いてある以下のコマンドを順番に実行して、スワップファイルを作る&有効化する。/bin/dd if=/dev/zero of=/var/swap.1 bs=1M count=1024 /sbin/mkswap /var/swap.1 /sbin/swapon /var/swap.1Permission denied と出た場合は
sudoをつけてください。参考
https://getcomposer.org/doc/articles/troubleshooting.md#proc-open-fork-failed-errors
https://evisu0414.com/engineer/engineer010/
- 投稿日:2019-12-19T13:21:15+09:00
php7.4からついにクラスのメンバ変数に型宣言できるようになった
php7.4からクラスのプロパティは、新たに型宣言をサポートするようになりました。
遂に!って感じですね。
php7系、特に7.3など使用しているとアップデートするか悩ましいですね。
パフォーマンスの改善も結構されているみたいですし。<?php class User { public int $id; public string $name; } ?>上の例では、
$user->idにはintの値だけを、$user->nameにはstringの値だけを代入できるように強制します。
- 投稿日:2019-12-19T12:01:28+09:00
CakePHP3をはじめて触ったときに読んだものをまとめておく
このエントリは、 コネヒト Advent Calendar 2019 の19日目のエントリです。
はじめに
今年7月にコネヒトに入社しまして、5ヶ月ほど経過しました @takoba です。
コネヒトではサーバーサイドの主要言語としてPHPを採用しており、その上で採用しているアプリケーションフレームワークは CakePHP です。一方でぼくは、PHPの経験こそあれど入社するまでCakePHPを触ったことがありませんでした。
先日、同じような書き出しで 所感エントリ を書いたのですが、今回はCakePHPにはじめて触った際に、理解を進めたりするために読んだものをご紹介しようと思います。
前提
筆者のおおよそのスペック
- PHPは8年以上経験
- 基本LAMP環境で触ってきた
- CodeIgniter v1(PHP5.4)でのアプリケーション開発経験を3年ほど
- Zend Framework v1(PHP5.3)でのアプリケーション開発経験が3年ほど
- Laravel v5(PHP7.1)でのアプリケーション開発経験が半年ほど
- その他、主に以下を経験
- Ruby on Rails v4
- Vue.js v2
読んだもの
CakePHP Cookbook
https://book.cakephp.org/3/ja/index.html公式のドキュメントです。概要から細かいリファレンスまで諸々書いてあります。
一通り日本語訳もなされているので、「英語が苦手で...」という方も安心です。個人的によく読んだトピックを以下に挙げてみました。
サンプルコードなどを見て「なんとなく書けるかな〜〜」というところから自らが実現したい処理を書けるようになるためには、ツールの詳しい使い方を知る必要があると思います。そんなときに役に立った印象です。
- エンティティー - 3.8
- クエリービルダー - 3.8
- 発行したいSQLをどうやって表現すべきか、めちゃくちゃこのページとにらめっこしてましたね...
- データベースの基本 - 3.8
- トランザクションをどう実装するかを探してたらここにたどり着きました
- テスト - 3.8
- Controllerのテスト や Tableクラスのテスト あたりは頻出なのでちょくちょく読んでた気がします
- エラーと例外の処理 - 3.8
- この辺の機構が割とメタ的に(というかフレームワークでだいぶよしなにしてくれるように)書かれてたりするので、仕組みを知りたくて読んでました
インターネット上のエントリ
この場を借りて、めちゃくちゃお世話になりましたので御礼申し上げます...?
具体的な問題にハマったときだけではなく、「あれ?この機能って普通どう使うんだろう??」みたいなtipsや「こういう実装を実現したいとき、何を使うとよいのだろう...」といったフレームワークを用いた実装パターンを捉えるのにも役に立った印象です。以下に、手元に保存してあったエントリをいくつか置いといてみるんですが、見事にQiitaのエントリばかりでした...Qiitaさすが...
また、CakePHPのアドベントカレンダーにもめちゃくちゃお世話になってました。機能の紹介やCakePHPを用いたアプリケーションの設計方針など、参考になるエントリがたくさんです。
来年はひとつくらいエントリ書いて貢献できるとよいなあ...
- CakePHP Advent Calendar 2014 - Qiita
- CakePHP Advent Calendar 2017 - Qiita
- CakePHP Advent Calendar 2018 - Qiita
- CakePHP Advent Calendar 2019 - Qiita
コーディング規約
https://book.cakephp.org/3/ja/contributing/cakephp-coding-conventions.htmlCakePHP3は PSR-21 をベースにしたコーディング規約を定義しています。
cakephp/cakephp-codesniffer という、上記のコーディング規約に準拠するための PHP_CodeSniffer 向け設定もあるので、IDEに組み込んだりcommit前に実行するようにして、規約にそったコーディングをするようにします。
その上で、上記のようなフォーマッターに指摘されないようなコーディングをするために、ざっと内容を読みつつ実践できるようにしていきました。最初、自社のアプリケーションにPull Requestを投げた際には、コーディング規約を踏まえた変更になっておらず、レビュアーにコードフォーマットに関する指摘をさせてしまいました...
コードフォーマットの指摘も度々レビュアーのコストになってしまうので、ツールに頼ったりしつつ事前にコーディング規約の検査をしておかないといけないですね。フレームワーク自体のソースコード
正直、ホントに困ったときはこれが一番です。フレームワークが提供しているクラスや関数が、受け取る引数によってどのような振る舞いをするのかは、実際にその定義を読んだり実行してみたりすることで理解が深まります。
手元にCakePHPで作られたプロジェクトがあれば、その中にあるControllerクラスなどの実装から辿っていくとわかりやすくてよいです。
もしそういうプロジェクトが手元にない場合、GitHubに公開されているCakePHPのサンプルアプリなどでとりあえず補ってみてもよいかもしれません。ちなみに、以下は ぼくが知ってる書籍 のサンプルコードがホストされてるレポジトリです。実際にlocalで動かしながら観察できるので、オススメです?
https://github.com/php-book/php-qa-plaza/定義元ジャンプ機能を有効活用
ソースコードリーディングをする場合、IDEなどにある「定義元ジャンプ機能」を用いると、とっても捗ります。
ぼくはコネヒトに入ってから、 PhpStorm をメインで使うようになりましたが、プロジェクトのディレクトリをIDEで読み込んで置いて、composer installをしてパッケージを落としておけば、採用しているパッケージ側の定義までサクサク読みにいけます。
もともとvimをメインで使ってて、vimでも universal-ctags を利用すれば定義元ジャンプはできるのですが、PhpStorm(IntelliJ)にはここでは語り切れないたくさんの機能があり、そういうのを含めて「やっぱりIDE便利やな...」ってなっている昨今です。番外編:
cake consoleコマンド(インタラクティブコンソール)とはいえ、ソースコードを読むだけだと、具体的な挙動については頭の中で想像するしかありません。手元にCakePHPで作られたプロジェクトがあるならば、その中に組み込まれているインタラクティブコンソールを利用すると具体的に動作が確認できて便利です。
例えば、「FrozenTimeで使えるmethodを雑に確認したいな〜〜」ってときに、
bin/cake consoleコマンドを実行して、以下のようなかんじに確かめられます。>>> use Cake\I18n\FrozenTime >>> new FrozenTime => Cake\I18n\FrozenTime @1576723101 {#2520 date: 2019-12-18 20:38:21.933676 Asia/Tokyo (+09:00), } >>> (new FrozenTime)->subDays(5) => Cake\I18n\FrozenTime @1576291118 {#2522 date: 2019-12-13 20:38:38.390307 Asia/Tokyo (+09:00), }実際にORMを用いてクエリも実行できるので、サクッと検証したい場合も便利です。
おわりに
挙げてみたら、思ったほどCakePHPに限らず"新たなアプリケーションフレームワークを触るときに読むとよさそうなもの"としても捉えられそうかな?と思ったので、他のアプリケーションフレームワークを触るときにも上記に倣って学んでいこうな!と思った次第です。
というわけで以上になります!新たにCakePHPを触るみなさんの参考になれば幸いです〜〜?
*
[PR] エンジニア絶賛募集中です!!!
PHPとAWSで開発!生活領域における課題を解決したいエンジニア募集!
*
[PR] 弊社で採用している開発環境を参考に書かれたCakePHP本もよろしくな!!!
(こちらもCakePHP3をはじめて触る方にオススメです!)TECHNICAL MASTER はじめてのPHPプロフェッショナル開発 PHP7対応 | 伊藤 翔, 金城 秀樹, 高野 福晃, 永井 勝一郎 |本 | 通販 | Amazon
ちなみにPSR-2はすでにdeprecatedになり、新たに PSR-12 を利用することが推奨されています。CakePHPも 4.0.0の時点ではまだPSR-2準拠のままになっている ようですが、 cakephp/cakephp-codesniffer を眺めてみると4.0.0に向けたbranchではPSR-12に準拠するための準備が始まっているようです。今後に期待! ↩
- 投稿日:2019-12-19T11:35:03+09:00
SQLインジェクションとその対策(PHP + PDO)
この記事は、North Detail Advent Calendar 2019 の21日目の記事です。
はじめに
先日、社内で「安全なウェブサイトの作り方」を使った読書会が行われました
SQLインジェクションについて議題に上がったので、
勉強がてら実際に試してみよう!というのが今回の趣旨になります。SQLインジェクション?
SQLインジェクション(英: SQL Injection)とは、
アプリケーションのセキュリティ上の不備を意図的に利用し、
アプリケーションが想定しないSQL文を実行させることにより、
データベースシステムを不正に操作する攻撃方法のこと。「Injection」という単語が「注入する」という意味のようです(知らなかった)。
アプリケーションが実行するSQL文に変なものを注入して
意図しない動作をさせる攻撃なので「SQL Injection」ってことですね実際に起こしてみる
データを作る
まずはMySQLにテスト用データを作成します。
CREATE TABLE users ( id int NOT NULL AUTO_INCREMENT, name varchar(10), PRIMARY KEY (id) ); INSERT INTO users(name) VALUES ('フグ田サザエ'), ('フグ田マスオ'), ('磯野波平'), ('磯野フネ'), ('磯野カツオ'), ('磯野ワカメ'), ('フグ田タラオ'), ('タマ') ;
テスト用画面を作る
次は、IDを入力してユーザーを検索できる画面を作ります。
PDOクラスを使ってMySQLにアクセスしています。
injection.php<?php try { $records = []; $id = $_POST['id']; $pdo = new PDO( 'DSN', 'USERNAME', 'PASSWORD' ); $prepare = $pdo->prepare('SELECT * FROM users WHERE id = '. $id. ';'); $prepare->execute(); $records = $prepare->fetchAll(PDO::FETCH_ASSOC); } catch(PDOException $e) { echo $e->getMessage(); } ?>うーんシンプル。
ここで…文字列結合を使ってクエリ組み立てていますね
$prepare = $pdo->prepare('SELECT * FROM users WHERE id = '. $id. ';'); $prepare->execute();攻撃してみる
先ほどの例では「3」と検索して「磯野波平」のデータを取得しました。
じゃあ次は「0 OR TRUE; --」で…
usersテーブルの全レコードがヒットしてしまいましたユーザー入力をそのまま採用した結果、こんなSQL文になりました。
WHERE句が常にTRUEになるので、全てのレコードが対象になったんですね。SELECT * FROM users WHERE id = 0 OR TRUE; --;ちょっと付け加えて、お次は「0 OR TRUE; DELETE FROM users; --」で検索…
あれ?さっきと同じ??
MySQLを覗いてみると…
な ん と い う こ と で し ょ う
匠の手によって全てのレコードが削除されてしまいました先ほどの例とだいたい同じですが、
検索クエリを正常終了させた後にDELETE文が実行されちゃってますね…SELECT * FROM users WHERE id = 0 OR TRUE; DELETE FROM users; --;対策する
SQL 文の組み立ては全てプレースホルダで実装する。
「安全なウェブサイトの作り方」ではmysqliが使われていましたが、PDOでもプレースホルダを使って実装できます!
先ほどは文字列結合を使ってクエリを組み立てていましたが、
今度は「:id」のようにコロン+パラメータ名の形でクエリ内に記載します。そしてクエリ実行の前に
bindValue関数を使ってバインドします。
(bindParam関数でもバインドできます。 変数が評価されるタイミングが異なるそうです。)修正前$prepare = $pdo->prepare('SELECT * FROM users WHERE id = '. $id. ';'); $prepare->execute();
修正後$prepare = $pdo->prepare('SELECT * FROM users WHERE id = :id;'); $prepare->bindValue(':id', $id, PDO::PARAM_INT); $prepare->execute();プレースホルダを使ってバインドすると、クエリとして解釈されないようです。
あくまで「値」として扱われるため、変なものを注入した場合でもクエリとして実行されないんですね。実際にさっきの攻撃を試してみましょう
何も検索されなくなりました!
今度は磯野家も無事でした…
まとめ
SQLインジェクションによる攻撃と、その対策方法について実際に試してみました。
SQLインジェクションやプレースホルダによる対策自体は知っていて、学生時代にJavaで一度やったことある程度でした。
その時は理解が浅い状態だったので、仕組みも含めて復習できて良かったです![]()
明日は
「North Detail Advent Calendar」 22日目は @yamatohkd さんです!
→ スマホカメラで手のモーションを記録してUnityでピアノ演奏したかった参考
安全なウェブサイトの作り方
https://www.ipa.go.jp/security/vuln/websecurity.htmlPDOStatementクラス
https://www.php.net/manual/ja/class.pdostatement.phpサザエさんの登場人物
https://ja.wikipedia.org/wiki/サザエさんの登場人物
- 投稿日:2019-12-19T11:26:23+09:00
macOS 10.15 Catalina 上で phpenv を使って PHP 5.6 を build するのにものすごく苦労した話
はじめに
この記事は GMOペパボ Advent Calendar 2019 19日目の記事です。
新しく入ったメンバーが macOS 10.15 Catalina 上で PHP 5.6 を build しようとしてハマっていたのを、いろいろ試行錯誤してなんとか解決した(というか回避した)ので、その話を書こうと思います。
Mojave + XCode 10 から /usr/include がつくられなくなった
この話は、macOS 10.15 Catalina から始まった話ではなく、macOS 10.14 Mojave の仕様変更がそもそもの発端になっています。
どのような仕様変更があったかというと、Mojave + XCode 10 から
/usr/includeが作られなくなりました。 (/usr/libは今まで通りあります)じゃあ、ヘッダーファイルはどこにあるんだ?ということですが、次の場所にインストールされています。
- /Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/include
xcrun --show-sdk-pathというコマンドを実行すると、SDKのパスが返却されます。Mojave で実行すると
/Library/Developer/CommandLineTools/SDKs/MacOSX10.14.sdkが返却されますが、これは/Library/Developer/CommandLineTools/SDKs/MacOSX.sdkへのシンボリックリンクになっています。Mojave + XCode 10 では、無理やり
/usr/includeを作るための回避策が準備されており、以下のコマンドを実行すると作ることができました。sudo installer -pkg /Library/Developer/CommandLineTools/Packages/macOS_SDK_headers_for_macOS_10.14.pkg -target / -allowUntrustedこの回避策は、Catalina + XCode 11 では使えなくなっており、Catalina では
/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/includeといい感じに付き合っていく必要があります。そして本題の PHP 5.6 を phpenv でいれる話
Catalina でも PHP 5.6 を使えるようにしようということで、いつもどおり次のコマンドを実行してもらいました。
phpenv -i development 5.6.40その結果、よく見る Configure エラーが出たので、はいはいって感じで対処していきました。(このあたりはまだ平和)
(注意) ご存知の方はご存知だと思いますが、次のものがまとめて出るのではなく、アレがないといわれえた結果アレを入れると次はコレというように順々に出ていく感じで一つ一つ潰していく感じです
configure: WARNING: This bison version is not supported for regeneration of the Zend/PHP parsers (found: 2.3, min: 204, excluded: 3.0). configure: WARNING: You will need re2c 0.13.4 or later if you want to regenerate PHP parsers. configure: error: Cannot find OpenSSL's <evp.h> configure: error: Cannot find libz configure: error: Cannot find libzconfigure: error: Please reinstall the BZip2 distribution configure: error: Please reinstall the libcurl distribution - easy.h should be in <curl-dir>/include/curl/ configure: error: Please specify the install prefix of iconv with --with-iconv=<DIR> configure: error: Please reinstall libedit - I cannot find readline.h configure: error: mcrypt.h not found. Please reinstall libmcrypt.上記のエラーに対応するために、必要なものを homebrew を使ってインストール。
brew install bison@2.7 re2c zlib bzip2 curl libiconv libedit mcryptあわせて、 PHP 5.6 では OpenSSL 1.0 が必要なので、つい最近書いた次の記事の通り、OpenSSL 1.0.2t をインストールした。
brew install https://raw.githubusercontent.com/Homebrew/homebrew-core/64555220bfbf4a25598523c2e4d3a232560eaad7/Formula/openssl.rbここまでの下準備をしつつ、次のコマンドでインストールが完了すると思ってた時期がありました。(実際、Mojaveだとこれでちゃんとインストールできます)
YACC=$(brew --prefix bison@2.7)/bin/bison \ PHP_BUILD_CONFIGURE_OPTS="--with-openssl=/usr/local/Cellar/openssl/1.0.2t --with-zlib-dir=/usr/local/opt/zlib --with-bz2=/usr/local/opt/bzip2 --with-curl=/usr/local/opt/curl --with-libedit=/usr/local/opt/libedit --with-iconv=/usr/local/opt/libiconv --with-mcrypt=/usr/local/opt/mcrypt --with-tidy" \ PHP_BUILD_EXTRA_MAKE_ARGUMENTS=-j4 \ phpenv install -i development 5.6.40上記のコマンドを実行した結果、出たのが次のエラーになります。(config.log の関連部分を抜粋)
Undefined symbols for architecture x86_64: "_libiconv", referenced from: _do_convert in gdkanji.o _zif_iconv_substr in iconv.o _zif_iconv_mime_encode in iconv.o _php_iconv_string in iconv.o __php_iconv_strlen in iconv.o __php_iconv_strpos in iconv.o __php_iconv_appendl in iconv.o ... "_libiconv_close", referenced from: _do_convert in gdkanji.o _zif_iconv_substr in iconv.o _zif_iconv_mime_encode in iconv.o _php_iconv_string in iconv.o __php_iconv_strlen in iconv.o __php_iconv_strpos in iconv.o __php_iconv_mime_decode in iconv.o ... "_libiconv_open", referenced from: _do_convert in gdkanji.o _zif_iconv_substr in iconv.o _zif_iconv_mime_encode in iconv.o _php_iconv_string in iconv.o __php_iconv_strlen in iconv.o __php_iconv_strpos in iconv.o __php_iconv_mime_decode in iconv.o ... ld: symbol(s) not found for architecture x86_64 clang: error: linker command failed with exit code 1 (use -v to see invocation) make: *** [sapi/cli/php] Error 1どうも iconv 関連がうまくリンクできてないという結果に...
なぜこうなったのか
- 本来は
/usr/local/opt/libiconv/lib以下のライブラリファイルの方をリンクしてほしいのに、XCode 11 の SDK 側のライブラリとリンクしてしまい、シンボルのズレが発生している- 実際に見てしまっているのは
/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/libを優先でみているconfigureコマンドを実行した結果生成されたMakefileを見る限り、PHP_RPATHSが次のようになっていたPHP_RPATHS = -R /Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/lib -R /usr/local/Cellar/openssl/1.0.2t/lib -R /usr/local/opt/zlib/lib -R /usr/local/opt/bzip2/lib -R /usr/local/Cellar/curl/7.67.0/lib -R /usr/local/opt/curl/lib -R /usr/local/lib -R /usr/local/opt/libiconv/lib -R /usr/local/Cellar/icu4c/64.2/lib -R /usr/local/opt/mcrypt/lib -R /usr/local/opt/libedit/libどのように回避したのか
PHP_RPATHSを指定することにより、homebrew のライブラリを優先的にリンクするようにしました。最終的には次のようなコマンドで、Catalina で PHP 5.6 を build することに成功しました。
(注意) 最低限だと
PHP_RPAHTSに/usr/local/opt/libiconv/libだけを指定すれば回避できるはずですが、他のライブラリも XCode 11 SDK 側が優先されるといやだなぁということで全部指定してますYACC=$(brew --prefix bison@2.7)/bin/bison \ PHP_RPATHS="/usr/local/Cellar/openssl/1.0.2t/lib /usr/local/opt/zlib/lib /usr/local/opt/bzip2/lib /usr/local/opt/curl/lib /usr/local/opt/libedit/lib /usr/local/opt/libiconv/lib /usr/local/opt/mcrypt/lib" \ PHP_BUILD_CONFIGURE_OPTS="--with-openssl=/usr/local/Cellar/openssl/1.0.2t --with-zlib-dir=/usr/local/opt/zlib --with-bz2=/usr/local/opt/bzip2 --with-curl=/usr/local/opt/curl --with-libedit=/usr/local/opt/libedit --with-iconv=/usr/local/opt/libiconv --with-mcrypt=/usr/local/opt/mcrypt --with-tidy" \ PHP_BUILD_EXTRA_MAKE_ARGUMENTS=-j4 \ phpenv install -i development 5.6.40こんなのやってられるか!!!
という声が多数あがりそうなので、
PHP_RPATHSやPHP_BUILD_CONFIGURE_OPTSを設定するようにphp-buildにフィードバックをこの年末年始にしようと思ってます。今、ちょうど、
php-buildのメインコミッターが travis の macOS 側の CI ベースを Xcode 10 にあげようとしているので、それに乗っかって、このフィードバックが取り込まれれば、Catalina でもphpenv installだけでいけるようになるはずです。最後に
この件に関係するような記事で、
php-buildのdefault_configure_optionsを変更するという方法を書かれているものがありますが、正直なところ、このファイルを変更することはおすすめできません。このファイルは
php-buildに含まれるファイルなので、このファイルを変更してしまうと、php-buildが更新されるたびに conflict するので、素直にphp-buildを更新できなくなります。(いちいち毎回毎回変更点をマージする、めんどくさいですよね...)
php-bulidの configure のオプションを変更する場合には、今回の対処でやったようにPHP_BUILD_CONFIGURE_OPTS環境変数を使うことをおすすめします。
php-buildへのフィードバックが取り込まれ次第、この記事を更新して、もうこんな苦労しないでいいよという風にしたいと思います。(追記) ちなみに...
試行錯誤していた際に一番長くなった時には次のように指定してました(最終的に CPPFLAGS と PHP_LDFLAGS は指定しないでいいことに気がついたので、上記のようなものになっています)
YACC=$(brew --prefix bison@2.7)/bin/bison \ PHP_RPATHS="/usr/local/Cellar/openssl/1.0.2t/lib /usr/local/opt/zlib/lib /usr/local/opt/bzip2/lib /usr/local/opt/curl/lib /usr/local/opt/libedit/lib /usr/local/opt/libiconv/lib /usr/local/opt/mcrypt/lib" \ CPPFLAGS="-I/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/include -I/usr/local/Cellar/openssl/1.0.2t/include -I/usr/local/opt/zlib/include -I/usr/local/opt/bzip2/include -I/usr/local/opt/curl/include -I/usr/local/opt/libedit/include -I/usr/local/opt/libiconv/include -I/usr/local/opt/mcrypt/include" \ PHP_LDFLAGS="-L/usr/local/Cellar/openssl/1.0.2t/lib -L/usr/local/opt/zlib/lib -L/usr/local/opt/bzip2/lib -L/usr/local/opt/curl/lib -L/usr/local/opt/libedit/lib -L/usr/local/opt/libiconv/lib -L/usr/local/opt/mcrypt/lib" \ PHP_BUILD_CONFIGURE_OPTS="--with-openssl=/usr/local/Cellar/openssl/1.0.2t --with-zlib-dir=/usr/local/opt/zlib --with-bz2=/usr/local/opt/bzip2 --with-curl=/usr/local/opt/curl --with-libedit=/usr/local/opt/libedit --with-iconv=/usr/local/opt/libiconv --with-mcrypt=/usr/local/opt/mcrypt --with-tidy" \ PHP_BUILD_EXTRA_MAKE_ARGUMENTS=-j4 \ phpenv install -i development 5.6.40
- 投稿日:2019-12-19T10:37:41+09:00
PHP7.4対応版Laradockを、ビルドしただけの話(2019/12/17時点)
この内容は2019/12/17時点の話です。
あなたがこの記事をご覧になっているころには、LaraDockが対策済みになっているかもしれません。
(きっとこの記事を書いている最中にでも修正されているに違いない。)始めに
本記事はDocker ComposeのプロジェクトであるLaradockのPHP7.4対応版の、以下のサービスを動作させることを目的としています。
- workspace
- nginx
- php-fpm
- php-worker
- mysql
他のコンテナの動作については言及しません。
各コンテナをビルドする際に変更したファイルと変更箇所
1. mysql/my.cnf
MySQL8を使いたいのでデフォルトに認証方式を変更する。
mysql/my.cnfの最終行に追記+ default_authentication_plugin=mysql_native_password2. php-fpm/Dockerfile
--with-libzipオプションがなくなったから削除。
php-fpm/Dockerfileの54行目付近- docker-php-ext-configure zip --with-libzip && \ + docker-php-ext-configure zip && \3. php-worker/Dockerfile
oniguruma-devが必要らしい。
php-worker/Dockerfileの32行目付近- supervisor + supervisor \ + oniguruma-dev--with-libzipオプションがなくなったから削除。
php-worker/Dockerfileの70行目付近- docker-php-ext-configure zip --with-libzip && \ + docker-php-ext-configure zip && \追伸1
LaradockはPHPのiniファイルで、
error_reporting = E_ALL & ~E_DEPRECATED & ~E_STRICTと指定されているため、arrayかstring以外の型に対して配列スタイルでアクセルするとNotice警告でエラー扱いになります。
依存関係のライブラリが対応していないケースがあるため、しっかり動作検証を行う必要がありそうです。
(実際に手元の開発環境で発生しました…。)追伸2
php本家ドキュメント(https://www.php.net/)の調子が悪いように感じる…。
- 投稿日:2019-12-19T02:05:54+09:00
あの日見た言語の名前を僕はまだ知らない。~XMPPでローカルDB(初歩)~
初カキコ・・・ども・・・
俺みたいな専1でいきなりPHP触る、腐れ野郎他に、いますかって・・・・・・いるでしょう。
なぜ僕がPHPをやることになったのか、聞かれてはぐらかしていましたが、恥ずかしさを捨て去り此処に記します。
※前置きが長くなります。過去編
当時、僕は小学生でした。
毎日niconicoをニコニコしながら見ていた10年前のある時、ふと目に留まった動画がありました。それは・・・「絶対バグらないR〇G制作勉強会」
当時、小学生高学年だった僕は「ウヒョー、ゲームだー」と飛びつきました。
長々と淡々に作り上げた、今は亡き制作したゲームの数々は古いPCと一緒に封印されました。~10年後~
実際にプログラミングでゲームを制作したことを覚えているものの、一体どうやって作っていたのか、なんの言語を使っていたのか。全て忘れてしまった僕「確か・・・HPS・・・?SPH・・・?PHP・・・?なら触ったことあるわいな・・・」等とコークハイを飲みながら友人と話した訳ですよ。
冬も訪れ寒くなってきたこの時期。私はPHPという2種3文字を見て、「へ、チョロいぜ!!」と内心思うわけですよ。そして懐かしくなってしまった僕は、久しぶりにniconicoを開きました。
お分かりでしょうか?
「絶対バグらないR〇G制作勉強会」
ここで取り扱っている言語がHSPという3種3文字のスクリプト言語なのです。
もうお分かりでしょうか?
そう、僕はechoではなく、mesを書いていたのです。
本題
まずは・・・
HSPとPHPを勘違いしていた腐れヤローな僕は、急いでパソコン叩きます。
webのスクリプト言語と聞いて、JavaScriptとPHPの違いの認知すらしていなかった哀れな僕は、恥ずかしさを捨て去り、Pr〇gateで吐きそうになりながら、〇ECH〇CADEMYとか侍〇ンジニアとかで0.5mm程PHPに対して理解を深めたわけです。そして、目標を立てました。
それは、日々commitしている(PHPのせいで数日止まっている)ブラウザにローカルDBからデータを引っ張ってくる事を今回の最終目標としました。XAMPP
いくら理解を深めても環境を用意しなければ、話は始まりません。SQLの成績だけは悪かった僕は、GUIにお助けを求めてザンプに頼りmarinaDBを立ち上げます。(楽)
日本語サイト:https://www.apachefriends.org/jp/index.html
セットアップ
本来ならポート番号の確認とかやらなきゃいけないそうなんですが、思いつきでPHP触っている僕に死角はないので確認しません。
そしたら、ApacheとMySQLのStartボタンを押します。その後、
C:\xampp\htdocs
☝ファイルにアクセスして、ファイル内にPHPファイルを作成します。
このファイルに適当なコードを書きます。
http://localhost/aho.php
ブラウザで開きます。
そういえば、XAMPPのアイコンを右クリックして、管理者として実行をしてください。ApacheとMySQLはチェックボックスにチェックを入れておいてください。
これでとりあえず、PHPを書ける環境は構築できました。
DBとの接続
XAMPPのShell(黒いとこ)をクリックします。
ターミナルが立ち上がりますので、
mysqladmin -u root passwordを打ち込んでください。したらば、登録するパスワードを聞かれるので、適当に打ち込む。
そしたら、MarinaDB君と繋げてみます。
mysql -h localhost -u root -pを打ち込むとパスワードを求められるので、さっき登録したパスワードを入れるとこんな画面がでたらOK。
quitを打ち込んでMarinaDBとbyebyeしましょう。パスワードの設定注意
このままだとPhpMyAdminからはぶりを食らいます。
C:\xampp\phpMyAdminにアクセスして、config.inc.phpの中身をちょっと変えます。
/* Authentication type and info */ってとこの$cfg['Servers'][$i]['password'] = '';
に設定したパスワードを入れます。
$cfg['Servers'][$i]['password'] = 'ここですよここ';DBを利用する
利用するデータベースを立てる
ここまでくれば早いです。
まずは、利用するデータベースを立てましょう。XAMPPのMySQL欄の所にあるAdminをクリックすると、phpmyadminのページが開きます。
多分そのまま開くと思いますが、名前とパスワードを求められたら、名前の欄はrootと打ち込んでパスワードはさっき設定したものをいれてください。
その後、データベースのタブをクリックして以下を開きます。
とりあえずgomikuzuというデータベースを立てます。
gomikuzunameと名付けます。カラムは2。こーんな感じに立ち上がるので名前の所を上から順にsyuzinIDとnameと名付けます。syuzinIDのカラムには右にスクロールすると出てくるA_Iにチェックを入れましょう。そしたら実行ボタンを押して保存します。
これでテーブルは完成しました。
左側のデータベース一覧から(表示がない場合は左上のマークをクリック)
これねさっき作ったデータベースのテーブルgomikasunameをクリックする。
こんな画面になるので、この上のタブの挿入の所をクリックしてデータを入れていきましょう。
nameの値に好きな人の名前を入れましょう!
おっと、データ型を変えるの忘れてました。
構造のタブからnameのカラムだけを選択して編集することで、変えられます。Intからvarcharに変更しましょう。
今度は文句を言われませんでした。
そしたら、名前を入れて実行をします。
そうしたら、あとは同じ手順で好きなだけデータを入れてください。僕は友達の名前を3人入れました。
(↑は表示タブから確認できます。)PHPファイルでデータベースからデータを読み出す
パスワードの所は自分の設定したパスワードですので、このまま写していただければ問題ありません。(画像で申し訳ない)
そうしたら後はaho.phpを開けば・・・(http://localhost/aho.php)ブラウザで表示できました。
多分細かな所はすごく省かれています。もし試して動かなかったら申し訳ありません。そういった苦情を見かけたら、やがて復讐にきれいな記事を書きます。
MakeITのadvent-calendar19日目にして内容が弱いのは申し訳ございません。破門は辞めてください、お願いします。正直PHP苦痛で・・・もう打ちたくないですよ・・・。・・・あれ?PHPまともに打ってなくね??
ここまでお読み頂きありがとうございました。
- 投稿日:2019-12-19T02:05:54+09:00
あの日見た言語の名前を僕はまだ知らない。~XAMPPでローカルDB(初歩)~
初カキコ・・・ども・・・
俺みたいな専1でいきなりPHP触る、腐れ野郎他に、いますかって・・・・・・いるでしょう。
なぜ僕がPHPをやることになったのか、聞かれてはぐらかしていましたが、恥ずかしさを捨て去り此処に記します。
※前置きが長くなります。過去編
当時、僕は小学生でした。
毎日niconicoをニコニコしながら見ていた10年前のある時、ふと目に留まった動画がありました。それは・・・「絶対バグらないR〇G制作勉強会」
当時、小学生高学年だった僕は「ウヒョー、ゲームだー」と飛びつきました。
長々と淡々に作り上げた、今は亡き制作したゲームの数々は古いPCと一緒に封印されました。~10年後~
実際にプログラミングでゲームを制作したことを覚えているものの、一体どうやって作っていたのか、なんの言語を使っていたのか。全て忘れてしまった僕「確か・・・HPS・・・?SPH・・・?PHP・・・?なら触ったことあるわいな・・・」等とコークハイを飲みながら友人と話した訳ですよ。
冬も訪れ寒くなってきたこの時期。私はPHPという2種3文字を見て、「へ、チョロいぜ!!」と内心思うわけですよ。そして懐かしくなってしまった僕は、久しぶりにniconicoを開きました。
お分かりでしょうか?
「絶対バグらないR〇G制作勉強会」
ここで取り扱っている言語がHSPという3種3文字のスクリプト言語なのです。
もうお分かりでしょうか?
そう、僕はechoではなく、mesを書いていたのです。
本題
まずは・・・
HSPとPHPを勘違いしていた腐れヤローな僕は、急いでパソコン叩きます。
webのスクリプト言語と聞いて、JavaScriptとPHPの違いの認知すらしていなかった哀れな僕は、恥ずかしさを捨て去り、Pr〇gateで吐きそうになりながら、〇ECH〇CADEMYとか侍〇ンジニアとかで0.5mm程PHPに対して理解を深めたわけです。そして、目標を立てました。
それは、日々commitしている(PHPのせいで数日止まっている)ブラウザにローカルDBからデータを引っ張ってくる事を今回の最終目標としました。XAMPP
いくら理解を深めても環境を用意しなければ、話は始まりません。SQLの成績だけは悪かった僕は、GUIにお助けを求めてザンプに頼りmarinaDBを立ち上げます。(楽)
日本語サイト:https://www.apachefriends.org/jp/index.html
セットアップ
本来ならポート番号の確認とかやらなきゃいけないそうなんですが、思いつきでPHP触っている僕に死角はないので確認しません。
そしたら、ApacheとMySQLのStartボタンを押します。その後、
C:\xampp\htdocs
☝ファイルにアクセスして、ファイル内にPHPファイルを作成します。
このファイルに適当なコードを書きます。
http://localhost/aho.php
ブラウザで開きます。
そういえば、XAMPPのアイコンを右クリックして、管理者として実行をしてください。ApacheとMySQLはチェックボックスにチェックを入れておいてください。
これでとりあえず、PHPを書ける環境は構築できました。
DBとの接続
XAMPPのShell(黒いとこ)をクリックします。
ターミナルが立ち上がりますので、
mysqladmin -u root passwordを打ち込んでください。したらば、登録するパスワードを聞かれるので、適当に打ち込む。
そしたら、MarinaDB君と繋げてみます。
mysql -h localhost -u root -pを打ち込むとパスワードを求められるので、さっき登録したパスワードを入れるとこんな画面がでたらOK。
quitを打ち込んでMarinaDBとbyebyeしましょう。パスワードの設定注意
このままだとPhpMyAdminからはぶりを食らいます。
C:\xampp\phpMyAdminにアクセスして、config.inc.phpの中身をちょっと変えます。
/* Authentication type and info */ってとこの$cfg['Servers'][$i]['password'] = '';
に設定したパスワードを入れます。
$cfg['Servers'][$i]['password'] = 'ここですよここ';DBを利用する
利用するデータベースを立てる
ここまでくれば早いです。
まずは、利用するデータベースを立てましょう。XAMPPのMySQL欄の所にあるAdminをクリックすると、phpmyadminのページが開きます。
多分そのまま開くと思いますが、名前とパスワードを求められたら、名前の欄はrootと打ち込んでパスワードはさっき設定したものをいれてください。
その後、データベースのタブをクリックして以下を開きます。
とりあえずgomikuzuというデータベースを立てます。
gomikuzunameと名付けます。カラムは2。こーんな感じに立ち上がるので名前の所を上から順にIDとnameと名付けます。IDのカラムには右にスクロールすると出てくるA_Iにチェックを入れましょう。そしたら実行ボタンを押して保存します。
これでテーブルは完成しました。
左側のデータベース一覧から(表示がない場合は左上のマークをクリック)
これねさっき作ったデータベースのテーブルgomikasunameをクリックする。
こんな画面になるので、この上のタブの挿入の所をクリックしてデータを入れていきましょう。
nameの値に好きな人の名前を入れましょう!
(データ型を変えるの忘れてました。構造のタブからnameのカラムだけを選択して編集することで、変えられます。Intからvarcharに変更しておきましょう)そしたら、名前を入れて実行をします。
そうしたら、あとは同じ手順で好きなだけデータを入れてください。僕は友達の名前を3人入れました。
(↑は表示タブから確認できます。)
PHPファイルでデータベースからデータを読み出す
さっきのaho.phpを使うので、エディタで開きましょう。
パスワードの所は自分の設定したパスワードですので、このまま写していただければ問題ありません。(画像で申し訳ない)
そうしたら後はaho.phpを開けば・・・(http://localhost/aho.php)ブラウザで表示できました。
多分細かな所はすごく省かれています。もし試して動かなかったら申し訳ありません。そういった苦情を見かけたら、やがて復讐にきれいな記事を書きます。
MakeITのadvent-calendar19日目にして内容が弱いのは申し訳ございません。破門は辞めてください、お願いします。正直PHP苦痛で・・・もう打ちたくないですよ・・・。・・・あれ?PHPまともに打ってなくね??
ここまでお読み頂きありがとうございました。
- 投稿日:2019-12-19T02:04:20+09:00
PHPStanによる静的解析をLaravelアプリケーションに導入するためにやったこと(レベル1編)
この記事はLaravel #2 Advent Calendar 2019の19日目です。
そして去年のアドベントカレンダーの記事の続きです。PHPStanによる静的解析をLaravelアプリケーションに導入するためにやったこと
はじめに
弊社サービス「リネット1」のLaravelアプリケーションのPHPStanをレベル1に上げました。
そのためにやったことについて書きます。使用しているソフトウェアのバージョン
この記事で使用しているソフトウェアのバージョンは下記の通りです。
ソフトウェア バージョン PHP 7.2.12 Laravel 5.5.44 PHPStan 0.10.5 Laravel 5 IDE Helper Generator 2.5.1 Laravel 5.6以上を使っている人は、この記事を読まずLarastanを使うのが良いと思います。
Larastanを使えばこの記事でやっているようなことを自分でやらなくて良いはずです。多分。Laravel 5.5を使っている人には参考になると思います。
PHPStanのレベル1で検知できること
PHPStanをレベル1にすると、下記のことが検知可能になります。2
1. 存在しないマジックメソッドとマジックプロパティの呼び出し
2. 未定義の可能性がある変数の使用
if ($flag) { $hoge = 'hoge'; } echo $hoge; // $flagの値次第で未定義の可能性があるVariable $hoge might not be defined.3. 存在しない定数の使用
date(w); // date('w')の間違いConstant w not found.4. 無駄なisset関数の使用
$hoge = 'hoge'; isset($hoge); // 常にtrueになるので無駄Variable $hoge in isset() always exists and is not nullable.1を除いては、検知されたら粛々と修正すればOKです。
しかし、1は誤検知が多いと思います。
Eloquentのマジックメソッド(findやwhere)が誤検知されてしまいますし、自分でマジックメソッドやマジックプロパティを使っている箇所も同様です。マジックメソッドやマジックプロパティの存在をPHPStanに教える方法
1. エクステンションを書く
PHPStanにはエクステンションという機構があります。3
エクステンションを書くことで、マジックメソッドやマジックプロパティの存在をPHPStanに教えることができます。リネットでは次のようなエクステンションを書いてみました。
これがベストな書き方なのかはまったく自信がありませんので参考程度でお願いします。Eloquentモデルのメソッド・リフレクション用のエクステンション<?php declare(strict_types=1); use Eloquent; use Illuminate\Database\Eloquent\Model; use PHPStan\Analyser\OutOfClassScope; use PHPStan\Reflection\ClassReflection; use PHPStan\Reflection\MethodReflection; use PHPStan\Reflection\MethodsClassReflectionExtension; use PHPStan\Type\ObjectType; class EloquentModelMethodsClassReflectionExtension implements MethodsClassReflectionExtension { public function hasMethod(ClassReflection $classReflection, string $methodName): bool { // 無限ループ防止 if ($classReflection->getName() == Eloquent::class) { return false; } if (!$this->isEloquentModel($classReflection)) { return false; } return $this->findMethod($methodName) !== null; } public function getMethod(ClassReflection $classReflection, string $methodName): MethodReflection { assert($this->isEloquentModel($classReflection)); $method = $this->findMethod($methodName); assert(null !== $method); return $method; } private function findMethod(string $methodName): ?MethodReflection { $type = new ObjectType(Eloquent::class); if (!$type->hasMethod($methodName)) { return null; } return $type->getMethod($methodName, new OutOfClassScope()); } private function isEloquentModel(ClassReflection $classReflection): bool { $parents = $classReflection->getParents(); foreach ($parents as $parent) { // \Illuminate\Database\Eloquent\ModelのサブクラスであればEloquentモデル if ($parent->getName() == Model::class) { return true; } } return false; } }リネットのクラスのメソッド・リフレクション用のエクステンション<?php declare(strict_types=1); use PHPStan\Analyser\OutOfClassScope; use PHPStan\Reflection\ClassReflection; use PHPStan\Reflection\MethodReflection; use PHPStan\Reflection\MethodsClassReflectionExtension; use PHPStan\Type\ObjectType; class LenetMethodsClassReflectionExtension implements MethodsClassReflectionExtension { private $reflect = [ // リフレクション元 => リフレクション先 SomePresenter::class => SomeModel::class, ]; public function hasMethod(ClassReflection $classReflection, string $methodName): bool { $reflectFrom = $classReflection->getName(); $reflectTo = $this->reflect[$reflectFrom] ?? null; if (is_null($reflectTo)) { return false; } return $this->findMethod($methodName, $reflectTo) !== null; } public function getMethod(ClassReflection $classReflection, string $methodName): MethodReflection { $reflectFrom = $classReflection->getName(); assert(isset($this->reflect[$reflectFrom])); $reflectTo = $this->reflect[$reflectFrom]; $method = $this->findMethod($methodName, $reflectTo); assert(null !== $method); return $method; } private function findMethod(string $methodName, string $reflectTo): ?MethodReflection { $type = new ObjectType($reflectTo); if (!$type->hasMethod($methodName)) { return null; } return $type->getMethod($methodName, new OutOfClassScope()); } }2. PHPDocコメントを書く
PHPStanはPHPDocコメントを理解します。
エクステンションではなくPHPDocコメントでマジックメソッドやマジックプロパティの存在を教えることもできます。リネットではPHPで列挙型(enum)を作るで紹介されている列挙型を利用させてもらっていますが、列挙型のファクトリメソッドについては列挙可能なのでPHPDocコメントを書いています。
<?php declare(strict_types=1); /** * @method static self irui() * @method static self futon() * @method static self hokan() * @method static self kutsu() */ class ServiceCode { use EnumTrait; private const ENUM = [ 'irui' => '1', 'futon' => '2', 'hokan' => '3', 'kutsu' => '5', ]; }こちらのブログにはエクステンションで対応する方法が紹介されていました。
https://medium.com/@hatajoe/how-to-use-phpstan-940ba1de6832まとめ
Laravel 5.5のアプリケーションのPHPStanをレベル1に上げるためにやったことを書きました。
レベル2に上げる頃にはリネットもLaravel 6になっていると思うので、Larastanを使っていると思います。多分。
- 投稿日:2019-12-19T02:01:49+09:00
Amazon Linux 2 で Remi Repository の php を使って php-imagick を動かす
はじめに
Amazon Linux 2 で PHP を使うとなると
amazon-linux-extrasで php をインストールするのが簡単だが、今回は(現時点で) PHP 7.4 を使いたかったので remi リポジトリから PHP をインストールした。
この時に php-imagick を入れようとして四苦八苦したので忘れないようにメモ。作業手順
まずは remi リポジトリを yum で利用可能にしていく。
# remi には epel が必要 sudo amazon-linux-extras install -y epel # remi repository を追加 sudo rpm -ivh http://rpms.famillecollet.com/enterprise/remi-release-7.rpm sudo rpm --import http://rpms.famillecollet.com/RPM-GPG-KEY-remi続いて PHP 7.4 のインストール
# 必要なライブラリを先に入れる sudo yum -y install ncurses-compat-libs # amzn2-core を無効にしないと amzn2-core の php が選択されてしまう # 依存関係の解決を手動で実施するため、必要なライブラリは先に入れる sudo yum -y install php --enablerepo=remi-php74 --disablerepo=amzn2-coreこれで php 7.4 がインストールされたことを確認
$ php -v PHP 7.4.1 (cli) (built: Dec 17 2019 16:35:58) ( NTS ) Copyright (c) The PHP Group Zend Engine v3.4.0, Copyright (c) Zend Technologiesそして、PHP で ImageMagick の機能を使うために
php-imagickをインストールすると、残念ながら依存関係が足りない。$ sudo yum -y install php-imagick --enablerepo=remi-php74 Loaded plugins: extras_suggestions, langpacks, priorities, update-motd (中略) ---> Package mesa-libglapi.x86_64 0:17.2.3-8.20171019.amzn2.0.4 will be installed --> Finished Dependency Resolution Error: Package: ImageMagick6-libs-6.9.10.79-1.el7.remi.x86_64 (remi-safe) Requires: libraw_r.so.19()(64bit) You could try using --skip-broken to work around the problemこれは LibRaw というライブラリの version 0.19 が必要なのだが、残念ながら amzn2-core では version 0.14 しか提供されていない(※記事執筆現在)。
そのため、依存関係が解決できずにエラーとなる。ここでは、外部から RPM を取得・インストールすることで必要なバージョンを解決する。
$ sudo rpm -ivh http://mirror.centos.org/centos/7/os/x86_64/Packages/LibRaw-0.19.2-1.el7.x86_64.rpm Retrieving http://mirror.centos.org/centos/7/os/x86_64/Packages/LibRaw-0.19.2-1.el7.x86_64.rpm warning: /var/tmp/rpm-tmp.iP7cfm: Header V3 RSA/SHA256 Signature, key ID f4a80eb5: NOKEY Preparing... ################################# [100%] Updating / installing... 1:LibRaw-0.19.2-1.el7 ################################# [100%]この後、もう一度インストールを実施すると、うまくいったように見える。
$ sudo yum -y install php-imagick --enablerepo=remi-php74 Loaded plugins: extras_suggestions, langpacks, priorities, update-motd (中略) Installing : fribidi-0.19.4-6.amzn2.0.2.x86_64 (中略) Installed: php-pecl-imagick.x86_64 0:3.4.4-6.el7.remi.7.4 Dependency Installed: ImageMagick6-libs.x86_64 0:6.9.10.79-1.el7.remi OpenEXR-libs.x86_64 0:1.7.1-7.amzn2.0.2 atk.x86_64 0:2.22.0-3.amzn2.0.2 avahi-libs.x86_64 0:0.6.31-19.amzn2 cairo.x86_64 0:1.14.8-2.amzn2.0.2 cups-libs.x86_64 1:1.6.3-35.amzn2 fftw-libs-double.x86_64 0:3.3.3-8.amzn2.0.2 fontconfig.x86_64 0:2.10.95-11.amzn2.0.2 fontpackages-filesystem.noarch 0:1.44-8.amzn2 fribidi.x86_64 0:0.19.4-6.amzn2.0.2 gd.x86_64 0:2.0.35-26.amzn2.0.2 gdk-pixbuf2.x86_64 0:2.36.5-1.amzn2.0.2 ghostscript.x86_64 0:9.06-8.amzn2.0.5 ghostscript-fonts.noarch 0:5.50-32.amzn2 graphite2.x86_64 0:1.3.10-1.amzn2.0.2 graphviz.x86_64 0:2.30.1-21.amzn2.0.1 gtk-update-icon-cache.x86_64 0:3.22.26-4.amzn2 gtk2.x86_64 0:2.24.31-1.amzn2.0.2 harfbuzz.x86_64 0:1.3.2-1.amzn2.0.2 hicolor-icon-theme.noarch 0:0.12-7.amzn2 ilmbase.x86_64 0:1.0.3-7.amzn2.0.2 libICE.x86_64 0:1.0.9-9.amzn2.0.2 libSM.x86_64 0:1.2.2-2.amzn2.0.2 libX11.x86_64 0:1.6.5-2.amzn2.0.2 libX11-common.noarch 0:1.6.5-2.amzn2.0.2 libXau.x86_64 0:1.0.8-2.1.amzn2.0.2 libXaw.x86_64 0:1.0.13-4.amzn2.0.2 libXcomposite.x86_64 0:0.4.4-4.1.amzn2.0.2 libXcursor.x86_64 0:1.1.15-1.amzn2 libXdamage.x86_64 0:1.1.4-4.1.amzn2.0.2 libXext.x86_64 0:1.3.3-3.amzn2.0.2 libXfixes.x86_64 0:5.0.3-1.amzn2.0.2 libXfont.x86_64 0:1.5.2-1.amzn2.0.2 libXft.x86_64 0:2.3.2-2.amzn2.0.2 libXi.x86_64 0:1.7.9-1.amzn2.0.2 libXinerama.x86_64 0:1.1.3-2.1.amzn2.0.2 libXmu.x86_64 0:1.1.2-2.amzn2.0.2 libXpm.x86_64 0:3.5.12-1.amzn2.0.2 libXrandr.x86_64 0:1.5.1-2.amzn2.0.2 libXrender.x86_64 0:0.9.10-1.amzn2.0.2 libXt.x86_64 0:1.1.5-3.amzn2.0.2 libXxf86vm.x86_64 0:1.1.4-1.amzn2.0.2 libfontenc.x86_64 0:1.1.3-3.amzn2.0.2 libglvnd.x86_64 1:1.0.0-1.amzn2.0.2 libglvnd-egl.x86_64 1:1.0.0-1.amzn2.0.2 libglvnd-glx.x86_64 1:1.0.0-1.amzn2.0.2 libpng.x86_64 2:1.5.13-7.amzn2.0.2 libraqm.x86_64 0:0.7.0-1.el7 librsvg2.x86_64 0:2.40.16-1.amzn2.0.2 libthai.x86_64 0:0.1.14-9.amzn2.0.2 libtool-ltdl.x86_64 0:2.4.2-22.2.amzn2.0.2 libwayland-client.x86_64 0:1.14.0-2.amzn2.0.1 libwayland-server.x86_64 0:1.14.0-2.amzn2.0.1 libwebp7.x86_64 0:1.0.2-1.el7.remi libwmf-lite.x86_64 0:0.2.8.4-41.amzn2.0.3 libxcb.x86_64 0:1.12-1.amzn2.0.2 libxshmfence.x86_64 0:1.2-1.amzn2.0.2 lyx-fonts.noarch 0:2.2.3-1.el7 mesa-libEGL.x86_64 0:17.2.3-8.20171019.amzn2.0.4 mesa-libGL.x86_64 0:17.2.3-8.20171019.amzn2.0.4 mesa-libgbm.x86_64 0:17.2.3-8.20171019.amzn2.0.4 mesa-libglapi.x86_64 0:17.2.3-8.20171019.amzn2.0.4 pango.x86_64 0:1.40.4-1.amzn2.0.2 pixman.x86_64 0:0.34.0-1.amzn2.0.2 poppler-data.noarch 0:0.4.6-3.amzn2.0.1 urw-fonts.noarch 0:2.4-16.amzn2 xorg-x11-font-utils.x86_64 1:7.5-20.amzn2.0.2 Complete!しかし、この状態で
php -mとして利用可能なモジュールを確認するとエラーが出ており、正常に使える状態になっていない。$ php -m PHP Warning: PHP Startup: Unable to load dynamic library 'imagick.so' (tried: /usr/lib64/php/modules/imagick.so (/lib64/libraqm.so.0: undefined symbol: fribidi_get_bracket_types), /usr/lib64/php/modules/imagick.so.so (/usr/lib64/php/modules/imagick.so.so: cannot open shared object file: No such file or directory)) in Unknown on line 0 [PHP Modules] bz2 calendar Core ctype curl date exif fileinfo filter ftp gettext hash iconv json libxml openssl pcntl pcre Phar readline Reflection session sockets SPL standard tokenizer zlib [Zend Modules]エラーメッセージである
/lib64/libraqm.so.0: undefined symbol: fribidi_get_bracket_typesについて調べると、これは fribidi のバージョンが 1.0 未満の場合に発生する問題であるらしい。
上記のインストールメッセージのなかにわざとらしく残しているが、amzn2-core でインストールできるバージョンは 1.0 未満なので、こちらも同様に rpm で解決する。$ sudo rpm -ivh http://mirror.centos.org/centos/7/os/x86_64/Packages/fribidi-1.0.2-1.el7.x86_64.rpm Retrieving http://mirror.centos.org/centos/7/os/x86_64/Packages/fribidi-1.0.2-1.el7.x86_64.rpm warning: /var/tmp/rpm-tmp.plm1q2: Header V3 RSA/SHA256 Signature, key ID f4a80eb5: NOKEY Preparing... ################################# [100%] file /usr/bin/fribidi from install of fribidi-1.0.2-1.el7.x86_64 conflicts with file from package fribidi-0.19.4-6.amzn2.0.2.x86_64 file /usr/lib64/libfribidi.so.0 from install of fribidi-1.0.2-1.el7.x86_64 conflicts with file from package fribidi-0.19.4-6.amzn2.0.2.x86_64 # コンフリクトするので既存パッケージを remove する $ sudo yum -y remove fribidi Loaded plugins: extras_suggestions, langpacks, priorities, update-motd (中略) Dependency Removed: ImageMagick6-libs.x86_64 0:6.9.10.79-1.el7.remi libraqm.x86_64 0:0.7.0-1.el7 php-pecl-imagick.x86_64 0:3.4.4-6.el7.remi.7.4 Complete! # 改めてインストールし、上記の削除時に依存関係で削除された部分を再インストールする $ sudo rpm -ivh http://mirror.centos.org/centos/7/os/x86_64/Packages/fribidi-1.0.2-1.el7.x86_64.rpm Retrieving http://mirror.centos.org/centos/7/os/x86_64/Packages/fribidi-1.0.2-1.el7.x86_64.rpm warning: /var/tmp/rpm-tmp.AIsn76: Header V3 RSA/SHA256 Signature, key ID f4a80eb5: NOKEY Preparing... ################################# [100%] Updating / installing... 1:fribidi-1.0.2-1.el7 ################################# [100%] $ sudo yum -y install php-imagick --enablerepo=remi-php74 (中略) Installed: php-pecl-imagick.x86_64 0:3.4.4-6.el7.remi.7.4 Dependency Installed: ImageMagick6-libs.x86_64 0:6.9.10.79-1.el7.remi libraqm.x86_64 0:0.7.0-1.el7 Complete!改めて
php -mをすると、エラーメッセージは表示されずにモジュール一覧の中に imagick が表示される。$ php -m [PHP Modules] bz2 calendar Core ctype curl date exif fileinfo filter ftp gettext hash iconv imagick json libxml openssl pcntl pcre Phar readline Reflection session sockets SPL standard tokenizer zlib [Zend Modules]あとは簡単なコードを書いてうまく動くかを確認。
test.php<?php # 画像の型を変更 $image = new Imagick('input.png'); $image->writeImage("output.jpg");$ php test.php $ ls -al ... -rw-rw-r-- 1 ec2-user ec2-user 6953 Dec 18 16:49 input.png -rw-rw-r-- 1 ec2-user ec2-user 33240 Dec 18 16:49 output.jpg -rw-rw-r-- 1 ec2-user ec2-user 84 Dec 18 16:47 test.php ...というわけで、うまく動きました。
なお、本当は yum のみで解決したかったけれど、今回は手っ取り早く rpm を使っています。オマケ
これだけだとコマンドラインの ImageMagick は入っていないので、これを入れたい場合は remi リポジトリから以下のようにインストールする。
amzn2-core の場合、6.7.8.9 というやや古いバージョンが入ってくるので、7系などのより新しいバージョンを入れたい場合に。$ sudo yum -y install ImageMagick6 --enablerepo=remi $ convert
- 投稿日:2019-12-19T00:53:59+09:00
CakePHP4.1に導入されるかもしれないDIコンテナ「phpleague/container」を試す
本記事は CakePHP Advent Calendar 2019 の19日目のエントリーになります。
はじめに
先日、待ちに待ったCakePHP4.0.0が 正式リリース されましたね
そして、気が早いのですが4.0系がリリースされたということは、次の大きなアップデートは4.1系のリリースということになります。(本当に気が早くてごめんなさい)
そんなこともあり早速 4.1のロードマップ をチラ見してみたのですが Application の欄に以下のような記述があり、何やら面白そうな変化が起きそうだなと感じました。
- Experimental support for PSR11 compliant dependency injection container. Potential libraries that we could use are:
- phpleague/container - https://container.thephpleague.com/3.x
日本語に訳すと「PSR11準拠のDIコンテナを試験導入するかも。
phpleague/containerというライブラリに可能性を感じてるやで。」といった感じでしょうか。というわけで、本記事ではCake4.1に導入されるかもしれないDIコンテナライブラリである phpleague/container を試してみたいと思います。
免責事項
- DIやDIコンテナそのものの説明は行いません。
- インターネットや書籍に素晴らしい記事や文献があるのでそちらをご参照ください。
インストール
composerでサクッとインストール出来ます。ちなみに、2019年12月時点では3.3.0がインストールされます。
$ composer require league/containerクライアントとサービスクラスの作成
まずはクライアントクラス(オブジェクトが注入されるクラス)とサービスクラス(注入対象となるオブジェクトのクラス)を作成します。よくある
Constructor Injectionです。basic.php<?php declare(strict_types=1); namespace Basic; class Client { /** * @var Service */ public $service; /** * Constructor Injection * * @param Service $service */ public function __construct(Service $service) { $this->service = $service; } } class Service { }DIコンテナの作成
準備が整ったので
phpleague/containerを利用してDIコンテナを作成します。basic.php// DI Container require 'vendor/autoload.php'; use League\Container\Container; $container = new Container; $container->add(Service::class); $container->add('client_alias', Client::class)->addArgument(Service::class); $client1 = $container->get('client_alias'); $client2 = $container->get('client_alias'); var_dump($client1 instanceof Client); // true var_dump($client1->service instanceof Service); // true var_dump($client1 === $client2); //false
add()メソッドを利用して登録を行い、get()メソッドを利用してインスタンスを取得します。エイリアス名の設定なども簡単に出来ます。インターフェイスの利用
実際にDIコンテナを利用する時は、インターフェイスを利用して具象クラスを隠蔽することが多いと思いますので、そのパターンも簡単に書いてみたいと思います。
advance.php<?php declare(strict_types=1); namespace Advance; class Client { /** * @var ServiceInterface */ public $service; /** * Constructor Injection * * @param ServiceInterface $service */ public function __construct(ServiceInterface $service) { $this->service = $service; } } interface ServiceInterface { } class CakeService implements ServiceInterface { } class LaravelService implements ServiceInterface { } // DI Container require 'vendor/autoload.php'; use League\Container\Container; $container = new Container; $container->add(ServiceInterface::class, CakeService::class); $container->add(Client::class)->addArgument(ServiceInterface::class); $client = $container->get(Client::class); var_dump($client instanceof Client); // true var_dump($client->service instanceof ServiceInterface); // true var_dump($client->service instanceof CakeService); // true var_dump($client->service instanceof LaravelService); // falseインターフェイスを利用した場合でも同じようにサクッとDIコンテナを作成することが出来ます。
まとめ
数行の記述で簡単に1DIコンテナが実現出来ました。今回は本当に試してみたレベルですが、もう少し深堀りして、CakePHPでこのライブラリがどのように使われるのか想像するのも面白そうです。2ちなみに ドキュメンテーションサイト がかっこよくてテンションが上りました
というわけで、2020年もCakePHPの動向から目が離せません!







































