Posted on

PHPによるDBUnit超入門

例えば簡単なWebサービスでMVCのフレームワークを使っていてビジネスロジックを書く用にコントローラとモデルの間にサービス層を追加して開発している場合、コントローラやサービスはモックを駆使しながらテストを書いていくことができます。ただ、例えばフレームワークをバージョンアップしたい、PHPをバージョンアップしたいなどの場合に既存のモデル層に影響がないかをテストで確認したいなんてことがあります。そのような場合には、DBUnitを導入してみてもいいかもしれません。ということで本記事ではPHPによるDBUnitの使い方を書いてみます。

事前情報

phpunit/dbunitをインストールしようとすると以下の文言が出力されます。詳しくはこちらのissueに書いてありますが、どうもSebastianさんdbunitのメンテナンスをやめるようです。ただそれを受けてforkしたプロジェクトが出てきているようなので大丈夫かと思います。今回はSebastianさんの純正dbunitを使っています。

Package phpunit/dbunit is abandoned, you should avoid using it. No replacement was suggested

また、DBUnitに関する詳しい情報はマニュアルにありますのでご確認ください。
https://phpunit.de/manual/6.5/ja/database.html#database.implementing-getdataset

作成したサンプルプロジェクト

今回は、dbunitの確認だけをしたいので、dietcakemessage-boardというサンプルプロジェクトを利用しました。今回作成したDBUnit用のサンプルプロジェクトは GitHub からダウンロードして確認できます。

git clone git@github.com:taisa831/phpunit-dbunit-sample.git
cd phpunit-dbunit-sample
composer install
# mysqlサーバを立て`app/config/sql/board.sql`を実行する(SQLは下記に記載しています)
# テスト実行
./vendor/bin/phpunit
PHPUnit 7.5.8 by Sebastian Bergmann and contributors.
.....                                                               5 / 5 (100%)
Time: 207 ms, Memory: 4.00 MB
OK (5 tests, 14 assertions)

アプリ用のDDLです。開発用DBとは違うのでboard_dbunitというテーブル名にしています。

--
--
-- Create database
--
CREATE DATABASE IF NOT EXISTS board_dbunit;
GRANT SELECT, INSERT, UPDATE, DELETE ON board.* TO board_root@localhost IDENTIFIED BY 'board_root';
FLUSH PRIVILEGES;
--
-- Create tables
--
USE board_dbunit;
CREATE TABLE IF NOT EXISTS thread (
id                      INT UNSIGNED NOT NULL AUTO_INCREMENT,
title                   VARCHAR(255) NOT NULL,
created                 DATETIME NOT NULL,
PRIMARY KEY (id)
)ENGINE=InnoDB;
CREATE TABLE IF NOT EXISTS comment (
id                      INT UNSIGNED NOT NULL AUTO_INCREMENT,
thread_id               INT UNSIGNED NOT NULL,
username                VARCHAR(255) NOT NULL,
body                    TEXT NOT NULL,
created                 DATETIME NOT NULL,
PRIMARY KEY (id),
INDEX (thread_id, created)
)ENGINE=InnoDB;

モデルのテストは以下です。

assertEquals('1', $thread->id);
        $this->assertEquals('test', $thread->title);
        $this->assertEquals('2019-03-27 00:00:00', $thread->created);
    }
    public function test_getAll()
    {
        $threadAll = Thread::getAll();
        $this->assertEquals('1', $threadAll[0]->id);
        $this->assertEquals(1, $this->getConnection()->getRowCount('thread'));
    }
    public function test_getComments()
    {
        $thread = Thread::get(1);
        $comments = $thread->getComments();
        $this->assertEquals('1', $comments[0]->id);
    }
    public function test_create()
    {
        $comment = new Comment();
        $comment->thread_id = 1;
        $comment->username = 'user';
        $comment->body = 'comment';
        $thread = Thread::get(1);
        $thread->title = 'hoge';
        $thread->create($comment);
        $threadQueryTable = $this->getConnection()->createQueryTable('thread', 'select * from thread');
        $this->assertEquals(2, $threadQueryTable->getRowCount());
        $this->assertEquals('hoge', $threadQueryTable->getValue(1, 'title'));
        $commentQueryTable = $this->getConnection()->createQueryTable('comment', 'select * from comment');
        $this->assertEquals(2, $commentQueryTable->getRowCount());
        $this->assertEquals('user', $commentQueryTable->getValue(1, 'username'));
        $this->assertEquals('comment', $commentQueryTable->getValue(1, 'body'));
    }
    public function test_write()
    {
        $comment = new Comment();
        $comment->thread_id = 1;
        $comment->username = 'user';
        $comment->body = 'comment';
        $thread = Thread::get(1);
        $thread->write($comment);
        $commentQueryTable = $this->getConnection()->createQueryTable('comment', 'select * from comment');
        $this->assertEquals(2, $commentQueryTable->getRowCount());
        $this->assertEquals('user', $commentQueryTable->getValue(1, 'username'));
        $this->assertEquals('comment', $commentQueryTable->getValue(1, 'body'));
    }
    public function getTearDownOperation()
    {
        return \PHPUnit\DbUnit\Operation\Factory::TRUNCATE();
    }
}

カバレッジレポートはこのようになりました。

これで一通りの確認ができました。細かい書き方は色々あると思いますが、とにかくやりたいことはこれでできると思います。DBUnitではPDOを利用していますが、アプリケーションではPDOを使っていなくても大丈夫ですし、基本的にはモックなどを利用する必要もありませんので手軽に導入できると思います。

PDO を使ったアプリケーションじゃないと Database Extension を使えないの?
いいえ。PDO が必要なのは、フィクスチャの準備や後始末とアサーションのときだけです。 テスト対象のコード内では、なんでもお好みの方法でデータベースにアクセスできます。

プロジェクト作成までの流れ

それではここからプロジェクト作成までの流れをさらっとやってみます。

プロジェクトを作成する

git clone git@github.com:dietcake/dietcake-message-board.git
cd dietcake-message-board

`composer`の`require-dev`にライブラリを追加する

composer.jsonがないので`composer init`をしてファイルを作成します。そして以下のライブラリを追加し`composer install –dev`を実行します。

    "require-dev": {
        "phpunit/phpunit": "^7.5",
        "phpunit/dbunit": "^4.0"
    },

DBアクセスの為の`abstract`クラスを作成する

`DatabaseTestCase.php`という `abstract`クラスを作成します。`$GLOBALS`は後ほど作成する`phpunit.xml`に定義します。

conn === null) {
            if (self::$pdo == null) {
                self::$pdo = new PDO($GLOBALS['DB_DSN'], $GLOBALS['DB_USER'], $GLOBALS['DB_PASSWD']);
            }
            $this->conn = $this->createDefaultDBConnection(self::$pdo, $GLOBALS['DB_DBNAME']);
        }
        return $this->conn;
    }
}

モデルのテストクラスを作成する

フィクスチャはあらかじめ用意しておくデータのことです。まだテストを書いていない状態ですが、フィクスチャである`getDataSet`の実装とあと処理様の`getTearDownOperation`を実装しておきます。`getDataSet`を実装しておくと、テスト実行前にDBに値を入れてくれて、`getTearDownOperation`を実装するとテスト実行後にテーブルをクリアしてくれます。

<?php
require_once dirname(__FILE__) . '/../DatabaseTestCase.php';
require_once dirname(__FILE__) . '/../../../models/thread.php';
use PHPUnit\DbUnit\DataSet\YamlDataSet;
class ThreadTest extends DatabaseTestCase
{
    /**
     * Returns the test dataset.
     */
    protected function getDataSet()
    {
        return new YamlDataSet(dirname(__FILE__) . '/_files/thread.yml');
    }
    public function getTearDownOperation()
    {
        return \PHPUnit\DbUnit\Operation\Factory::TRUNCATE();
    }
}

フィクスチャを作成する

DBにあらかじめ入れておくレコード情報を作成します。様々なフォーマットが使えますが`Yaml`が一番扱いやすいので`Yaml`で記述しています。

thread:
  -
    id: '1'
    title: 'test'
    created: '2019-03-27 00:00:00'
comment:
  -
    id: '1'
    thread_id: '1'
    username: 'taro'
    body: 'comment'
    created: '2019-03-27 00:00:00'

`bootstrap.php`を作成する

`dietcake`が最低限動くようにテスト用の`bootstrap.php`を作成します。

<?php
define('ROOT_DIR', dirname(dirname(__DIR__)).'/../');
define('APP_DIR', ROOT_DIR.'app/');
require_once ROOT_DIR.'dietcake/dietcake.php';
require_once CONFIG_DIR.'bootstrap.php';
require_once CONFIG_DIR.'core.php';

ここまでで、最終的なディレクトリ構成はこのようになりました。

tests
├── dbunit
│   ├── DatabaseTestCase.php
│   ├── bootstrap.php
│   └── models
│       ├── ThreadTest.php
│       └── _files
│           └── thread.yml
└── unittest
    └── empty

`phpunit.xml`を作成する

場所はどこでもよいですが、プロジェクト直下に`phpunit.xml`ファイルを作成します。このファイルにはDBのアクセス情報、テスト対象、ホワイトリスト対象、bootstrapの指定などを行なっています。ホワイトリストディレクトリを指定しておくことでカバレッジレポートを確認することできるようになります。



    
        
        
        
        
    
    
        
            ./app/tests/dbunit/models
        
    
    
        
            ./app/models/
        
    

テストを作成する

やり方はいくつかあると思いますが、とにかくやりたいのは、modelsの処理を実行し、アサートチェックし、あと処理で元に戻すことなので、その目的を達成することを一番に考えました。このプロジェクトはthread.phpに処理が書かれているのでそれをテストします。データ取得とデータ作成処理があるので、そのパターンにおける確認ができます。
データ取得処理は、フィクスチャで入ったレコードをモデルで取得し結果をアサーションしました。

public function test_getTest()
{
    $thread = Thread::get(1);
    $this->assertEquals('1', $thread->id);
    $this->assertEquals('test', $thread->title);
    $this->assertEquals('2019-03-27 00:00:00', $thread->created);
}

書き込み処理は、モデルで書き込みをした後、DBUnitに用意されているcreateQueryTableを利用してデータを取得し結果をアサーションしました。データセットを利用したりテーブル結果をアサーションするような方法もあるようですが、ここでは利用していません。

public function test_write()
{
    $comment = new Comment();
    $comment->thread_id = 1;
    $comment->username = 'user';
    $comment->body = 'comment';
    $thread = Thread::get(1);
    $thread->write($comment);
    $commentQueryTable = $this->getConnection()->createQueryTable('comment', 'select * from comment');
    $this->assertEquals(2, $commentQueryTable->getRowCount());
    $this->assertEquals('user', $commentQueryTable->getValue(1, 'username'));
    $this->assertEquals('comment', $commentQueryTable->getValue(1, 'body'));
}

カバレッジレポートを出力する

以下のコマンドでカバレッジレポートを作成することができます。

./vendor/bin/phpunit -c phpunit.xml --coverage-html coverage

まとめ

調べたところDBUnit自体の記事が少なかったので、実際に利用しているサービスはあんまりないのではないかと思いましたが実際のところはどうなのかはわかりません。自分も今回初めて触ってみましたが、バージョンアップ時や新しくジョインしたプロジェクトなどモデル層に不安がある場合には導入してがっつりテストを書いてしまうなどはありかなと思いました。

Posted on

C言語初級者がMacのコンソールで実行可能なテトリスを作ってみた

C言語初級者がMacのコンソールで実行できるテトリスを作ってみました。参考にした動画はこちらです。テトリスについてはWikipediaも参考にしました。この投稿では作ってみた上で気になった箇所をピックアップして解説していきます。全ソースコードはこちらで確認できます。これについては、Youtubeにあげている方にも許可を頂いています。

テトリス – Wikipedia

日本では、 1988年にセガ・エンタープライゼス(後の セガ・インタラクティブ)から発売された アーケード版( セガ・システム16版)の人気により浸透した。当時はまだ操作法が確立されていなかったが、このシステム16版の登場以降は同作のものが日本国内における 事実上の標準となり、その影響力から特に「 セガテトリス」とよく呼ばれる( 2000年にアーケードと …

まずテトリスの枠を作る

まず最初にテトリスの枠を作ります。テトリスの枠は横が12個、縦が22個のブロックでできています。なので下の図のようにそのブロック箇所へ1を立てて、ブロックを描画していけばよいことになります。

単純に書くと以下のようになりますが、それを少し整理してchar field[FIELD_HEIGHT][FIELD_WIDTH];のフィールドに値を格納する書き方に変更すると以下のようになります。

# 単純に書いた方
#include <stdio.h>
#include <memory.h>
#define FIELD_WIDTH 12
#define FIELD_HEIGHT 22
int main() {
    for (int i = 0; i < FIELD_HEIGHT; i++) {
        for (int j = 0; j < FIELD_WIDTH; j++) {
            if (j == 0 || j == FIELD_WIDTH - 1 || i == FIELD_HEIGHT - 1) {
                printf("■");
            } else {
                printf(" ");
            }
        }
        printf("\n");
    }
}
# 整理した方
#include 
#include 
#define FIELD_WIDTH 12
#define FIELD_HEIGHT 22
char field[FIELD_HEIGHT][FIELD_WIDTH];
int main() {
    memset(field, 0, sizeof(field));
    // 左右の壁
    for (int i = 0; i < FIELD_HEIGHT; i++) {
        field[i][0] = 1;
        field[i][FIELD_WIDTH - 1] = 1;
    }
    // 下の壁
    for (int i = 0; i < FIELD_WIDTH; i++) {
        field[FIELD_HEIGHT - 1][i] = 1;
    }
    // 描画
    for (int i = 0; i < FIELD_HEIGHT; i++) {
        for (int j = 0; j < FIELD_WIDTH; j++) {
            printf(field[i][j] ? "■" : " ");
        }
        printf("\n");
    }
}

ミノを表示する

枠が表示できたら次は、ミノを表示します。ミノは下図のように7種類ありますが、ここではテトリス棒を表示することをやってみます。

枠を表示したFieldを元にし、あらたにミノを表示するdisplayBuffer領域を確保しミノを表示します。最初にミノを表示する箇所、右の黒枠箇所となります。

#define FIELD_WIDTH 12
#define FIELD_HEIGHT 22
#define MINO_TYPE_MAX 7
#define MINO_ANGLE_MAX 4
#define MINO_WIDTH 4
#define MINO_HEIGHT 4
char field[FIELD_HEIGHT][FIELD_WIDTH];
char displayBuffer[FIELD_HEIGHT][FIELD_WIDTH];
char minoShapes[MINO_TYPE_MAX][MINO_ANGLE_MAX][MINO_HEIGHT][MINO_WIDTH] = {
        // MINO_TYPE_I
        {
                // MINO_ANGLE_0
                {
                        0, 1, 0, 0,
                        0, 1, 0, 0,
                        0, 1, 0, 0,
                        0, 1, 0, 0,
                },
         }
};
void display() {
    memcpy(displayBuffer, field, sizeof(field));
    for (int i = 0; i < MINO_HEIGHT; i++) {
        for (int j = 0; j < MINO_WIDTH; j++) {
            displayBuffer[minoY + i][minoX + j] |= minoShapes[minoType][minoAngle][i][j];
        }
    }
    system("clear");
    for (int i = 0; i < FIELD_HEIGHT; i++) {
        for (int j = 0; j < FIELD_WIDTH; j++) {
            printf(displayBuffer[i][j] ? "■" : " ");
        }
        printf("\n");
    }
}
int main() {
    memset(field, 0, sizeof(field));
    for (int i = 0; i < FIELD_HEIGHT; i++) {
        field[i][0] = field[i][FIELD_WIDTH - 1] = 1;
    }
    for (int i = 0; i < FIELD_WIDTH; i++) {
        field[FIELD_HEIGHT - 1][i] = 1;
    }
    display();
}

1秒に1回更新する

以下のコードを使えば1秒に1回更新することができます。

#include 
time_t t = time(NULL);
while (true) {
    if (t != time(NULL)) {
        t = time(NULL);
        printf("%ld\n", t);
    }
}

キーボード入力を取得する

Windowsとは違いLinux環境ではkbhit()に相当するものが内容なのでこちらのサイトにある関数をそのまま利用しました。また、動画にはなかったですが、booleanを扱うために#include <stdbool.h>を追記しています。

#include 
#include 
#include 
#include 
bool kbhit() {
    struct termios oldt, newt;
    int ch;
    int oldf;
    tcgetattr(STDIN_FILENO, &oldt);
    newt = oldt;
    newt.c_lflag &= ~(ICANON | ECHO);
    tcsetattr(STDIN_FILENO, TCSANOW, &newt);
    oldf = fcntl(STDIN_FILENO, F_GETFL, 0);
    fcntl(STDIN_FILENO, F_SETFL, oldf | O_NONBLOCK);
    ch = getchar();
    tcsetattr(STDIN_FILENO, TCSANOW, &oldt);
    fcntl(STDIN_FILENO, F_SETFL, oldf);
    if (ch != EOF) {
        ungetc(ch, stdin);
        return true;
    }
    return false;
}

aキーで左、sキーで下、dキーで右、スペースキーで回転するようになっています。

if (kbhit()) {
    switch (getchar()) {
        case 's':
            if (!isHit(minoX, minoY + 1, minoType, minoAngle)) {
                minoY++;
            }
            break;
        case 'a':
            if (!isHit(minoX - 1, minoY, minoType, minoAngle)) {
                minoX--;
            }
            break;
        case 'd':
            if (!isHit(minoX + 1, minoY, minoType, minoAngle)) {
                minoX++;
            }
            break;
        case 0x20:
            if (!isHit(minoX, minoY, minoType, (minoAngle + 1) % MINO_ANGLE_MAX)) {
                minoAngle = (minoAngle + 1) % MINO_ANGLE_MAX;
            }
            break;
    }
    display();
}

ミノが壁にあたるかを判定する

ミノが壁にあたるかを判定するには以下のような関数でチェックします。

bool isHit(int _minoX, int _minoY, int _minoType, int _minoAngle) {
    for (int i = 0; i < MINO_HEIGHT; i++) {
        for (int j = 0; j < MINO_WIDTH; j++) {
            if (minoShapes[_minoType][_minoAngle][i][j] && field[_minoY + i][_minoX + j]) {
                return true;
            }
        }
    }
    return false;
}

行が揃ったら消す

if (t != time(NULL)) {
    t = time(NULL);
    if (isHit(minoX, minoY + 1, minoType, minoAngle)) {
        for (int i = 0; i < MINO_HEIGHT; i++) {
            for (int j = 0; j < MINO_WIDTH; j++) {
                field[minoY + i][minoX + j] |= minoShapes[minoType][minoAngle][i][j];
            }
        }
        for (int i = 0; i < FIELD_HEIGHT - 1; i++) {
            int lineFill = 1;
            for (int j = 1; j < FIELD_WIDTH - 1; j++) {
                if (!field[i][j]) {
                    lineFill = 0;
                }
            }
            if (lineFill) {
                for (int j = i; 0 < j; j--) {
                    memcpy(field[j], field[j - 1], FIELD_WIDTH);
                }
            }
        }
        resetMino();
    } else {
        minoY++;
    }
    display();
}

まとめ

まだWindowsで実行していないのでわかりませんが、Windowsとの違いは、kbhit()#include <stdbool.h>includeするところあたりかと思います。また、Macのコンソール場合はclearコマンドで描画し直しているので履歴には残ってしまいます。動画を見て写経しただけですがテトリスってこんな感じで作れるんだなと思い楽しむことができました。改善や機能追加する点はまだたくさんあるので時間をみて更新していこうと思います。

Posted on

GitLabのprivateなPHPライブラリをcomposer installするには

社内ツールでprivateなリポジトリに置いておきたいけど、いろんなプロジェクトでcomposer installしたいというケースは以外とあるんじゃないかと思います。そういう時は、composer.jsonrepositoriesを追加して、GitLab(ここではGitLabとしています)のURLを指定するとインストールが可能になります。しかしそのままだとpublicなリポジトリしかだめですが、privateなリポジトリであれば、GitLabからPersonal AcessTokenを取得して、composer config --global --auth gitlab-token.gitlab.com [ACESS_TOKEN]を実行すればcomposer installが可能になります。

{
    "name": "taisa831/sample-framework-app",
    "license": "MIT",
    "authors": [
        {
            "name": "taisa",
            "email": "g5.taisa831@gmail.com"
        }
    ],
    "require": {
            "taisa831/sample-framework": "dev-master"
    },
    "repositories": [
            {
                    "type": "vcs",
                    "url": "git@gitlab.com:taisa831/sample-framework.git"
            }
    ]
}

では、Webフレームワークをprivateなリポジトリに公開して利用するところまでをやってみます。
(今回は便宜上publicにしています)

事前準備

ここではサンプルのWebフレームワーク(実装なし)をプロジェクトにインストールできるようにすることにします。リポジトリは2つで、フレームワークの実態であるsample-frameworkとフレームワークの雛形となるsample-framework-appを用意しておきました。それぞれの構成は以下の通りです。
https://gitlab.com/taisa831/sample-framework.git

# フレームワークの実体
.
├── README.md
├── composer.json
├── src
├── tests
└── vendor

https://gitlab.com/taisa831/sample-framework-app.git

# フレームワークの雛形
.
├── README.md
├── composer.json
├── composer.lock
├── config
├── controllers
├── models
├── public
│   └── index.php
├── routes
├── tests
└── views

このsample-framework-appcomposer.jsonには上記でも記載した内容が書かれています。requiretaisa831/sample-frameworkを指定し、repositoriesにGitLabのURLを指定することで探してくれるようになります。

{
    "name": "taisa831/sample-framework-app",
    "license": "MIT",
    "authors": [
        {
            "name": "taisa",
            "email": "g5.taisa831@gmail.com"
        }
    ],
    "require": {
            "taisa831/sample-framework": "dev-master"
    },
    "repositories": [
            {
                    "type": "vcs",
                    "url": "git@gitlab.com:taisa831/sample-framework.git"
            }
    ]
}

Webフレームワークを利用したプロジェクトを作成する

これでWebフレームワークを利用するプロジェクトを作成することができます。通常ならcomposer create-project --prefer-dist taisa831/sample-framework-app dev-masterこのようなコマンドでいけるのですが、packagistに登録していないので手動やります。(satisを使えばというのもありますがそこまでしたくはないので)

git clone git@gitlab.com:taisa831/sample-framework-app.git web-app
cd web-app
rm -rf .git
# privateなリポジトリの場合はここ(https://gitlab.com/profile/personal_access_tokens)からアクセストークンを取得し設定します。
# 今回はpublicなので不要です
composer config --global --auth gitlab-token.gitlab.com [ACESS_TOKEN]
composer install
git init
git remote add origin [新しいリポジトリ]

これでフレームワークの雛形と実体が合わさり環境構築の完了となります。

.
├── README.md
├── composer.json
├── composer.lock
├── config
├── controllers
├── models
├── public
│   └── index.php
├── routes
├── tests
├── vendor
│   ├── autoload.php
│   ├── composer
│   │   ├── ClassLoader.php
│   │   ├── LICENSE
│   │   ├── autoload_classmap.php
│   │   ├── autoload_namespaces.php
│   │   ├── autoload_psr4.php
│   │   ├── autoload_real.php
│   │   ├── autoload_static.php
│   │   └── installed.json
│   └── taisa831
│       └── sample-framework ← フレームワークの実体
│           ├── README.md
│           ├── composer.json
│           ├── src
│           └── tests
└── views
Posted on

PHPライブラリをPackagistに登録する方法

PHPのライブラリをPackagistに登録する方法を書いておきます。PackagistはPHPのパッケージリポジトリで、登録しておくとcomposerを使ってプロジェクトへインストールすることができます。ここではとあるプロジェクトをPackagistに登録する前提の流れで進めていきます。

Packagistに登録するプロジェクトを作成する

新規でプロジェクトを作成しcomposer initを実行します。

mkdir amazon-photo-formatter
cd amazon-photo-formatter
composer init

composer initを実行すると色々と聞かれるので順番に進めていきます。まずはパッケージ名が聞かれます。<vendor>にはGitHubのアカウント名を指定し、<name>にはライブラリ名を記載します。ここではtaisa831/amazon-photo-formatterと記載しました。

Package name (/) [taisa831/packagist]:

Descriptionはライブラリについての説明文なので、Format amazon photo file name to amazon photo's format.と書きました。その他についてもサジェストされている内容とするか必要な内容を決めて進めていきます。

Description []:
Author [Masaki Sato , n to skip]:
Minimum Stability []:
Package Type (e.g. library, project, metapackage, composer-plugin) []: library
License []: MIT

次にこのライブラリが依存しているものがあればこの時点で指定することができます(後から手動で記載することも可能)。ここではphpunitを利用するのでrequire-devphpunitを指定しました。

Would you like to define your dependencies (require) interactively [yes]? no
Would you like to define your dev dependencies (require-dev) interactively [yes]?
Search for a package: phpunit
Found 15 packages matching phpunit
   [0] phpunit/phpunit
   [1] phpunit/phpunit-mock-objects Abandoned. Use  instead.
   [2] phpunit/php-token-stream
   [3] phpunit/php-timer
   [4] phpunit/php-text-template
   [5] phpunit/php-file-iterator
   [6] phpunit/php-code-coverage
   [7] symfony/phpunit-bridge
   [8] phpunit/phpunit-selenium
   [9] johnkary/phpunit-speedtrap
  [10] codedungeon/phpunit-result-printer
  [11] jean85/pretty-package-versions
  [12] brianium/paratest
  [13] codeception/stub
  [14] spatie/phpunit-snapshot-assertions
Enter package # to add, or the complete package name if it is not listed: 0
Enter the version constraint to require (or leave blank to use the latest version):
Using version ^7.5 for phpunit/phpunit

そうすることで次のようなcomposer.jsonファイルができあがり、このままgenerateするか、composer installするかを聞かれるのでyesとして進めていきます。実行が終わったらプロジェクト直下にsrctestsディレクトリを作成しておきます。

{
    "name": "taisa831/amazon-photo-formatter",
    "description": "Format amazon photo file name to amazon photo format.",
    "type": "library",
    "license": "MIT",
    "authors": [
        {
            "name": "taisa",
            "email": "g5.taisa831@gmail.com"
        }
    ],
    "require": {},
    "require-dev": {
        "phpunit/phpunit": "^7.5"
    }
}
Do you confirm generation [yes]? yes
Would you like to install dependencies now [yes]? yes
Loading composer repositories with package information
Updating dependencies (including require-dev)
Package operations: 28 installs, 0 updates, 0 removals
  - Installing sebastian/version (2.0.1): Loading from cache
  - Installing sebastian/resource-operations (2.0.1): Loading from cache
  - Installing sebastian/recursion-context (3.0.0): Loading from cache
  - Installing sebastian/object-reflector (1.1.1): Loading from cache
  - Installing sebastian/object-enumerator (3.0.3): Loading from cache
  - Installing sebastian/global-state (2.0.0): Loading from cache
  - Installing sebastian/exporter (3.1.0): Loading from cache
  - Installing sebastian/environment (4.1.0): Loading from cache
  - Installing sebastian/diff (3.0.2): Loading from cache
  - Installing sebastian/comparator (3.0.2): Loading from cache
  - Installing phpunit/php-timer (2.1.1): Loading from cache
  - Installing phpunit/php-text-template (1.2.1): Loading from cache
  - Installing phpunit/php-file-iterator (2.0.2): Loading from cache
  - Installing theseer/tokenizer (1.1.0): Loading from cache
  - Installing sebastian/code-unit-reverse-lookup (1.0.1): Loading from cache
  - Installing phpunit/php-token-stream (3.0.1): Loading from cache
  - Installing phpunit/php-code-coverage (6.1.4): Loading from cache
  - Installing doctrine/instantiator (1.1.0): Loading from cache
  - Installing symfony/polyfill-ctype (v1.10.0): Loading from cache
  - Installing webmozart/assert (1.4.0): Loading from cache
  - Installing phpdocumentor/reflection-common (1.0.1): Loading from cache
  - Installing phpdocumentor/type-resolver (0.4.0): Loading from cache
  - Installing phpdocumentor/reflection-docblock (4.3.0): Loading from cache
  - Installing phpspec/prophecy (1.8.0): Loading from cache
  - Installing phar-io/version (2.0.1): Loading from cache
  - Installing phar-io/manifest (1.0.3): Loading from cache
  - Installing myclabs/deep-copy (1.8.1): Loading from cache
  - Installing phpunit/phpunit (7.5.7): Downloading (100%)
sebastian/global-state suggests installing ext-uopz (*)
phpunit/php-code-coverage suggests installing ext-xdebug (^2.6.0)
phpunit/phpunit suggests installing phpunit/php-invoker (^2.0)
phpunit/phpunit suggests installing ext-xdebug (*)
Writing lock file
Generating autoload files

登録するライブラリを作成する

ここまででプロジェクトの形ができあがったのでsrc配下にソースファイルを作成します。今回は簡単なサンプルプログラムとしています。

.
├── composer.json
├── composer.lock
├── package-lock.json
├── vendor
├── src
└── tests
setCurrentDir($currentDir);
    }
    public function format()
    {
        $this->createFormatDir();
        $this->formatPictures();
    }
    public function createFormatDir()
    {
        if (file_exists('format') === false) {
            mkdir('format');
        }
    }
    /**
     * @throws \Exception
     */
    public function formatPictures()
    {
        foreach(glob($this->getCurrentDir() . '/{*.jpeg,*.jpg}', GLOB_BRACE) as $fileName) {
            if (is_file($fileName) === false) continue;
            $photoImg = file_get_contents($fileName);
            $ext = substr($fileName, strrpos($fileName, '.') + 1);
            $exif = @exif_read_data($fileName);
            $dateTime = new \DateTime($exif['DateTimeOriginal']);
            $formatFileName = $dateTime->format('Y-m-d_H-i-s');
            file_put_contents('./format/' . $formatFileName . '.' . $ext, $photoImg);
        }
    }
    /**
     * @return mixed
     */
    public function getCurrentDir()
    {
        return $this->currentDir;
    }
    /**
     * @param mixed $currentDir
     */
    public function setCurrentDir($currentDir)
    {
        $this->currentDir = $currentDir;
    }
}

unittestを作成する

続いて、`src`配下に先程作成したソースのテストを書いていきます。他の依存ライブラリを利用している場合は`bootstrap.php`ファイルも作成しておきます。

getMockBuilder(AmazonPhotoFormatter::class)
            ->setConstructorArgs([$dir])
            ->setMethods(null)
            ->getMock();
        $currentDir = $amazonPhotoFormatter->getCurrentDir();
        $this->assertEquals($dir, $currentDir);
    }
    public function test_format()
    {
        $amazonPhotoFormatter = $this->createPartialMock(AmazonPhotoFormatter::class, ['createFormatDir', 'formatPictures']);
        $amazonPhotoFormatter->expects($this->once())->method('createFormatDir');
        $amazonPhotoFormatter->expects($this->once())->method('formatPictures');
        $amazonPhotoFormatter->format();
    }
}

`bootstrap.php`を作成する場合はこんな感じ

<?php
error_reporting(E_ALL | E_STRICT);
require_once __DIR__ . '/../vendor/autoload.php';

これで`tests`配下で以下のコマンドを実行するとテストがいくつか実行されます。

../vendor/bin/phpunit AmazonPhotoFormatterTest.php
PHPUnit 7.5.7 by Sebastian Bergmann and contributors.
...                                                                 3 / 3 (100%)
Time: 51 ms, Memory: 4.00 MB
OK (3 tests, 3 assertions)

`phpunit.xml.dist`ファイルをプロジェクト直下に置いておくとプロジェクト直下で次のコマンドでも実行可能になります。

vendor/bin/phpunit
PHPUnit 7.5.7 by Sebastian Bergmann and contributors.
...                                                                 3 / 3 (100%)
Time: 25 ms, Memory: 4.00 MB
OK (3 tests, 3 assertions)


    
        
            ./tests/
        
    
    
        
            ./src/
        
    

Travis CIと連携する

Travis CIとGitHubのリポジトリを連携させ、以下のような.travis.ymlを作成しプロジェクト直下に配置することでgit pushした際にテストが実行されるようになります。

language: php
php:
  - 7.3
before_script:
  - composer self-update
  - composer install
script:
  - vendor/bin/phpunit

Packagistに登録する

作成したプロジェクトを自分のGitHubへアップしたら、Packagistへ登録をします。packagistにGitHubアカウントでログインした後、Submit 画面へ進み、登録したいリポジトリのURLを入力すると登録することができます。

GitHubとPackagistを連携する

最後にGitHubにプッシュしたらPackagistも自動的に更新するように連携しておきます。連携は、GitHubリポジトリのSettings>Webhooksから行うことができます。PackagistのプロフィールからAPI Tokenを取得しておき、GitHubのWebhooksへ次のように登録します。

  • Payload URL:https://packagist.org/api/github?username=[GitHubのユーザ名]
  • Content type:application/json
  • Secret:PackagistのAPI Token

ライブラリを使ってプロジェクトを作成してみる

ここまでで一通りの登録が完了したので、実際にプロジェクトから呼び出してみます。新規でプロジェクトを作成するには以下のコマンドを実行します。プロジェクト名の後ろにdev-master(masterブランチ)やGitHubのタグを指定することでバージョンを指定することが可能です。また、composer.jsonにライブラリ名とバージョンを記載してcomposer installして利用することも可能です。

composer create-project taisa831/amazon-photo-formatter project-name dev-master
composer create-project taisa831/amazon-photo-formatter project-name v1.0.0
{
    "name": "masakisato/testprj",
    "authors": [
        {
            "name": "taisa",
            "email": "g5.taisa831@gmail.com"
        }
    ],
    "require": {
            "taisa831/amazon-photo-formatter": "1.0.0"
    }
}
$ composer install
Loading composer repositories with package information
Updating dependencies (including require-dev)
Package operations: 1 install, 0 updates, 0 removals
  - Installing taisa831/amazon-photo-formatter (v1.0.0): Loading from cache
Writing lock file
Generating autoload files

参考

Packagistに登録するのはもう怖くない

Posted on

PHPの empty, isset, is_null の違いをしっかり理解する

PHPの isset、empty、is_null をしっかり理解して使おうと思い整理してみました。既にこのような記事「PHP isset, empty, is_null の違い早見表」もあるのでここではこれより少し踏み込んだところまで書いてみます。

empty, isset, is_nullの違い早見表

if ($var) empty isset is_null
$var = 1 true false true false
$var = array(1) true false true false
$var = “” false true true false
$var = “0” false true true false
$var = 0 false true true false
$var = array() false true true false
$var = false false true true false
$var = NULL; false true false true
$var false true false true

まず表ですが、順番を理解しやすい形に変えてみました。下記のように赤と青のグループで分けて考えておくと理解しやすいです。これをみると「if ($var)empty」、「issetis_null」が対になっているのがわかります。

感覚的には、if ($var)は値がありそうだなと思うものがtrueになり、emptyも値がなさそうだな思うものがtrueになる感じがします。issetは、何かしら値がセットされてばtrue(つまり値がfalseでも結果はtrue)、is_nullは値がnullであればtrueということになります。
実際の挙動の動作確認についてはPHPUnitを使ってテストしたものをGitHubにあげているので合わせて確認してみてください。https://github.com/taisa831/AimaiPHP

empty, isset, is_null の Notice や Error 出力早見表

次に、PHPのerror_reportingE_ALLにした場合に、indexのない配列にアクセスした場合やオブジェクトが空の変数や関数にアクセスした場合の挙動をまとめてみました。

if ($var) empty isset is_null
$var = [];
$var[0];
Undefined offset: 0 true false Undefined offset: 0
$var = [];
$var[‘hoge’]
Undefined index: hoge true false Undefined index: hoge
$var = null;
$var[‘hoge’]
null true false null
$var = (object)[];
$var->var;
Undefined property: stdClass::$func true false Undefined property: stdClass::$func
$var = null;
$var->var;
Trying to get property of non-object true false Trying to get property of non-object
$var = (object)[];
$var->func();
Error: Call to undefined method stdClass::func() Error: Call to undefined method stdClass::func() 実行不可 Error: Call to undefined method stdClass::func()
$var = null;
$var->func();
Error: Call to undefined method stdClass::func() Error: Call to undefined method stdClass::func() 実行不可 Error: Call to undefined method stdClass::func()

結果は、上の表のようになりました。オブジェクトに対して未定義の関数にアクセスした場合やnullから関数を呼ぼうとした場合全てにおいてErrorになります。それ以外では、emptyissetを使うとNoticeは出でませんが、if ($var)is_nullを使うとNoticeがでるという違いがあります。この辺の違いを理解しておくとPHPによる開発が進めやすくなると思います。スマホだと表が切れてしまうので画像も貼っておきます。