Symfony以外のPHPのフレームワークの選択肢はBEAR.Sunday以外考えられないので、学ぶことにしました。
少しだけチュートリアルを触っていましたが、すっかり忘れていたので、TODOアプリを作りながら、学ぼうと思います。
TODOアプリ
以前SymfonyのサンプルでTODOアプリを作りました。
そのTODOアプリのBEAR.Sunday版ということで作ります。
今回作ったやつもgithubにあげてあるので、欲しい方は適当にcloneしてください。
テーブル構造
テーブル構造はいたってシンプル。
- todo
- id
- title
- status
- created_at
- updated_at
手順
- BEAR projectの作成
- TwigModuleの用意
- データベースの準備
- Todoリソースの作成
- 入力フォームの作成
- Twigファイルの準備
- 動かしてみる
- 一覧の作成
- Done処理の実装
- 最後に
BEAR projectの作成
まずはBEAR Sandayのプロジェクトを作成します。
今回はvendor名をPolidog
,プロジェクト名をTodo
とします。
1
|
$ composer create-project bear/skeleton Polidog.Todo
|
TwigModuleの用意
今回はSPAなアプリではなく、普通にWebアプリケーションなのでテンプレートエンジンにTwigを使います。
1
|
$ composer require madapaja/twig-module
|
あとはHtmlModule
を用意します。ディレクトリはsrc/Module/HtmlModule.php
に用意します。
今回はtwigファイルを配置するディレクトリを/var/twig
に変更しています。(なんとなく)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
<?php
namespace Polidog\Todo\Module;
use Madapaja\TwigModule\TwigModule;
use Ray\Di\AbstractModule;
class HtmlModule extends AbstractModule
{
protected function configure()
{
$this->install(new TwigModule());
$appDir = dirname(dirname(__DIR__));
$paths = [$appDir . '/var/twig/'];
$this->bind()->annotatedWith(TwigPaths::class)->toInstance($paths);
}
}
|
データベースの準備
今回はめんどくさいのでSQLiteを利用します。
1
2
3
4
5
|
$ mkdir var/db
$ sqlite3 var/db/todo.sqlite3
sqlite3> create table todo(id integer primary key, title, status, created, updated);
sqlite3> .exit
|
今回はAuraSqlを使うのでAuraSqlModuleを導入します。
1
|
$ composer require ray/aura-sql-module "~1.0"
|
AppModuleにも追記します。
1
2
3
|
// Database
$dbConfig = 'sqlite:' . dirname(dirname(__DIR__)). '/var/db/todo.sqlite3';
$this->install(new AuraSqlModule($dbConfig));
|
Todoリソースの作成
まずはTodoリソースをsrc/Resource/App/Todo.php
に作成します。
単純にTodoの作成、削除を行うためのリソースです。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
|
<?php
namespace Polidog\Todo\Resource\App;
use BEAR\Resource\ResourceObject;
use Ray\AuraSqlModule\AuraSqlInject;
class Todo extends ResourceObject
{
use AuraSqlInject;
const INCOMPLETE = 1;
const COMPLETE = 2;
public function onGet($id)
{
$todo = $this->pdo->fetchOne("SELECT * FROM todo WHERE id = :id", ['id' => $id]);
if (empty($todo)) {
$this->code = 404;
return $this;
}
$todo['status_name'] = $todo['status'] == self::INCOMPLETE ? "完了" : "未完了";
$this['todo'] = $todo;
return $this;
}
public function onPost($title)
{
$sql = 'INSERT INTO todo (title, status, created, updated) VALUES(:title, :status, :created, :updated)';
$bind = [
'title' => $title,
'status' => self::INCOMPLETE,
'created' => date("Y-m-d H:i:s"),
'updated' => date("Y-m-d H:i:s"),
];
$statement = $this->pdo->prepare($sql);
$statement->execute($bind);
$id = $this->pdo->lastInsertId();
$this->code = 201;
$this->headers['Location'] = "/todo/?id={$id}";
}
public function onPut($id, $status)
{
$sql = "UPDATE todo SET status = :status WHERE id = :id";
$statement = $this->pdo->prepare($sql);
$statement->execute([
'id' => $id,
'status' => $status
]);
$this->code = 202;
$this->headers['location'] = '/todo/?id=' . $id;
}
public function onDelete($id)
{
$sql = "DELETE FROM todo WHERE id = :id";
$statement = $this->pdo->prepare($sql);
$statement->execute(['id' => $id]);
}
}
|
実装出来たら、実際に動くか試してみましょう。
まずは登録します。
1
|
$ php bootstrap/api.php post "/todo?title=test"
|
次にちゃんと登録できたか確認しましょう。
1
|
$ php bootstrap/api.php get "/todo?id=1"
|
ステータスを更新する場合はPUTメソッドを使います。
1
|
$ php bootstrap/api.php put "/todo?id=1&status=2"
|
最後に削除する場合はDELETEを使います。
1
|
$ php bootstrap/api.php delete "/todo?id=1"
|
入力フォームの作成
今回はsrc/Page/Index.php
つまり、トップページが表示されたら入力フォームを表示させます。
まずはray/web-form-module
をインストールします。
1
|
$ composer require ray/web-form-module
|
フォームクラスの作成
次にフォームクラスを作成しましょう。
今回用意する入力フォームはtitle
になります。
あと送信ボタン用のsubmit
だけ。
なのでinit()
メソッドの中で、title
,‘submit’に関する記述を
src/Form/TodoForm
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
|
<?php
/**
* Created by PhpStorm.
* User: polidog
* Date: 2016/04/28
*/
namespace Polidog\Todo\Form;
use Aura\Html\Helper\Tag;
use Ray\WebFormModule\AbstractForm;
class TodoForm extends AbstractForm
{
/**
* {@inheritdoc}
*/
public function init()
{
$this->setField('title')
->setAttribs([
'id' => 'todo[title]',
'name' => 'todo[title]',
'class' => 'form-control',
'size' => 20
]);
$this->setField('submit', 'submit')
->setAttribs([
'name' => 'submit',
'value' => '登録',
'class' => 'btn btn-primary'
]);
// validationの設定
$this->filter->validate('title')->is('strlenMin', 1);
$this->filter->useFieldMessage('title', '必ず入力してください');
}
public function __toString()
{
$form = $this->form([
'method' => 'post',
'action' => '/',
]);
/** @var Tag $tag */
$tag = $this->helper->get('tag');
$form .= $tag('div', ['class' => 'form-group']);
$form .= $this->input('title');
$form .= $this->error('title');
$form .= $this->helper->tag('/div') . PHP_EOL;
// submit
$form .= $this->input('submit');
$form .= $this->helper->tag('/form');
return $form;
}
}
|
DIに登録するためにAppModuleに記述します。
src/Module/AppModule
1
2
3
4
|
// Form
$this->install(new AuraInputModule());
$this->bind(TodoForm::class);
$this->bind(FormInterface::class)->annotatedWith('todo_form')->to(TodoForm::class);
|
Indexリソースの変更
デフォルトで用意されていたsrc/Resource/Page/Index.php
を編集します。
- 登録フォームの表示
- Todoの登録処理
を実装します。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
|
<?php
namespace Polidog\Todo\Resource\Page;
use BEAR\Resource\ResourceObject;
use BEAR\Sunday\Inject\ResourceInject;
use Ray\Di\Di\Inject;
use Ray\Di\Di\Named;
use Ray\WebFormModule\Annotation\FormValidation;
use Ray\WebFormModule\FormInterface;
class Index extends ResourceObject
{
use ResourceInject;
/**
* @var FormInterface
*/
public $todoForm;
/**
* @Inject()
* @Named("todoForm=todo_form")
*
* Index constructor.
* @param FormInterface $todoForm
*/
public function __construct(FormInterface $todoForm)
{
$this->todoForm = $todoForm;
}
public function onGet()
{
$this['todo_form'] = $this->todoForm;
return $this;
}
/**
* @param array $todo
* @return $this
*/
public function onPost($todo = [])
{
return $this->createTodo($todo['title']);
}
public function onFailure()
{
$this->code = 400;
return $this->onGet();
}
/**
* @FormValidation(form="todoForm", onFailure="onFailure")
* @param string $title
* @return $this
*/
public function createTodo($title)
{
$request = $this->resource
->post
->uri("app://self/todo")
->withQuery(['title' => $title])
->eager
->request();
$this->code = $request->code;
$this->headers['location'] = "/";
$this['todo_form'] = $this->todoForm;
return $this;
}
}
|
Todoリソースがあるので、$this->resource->post->uri("app://self/todo")->withQuery(['title' => $title])->eager->request();
で登録処理を行っています。
ここの実装は時間がかかってしまいました。
URIの指定
BEAR.SundayにURIって同じアプリケーション内のリソースを呼び出す場合はapp://self/
で指定しなければいけないんですよね・・・。
すっかり忘れててapp://todo/
と書いていたらエラーになります。
データの渡し方
URIの指定時に渡したいデータをパラメータとして渡せると思ってしまったんですよね。
->uri("app://self/todo/?title={$title}")
こんな感じに。
コマンドラインから操作するときに$ php bootstrap/api.php post "/todo?title=test"
に指定してたのでできるかなぁーと思ったらダメでした。
withQuery
でデータを渡さないとだめなようです。
1
2
3
4
5
6
|
$request = $this->resource
->post
->uri("app://self/todo")
->withQuery(['title' => $title])
->eager
->request();
|
Twigファイルの準備
これでtwigファイルを用意すれば、画面が表示出来るので、twigファイルを用意しましょう。
var/twig/base.html.twig
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
|
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>{% block title %}Welcome!{% endblock %}</title>
{% block stylesheets %}
<link rel="stylesheet" href="//maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css">
<link rel="stylesheet" href="//maxcdn.bootstrapcdn.com/font-awesome/4.3.0/css/font-awesome.min.css">
{% endblock %}
</head>
<body>
{% block body %}
{% block contents %}
<div class="container">
{% block content %}
{% endblock %}
</div>
{% endblock %}
{% endblock %}
{% block javascripts %}
<script src="//ajax.googleapis.com/ajax/libs/jquery/2.1.3/jquery.min.js"></script>
<script src="//maxcdn.bootstrapcdn.com/bootstrap/3.3.5/js/bootstrap.min.js"></script>
{% endblock %}
</body>
</html>
|
var/twig/Index.html.twig
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
{% extends 'base.html.twig' %}
{% block content %}
<h1 class="page-header">タスク</h1>
<div class="row">
<div class="col-sm-8"></div>
<div class="col-sm-4">
<div class="panel panel-default">
<div class="panel-heading">新規作成</div>
<div class="panel-body">
{{ todo_form|raw }}
</div>
</div>
</div>
</div>
{% endblock %}
|
動かしてみる
ここまでくればブラウザで登録の動作確認まで出来ると思います。
ビルトインサーバを利用して動作を確認してみましょう。
1
|
php -S 127.0.0.1:8080 var/www/index.php
|
ブラウザで確認すると以下のように画面が表示されていると思います。
一覧の作成
動くのが確認できたところで、今度は一覧を表示する処理を実装します。
ここはちょっとリソースオブジェクトの名前をどうしようか迷ってしまったんですが、とりあえずTodos
としました。
本当はどういう名前がいいんですかね?
src/Resource/App/Todos
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
<?php
namespace Polidog\Todo\Resource\App;
use BEAR\Resource\ResourceObject;
use Ray\AuraSqlModule\AuraSqlInject;
class Todos extends ResourceObject
{
use AuraSqlInject;
public function onGet($status = null)
{
if (!empty($status)) {
$this->body = $this->pdo->fetchAll("SELECT * FROM todo WHERE status = :status",[
'status' => $status
]);
} else {
$this->body = $this->pdo->fetchAll("SELECT * FROM todo");
}
return $this;
}
}
|
Page/Index.phpの修正
次にsrc/Resource/Page/Index.php
を修正します。
onGet
メソッドに追加するだけで大丈夫です。
1
2
3
4
5
6
|
$this['todos'] = $this->resource
->get
->uri('app://self/todos')
->withQuery(['status' => $status])
->eager
->request();
|
Twigファイルの修正
var/twig/Index.twig
も修正します。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
|
{% extends 'base.html.twig' %}
{% block content %}
<h1 class="page-header">タスク</h1>
<div class="row">
<div class="col-sm-8">
<div class="panel panel-default">
<div class="panel-heading">タスク一覧</div>
<div class="panel-body">
{% if todos %}
<table class="table">
<thead>
<tr>
<th>Id</th>
<th>Title</th>
<th>CreatedAt</th>
<th> </th>
</tr>
</thead>
<tbody>
{% for todo in todos %}
<tr>
<td>{{ todo.id }}</td>
<td>{{ todo.title }}</td>
<td>{{ todo.createdAt|date("Y/m/d H:i:s") }}</td>
<td>
{% if todo.status == 1 %}
<a class="btn btn-danger" href="">Done</a>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<div class="panel-body">
<p>タスクがありません</p>
</div>
{% endif %}
</div>
</div>
</div>
<div class="col-sm-4">
<div class="panel panel-default">
<div class="panel-heading">新規作成</div>
<div class="panel-body">
{{ todo_form|raw }}
</div>
</div>
</div>
</div>
{% endblock %}
|
Done処理の実装
Todoを完了にするための処理を実装します。
TodoリソースのonPutを実行すればいいだけです。
HTMLではput使えないのでsrc/Resource/Done.php
を用意して実装します。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
|
<?php
namespace Polidog\Todo\Resource\Page;
use BEAR\Resource\ResourceObject;
use BEAR\Sunday\Inject\ResourceInject;
use Polidog\Todo\Resource\App\Todo;
class Done extends ResourceObject
{
use ResourceInject;
public function onGet($id)
{
/** @var ResourceObject $res */
$res = $this->resource
->put
->uri('app://self/todo')
->withQuery([
'id' => $id,
'status' => Todo::COMPLETE
])
->eager
->request();
;
if ($res->code == 202) {
$this->code = 301;
$this->headers['Location'] = "/";
return $this;
}
$this->code = $res->code;
return $this;
}
}
|
テンプレート側も修正しましょう。
var/twig/index.html
1
2
3
4
5
6
7
8
9
10
11
12
|
{% for todo in todos %}
<tr>
<td>{{ todo.id }}</td>
<td>{{ todo.title }}</td>
<td>{{ todo.createdAt|date("Y/m/d H:i:s") }}</td>
<td>
{% if todo.status == 1 %}
<a class="btn btn-danger" href="/done?id={{ todo.id }}">Done</a>
{% endif %}
</td>
</tr>
{% endfor %}
|
これだけで、動きます。
リダイレクトの設定
最初はheader関数つかうのかと思ってしまっていたんですが、どうやら違ったみたいです。
BEAR.Sundayの場合は$this->headers['Location'] = "/"
みたいな形でリダイレクトの指定をするそうです。
最後に
PHPerの皆さん、ぜひゴールデンウィークにBEAR.Sundayを学んでみてはいかがでしょうか?