PHPの軽量フレームワークSlim 4とSQLiteで簡易Todoリストを作成して勉強してみた

**私はPHP素人です。Slimもほぼ初めてさわってみてググりながら書いた内容ですので注意してください。**

---

私が最終的に作成したコードはこちらです。

https://github.com/okumurakengo/slim-simple-todo

---

公式のサンプルなどをみながら作成してみたのですが、この辺のライブラリが必要だったのでcomposerで追加しました

```bash
composer require slim/slim slim/psr7 slim/php-view php-di/php-di
```

```composer.json
{
    "require": {
        "slim/slim": "^4.2",
        "slim/psr7": "^0.5.0",
        "slim/php-view": "^2.2",
        "php-di/php-di": "^6.0"
    }
}
```

# Todoリスト作成

`public/index.php`を作成します。基本的な処理は全てここに書きました。
エラー処理をほとんど書いていないのですが、指定のhtmlを表示、jsからデータをやり取りするapiの部分まで作成、という自分なりに目標にしていた部分ができたので今回はこんな感じになりました。

```public/index.php
<?php
use DI\ContainerBuilder;
use Psr\Container\ContainerInterface;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use Slim\Factory\AppFactory;
use Slim\Routing\RouteCollectorProxy;
use Slim\Views\PhpRenderer;

require __DIR__ . '/../vendor/autoload.php';

$containerBuilder = new ContainerBuilder();

$containerBuilder->addDefinitions([
    'settings' => [
        'db' => [
            'connection'  => 'sqlite',
            'user'   => null,
            'pass'   => null,
            'dbname' => __DIR__.'/db.sqlite',
        ],
        'view' => [
            'template_path' => 'views',
        ],
    ],
]);

$containerBuilder->addDefinitions([
    'db' => function (ContainerInterface $c) {
        ['db' => [
            'connection' => $connection,
            'user' => $user,
            'pass' => $pass,
            'dbname' => $dbname,
        ]] = $c->get('settings');

        return new PDO("{$connection}:{$dbname}", $user, $pass, [
            PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
            PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
        ]);
    },
]);

$containerBuilder->addDefinitions([
    'renderer' => function (ContainerInterface $c) {
        ['view' => ['template_path' => $templatePath]] = $c->get('settings');
        return new PhpRenderer($templatePath);
    },
]);

$container = $containerBuilder->build();

$container->get('db')->exec(
    'CREATE TABLE IF NOT EXISTS todos (
        id        INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
        text      VARCHAR NOT NULL,
        completed BOOLEAN NOT NULL
    );'
);

AppFactory::setContainer($container);
$app = AppFactory::create();

$app->addErrorMiddleware(true, true, true);

$app->get('/', function (Request $request, Response $response, $args) {
    return $this->get('renderer')->render($response, 'index.html');
});

$app->group('/api', function (RouteCollectorProxy $group) {
    $group->get('/list', function (Request $request, Response $response) {
        $response->getBody()
             ->write(json_encode($this->get('db')->query('SELECT * FROM todos')->fetchAll()));
        return $response;
    });
    
    $group->post('/add', function (Request $request, Response $response) {
        ['text' => $text] = $request->getParsedBody();
    
        $db = $this->get('db');
        $stmt = $db->prepare('INSERT INTO todos(text, completed) VALUES(?, ?)');
        $stmt->execute([$text, 0]);
        $response->getBody()->write(json_encode(['res' => 'ok']));
        return $response;
    });
    
    $group->post('/complete/{id}', function (Request $request, Response $response, array $args) {
        ['id' => $id] = $args;

        $db = $this->get('db');
        $stmt = $db->prepare('UPDATE todos SET completed = NOT(completed) WHERE id = ?');
        $stmt->execute([$id]);
        $response->getBody()->write(json_encode(['res' => 'ok']));
        return $response;
    });
})->add(function ($request, $handler) {
    return $handler->handle($request)
        ->withHeader('Content-Type', 'application/json');
});

$app->run();
```

---

`views/index.html`を作成します。jsの部分は結構適当です。

```views/index.html
<!DOCTYPE html>
<meta charset="UTF-8">
<title>Document</title>

<h1>Simple Todo List</h1>
<form><input type="text" name="text"><button>追加</button></form>
<ul></ul>

<template>
    <li>
        <label>
            <input type="checkbox" name="todo[]">
            <span>hoge</span>
        </label>
    </li>
</template>

<script>
    const ul = document.querySelector("ul")
    const li = document.querySelector("template").content.querySelector("li")

    // リスト取得
    const getList = async () => {
        ul.innerHTML = ""
        for (const { id, text, completed } of await fetch("./api/list").then(res => res.json())) {
            const clone = li.cloneNode(true)

            const checkbox = clone.querySelector("[type=checkbox]")
            checkbox.checked = !!Number(completed)
            checkbox.setAttribute("data-id", id)
            checkbox.addEventListener("change", async e => {
                // チェックする
                await fetch(`./api/complete/${e.target.dataset.id}`, { method: "POST" })
                getList()
            })

            const span = clone.querySelector("span")
            span.textContent = text
            span.style.textDecoration = !!Number(completed) ? "line-through" : ""

            ul.insertAdjacentElement("beforeend", clone)
        }
    }

    // リスト追加
    document.querySelector("form").addEventListener("submit", async e => {
        e.preventDefault()
        const input = document.querySelector("[type=text]")
        if (!input.value) return;

        await fetch("./api/add", { method: "POST", body: new FormData(e.target) })
        getList()

        input.value = ""
    })

    getList()
</script>
```

---

この状態でサーバーを起動します。

```bash
php -S localhost:8080 -t public public/index.php
```

![ITGpMhOSfs.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/142910/8149ee40-4c13-d920-0db9-296e73a907aa.gif)

todoリストを動かすことができました。見ていただいてありがとうございました。m(_ _)m