PHPフレームワーク「Nette」で最初のアプリ(簡易ブログ)を作成する(1)

# 簡単なブログを作成する

[前回](https://qiita.com/masuipeo/items/e7962477e246cbdac782)はNette Frameworkの概要について紹介しました。
今回も公式ページに記載されているサンプルを参考に、コメントが投稿できる簡単なブログを作成しながら、Nette Frameworkの開発手順を紹介します。

今回の内容を最後まで進めると、簡単なブログの一覧表示と、個別の投稿を表示するページが作成できます。
記事の投稿やコメントの投稿の処理については次回に解説します。
なお、この内容を動作させるには、Nette Framework 3.0とPHP 7.1以降が必要です。

Nette Frameworkは手動でダウンロードすることもできますが、ここではComposerの使用を推奨します。
Composerを使ったことがない人は、まずはComposerの導入からはじめてください。
Qiita上にもいくつも記事がありますし、「[Composerの教科書](https://booth.pm/ja/items/1458660)」を読んでいただいても構いません。
Nette Frameworkも含め、フレームワークを使ってWebアプリケーションを開発する場合は、Composerを使って始めると簡単な環境を構築できます。

## 初期設定

Composerがインストールできていれば、次のコマンドを実行すると、「nette-blog」というディレクトリが作成され、その中に必要なファイルがダウンロードされます。

```bash
$ composer create-project nette/web-project nette-blog
```

この時点で、Webプロジェクトの初期ページが生成されているはずです。
Webサーバー上で上記の「nette-blog/www」にアクセスするか、手元でPHPのビルトインWebサーバーを実行して、アクセスしてみましょう。

「nette-blog/www」ディレクトリに移動し、ビルトインWebサーバーを起動します。

```bash
$ cd nette-blog/www
$ php -S localhost:8000
```

この場合、Webブラウザから次のURLにアクセスします。
http://localhost:8000/

これで、Nette Frameworkのウェルカムページが表示されます。
<img width="817" alt="Netteのウェルカムページ" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/309006/9cdf0c5d-eaeb-d371-561b-77187c6298cf.png">

## Nette Frameworkの構成

Nette FrameworkでWeb Projectを作成すると、次のような構成で作成されます。

```text
www/                      ← ルートディレクトリ
└── nette-blog/
    ├── app/              ← アプリケーションディレクトリ
    │   ├── config/       ← 設定ファイル
    │   ├── presenters/   ← プレゼンター
    │   │   └── templates/← テンプレート
    │   ├── router/       ← ルーター(URLの設定）
    │   └── Bootstrap.php ← 起動ファイル
    ├── log/              ← ログ
    ├── temp/             ← キャッシュやセッションなどの一時ファイル
    │
    ├── vendor/           ← アプリケーションのライブラリ
    │   └── nette/        ← フレームワーク 
    │
    └── www/              ← Webルート
```

wwwディレクトリに、画像やJavaScript、CSSなどの公開ファイルを保存します。
重要なディレクトリは「app/」で、ここにアプリケーションの実行に必要なファイルを格納していきます。

Webプロジェクトのウェルカムページは、以下のファイルに格納されています。

```
app/Presenters/templates/Homepage/default.latte
```

## デバッガー「Tracy」

Nette Frameworkには「Tracy」と呼ばれるデバッガーが用意されており、エラーが発生した場合、その詳細を表示できます。
例えば、次のファイルに対してエラーを作り込んでみます。

```php:app/Presenters/HomepagePresenter.php
<?php

declare(strict_types=1);

namespace App\Presenters;

use Nette;


final class HomepagePresenter extends Nette\Application\UI\Presenter
{
}

```

このファイルの最後の括弧を削除してみます。
すると、次のようにエラーの詳細が表示された赤い画面のページが表示されます。

<img width="817" alt="Nette Framework Tracy.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/309006/330f1f49-3896-ce3e-9a10-8cdb97b8c8a3.png">

Tracyを使うと、エラーの原因を調べるのに非常に役立ちます。
上記の右下に表示されている「TRACY」というフローティングバーにマウスを合わせると、詳しい情報を表示することもできます。

もちろん、公開モードではTracyが無効になっているため、ソースコードやパスワードなどの機密情報は公開されません。
エラーは「log/」ディレクトリに保存されます。

公開モードに設定するには、Bootstrapファイルでコメントアウトされている部分を以下のように変更します。

```php:app/Bootstrap.php
$configurator->setDebugMode(false); 
```

これにより、エラー画面はユーザー向けの内容になります。
<img width="817" alt="Nette Framework エラー画面" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/309006/c168c231-d8b9-020c-4523-d77839ca8cf0.png">

「log/」ディレクトリにはエラーログ(exception.log)が作成されています。
なお、Tracyはlocalhost環境ではデバッグモードを自動的に有効にし、その他の環境では無効にします。

## MVPに沿って作成する

今回はブログなので、最近の投稿から順に表示するページを作成してみます。
Nette Frameworkでは、MVC(Model-View-Controller)に似たMVP(Model-View-Presenter)に沿って開発します。

ブログのような単純なアプリケーションでは、Modelはデータベースへのクエリのみで構成されます。
このため、追加のPHPコードは必要なく、PresenterとViewを作成するだけです。
Netteでは、PresenterごとにViewが存在するため、両方を同時に開発します。

## データベースへの保存

データを保存するデータベースとして、ここではMySQLを使います（好きなDBMSを使えます）。
まずはブログの投稿を保存するデータベースを作成します。

「posts」というテーブルを、以下のデータ項目で作成します。

|カラム名|型|サイズ|その他|
|:---|:---|:---|:---|
|id|int|11|AUTO INCREMENT|
|title|varchar|255||
|content|text|||
|created_at|timestamp||CURRENT_TIMESTAMP|

SQLで書くと、次のような内容になります。

```sql
CREATE TABLE `posts` (
	`id` int(11) NOT NULL AUTO_INCREMENT PRIMARY KEY,
	`title` varchar(255) NOT NULL,
	`content` text NOT NULL,
	`created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB CHARSET=utf8;
```

テスト用に、いくつかデータを入れておきます。

```sql
INSERT INTO `posts` (`id`, `title`, `content`, `created_at`) VALUES
(1,	'Article One',	'Lorem ipusm dolor one',	CURRENT_TIMESTAMP),
(2,	'Article Two',	'Lorem ipsum dolor two',	CURRENT_TIMESTAMP),
(3,	'Article Three',	'Lorem ipsum dolor three',	CURRENT_TIMESTAMP);
```

## データベースとの接続

作成したデータベースを使うには、Webアプリケーションから接続する必要があります。
接続する設定は、以下のファイルに保存されています。

```text:app/config/local.neon
database:
	dsn: 'mysql:host=127.0.0.1;dbname=quickstart'
	user: root
	password: *enter password here*
```

上記のdsn、user、passwordの部分を書き換えてください。

なお、このファイルは.neonという拡張子ですが、YAMLと同じような形式のため、インデントやスペースの位置に注意が必要です。
（インデントとしてタブがデフォルトになっており、「:」の後にはスペースが必要です。）

このように、さまざまな設定は「app/config/」にある「.neon」という拡張子のファイルで行います。
「common.neon」というファイルは、アプリケーションのグローバル設定、「local.neon」には使用環境(開発環境と本番環境など)におけるパラメータを格納します。

## プレゼンターを変更して一覧画面を作る

データベースから取得するには、プレゼンターからデータベース接続が必要です。
例えば、上記で紹介したHomepagePresenterに次のようなコンストラクタを作成します。

さらに、データベースから投稿を取得し、テンプレートに渡すメソッド「render」を追加します。

```php:app/Presenters/HomepagePresenter.php
<?php

declare(strict_types=1);

namespace App\Presenters;

use Nette;


final class HomepagePresenter extends Nette\Application\UI\Presenter
{
	/** @var Nette\Database\Context */
	private $database;

	public function __construct(Nette\Database\Context $database)
	{
		$this->database = $database;
	}

	public function renderDefault(): void
	{
		$this->template->posts = $this->database->table('posts')
			->order('created_at DESC')
			->limit(5);
	}
}

```

この記述により、「default」というビューにデータが渡されます。
つまり、「app/Presenters/templates/{PresenterName}/{viewName}.latte」にプレゼンターのテンプレートが保存されていますが、上記の場合は「app/Presenters/templates/Homepage/default.latte」というテンプレートが使用されます。

また、テンプレート側では```$posts```という変数が使用可能になり、この変数を使用して投稿内容を表示できます。

## テンプレートで一覧ページのデザインを作成する

Nette Frameworkのテンプレートには、ページ全体を指定するテンプレートと、ページの一部を指定するテンプレートがあります。
全体のテンプレートでは、レイアウトやヘッダー、スタイルシート、フッターなどを指定します。
一部のテンプレートでは、ブログのリストを表示する、などの使い方ができます。

全体のテンプレートのデフォルトは、次のように作成されています。

```html:app/Presenters/templates/@layout.latte
<!DOCTYPE html>
<html>
<head>
	<meta charset="utf-8">
	<meta name="viewport" content="width=device-width">

	<title>{ifset title}{include title|stripHtml} | {/ifset}Nette Web</title>
</head>

<body>
	<div n:foreach="$flashes as $flash" n:class="flash, $flash->type">{$flash->message}</div>

	{include content}

	{block scripts}
	<script src="https://nette.github.io/resources/js/3/netteForms.min.js"></script>
	{/block}
</body>
</html>
```

ここで、```{include content}```は、本文に該当する部分です。
今回のようにブログ記事を表示する場合、この部分に反映されます。

今回は次のようにテンプレートを編集します。

```php:app/Presenters/templates/Homepage/default.latte
{block content}
	<h1 n:block="title">Sample blog</h1>

	{foreach $posts as $post}
	<div class="post">
		<div class="date">{$post->created_at|date:'F j, Y'}</div>

		<h2>{$post->title}</h2>

		<div>{$post->content}</div>
	</div>
	{/foreach}
{/block}
```

```{foreach}```という部分は、変数```$posts```に格納されている投稿を、順に反復処理し、各投 稿のHTMLを出力します。
```|date:```の部分は「フィルター」と呼ばれ、出力をフォーマットできます。今回は指定されたタイムスタンプを読みやすいように変換しています。
```{link}```と書くことで、他のページへのリンクを作成できます。ここでは、以下で作成する```Post:show```を呼び出すURLアドレスを生成しています。また、投稿のIDを引数として設定しています。

タイトルを表示する部分では、```n:block="title"```という記述をしています。
これは、次の記述と同じことを表します。

```php
{block title}<h1>Sample blog</h1>{/block}
```

このように書くと、```title```というブロックを再定義できます。
このブロックはレイアウトのテンプレート```/app/Presenters/templates/@layout.latte```で定義されていますが、この指定で上書きできます。
これにより、ページの```<title>```タグに、ブログ名がセットされます。

ブラウザを更新すると、登録しておいたブログの投稿内容がリスト形式で表示されます。
<img width="698" alt="ブログ投稿一覧" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/309006/3b6b9dc2-e30d-ed2b-fce5-57dd8f6ccf87.png">

デザインを変更したい場合は、CSSを「www/css/style.css」に追加して、レイアウトのテンプレートに次の記述を追加します。

```html:app/Presenters/templates/@layout.latte
<link rel="stylesheet" href="{$basePath}/css/style.css">
```

例えば、次のようなスタイルシートを作成すると、少し見栄えが良くなります。

```css:www/css/style.css
.post {
  margin: 10px;
  border: 1px dotted #808080;
}

.post > div {
  margin: 5px 20px;
}

.date {
  text-align: right;
  float: right;
}

h2 {
  border-left: 1em solid #808000;
  border-bottom: 1px solid #808000;
  margin-left: 10px;
  padding: 3px;
}
```

<img width="698" alt="ブログ投稿一覧デザイン" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/309006/2d7d4c4b-573b-7e7b-1191-6c2ced21a048.png">

## 特定の投稿を表示するページを作る

ブログの一覧画面から、特定のブログを選択し、その内容を表示するページを作成します。
このためには、投稿のIDを取得し、その内容を表示する新しい```render```メソッドを作成します。 

新しいプレゼンターのクラス```PostPresenter```を次のように作成します。
コンストラクタでデータベースの設定をしているのは上記と同様で、投稿を表示する```renderShow```では投稿のIDを取得しています。

```php:app/Presenters/PostPresenter.php
<?php

declare(strict_types=1);

namespace App\Presenters;

use Nette;


final class PostPresenter extends Nette\Application\UI\Presenter
{
	/** @var Nette\Database\Context */
	private $database;

	public function __construct(Nette\Database\Context $database)
	{
		$this->database = $database;
	}

	public function renderShow(int $postId): void
	{
        $post = $this->database->table('posts')->get($postId);
        if (!$post) {
            $this->error('Post not found');
        }

        $this->template->post = $post;
	}
}
```

存在しないIDが指定された場合、「ページが見つからない」というエラーを表示しています。
```$this->error(...)```が呼び出されると、わかりやすいメッセージとともにHTTPのステータスコード404のページが表示されます。
開発環境では、エラーページではなく、Tracyによって詳細な情報が表示されます。

次に、テンプレートを作成します。

```php:app/templates/Post/show.latte
{block content}
<p><a href="{link Homepage:default}">← back to posts list</a></p>
<div class="date">{$post->created_at|date:'F j, Y'}</div>
<h1 n:block="title">{$post->title}</h1>
<div class="post">{$post->content}</div>
{/block}
```

最初にブログ投稿一覧へ戻るためのリンクを設定しています（```default```は省略することもできます)。

そして、ブログ投稿一覧画面から各投稿へのリンクを作成します。
タイトルをクリックすると、投稿画面に遷移するように、タイトル部分を次のように変更します。

```php:app/templates/Homepage/default.latte
<h2><a href="{link Post:show $post->id}">{$post->title}</a></h2>
```

実際にアクセスしてみると、次のように表示されます。

<img width="698" alt="一覧画面にリンクが追加" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/309006/3abca71b-5ce3-bc18-11da-e9467266db44.png">
<img width="698" alt="投稿画面" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/309006/8e652d02-903e-e5d2-c6df-85f992141fb8.png">

次回は記事の投稿やコメント入力を作成します。