Posted on

Go言語 GORM+GinでTODOリストのAPIを作ってみた

前回の「Go言語 GORM+GinでTODOリストを作ってみた」に続いて「GORM+Gin」でTODOリストのAPIを作ってみました。ソースコードは前回からの差分だけを記載しています。できたものは下記URLから確認できます。
http://sandbox.taisablog.com/api/v1/

GinのGithub

事前情報

ルーティングは今回はAPIなので以下としました。モデルをtasksにすればよかったと思いましたが一旦このままにしておきます。

[GIN-debug] GET    /todo                     --> main.main.func1 (3 handlers) // 一覧表示
[GIN-debug] POST   /todo                     --> main.main.func2 (3 handlers) // 新規作成
[GIN-debug] GET    /todo/:id                 --> main.main.func3 (3 handlers) // 編集画面表示
[GIN-debug] PUT    /todo/:id                 --> main.main.func4 (3 handlers) // 編集
[GIN-debug] DELETE /todo/:id                 --> main.main.func5 (3 handlers) // 削除

ディレクトリ構成

.
├── api
│   └── v1
│       └── todo.go
├── controllers
│   └── todo.go
├── db
│   └── db.go
├── main.go
├── models
│   └── todo.go
├── router
    └── router.go

router.go

router.gor.Group("/api/v1")のAPI用のグループを追加してルーティングを追加しました。

package router
import (
  "github.com/gin-contrib/cors"
  "github.com/gin-gonic/gin"
  "github.com/jinzhu/gorm"
  v1 "github.com/taisa831/sandbox-gin/api/v1"
  "github.com/taisa831/sandbox-gin/controllers"
  "time"
)
func Router(dbConn *gorm.DB) {
  todoHandler := controllers.TodoHandler{
    Db: dbConn,
  }
  r := gin.Default()
  r.Use(cors.New(cors.Config{
    AllowOrigins:     []string{"*"},
    AllowMethods:     []string{"PUT", "PATCH", "DELETE", "POST", "GET"},
    AllowHeaders:     []string{"Origin"},
    ExposeHeaders:    []string{"Content-Length"},
    AllowCredentials: true,
    AllowOriginFunc: func(origin string) bool {
      return origin == "*"
    },
    MaxAge: 12 * time.Hour,
  }))
  r.LoadHTMLGlob("templates/*")
  r.GET("/todo", todoHandler.GetAll) // 一覧画面
  r.POST("/todo", todoHandler.CreateTask) // 新規作成
  r.GET("/todo/:id", todoHandler.EditTask) // 編集画面
  r.POST("/todo/edit/:id", todoHandler.UpdateTask) // 更新
  r.POST("/todo/delete/:id", todoHandler.DeleteTask) // 削除
  apiV1 := r.Group("/api/v1")
  {
    apiTodoHandler := v1.TodoHandler{
      Db: dbConn,
    }
    apiV1.GET("/todo", apiTodoHandler.GetAll) // 一覧画面
    apiV1.POST("/todo", apiTodoHandler.CreateTask) // 新規作成
    apiV1.GET("/todo/:id", apiTodoHandler.EditTask) // 編集画面
    apiV1.PUT("/todo/:id", apiTodoHandler.UpdateTask) // 更新
    apiV1.DELETE("/todo/:id", apiTodoHandler.DeleteTask) // 削除
  }
  r.Run(":9000")
}

api/v1/todo.go

JSONで受けた値を処理してJSONを返すように変更しました。

package v1
import (
  "github.com/gin-gonic/gin"
  "github.com/jinzhu/gorm"
  "github.com/taisa831/gin-sandbox/models"
  "net/http"
)
type TodoHandler struct {
  Db *gorm.DB
}
func (h *TodoHandler) GetAll(c *gin.Context) {
  var todos []models.Todo
  h.Db.Find(&todos)
  c.JSON(http.StatusOK, todos)
}
func (h *TodoHandler) CreateTask(c *gin.Context) {
  todo := models.Todo{}
  err := c.BindJSON(&todo)
  if err != nil {
    c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{
      "error": err.Error(),
    })
    return
  }
  h.Db.Create(&todo)
  c.JSON(http.StatusOK, &todo)
}
func (h *TodoHandler) EditTask(c *gin.Context) {
  todo := models.Todo{}
  id := c.Param("id")
  h.Db.First(&todo, id)
  c.JSON(http.StatusOK, todo)
}
func (h *TodoHandler) UpdateTask(c *gin.Context) {
  todo := models.Todo{}
  id := c.Param("id")
  h.Db.First(&todo, id)
  err := c.BindJSON(&todo)
  if err != nil {
    c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{
      "error": err.Error(),
    })
  }
  h.Db.Save(&todo)
  c.JSON(http.StatusOK, &todo)
}
func (h *TodoHandler) DeleteTask(c *gin.Context) {
  todo := models.Todo{}
  id := c.Param("id")
  h.Db.First(&todo, id)
  err := h.Db.First(&todo, id).Error
  if err != nil {
    c.AbortWithStatus(http.StatusNotFound)
    return
  }
  h.Db.Delete(&todo)
  c.JSON(http.StatusOK, gin.H{
    "status": "ok",
  })
}

動作確認

ターミナルでjsonをみやすくするようにjsonppを入れておきます。

brew install jsonpp

一覧取得

% curl -X GET -H "Content-Type: application/json" http://localhost:9000/api/v1/todo | jsonpp
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100   271  100   271    0     0  44172      0 --:--:-- --:--:-- --:--:-- 45166
[
  {
    "ID": 1,
    "CreatedAt": "2019-07-04T10:16:28+09:00",
    "UpdatedAt": "2019-07-04T10:16:28+09:00",
    "DeletedAt": null,
    "Text": "テスト",
    "Status": 1
  },
  {
    "ID": 2,
    "CreatedAt": "2019-07-04T10:16:38+09:00",
    "UpdatedAt": "2019-07-04T10:16:38+09:00",
    "DeletedAt": null,
    "Text": "実装",
    "Status": 1
  }
]

新規作成

% curl -X POST -H "Content-Type: application/json" -d '{"text":"test", "status":2}' http://localhost:9000/api/v1/todo | jsonpp
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100   172  100   145  100    27  23592   4393 --:--:-- --:--:-- --:--:-- 24166
{
  "ID": 3,
  "CreatedAt": "2019-07-04T10:18:45.041387+09:00",
  "UpdatedAt": "2019-07-04T10:18:45.041387+09:00",
  "DeletedAt": null,
  "Text": "test",
  "Status": 2
}

更新

% curl -X PUT -H "Content-Type: application/json" -d '{"text":"update", "status":3}' http://localhost:9000/api/v1/todo/1 | jsonpp
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100   169  100   140  100    29  20951   4340 --:--:-- --:--:-- --:--:-- 23333
{
  "ID": 1,
  "CreatedAt": "2019-07-04T10:16:28+09:00",
  "UpdatedAt": "2019-07-04T10:19:45.818126+09:00",
  "DeletedAt": null,
  "Text": "update",
  "Status": 3
}

削除

% curl -X DELETE -H "Content-Type: application/json" http://localhost:9000/api/v1/todo/1 | jsonpp
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100    16  100    16    0     0   2025      0 --:--:-- --:--:-- --:--:--  2285
{
  "status": "ok"
}

参考:
https://github.com/hugomd/go-todo
https://github.com/gin-gonic/gin
今回のソース:
https://github.com/taisa831/gin-sandbox

Posted on

Go言語 GORM+GinでTODOリストを作ってみた

前回の「Go言語 ORMライブラリ GORMの使い方」に続いて「GORM+Gin」でTODOリストを作ってみました。使い方は「GitHubのREADME」を参考にしました。できたものは下記URLから確認できます。装飾は別途やれればと。
http://gin.taisablog.com/todo

事前情報

ルーティングは通常のフォームだとPUT/DELETEが使えないので以下のようにしました。

[GIN-debug] GET    /todo                     --> main.main.func1 (3 handlers) // 一覧表示
[GIN-debug] POST   /todo                     --> main.main.func2 (3 handlers) // 新規作成
[GIN-debug] GET    /todo/:id                 --> main.main.func3 (3 handlers) // 編集画面表示
[GIN-debug] POST   /todo/edit/:id            --> main.main.func4 (3 handlers) // 編集
[GIN-debug] POST   /todo/delete/:id          --> main.main.func5 (3 handlers) // 削除

main.goだけで作成した場合

main.goに全ての処理を記述しています。

package main
import (
  "github.com/gin-gonic/gin"
  "github.com/jinzhu/gorm"
  _ "github.com/jinzhu/gorm/dialects/mysql"
  "net/http"
  "strconv"
)
type Todo struct {
  gorm.Model
  Text   string
  Status uint64
}
func main() {
  db, err := gorm.Open("mysql", "gorm:gorm@/sandbox_gin?charset=utf8mb4&parseTime=True&loc=Local")
  if err != nil {
    panic("データベースへの接続に失敗しました")
  }
  defer db.Close()
  db.LogMode(true)
  db.AutoMigrate(&Todo{})
  r := gin.Default()
  r.LoadHTMLGlob("templates/*")
  // 一覧画面
  r.GET("/todo", func(c *gin.Context) {
    var todos []Todo
    db.Find(&todos)
    c.HTML(http.StatusOK, "index.html", gin.H{
      "todos": todos,
    })
  })
  // 新規作成
  r.POST("/todo", func(c *gin.Context) {
    text, _ := c.GetPostForm("text")
    status, _ := c.GetPostForm("status")
    istatus, _ := strconv.ParseUint(status, 10, 32)
    db.Create(&Todo{Text: text, Status: istatus})
    c.Redirect(http.StatusMovedPermanently, "/todo")
  })
  // 編集画面
  r.GET("/todo/:id", func(c *gin.Context) {
    todo := Todo{}
    id := c.Param("id")
    db.First(&todo, id)
    c.HTML(http.StatusOK, "edit.html", gin.H{
      "todo": todo,
    })
  })
  // 編集
  r.POST("/todo/edit/:id", func(c *gin.Context) {
    todo := Todo{}
    id := c.Param("id")
    text, _ := c.GetPostForm("text")
    status, _ := c.GetPostForm("status")
    istatus, _ := strconv.ParseUint(status, 10, 32)
    db.First(&todo, id)
    todo.Text = text
    todo.Status = istatus
    db.Save(&todo)
    c.Redirect(http.StatusMovedPermanently, "/todo")
  })
  // 削除
  r.POST("/todo/delete/:id", func(c *gin.Context) {
    todo := Todo{}
    id := c.Param("id")
    db.First(&todo, id)
    db.Delete(&todo)
    c.Redirect(http.StatusMovedPermanently, "/todo")
  })
  r.Run(":9000")
}

ファイルを分割した場合

main.goだけだとちょっと味気ないのでWebフレームワークっぽい構成にしてみました。書き方は色々だと思うので「参考」としてみてもらえればと思います。

ディレクトリ構成

.
├── controllers
│   └── todo.go
├── db
│   └── db.go
├── main.go
├── models
│   └── todo.go
├── router
│   └── router.go
└── templates
    ├── edit.html
    └── index.html

main.go

DB初期化とRouter初期化の呼び出し

package main
import (
  "github.com/taisa831/sandbox-gin/db"
  "github.com/taisa831/sandbox-gin/router"
)
func main() {
  dbConn := db.Init()
  router.Router(dbConn)
}

db/db.go

DB初期化

package db
import (
  "github.com/jinzhu/gorm"
  _ "github.com/jinzhu/gorm/dialects/mysql"
  "github.com/taisa831/sandbox-gin/models"
)
func Init() *gorm.DB {
  db, err := gorm.Open("mysql", "gorm:gorm@/sandbox_gin?charset=utf8mb4&parseTime=True&loc=Local")
  if err != nil {
    panic("データベースへの接続に失敗しました")
  }
  db.LogMode(true)
  db.AutoMigrate(&models.Todo{})
  return db
}

models/todo.go

modelsにはモデルの情報だけ記述

package models
import "github.com/jinzhu/gorm"
type Todo struct {
 gorm.Model
 Text string
 Status uint64
}

router/router.go

ルート情報を記述して起動

package router
import (
  "github.com/gin-gonic/gin"
  "github.com/jinzhu/gorm"
  "github.com/taisa831/sandbox-gin/controllers"
)
func Router(dbConn *gorm.DB) {
  todoHandler := controllers.TodoHandler{
    Db: dbConn,
  }
  r := gin.Default()
  r.LoadHTMLGlob("templates/*")
  r.GET("/todo", todoHandler.GetAll) // 一覧画面
  r.POST("/todo", todoHandler.CreateTask) // 新規作成
  r.GET("/todo/:id", todoHandler.EditTask) // 編集画面
  r.POST("/todo/edit/:id", todoHandler.UpdateTask) // 更新
  r.POST("/todo/delete/:id", todoHandler.DeleteTask) // 削除
  r.Run(":9000")
}

controllers/todo.go

実際にWebから呼び出された時の処理

package controllers
import (
  "github.com/gin-gonic/gin"
  "github.com/jinzhu/gorm"
  "github.com/taisa831/sandbox-gin/models"
  "net/http"
  "strconv"
)
type TodoHandler struct {
  Db *gorm.DB
}
func (h *TodoHandler) GetAll(c *gin.Context) {
  var todos []models.Todo
  h.Db.Find(&todos)
  c.HTML(http.StatusOK, "index.html", gin.H{
    "todos": todos,
  })
}
func (h *TodoHandler) CreateTask(c *gin.Context) {
  text, _ := c.GetPostForm("text")
  status, _ := c.GetPostForm("status")
  istatus, _ := strconv.ParseUint(status, 10, 32)
  h.Db.Create(&models.Todo{Text: text, Status: istatus})
  c.Redirect(http.StatusMovedPermanently, "/todo")
}
func (h *TodoHandler) EditTask(c *gin.Context) {
  todo := models.Todo{}
  id := c.Param("id")
  h.Db.First(&todo, id)
  c.HTML(http.StatusOK, "edit.html", gin.H{
    "todo": todo,
  })
}
func (h *TodoHandler) UpdateTask(c *gin.Context) {
  todo := models.Todo{}
  id := c.Param("id")
  text, _ := c.GetPostForm("text")
  status, _ := c.GetPostForm("status")
  istatus, _ := strconv.ParseUint(status, 10, 32)
  h.Db.First(&todo, id)
  todo.Text = text
  todo.Status = istatus
  h.Db.Save(&todo)
  c.Redirect(http.StatusMovedPermanently, "/todo")
}
func (h *TodoHandler) DeleteTask(c *gin.Context) {
  todo := models.Todo{}
  id := c.Param("id")
  h.Db.First(&todo, id)
  h.Db.Delete(&todo)
  c.Redirect(http.StatusMovedPermanently, "/todo")
}

templates/index.html




    
    TODOリスト
    



    新規追加:
    
        未対応
        対応中
        完了
    
    


{{range .todos}} {{end}}
TODO ステータス
{{.Text}} {{if eq .Status 1}} 未対応 {{else if eq .Status 2}} 対応中 {{else if eq .Status 3}} 完了 {{end}} 編集 削除
$(function(){ $('.delete').on('click', function(){ $('#frmDelete').attr('action', '/todo/delete/' + $(this).data('id')) $('#frmDelete').submit() }) })

templates/edit.html




    
    TODOリスト 編集



    テキスト
未対応 対応中 完了

GitHubはこちら
https://github.com/taisa831/sandbox-gin

Posted on

Go言語 ORMライブラリ GORMの使い方

Go言語 ORMライブラリのGORMの簡単な使い方を確認してみました。また、公式ドキュメントにしっかりと使い方が書いてありますので基本的にはそちらを参考にしてもらえればと思います(すべてではないですが日本語訳もされています)。その上でクイックスタートを元に簡単な使い方と挙動を確認してみます。
http://gorm.io/ja_JP/docs/

インストール

以下のコマンドでインストールできます。

go get -u github.com/jinzhu/gorm

クイックスタート

公式ドキュメントにあるクイックスタートを実行してみました。DBだけsqliteではなくmysqlに変更しています。

package main
import (
  "github.com/jinzhu/gorm"
  // _ "github.com/jinzhu/gorm/dialects/sqlite"
  _ "github.com/jinzhu/gorm/dialects/mysql"
)
type Product struct {
  gorm.Model
  Code string
  Price uint
}
func main() {
  // db, err := gorm.Open("sqlite3", "test.db")
  db, err := gorm.Open("mysql", "gorm:gorm@/sandbox?charset=utf8mb4&parseTime=True&loc=Local")
  if err != nil {
    panic("データベースへの接続に失敗しました")
  }
  defer db.Close()
  // スキーマのマイグレーション
  db.AutoMigrate(&Product{})
  // Create
  db.Create(&Product{Code: "L1212", Price: 1000})
  // Read
  var product Product
  db.First(&product, 1) // idが1の製品を探します
  db.First(&product, "code = ?", "L1212") // codeがL1212の製品を探します
  // Update - 製品価格を2,000に更新します
  db.Model(&product).Update("Price", 2000)
  // Delete - 製品を削除します
  db.Delete(&product)
}

実行してみるとproductsテーブルが作成され、以下のカラムとレコードができました。structでは宣言していない、idcreated_atupdated_atdeleted_atカラムができ、deleted_atに日付が入りソフトデリートが行われています。

go run main.go

gorm.Model

gorm.Modelを宣言するとidcreated_atupdated_atdeleted_atカラムが自動的に注入されます。また,deleted_atカラムがある場合、Deleteはソフトデリートになります。
参考:
http://gorm.io/ja_JP/docs/conventions.html

SQL実行ログ出力

先ほど実行したプログラムでどんなSQLが実行されたか確認してみます。db.LogMode(true)を設定するとSQLの実行ログが確認できます。

func main() {
  // db, err := gorm.Open("sqlite3", "test.db")
  db, err := gorm.Open("mysql", "gorm:gorm@/sandbox?charset=utf8mb4&parseTime=True&loc=Local")
  if err != nil {
    panic("データベースへの接続に失敗しました")
  }
  defer db.Close()
  // ログを出力する
  db.LogMode(true)
  // スキーマのマイグレーション
  db.AutoMigrate(&Product{})
  // Create
  db.Create(&Product{Code: "L1212", Price: 1000})
  // Read
  var product Product
  db.First(&product, 1)                   // idが1の製品を探します
  db.First(&product, "code = ?", "L1212") // codeがL1212の製品を探します
  // Update - 製品価格を2,000に更新します
  db.Model(&product).Update("Price", 2000)
  // Delete - 製品を削除します
  db.Delete(&product)
}

実行SQLや実行時間が確認できるようになりました。

(/Users/masakisato/.go/src/github.com/taisa831/sandbox-gorm/main.go:28)
[2019-06-26 19:57:23]  [16.23ms]  CREATE TABLE `product` (`id` int unsigned AUTO_INCREMENT,`created_at` timestamp NULL,`updated_at` timestamp NULL,`deleted_at` timestamp NULL,`code` varchar(255),`price` int unsigned , PRIMARY KEY (`id`))
[0 rows affected or returned ]
(/Users/masakisato/.go/src/github.com/taisa831/sandbox-gorm/main.go:28)
[2019-06-26 19:57:23]  [1.58ms]  CREATE INDEX idx_product_deleted_at ON `product`(deleted_at)
[0 rows affected or returned ]
(/Users/masakisato/.go/src/github.com/taisa831/sandbox-gorm/main.go:31)
[2019-06-26 19:57:23]  [0.30ms]  INSERT  INTO `product` (`created_at`,`updated_at`,`deleted_at`,`code`,`price`) VALUES ('2019-06-26 19:57:23','2019-06-26 19:57:23',NULL,'L1212',1000)
[1 rows affected or returned ]
(/Users/masakisato/.go/src/github.com/taisa831/sandbox-gorm/main.go:35)
[2019-06-26 19:57:23]  [0.32ms]  SELECT * FROM `product`  WHERE `product`.`deleted_at` IS NULL AND ((`product`.`id` = 1)) ORDER BY `product`.`id` ASC LIMIT 1
[1 rows affected or returned ]
(/Users/masakisato/.go/src/github.com/taisa831/sandbox-gorm/main.go:36)
[2019-06-26 19:57:23]  [0.33ms]  SELECT * FROM `product`  WHERE `product`.`deleted_at` IS NULL AND `product`.`id` = 1 AND ((code = 'L1212')) ORDER BY `product`.`id` ASC LIMIT 1
[1 rows affected or returned ]
(/Users/masakisato/.go/src/github.com/taisa831/sandbox-gorm/main.go:39)
[2019-06-26 19:57:23]  [0.24ms]  UPDATE `product` SET `price` = 2000, `updated_at` = '2019-06-26 19:57:23'  WHERE `product`.`deleted_at` IS NULL AND `product`.`id` = 1
[1 rows affected or returned ]
(/Users/masakisato/.go/src/github.com/taisa831/sandbox-gorm/main.go:42)
[2019-06-26 19:57:23]  [0.22ms]  UPDATE `product` SET `deleted_at`='2019-06-26 19:57:23'  WHERE `product`.`deleted_at` IS NULL AND `product`.`id` = 1
[1 rows affected or returned ]

created_atupdated_atdeleted_atが不要な場合

テーブルによってはcreated_atupdated_atdeleted_atが不要な場合もあります。その時はgorm.Modelを使わなければカラムは作られません(IDだけはできるよう個別に記述しています)。

type Product struct {
  // gorm.Model
  ID uint `gorm:"primary_key"`
  Code  string
  Price uint
}

この場合、先ほどのSQL実行ログは下記のようになります。deleted_atカラムがないので今回のレコード削除はハードデリートになります。また、DELETE文でwhere product.id = 1となっているのは、db.Create(&Product{Code: "L1212", Price: 1000})実行時のIDを引き継いでいるようで、db.Create(&Product{Code: "L1212", Price: 1000})を削除するとwhere product.id = 1の指定はなくなります。もしくはproduct.ID = 2Delete実行前に記述するとwhere product.id = 2と条件が変わってくれます。

(/Users/masakisato/.go/src/github.com/taisa831/sandbox-gorm/main.go:31)
[2019-06-26 20:14:50]  [0.24ms]  INSERT  INTO `product` (`code`,`price`) VALUES ('L1212',1000)
[1 rows affected or returned ]
(/Users/masakisato/.go/src/github.com/taisa831/sandbox-gorm/main.go:35)
[2019-06-26 20:14:50]  [0.26ms]  SELECT * FROM `product`  WHERE (`product`.`id` = 1) ORDER BY `product`.`id` ASC LIMIT 1
[1 rows affected or returned ]
(/Users/masakisato/.go/src/github.com/taisa831/sandbox-gorm/main.go:36)
[2019-06-26 20:14:50]  [0.25ms]  SELECT * FROM `product`  WHERE `product`.`id` = 1 AND ((code = 'L1212')) ORDER BY `product`.`id` ASC LIMIT 1
[1 rows affected or returned ]
(/Users/masakisato/.go/src/github.com/taisa831/sandbox-gorm/main.go:39)
[2019-06-26 20:14:50]  [0.16ms]  UPDATE `product` SET `price` = 2000  WHERE `product`.`id` = 1
[0 rows affected or returned ]
(/Users/masakisato/.go/src/github.com/taisa831/sandbox-gorm/main.go:42)
[2019-06-26 20:14:50]  [0.16ms]  DELETE FROM `product`  WHERE `product`.`id` = 1
[1 rows affected or returned ]

テーブル名を単数形にする場合

マイグレーションを実行すると生成されるテーブル名はproductsのように自動的に複数系になりますが、複数形にしたくない場合もあります。その時はdb.SingularTable(true)を宣言するとテーブルが単数形で生成されます。
参考:
http://gorm.io/ja_JP/docs/conventions.html

GORM本体のテストカバレッジ

OSSライブラリを使う時、どのくらいテストが書かれているか気になったりするので確認してみました。GORM本体にはtest_all.shというテストすべてを実行するスクリプトが用意されているのでそれを元にカバレッジを出してみました。(初期実行時はライブラリが足りないなど出るので都度追加しました。)また、mysqlのテストをする為にmain_test.goに書かれている以下の記述に事前に設定を合わせておく必要があります。

case "mysql":
  fmt.Println("testing mysql...")
  if dbDSN == "" {
    dbDSN = "gorm:gorm@tcp(localhost:9910)/gorm?charset=utf8&parseTime=True"
  }
  db, err = gorm.Open("mysql", dbDSN)

今回は、postgresmssql環境がないのでmysqlsqliteだけに限定しました。go testのあとに-coverprofile=cover.outを追記してカバレッジを出力します。その後go tool cover -html=cover.out -o cover.htmlを実行してhtmlに変換しました。

#dialects=("postgres" "mysql" "mssql" "sqlite")
dialects=("mysql" "sqlite")
for dialect in "${dialects[@]}" ; do
    echo ${dialect}
    DEBUG=false GORM_DIALECT=${dialect} go test -coverprofile=cover.out
done

実行してみると全体ではcoverage: 81.7% of statementsとカバレッジが81.7%でファイル毎にみても以下のようになっていました。

ソース

https://github.com/taisa831/sandbox-gorm

まとめ

非常に簡単なところだけを触っただけで分かりませんが、通して導入しやすく安定したライブラリだと思いました。個人的にORMはあまり積極的に使わない方ですが簡単に扱える、学習コストが低い、実行SQLを確認しながら扱える、などがあると導入障壁が低くてよいですね。

Posted on

Firebase Cloud Firestore 使い方の勘ドコロ

FirebaseのCloud Firestoreを扱う際、本家ドキュメントが充実していて検索すればすぐに出てくるのでそれほど困ることはありません。ただ、最初の入りとしてFiresotreのDocumentって何? となったり、docdocRefcityRefcityDocRefsnapShotdocs など、本家ドキュメントの中でも若干変数名の書き方が異なっていたりします。その為、慣れてくれば大丈夫ですが、慣れるまで毎回どう書くんだっけ?と検索することが多かったので、自分なりに解釈して整理してみました。Firestoreを扱える言語は様々用意されていますが、ここでは、JavaScript(Nuxt.js)を例に進めていきます。(セットアップは省きます)

参照元:https://firebase.google.com/docs/firestore/manage-data/add-data?hl=ja

Firestoreの基本的な考え方

基本的な考え方は、正にこの図にあるように「フォルダ(collection)」「ドキュメント(document)」「データ」という構成になっています。

参照元:https://firebase.google.com/docs/firestore/data-model
これをFirestoreの管理画面でみるとこのようになります。

これを「フォルダ(collection)」「ドキュメント(document)」「データ」に当てはめてみるとこのようになります。イメージとしては、本棚にフォルダが並んでいてフォルダを選択するとフォルダの中のファイル一覧がみることができ、あるファイルを選択するとそのファイルに書かれている内容をみることができるという感じです。

◯◯Refとは

本家ドキュメントをみると、◯◯Refという変数がよく出てきます。これはその名の通り「参照」です。つまり本棚にあるフォルダ(collection)やフォルダ内のドキュメントを選択した状態です。この時点ではまだ参照なので取得はしていません。usersコレクションを選択した状態であればuserRefusersコレクションのドキュメントを選択した状態であればuserDocRefといった変数をつけると分かりやすいと思います。(本家ドキュメントではそのようなルール決めはありません。)

userRef = db.collection('users')
userDocRef = db.collection('users').doc('YLu7bG7PcRNYhAg3F6MY')

これらを踏まえて、実際にデータの取得、追加、更新、削除のコードを書いてみます。ここではあえて◯◯Refのように変数を使っています。

コレクション配下のドキュメントデータをすべて取得する

usersコレクションを参照した状態で、get().then()を呼び出すとその時点のドキュメント一覧(snapShot)が取得できます。ドキュメントは1つしか存在しない可能性もありますが、ドキュメント一覧なのでforEachでループすることで全データを取得することができます。ユーザIDはデフォルトでドキュメントに振られるIDです。

const db = firebase.firestore();
let userRef = db.collection('users')
userRef.get().then((snapShot) => {
  snapShot.forEach((doc) => {
    this.users.push({
      user: doc.data(),
      user_id: doc.id
    })
  })
})
ユーザID ユーザ名 ユーザメールアドレス
{{user.user_id}} {{user.user.name}} {{user.user.email}}

参考:https://firebase.google.com/docs/firestore/query-data/get-data?hl=ja

コレクション配下のドキュメントデータをフィルタして取得する

フィルタするのにwhereを利用することができます。whereで対象を絞った後、同様にget().then()を呼び出すとフィルタしたドキュメント一覧が取得できます。

const db = firebase.firestore();
let userRef = db.collection('users')
userRef.where('name', '==', '山田太郎1').get().then((snapShot) => {
  snapShot.forEach((doc) => {
    this.filteredUsers.push({
      user: doc.data(),
      user_id: doc.id
    })
  })
})
ユーザID ユーザ名 ユーザメールアドレス
{{user.user_id}} {{user.user.name}} {{user.user.email}}

参考:https://firebase.google.com/docs/firestore/query-data/get-data?hl=ja

単一のドキュメントデータを取得する

単一のドキュメントデータを取得するには、doc()IDを渡し、get().then()を呼び出すと指定したドキュメントのデータが取得できます。この場合、doc()で単一のドキュメントを指定しているので、取得できるのは一覧ではなく指定したドキュメントの値となります。

const db = firebase.firestore();
let userRef = db.collection('users')
userRef.doc('YLu7bG7PcRNYhAg3F6MY').get().then((doc) => {
  this.user = doc.data()
  this.user.user_id = doc.id
})

これは以下のようにも書くことができます。

const db = firebase.firestore();
let userDocRef = db.collection('users').doc('YLu7bG7PcRNYhAg3F6MY')
userDocRef.get().then((doc) => {
  this.user = doc.data()
  this.user.user_id = doc.id
})
ユーザID ユーザ名 ユーザメールアドレス
{{user.user_id}} {{user.name}} {{user.email}}

参考:https://firebase.google.com/docs/firestore/query-data/get-data?hl=ja

ドキュメントを新規追加する

ドキュメントを新規追加するには、add()またはset()を使います。ドキュメントIDをランダムに完全に新規で作成する場合はadd()を利用します。ドキュメントIDを任意の値に指定したい場合はset()を指定します。set()は、既存のドキュメントIDを指定するとドキュメント全体を更新する処理となります。

const db = firebase.firestore();
let userRef = db.collection('users')
// 完全新規で追加する場合
userRef.add({
  name: '山田花子1',
  email: 'sample1@sample.com'
})
// ドキュメントIDを指定して新規追加する場合
userRef.doc('new-user-id').set({
  name: '山田花子2',
  email: 'sample2@sample.com'
})
// 既存のIDを指定してドキュメントの値を更新する場合
userRef.doc('YLu7bG7PcRNYhAg3F6MY').set({
  name: '山田花子3',
  email: 'sample3@sample.com'
})

参考:https://firebase.google.com/docs/firestore/manage-data/add-data?hl=ja

ドキュメントを更新する

ドキュメント全体を上書きせずにドキュメントの一部のフィールドを更新するには、update()を使います。

const db = firebase.firestore();
let userRef = db.collection('users')
userRef.doc('YLu7bG7PcRNYhAg3F6MY').update({
  name: '山田花子1'
})

参考:https://firebase.google.com/docs/firestore/manage-data/add-data?hl=ja

ドキュメントを削除する

ドキュメントを削除するには、特定のドキュメントIDをしていしてdelete()を呼び出します。

const db = firebase.firestore();
let userRef = db.collection('users')
userRef.doc('2YkjKlEoEQtH0tBJnByr').delete()

参考:https://firebase.google.com/docs/firestore/manage-data/delete-data?hl=ja

まとめ

ある程度がっつり書いていけばすぐに慣れますが、ちょこちょこ書いていたので書くたびにドキュメントを参照するというのを何回もやっていたのでまとめてみました。自分なりにまとめたものなので誤っている解釈などがあれば指摘して頂ければ幸いです。

Posted on

小学校6年間の算数から中学3年間の数学までを復習してみた

はずかしながら小学校算数から中学校数学までを復習してみたのでまとめておきます。

動機

もともとはこちらの「文系エンジニアが機械学習に入門するために小学校の算数から高校数学までを一気に復習してみました。」の記事を見たのがきっかけでした。復習しようにもどうアプローチしたらよいかわからなかったからです(私の場合は機械学習よりかはただの興味という感じです)。もともと高校数学を少しやってみようかと思っていましたが、そもそも高校入ってからは真面目に授業を受けていなかったので高校数学などほとんど覚えていません(高校入るまではそれなりに勉強していましたが..)。それで高校数学をやるとすぐくじけそうなので、中学数学までをやってみることにしました。タイトルに時間があるのも心理的にとっつきやすくてよかったです。

教材

教材は記事にあった以下の教材を使いました。公立高校受験レベルですがとても分かりやすくまとまっていてよい本でした。ほとんど通勤時間にみていたのでかかった時間は分かりませんが、小学校算数は6時間もかからず、中学校数学は大体6時間くらいだったかと思います。ただ読んでいるだけだと分かった気になるだけなので、こちらの問題「都道府県別 公立高校入試[問題・正答]」を少し解いたりしました。
小学校6年間の算数が6時間でわかる本

中学3年間の数学を8時間でやり直す本

目次

  • 小学算数
    • PART1 分数の計算
    • PART2 少数の計算
    • PART3 面積図・線分図・方程式
    • PART4 すばやく計算
    • PART5 割合
    • PART6 比
    • PART7 単位量あたりの大きさ
    • PART8 速さ・時間・道のり
    • PART9 平面図形
    • PART10 立体図形
    • PART11 比例・反比例
    • PART12 場合の数
  • 中学数学
    • PART1 正の数と負の数
    • PART2 文字式
    • PART3 1次方程式
    • PART4 連立方程式
    • PART5 因数分解と展開
    • PART6 平方根
    • PART7 2次方程式
    • PART8 確率
    • PART9 1次関数
    • PART10 関数y=ax2
    • PART11 図形
    • PART12 三平方の定理

おまけ

ついでに暗算も少しは早くできるようになりたいということで検索して一番にヒットした「暗算を簡単にする10の方法」も少しやってみました。他にもよい暗算の方法があれば教えてほしいです。

  • 左から右に
  • 細かい文字をシンプルに
  • 分数、少数のきまりを覚えておく
  • 数字の「0」と「5」を利用する
  • 倍数を使いこなす
  • 大まかな数字を出す
  • 数字を”リフレーズ”と”リアレンジ”する
  • 答えが合えばそれでいい
  • 億単位の数字にも計算方法はある
  • チップを計算する簡単な方法

まとめ

まだまだ先ですが子供らの高校受験まではある程度教えられるかなという気持ちになりました。ただ実際にはちょっと問題を解いただけなので、またその頃になったら復習しているかもしれません。次は時間を見つけて高校数学で面白そうな分野(機会学習にも役立ちそうな?)を選択してやってみようと思います。

Posted on

私はこうしてGoogleに入社/退社しました系記事まとめ

ここ最近少しブームになっていた、「私はこうしてGoogleに入社/退社しました」系の投稿が、どれも興味深い内容だったので、あとから見返しやすいようにまとめてみました。

記事一覧

Google退職します|eqsan|note

2019/3/15が最終出社日でした。インターン期間も含めると4年ちょっと勤めたことになります。 ちょうど昇進してプロジェクトも一区切りついたタイミングで他にすごくやりたいことができたので転職という形です。 素晴らしい環境なのに情報が少なくて、入ると良さそうなのに敬遠している人を何度か見たので、この記事が参考になれば幸いです。辞める人が言うのも変な話ですが。 …

私はこうやってGoogleに入った (ソフトウェアエンジニア、中途採用編)

( 人にお願いするだけじゃなくて自分も貢献しろよということで昔話を書いてみました。 kazawa フォーマット に従っています。) 学生時代 …

私はこうやって(12年前) Google に入った

書け、という天の声 が聞こえたので書いてます。 私は修士まで物理学を専攻していて、情報系の勉強は特にしていませんでした。ただ、実験結果をまとめるために awk を使ったり(256倍本とか知っています?)、シミュレーションをするために Fortran 書いたり(関数名の長さに制限ありました)はしていました。 …

私はこうして Google に入った (SWE・新卒編) – ふしみのブログ

最近、 Google や大学が開催しているキャリアイベントなどに参加する機会がおおかったので、そこで話したことをまとめて書いてみました。2017年4月入社なので、現在2年目が終わろうとしているところです。 なるべく汎用的に、具体的な対策を中心に書いたので、他の 外資 系企業の採用面接にも役立つかもしれません。参考にしていただければ幸いです。

こうしてGoogleに入社した(kumagi編) – Software Transactional Memo

TL;DR AtCoder やろうぜ Google の(僕から見て)偉い人が立て続けにブログを書いており ここ数件の僕のブログへの反響を読んでも「Googlerだから特別」みたいな意見が散見され、入社へのハードルが変に高く見られてしまっている気がするので、僕が Google に入社する準備として取り組んでいた事とそのレベルを紹介する。程度の低さに安心して欲しい。 英語 …

こうしてGoogleに落ちた – Easy to type

TL;DR Leetcodeをもっとやる必要がありました Googleの社員が選考過程についてブログを書いています。ちょっと前にNTTブームを引き起こした id:kumagi さんとか。 ところで、僕も Google …

私はこうやってGoogleに入った(新卒ソフトウェアエンジニア) – n-yoda’s blog

長い記事を書く人が多いので短くて簡潔なものをと思って書きました、が、思ったより長くなりました。 小さい頃からやっていたところが強みです。周りに聞ける人が一人もいなかったので ググる のが得意。大学で習う以上の強そうな知識とかはあまり無いです。 研究室の先輩方がホームページに載せているものを参考にして書きました。 何もしませんでしたが、何度かやった TopCoder は多分役に立ちました。 TOEFL ITP 547/677点。

私はこうやってGoogleに入りました(Reiko編)

プログラミングスキルは? プログラミングは仕事用のコードを自己流で書きちらかしてるだけだったのでコーティングには全く自信がなく、そのリクルーターさんからいくつか資料が送られてきたので、参考にして勉強しました。Cracking the Coding Interview、プログラミングコンテストチャレンジブック、あといくつかアルゴリズムの本(思い出したら書きます)などをやりました。 …

Google に入るまでの話

Google に入るまでの話. GitHub Gist: instantly share code, notes, and snippets.

いかにしてわたしは Google に入社し、そして退職したか – The Decisive Strike

長山です。2019 年 3 月 29 日付けで 7.5 年勤めた Google を退職しました。 わたしは SWE ではなくアナリストとしてエンジニアリングの組織にいました。検索や Play …

Google に入るまでの話

コンテキスト: https://togetter.com/li/1331865 グーグルジャパンではなくてUSの本社での採用の話。私が受けたのはSoftware EngineerではなくてDeveloper Advocate。Engineering組織の下についているのでコーディング面接有り。ただし評価項目がSWEとは異なる。 …

2008年に中途採用でGoogleに入った時の話|ぽん|note

はじめに 日本でやってる祭りを見ていたら、見た人がビビりそうな話を書いている人がいたので、少しでも心理的なバーを下げるべく自分の話を書いてみました。効果は保証できません。 略歴 電子情報工学修士 > アクセンチュア (1年半) > 医療系スタートアップ (日本5年、アメリカ1年) > Google Japan >US >スイス 入社のきっかけ 前職の医療系スタートアップでアメリカに移住して10ヶ月の時に、会社都合で突然日本に帰ることになりました。生活にも慣れてきて楽しい時期だったので、もっと長くアメリカいたいと思いながらも、ビザが会社に紐付いて

わたしはこうしてVimとSplatoonでGoogleに入った(SWE/新卒/インターン編)|haya14busa|note

コンテキスト:https://togetter.com/li/1331865 # 自己紹介と略歴 haya14busa といいます(GitHub)。VimとGoとSplatoonが好きです。 ## 2012年 京都大学総合人間学部2回生。何もやりたいことがわからなかったころ、なんとなくウェブページ作りたい的なきっかけでプログラミングに触れる。最初期のころはブックマークレット自作したりしてベンリ~って遊んでた。 ほどなくしてVimと出会い、ハマる。 ## 2013-4年頃 Vim プラグインの開発や日本のVimのコミュニティーが楽しくてずっとVimと遊んでた(参考1, 参考2

2016年投稿の過去記事

G社に内定した – 更新しない備忘録改二

前回の更新は4年前らしい。あと15回くらい記事を書いたら寿命だろうか? さて、表題の通り。世の中何が起こるか分からないものである。 自分は インターン 経由の採用である。詳しく書けない部分はあるが、簡単に経緯をまとめようと思う。 ことの始めは 2015年3月の言語処理学会で、会場の隅で@taku910さんと個人的に話していた折、「うちで …

Togatterのまとめ記事

私はこうやってGoogleに入りました

Googleに入社した方々のツイートをまとめました。

記事の中で特にオススメされていた書籍やサイト

LeetCode – The World’s Leading Online Programming Learning Platform

Level up your coding skills and quickly land a job. This is the best place to expand your knowledge and get prepared for your next interview.

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に登録するのはもう怖くない