Featured image of post BEAR.Sundayを学習してみた

BEAR.Sundayを学習してみた

Twitter ツイート Hatena Bookmark ブックマーク

Symfony以外のPHPのフレームワークの選択肢はBEAR.Sunday以外考えられないので、学ぶことにしました。

少しだけチュートリアルを触っていましたが、すっかり忘れていたので、TODOアプリを作りながら、学ぼうと思います。

TODOアプリ

以前SymfonyのサンプルでTODOアプリを作りました。 そのTODOアプリのBEAR.Sunday版ということで作ります。

今回作ったやつもgithubにあげてあるので、欲しい方は適当にcloneしてください。

テーブル構造

テーブル構造はいたってシンプル。

  • todo
    • id
    • title
    • status
    • created_at
    • updated_at

手順

  1. BEAR projectの作成
  2. TwigModuleの用意
  3. データベースの準備
  4. Todoリソースの作成
  5. 入力フォームの作成
  6. Twigファイルの準備
  7. 動かしてみる
  8. 一覧の作成
  9. Done処理の実装
  10. 最後に

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

}

AppModuleに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を学んでみてはいかがでしょうか?

comments powered by Disqus
Built with Hugo
テーマ StackJimmy によって設計されています。