20191219のPHPに関する記事は20件です。

【 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ロードという名前・書き方にたどり着くまでにかなり時間がかかってしまった。一つ一つ経験を積み上げないとダメだなぁ・・・(´;ㅿ;`)

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

オペレーションミスを防ぐ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の環境が構築できます。

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

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);
    }
}

おわりに

これらのようなテスト対象が依存しているものの代替品のことをテストダブルといいます。
テスト対象以外の影響を受けなくなり、本当にテストしたいものに集中できる利点があります。
また、テストダブルを念頭に置いてクラス設計することで、クラスの責務を意識するようになり、神クラスを作りづらいという利点もあると思っています。
なんでもできる神クラスは作りたくないものですね。


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

Laravel で Rails の抽象 ActiveModel みたいなやつが欲しくなった

TL;DR

車輪の再発明してしまったのでソースコードの墓場にする記事

内容

データベースを使わないけど, app/Services 配下に置くような外部 API サービスのレスポンスエンティティを Eloquent ライクに使いたいニーズがあった。

最初 Eloquent Model を継承することも考えたが,意味のないメソッドがごちゃごちゃ入ってくるのが鬱陶しいので

  • HasAttributes
  • HidesAttributes
  • GuardsAttributes

の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 からの更新結果レスポンスをインスタンスに反映することを想定

既存のライブラリ

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

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からの変換を試して見ても直りませんでした。
どうやったら解決できそうですかね。

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

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'));
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

FuelPHPの詰まったところやらルールの備忘録

FuelPHPを使った際に詰まった点があったのでメモ。適宜更新されるかもしれない。

ファイル名は全部小文字

HelloWorld.phpをコントローラに突っ込んでたけどなんでかわからんけど全く動かない。
調べたらコーディング標準でファイル名はすべて小文字にするように定められているらしい。

PHPのコードしかないときは?>を省略する

見つけにくい「死の白い画面」を避けるためにこのようなルールになっているとのこと。

文字コードはUTF-8

まあそりゃそうだって感じですけどShift-JISで書いた日本語の文字列をビューに持っていったら無かったことにされた。

ClassやFunctionの単語区切りはアンダースコア(_)

キャメルケースでの単語区切りはダメ(例:ControllerHelloはダメ、Controller_Helloと書く)。
これはアンダースコアをオートロードの際のディレクトリの区切り記号に変えているかららしい。

参考

FuelPHP 1.8 コーディング標準

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

【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);
    }
}

イベントからリスナーが発動するまでの流れ

イベントが起こって、リスナーが発動するまでの流れを整理しておきます。

  1. コントローラーでeventヘルパを使って、イベントを起こす
  2. プロバイダーに登録してあるイベントが呼び出される
  3. イベントに記述した値がリスナーに受け渡される
  4. リスナーが発動する

このような流れになります。

イベントとリスナーの使いみち

一般的には、コントローラーをコンパクトにするために使われます。

例えばメール送信機能は、ユーザ登録時、購入時、更新時などで共通して使われますが、それをコントローラーに書いていたら内容が重複してしまいます。

なので、イベントとリスナーに記述しておいて、それぞれのコントローラからイベントを起こすだけで、メール送信ができるようにしておくのです。

使いみちはいろいろありますが、メールやslackへの通知の際に使うのが一般的かなと思います。

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

参照渡しでのエラー「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()側は参照渡しを要求しているのに、
呼び出し側で値渡ししているのが原因でおきてるそうです。

解決できたので、めでたしめでたし。

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

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.1

Permission denied と出た場合は sudo をつけてください。

参考

https://getcomposer.org/doc/articles/troubleshooting.md#proc-open-fork-failed-errors
https://evisu0414.com/engineer/engineer010/

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

php7.4からついにクラスのメンバ変数に型宣言できるようになった

php7.4からクラスのプロパティは、新たに型宣言をサポートするようになりました。

遂に!って感じですね。
php7系、特に7.3など使用しているとアップデートするか悩ましいですね。
パフォーマンスの改善も結構されているみたいですし。

<?php
class User {
    public int $id;
    public string $name;
}
?>

上の例では、
$user->idにはintの値だけを、$user->nameにはstringの値だけを代入できるように強制します。

PHP 7.3.x から PHP 7.4.x への移行

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

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

image.png
https://book.cakephp.org/3/ja/index.html

公式のドキュメントです。概要から細かいリファレンスまで諸々書いてあります。
一通り日本語訳もなされているので、「英語が苦手で...」という方も安心です。

個人的によく読んだトピックを以下に挙げてみました。
サンプルコードなどを見て「なんとなく書けるかな〜〜」というところから自らが実現したい処理を書けるようになるためには、ツールの詳しい使い方を知る必要があると思います。そんなときに役に立った印象です。

インターネット上のエントリ

この場を借りて、めちゃくちゃお世話になりましたので御礼申し上げます...?
具体的な問題にハマったときだけではなく、「あれ?この機能って普通どう使うんだろう??」みたいなtipsや「こういう実装を実現したいとき、何を使うとよいのだろう...」といったフレームワークを用いた実装パターンを捉えるのにも役に立った印象です。

以下に、手元に保存してあったエントリをいくつか置いといてみるんですが、見事にQiitaのエントリばかりでした...Qiitaさすが...

また、CakePHPのアドベントカレンダーにもめちゃくちゃお世話になってました。機能の紹介やCakePHPを用いたアプリケーションの設計方針など、参考になるエントリがたくさんです。
来年はひとつくらいエントリ書いて貢献できるとよいなあ...

コーディング規約

image.png
https://book.cakephp.org/3/ja/contributing/cakephp-coding-conventions.html

CakePHP3は PSR-21 をベースにしたコーディング規約を定義しています。

cakephp/cakephp-codesniffer という、上記のコーディング規約に準拠するための PHP_CodeSniffer 向け設定もあるので、IDEに組み込んだりcommit前に実行するようにして、規約にそったコーディングをするようにします。
その上で、上記のようなフォーマッターに指摘されないようなコーディングをするために、ざっと内容を読みつつ実践できるようにしていきました。

最初、自社のアプリケーションにPull Requestを投げた際には、コーディング規約を踏まえた変更になっておらず、レビュアーにコードフォーマットに関する指摘をさせてしまいました...
コードフォーマットの指摘も度々レビュアーのコストになってしまうので、ツールに頼ったりしつつ事前にコーディング規約の検査をしておかないといけないですね。

フレームワーク自体のソースコード

image.png

正直、ホントに困ったときはこれが一番です。フレームワークが提供しているクラスや関数が、受け取る引数によってどのような振る舞いをするのかは、実際にその定義を読んだり実行してみたりすることで理解が深まります。

手元にCakePHPで作られたプロジェクトがあれば、その中にあるControllerクラスなどの実装から辿っていくとわかりやすくてよいです。
もしそういうプロジェクトが手元にない場合、GitHubに公開されているCakePHPのサンプルアプリなどでとりあえず補ってみてもよいかもしれません。

ちなみに、以下は ぼくが知ってる書籍 のサンプルコードがホストされてるレポジトリです。実際にlocalで動かしながら観察できるので、オススメです?
https://github.com/php-book/php-qa-plaza/

定義元ジャンプ機能を有効活用

image.gif

ソースコードリーディングをする場合、IDEなどにある「定義元ジャンプ機能」を用いると、とっても捗ります。
ぼくはコネヒトに入ってから、 PhpStorm をメインで使うようになりましたが、プロジェクトのディレクトリをIDEで読み込んで置いて、composer installをしてパッケージを落としておけば、採用しているパッケージ側の定義までサクサク読みにいけます。
もともとvimをメインで使ってて、vimでも universal-ctags を利用すれば定義元ジャンプはできるのですが、PhpStorm(IntelliJ)にはここでは語り切れないたくさんの機能があり、そういうのを含めて「やっぱりIDE便利やな...」ってなっている昨今です。

番外編: cake consoleコマンド(インタラクティブコンソール)

とはいえ、ソースコードを読むだけだと、具体的な挙動については頭の中で想像するしかありません。手元にCakePHPで作られたプロジェクトがあるならば、その中に組み込まれているインタラクティブコンソールを利用すると具体的に動作が確認できて便利です。

インタラクティブ・コンソール (REPL) - 3.8

例えば、「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] エンジニア絶賛募集中です!!!

Connehito Image
PHPとAWSで開発!生活領域における課題を解決したいエンジニア募集!

[PR] 弊社で採用している開発環境を参考に書かれたCakePHP本もよろしくな!!!
(こちらもCakePHP3をはじめて触る方にオススメです!)

TECHNICAL MASTER はじめてのPHPプロフェッショナル開発 PHP7対応 | 伊藤 翔, 金城 秀樹, 高野 福晃, 永井 勝一郎 |本 | 通販 | Amazon


  1. ちなみにPSR-2はすでにdeprecatedになり、新たに PSR-12 を利用することが推奨されています。CakePHPも 4.0.0の時点ではまだPSR-2準拠のままになっている ようですが、 cakephp/cakephp-codesniffer を眺めてみると4.0.0に向けたbranchではPSR-12に準拠するための準備が始まっているようです。今後に期待! 

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

SQLインジェクションとその対策(PHP + PDO)

この記事は、North Detail Advent Calendar 2019 の21日目の記事です。

はじめに

先日、社内で「安全なウェブサイトの作り方」を使った読書会が行われました :green_book:

SQLインジェクションについて議題に上がったので、
勉強がてら実際に試してみよう!というのが今回の趣旨になります。

SQLインジェクション?

SQLインジェクション(英: SQL Injection)とは、
アプリケーションのセキュリティ上の不備を意図的に利用し、
アプリケーションが想定しないSQL文を実行させることにより、
データベースシステムを不正に操作する攻撃方法のこと。

https://ja.wikipedia.org/wiki/SQLインジェクション

「Injection」という単語が「注入する」という意味のようです(知らなかった)。

アプリケーションが実行するSQL文に変なものを注入して
意図しない動作をさせる攻撃なので「SQL Injection」ってことですね :smiley:

実際に起こしてみる

データを作る

まずはMySQLにテスト用データを作成します。

CREATE TABLE users (
  id int NOT NULL AUTO_INCREMENT,
  name varchar(10),
  PRIMARY KEY (id)
);

INSERT INTO
  users(name)
VALUES
  ('フグ田サザエ'),
  ('フグ田マスオ'),
  ('磯野波平'),
  ('磯野フネ'),
  ('磯野カツオ'),
  ('磯野ワカメ'),
  ('フグ田タラオ'),
  ('タマ')
;

:arrow_down:
スクリーンショット 2019-12-19 9.53.14.png

:tangerine:
:cat:
:tangerine:

テスト用画面を作る

次は、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();
  }
?>

スクリーンショット 2019-12-19 10.19.41.png
:arrow_down:
スクリーンショット 2019-12-19 10.20.07.png

うーんシンプル。

ここで…文字列結合を使ってクエリ組み立てていますね :imp:

$prepare = $pdo->prepare('SELECT * FROM users WHERE id = '. $id. ';');
$prepare->execute();

攻撃してみる

先ほどの例では「3」と検索して「磯野波平」のデータを取得しました。

じゃあ次は「0 OR TRUE; --」で…

スクリーンショット 2019-12-19 10.39.39.png
:arrow_down:
スクリーンショット 2019-12-19 10.21.06.png

users テーブルの全レコードがヒットしてしまいました :scream:

ユーザー入力をそのまま採用した結果、こんなSQL文になりました。
WHERE句が常にTRUEになるので、全てのレコードが対象になったんですね。

SELECT * FROM users WHERE id = 0 OR TRUE; --;

ちょっと付け加えて、お次は「0 OR TRUE; DELETE FROM users; --」で検索…

スクリーンショット 2019-12-19 10.40.14.png
:arrow_down:
スクリーンショット 2019-12-19 10.24.38.png

あれ?さっきと同じ??

MySQLを覗いてみると…

スクリーンショット 2019-12-19 10.24.46.png

な ん と い う こ と で し ょ う
匠の手によって全てのレコードが削除されてしまいました :skull:

先ほどの例とだいたい同じですが、
検索クエリを正常終了させた後に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();

:arrow_down:

修正後
$prepare = $pdo->prepare('SELECT * FROM users WHERE id = :id;');
$prepare->bindValue(':id', $id, PDO::PARAM_INT);
$prepare->execute();

プレースホルダを使ってバインドすると、クエリとして解釈されないようです。
あくまで「値」として扱われるため、変なものを注入した場合でもクエリとして実行されないんですね。

実際にさっきの攻撃を試してみましょう

スクリーンショット 2019-12-19 10.49.06.png
:arrow_down:
スクリーンショット 2019-12-19 10.48.46.png

何も検索されなくなりました!

スクリーンショット 2019-12-19 10.49.15.png

今度は磯野家も無事でした… :family_mwgb:

まとめ

SQLインジェクションによる攻撃と、その対策方法について実際に試してみました。

SQLインジェクションやプレースホルダによる対策自体は知っていて、学生時代にJavaで一度やったことある程度でした。
その時は理解が浅い状態だったので、仕組みも含めて復習できて良かったです :relaxed: :two_hearts:

明日は

「North Detail Advent Calendar」 22日目は @yamatohkd さんです! :clap:
スマホカメラで手のモーションを記録してUnityでピアノ演奏したかった

参考

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

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_RPATHSPHP_BUILD_CONFIGURE_OPTS を設定するように php-build にフィードバックをこの年末年始にしようと思ってます。

今、ちょうど、 php-build のメインコミッターが travis の macOS 側の CI ベースを Xcode 10 にあげようとしているので、それに乗っかって、このフィードバックが取り込まれれば、Catalina でも phpenv install だけでいけるようになるはずです。

最後に

この件に関係するような記事で、 php-builddefault_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
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

PHP7.4対応版Laradockを、ビルドしただけの話(2019/12/17時点)

この内容は2019/12/17時点の話です。
あなたがこの記事をご覧になっているころには、LaraDockが対策済みになっているかもしれません。
(きっとこの記事を書いている最中にでも修正されているに違いない。)

始めに

本記事はDocker ComposeのプロジェクトであるLaradockのPHP7.4対応版の、以下のサービスを動作させることを目的としています。

  1. workspace
  2. nginx
  3. php-fpm
  4. php-worker
  5. mysql

他のコンテナの動作については言及しません。

各コンテナをビルドする際に変更したファイルと変更箇所

1. mysql/my.cnf

MySQL8を使いたいのでデフォルトに認証方式を変更する。

mysql/my.cnfの最終行に追記
+ default_authentication_plugin=mysql_native_password

2. 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と指定されているため、arraystring以外の型に対して配列スタイルでアクセルするとNotice警告でエラー扱いになります。
依存関係のライブラリが対応していないケースがあるため、しっかり動作検証を行う必要がありそうです。
(実際に手元の開発環境で発生しました…。)

追伸2

php本家ドキュメント(https://www.php.net/)の調子が悪いように感じる…。

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

あの日見た言語の名前を僕はまだ知らない。~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を立ち上げます。(楽)

まずは、XAMPPをダウンロードします。
1ce4f6a5bc04be582b2f277c74d21665.png

日本語サイト:https://www.apachefriends.org/jp/index.html

あとはまぁインストールして立ち上げていきます。
a935a872f04f6b1dd175a1865ca849d5.png

セットアップ

本来ならポート番号の確認とかやらなきゃいけないそうなんですが、思いつきでPHP触っている僕に死角はないので確認しません。

そしたら、ApacheとMySQLのStartボタンを押します。その後、
C:\xampp\htdocs
☝ファイルにアクセスして、ファイル内にPHPファイルを作成します。
a2239115483a2fe5d34362ded0a63d54.png
このファイルに適当なコードを書きます。
0c2c2c4a2eee23c8dfe74ca315854b4b.png

http://localhost/aho.php
ブラウザで開きます。
3a8563af5cd977a0a83731a47bfde65b.png

そういえば、XAMPPのアイコンを右クリックして、管理者として実行をしてください。ApacheとMySQLはチェックボックスにチェックを入れておいてください。9791f35f03f3bddda9b3d689dc8f3dbb.png

これでとりあえず、PHPを書ける環境は構築できました。

DBとの接続

XAMPPのShell(黒いとこ)をクリックします。

ターミナルが立ち上がりますので、mysqladmin -u root passwordを打ち込んでください。

したらば、登録するパスワードを聞かれるので、適当に打ち込む。
p2-4.png

そしたら、MarinaDB君と繋げてみます。mysql -h localhost -u root -pを打ち込むとパスワードを求められるので、さっき登録したパスワードを入れると

5011ff1c24955dcf18418f5604ef3a04.png

こんな画面がでたら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と打ち込んでパスワードはさっき設定したものをいれてください。
その後、データベースのタブをクリックして以下を開きます。
b836313d6fddd9ba25f0df65934d35b1.png

とりあえずgomikuzuというデータベースを立てます。
299f05fd94cc7a85e6bb9a7e6aad1f6d.png
gomikuzunameと名付けます。カラムは2。

そうすると
241c02e0635ed9bf7ef17b86030ae70b.png

こーんな感じに立ち上がるので名前の所を上から順にsyuzinIDとnameと名付けます。syuzinIDのカラムには右にスクロールすると出てくるA_Iにチェックを入れましょう。そしたら実行ボタンを押して保存します。

これでテーブルは完成しました。

左側のデータベース一覧から(表示がない場合は左上のマークをクリック)
これね32f2732148db8520ca7b071c783bee1f.png

さっき作ったデータベースのテーブルgomikasunameをクリックする。
db74d5d174d7440c3255f8ed0453e946.png

こんな画面になるので、この上のタブの挿入の所をクリックしてデータを入れていきましょう。
24472c7b44df5614b9e46ceec97522fd.png

nameの値に好きな人の名前を入れましょう!
c6ab34954b7cba2482e220f1a14f51ce.png
おっと、データ型を変えるの忘れてました。
構造のタブからnameのカラムだけを選択して編集することで、変えられます。Intからvarcharに変更しましょう。
0d48bd0be0c14b0eb5723ff807c3cead.png
今度は文句を言われませんでした。
そしたら、名前を入れて実行をします。
7b8932b8dfc6ae6c7deaa8390bec2f33.png

そうしたら、あとは同じ手順で好きなだけデータを入れてください。僕は友達の名前を3人入れました。
acb0c67d9bd9cf95af9b5590c8bd0c07.png
(↑は表示タブから確認できます。)

PHPファイルでデータベースからデータを読み出す

さっきのaho.phpを使うので、エディタで開きましょう。
2e5359e2ab47f13e982cc24b468f6b74.png

パスワードの所は自分の設定したパスワードですので、このまま写していただければ問題ありません。(画像で申し訳ない)
そうしたら後はaho.phpを開けば・・・(http://localhost/aho.php)

f36a869aba935afbc933f6848ac120c1.png

ブラウザで表示できました。
多分細かな所はすごく省かれています。もし試して動かなかったら申し訳ありません。そういった苦情を見かけたら、やがて復讐にきれいな記事を書きます。
MakeITのadvent-calendar19日目にして内容が弱いのは申し訳ございません。破門は辞めてください、お願いします。正直PHP苦痛で・・・もう打ちたくないですよ・・・。

・・・あれ?PHPまともに打ってなくね??

ここまでお読み頂きありがとうございました。

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

あの日見た言語の名前を僕はまだ知らない。~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を立ち上げます。(楽)

まずは、XAMPPをダウンロードします。
1ce4f6a5bc04be582b2f277c74d21665.png

日本語サイト:https://www.apachefriends.org/jp/index.html

あとはまぁインストールして立ち上げていきます。
a935a872f04f6b1dd175a1865ca849d5.png

セットアップ

本来ならポート番号の確認とかやらなきゃいけないそうなんですが、思いつきでPHP触っている僕に死角はないので確認しません。

そしたら、ApacheとMySQLのStartボタンを押します。その後、
C:\xampp\htdocs
☝ファイルにアクセスして、ファイル内にPHPファイルを作成します。
a2239115483a2fe5d34362ded0a63d54.png
このファイルに適当なコードを書きます。
0c2c2c4a2eee23c8dfe74ca315854b4b.png

http://localhost/aho.php
ブラウザで開きます。
3a8563af5cd977a0a83731a47bfde65b.png

そういえば、XAMPPのアイコンを右クリックして、管理者として実行をしてください。ApacheとMySQLはチェックボックスにチェックを入れておいてください。9791f35f03f3bddda9b3d689dc8f3dbb.png

これでとりあえず、PHPを書ける環境は構築できました。

DBとの接続

XAMPPのShell(黒いとこ)をクリックします。

ターミナルが立ち上がりますので、mysqladmin -u root passwordを打ち込んでください。

したらば、登録するパスワードを聞かれるので、適当に打ち込む。
p2-4.png

そしたら、MarinaDB君と繋げてみます。mysql -h localhost -u root -pを打ち込むとパスワードを求められるので、さっき登録したパスワードを入れると

5011ff1c24955dcf18418f5604ef3a04.png

こんな画面がでたら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と打ち込んでパスワードはさっき設定したものをいれてください。
その後、データベースのタブをクリックして以下を開きます。
b836313d6fddd9ba25f0df65934d35b1.png

とりあえずgomikuzuというデータベースを立てます。
299f05fd94cc7a85e6bb9a7e6aad1f6d.png
gomikuzunameと名付けます。カラムは2。

そうすると
241c02e0635ed9bf7ef17b86030ae70b.png

こーんな感じに立ち上がるので名前の所を上から順にIDとnameと名付けます。IDのカラムには右にスクロールすると出てくるA_Iにチェックを入れましょう。そしたら実行ボタンを押して保存します。

これでテーブルは完成しました。

左側のデータベース一覧から(表示がない場合は左上のマークをクリック)
これね32f2732148db8520ca7b071c783bee1f.png

さっき作ったデータベースのテーブルgomikasunameをクリックする。

こんな画面になるので、この上のタブの挿入の所をクリックしてデータを入れていきましょう。
e29aba188b17fda4ac7cc9aa418f4dba.png

nameの値に好きな人の名前を入れましょう!
(データ型を変えるの忘れてました。構造のタブからnameのカラムだけを選択して編集することで、変えられます。Intからvarcharに変更しておきましょう)

そしたら、名前を入れて実行をします。

そうしたら、あとは同じ手順で好きなだけデータを入れてください。僕は友達の名前を3人入れました。
272dbcdc9fc24702fbb9d62f6db27d5d.png

(↑は表示タブから確認できます。)

PHPファイルでデータベースからデータを読み出す

さっきのaho.phpを使うので、エディタで開きましょう。

707fbc1757be967ebd44518195d7f068.png

パスワードの所は自分の設定したパスワードですので、このまま写していただければ問題ありません。(画像で申し訳ない)
そうしたら後はaho.phpを開けば・・・(http://localhost/aho.php)

6f74280589efdf77684151a6efcce106.png

ブラウザで表示できました。
多分細かな所はすごく省かれています。もし試して動かなかったら申し訳ありません。そういった苦情を見かけたら、やがて復讐にきれいな記事を書きます。
MakeITのadvent-calendar19日目にして内容が弱いのは申し訳ございません。破門は辞めてください、お願いします。正直PHP苦痛で・・・もう打ちたくないですよ・・・。

・・・あれ?PHPまともに打ってなくね??

ここまでお読み頂きありがとうございました。

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

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のマジックメソッド(findwhere)が誤検知されてしまいますし、自分でマジックメソッドやマジックプロパティを使っている箇所も同様です。

マジックメソッドやマジックプロパティの存在を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を使っていると思います。多分。

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

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 
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

CakePHP4.1に導入されるかもしれないDIコンテナ「phpleague/container」を試す

本記事は CakePHP Advent Calendar 2019 の19日目のエントリーになります。

はじめに

先日、待ちに待ったCakePHP4.0.0が 正式リリース されましたね :clap:

そして、気が早いのですが4.0系がリリースされたということは、次の大きなアップデートは4.1系のリリースということになります。(本当に気が早くてごめんなさい)

そんなこともあり早速 4.1のロードマップ をチラ見してみたのですが Application の欄に以下のような記述があり、何やら面白そうな変化が起きそうだなと感じました。

日本語に訳すと「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ちなみに ドキュメンテーションサイト がかっこよくてテンションが上りました :relaxed:

というわけで、2020年もCakePHPの動向から目が離せません!


  1. 簡単と言いつつドキュメントに書いてあるままやっていたらAutoloadの設定にハマって、作者の方に質問するなどしていました。(即レスで対応してくれて神でした:pray:) 

  2. Issueを細かくWatchしていないので、もしかしたら既に議論されているかもしれません。 

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