2021年4月5日月曜日

PHP:Symfony-06

1. blogコントローラの作成

>php bin/console make:controller BlogController

 created: src/Controller/BlogController.php

 created: templates/blog/index.html.twig

  Success! 

 Next: Open your new controller class and add some pages!

2.app/Resources/views/blog/index.html.twigの修正

<h1>Blog posts</h1>

3. エンティティ(モデル)の作成

テーブルの作成
CREATE TABLE `post` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `title` varchar(100) COLLATE utf8mb4_unicode_ci NOT NULL,
  `content` longtext COLLATE utf8mb4_unicode_ci NOT NULL,
  `created_at` datetime NOT NULL,
  `updated_at` datetime NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

CREATE TABLE `comment` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `post_id` bigint(20) NOT NULL,
  `author` varchar(20) COLLATE utf8mb4_unicode_ci NOT NULL,
  `content` longtext COLLATE utf8mb4_unicode_ci NOT NULL,
  `created_at` datetime NOT NULL,
  `updated_at` datetime NOT NULL,
  PRIMARY KEY (`id`),
  KEY `comment_post_id_idx` (`post_id`),
  CONSTRAINT `post_id` FOREIGN KEY (`post_id`) REFERENCES `post` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

既存のテーブルからエンティを作成
php bin/console doctrine:mapping:import "App\Entity" annotation --path=src/Entity

Getters&SettersまたはPHPクラスの生成

// generates getter/setter methods for all Entities
 php bin/console make:entity --regenerate App

// generates getter/setter methods for one specific Entity
 php bin/console make:entity --regenerate App\\Entity\\Country

4.コントローラの修正

use App\Entity\Post;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;

class BlogController extends AbstractController
{
    /**
     * @Route("/blog", name="blog")
     */
    public function index(): Response
    {
      $em = $this->getDoctrine()->getManager();
      $posts = $em->getRepository(Post::class)->findAll();

      return $this->render('blog/index.html.twig', [
          'posts' => $posts,
      ]);
    }
}

5.テンプレートの修正

<h1>Blog posts</h1>
{% if posts | length > 0 %}
  <table class="table" border="1">
    <thead>
    <tr>
      <td>ID</td>
      <td>タイトル</td>
      <td>作成日</td>
      <td>更新日</td>
    </tr>
    </thead>
    <tbody>
      {# posts配列をループして、投稿記事の情報を表示 #}
      {% for post in posts %}
        <tr>
          <td><a href="#">{{ post.id }}</a></td>
          <td>{{ post.title }}</td>
          <td>{{ post.createdAt|date('Y/m/d H:i') }}</td>
          <td>{{ post.updatedAt|date('Y/m/d H:i') }}</td>
        </tr>
      {% endfor %}
    </tbody>
  </table>
{% else %}
  <p>No Posts</p>
{% endif %}

6.データの挿入

INSERT INTO Post (title, content, createdAt, updatedAt) values ('初めての投稿', '初めての投稿です。', NOW(), NOW());

7. 記事詳細ページの作成

コントローラの追加

    /**
     * @Route("/blog/{id}", name="blog_show")
     */
    public function showAction($id)
    {
        $em = $this->getDoctrine()->getManager();
        $post = $em->getRepository(Post::class)->find($id);

        if (!$post) {
            throw $this->createNotFoundException('The post does not exist');
        }

        return $this->render('blog/show.html.twig', ['post' => $post]);
    }

テンプレートの追加

<h1>{{ post.title }}</h1>
<p><small>Created: {{ post.createdAt|date('Y/m/d H:i') }}</small></p>
<p>{{ post.content|nl2br }}</p>

8.ページをリンクで結ぶ

blog/show.html.twig
      {# posts配列をループして、投稿記事の情報を表示 #}
      {% for post in posts %}
        <tr>
          {# 詳細ページにリンク #}
          <td><a href="{{ path('blog_show', {id: post.id}) }}">{{ post.id }}</a></td>
          <td>{{ post.title }}</td>
          <td>{{ post.createdAt|date('Y/m/d H:i') }}</td>
          <td>{{ post.updatedAt|date('Y/m/d H:i') }}</td>
        </tr>
      {% endfor %}

blog/index.html.twig

<h1>{{ post.title }}</h1>
<p><small>Created: {{ post.createdAt|date('Y/m/d H:i') }}</small></p>
<p>{{ post.content|nl2br }}</p>
{# 戻るリンクを追加 #}
<p><a href="{{ path('blog_index') }}">一覧に戻る</a></p>

9.テンプレートの継承

base2.html.twig

<!doctype html>
<html>
<head>
  <meta charset=utf-8">
  <meta name="viewport" content="width=device-widthinitial-scale=1, shrink-to-fit=no">

  <link rel="stylesheet
    href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css"
    integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm
    crossorigin="anonymous">
  <script src="https://code.jquery.com/jquery-3.2.1.slim.min.js
    integrity="sha384-KJ3o2DKtIkvYIK3UENzmM7KCkRr/rE9/Qpg6aAZGJwFDMVNA/GpGFF93hXpG5KkN
    crossorigin="anonymous"></script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.9/umd/popper.min.js
    integrity="sha384-ApNbgh9B+Y1QKtv3Rn7W3mgPxhU9K/ScQsAP7hUibX39j7fakFPskvXusvfa0b4Q
    crossorigin="anonymous"></script>
  <script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/js/bootstrap.min.js
    integrity="sha384-JZR6Spejh4U02d8jOt6vLEHfe/JQGiRRSQQxSfFWpi1MquVdAyjUar5+76PVCmYl
    crossorigin="anonymous"></script>

  <title>{% block title %}{{ block('page_title') }} 
    - Symfony Blog{% endblock title %}</title>
</head>
<body>
  <header class="navbar navbar-dark bg-primary">
    <div class="container">
      <h1 class="navbar-brand">Symfony Blog</h1>
    </div>
  </header>
  <div class="container">
    <div class="row">
      <h2>{% block page_title %}{% endblock page_title %}</h2>
    </div>

    {% block content %}{% endblock content %}

    <footer>
      <p>&copy; 2021 青山システムズ</p>
    </footer>
  </div>
</body>
</html>

blog/index.html.twig

{% extends 'base2.html.twig' %}

{% block page_title %}Blog posts{% endblock %}

{% block content %}
  <div class="row">
    {% if posts | length > 0 %}
      <table class="table table-bordered">
        <thead>
        <tr>
          <td>ID</td>
          <td>タイトル</td>
          <td>作成日</td>
          <td>更新日</td>
        </tr>
        </thead>
        <tbody>
          {# posts配列をループして、投稿記事の情報を表示 #}
          {% for post in posts %}
            <tr>
              <td><a href="{{ path('blog_show', {id: post.id}) }}">
                    {{ post.id }}</a></td>
              <td>{{ post.title }}</td>
              <td>{{ post.createdAt|date('Y/m/d H:i') }}</td>
              <td>{{ post.updatedAt|date('Y/m/d H:i') }}</td>
            </tr>
          {% endfor %}
        </tbody>
      </table>
    {% else %}
      <p>No Posts</p>
    {% endif %}
  </div>
{% endblock %}


blog/show.html.twig

{% extends 'base2.html.twig' %}

{% block page_title %}{{ post.title }}{% endblock %}

{% block content %}
  <div class="row">
    <dl>
      <dt>作成日</dt>
      <dd><small>{{  post.createdAt|date('Y/m/d H:i') }}</small></dd>
      <dt>内容</dt>
      <dd>{{ post.content|nl2br }}</dd>
    </dl>
  </div>
  <div class="row">
    <a href="{{ path('blog_index') }}" class="btn btn-light">一覧に戻る</a>
  </div>
{% endblock %}

10.新規作成ページ

コントローラの追加と修正

use App\Entity\Post;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Annotation\Route;

class BlogController extends AbstractController
{
・・・・・・・

    /**
     * @Route("/blog/{id}", name="blog_show", requirements={"id"="\d+"})
     */
    public function showAction($id)
    {
        $em = $this->getDoctrine()->getManager();
        $post = $em->getRepository(Post::class)->find($id);

        if (!$post) {
            throw $this->createNotFoundException('The post does not exist');
        }

        return $this->render('blog/show.html.twig', ['post' => $post]);
    }

    /**
     * @Route("/blog/new", name="blog_new")
     */
    public function newAction(Request $request)
    {
        // フォームの組立
        $form = $this->createFormBuilder(new Post())
            ->add('title')
            ->add('content')
            ->getForm();

        return $this->render('blog/new.html.twig', [
            'form' => $form->createView(),
        ]);
    }    
}

blog/new.html.twigの追加

{% extends 'base2.html.twig' %}

{% form_theme form 'bootstrap_4_horizontal_layout.html.twig' %}

{% block page_title '新規作成' %}

{% block content %}
  {{ form_start(form) }}
    {{ form_widget(form) }}
    <button type="submit" class="btn btn-primary">作成</button>
  {{ form_end(form) }}
{% endblock content %}

index.html.twig に「新しい記事を書く」ボタンを設置

{% block content %}
<div class="row">
  <a class="btn btn-primary" href="{{ path('blog_new') }}">新しい記事を書く</a>
</div>
<div class="row">
    {% if posts | length > 0 %}

11.登録機能

コントローラの修正

    /**
     * @Route("/blog/new", name="blog_new")
     */
    public function newAction(Request $request)
    {
        // フォームの組立
        $post = new Post();
        $form = $this->createFormBuilder($post)
            ->add('title')
            ->add('content')
            ->getForm();

        // PSST判定&バリデーション
        $form->handleRequest($request);
        if ($form->isSubmitted() && $form->isValid()) {
            // エンティティを永続化
            $post->setCreatedAt(new \DateTime());
            $post->setUpdatedAt(new \DateTime());
            $em = $this->getDoctrine()->getManager();
            $em->persist($post);
            $em->flush();

            return $this->redirectToRoute('blog_index');
        }

        return $this->render('blog/new.html.twig', [
            'form' => $form->createView(),
        ]);
    }    

12. バリデーションの追加

ロケールを変更し、エラー文言などを日本語化

services.yaml

parameters:
    from_address'xxx@yyy.zzz'
    localeja
    default_locale'%locale%'

translation.yaml

framework:
    default_locale'%locale%'
    translator:
        default_path'%kernel.project_dir%/translations'
        fallbacks:
            - '%locale%' # en

エンティのPost.phpの変更

use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;

/**
 * Post
 *
 * @ORM\Table(name="post")
 * @ORM\Entity
 */
class Post
{
    /**
     * @var int
     *
     * @ORM\Column(name="id", type="bigint", nullable=false)
     * @ORM\Id
     * @ORM\GeneratedValue(strategy="IDENTITY")
     */
    private $id;

    /**
     * @var string
     *
     * @ORM\Column(name="title", type="string", length=255)
     * @Assert\NotBlank()
     * @Assert\Length(min="2", max="50")
     */
    private $title;

    /**
     * @var string
     *
     * @ORM\Column(name="content", type="text")
     * @Assert\NotBlank()
     * @Assert\Length(min="10")
     */
    private $content;

titleに10文字未満だと日本語でエラーが表示される

13.削除機能

削除アクションとテンプレートに削除ボタンを追加します。

コントローラーにdeleteActionを追加します。

    /**
     * @Route("/blog/{id}/delete", 
     *    name="blog_delete",
     *    requirements={"id"="\d+"})
     */
    function deleteAction($id)
    {
        $em = $this->getDoctrine()->getManager();
        $post = $em->getRepository(Post::class)->find($id);
        if (!$post) {
            throw $this->createNotFoundException(
                'No post found for id '.$id
            );
        }
        // 削除
        $em->remove($post);
        $em->flush();

        return $this->redirectToRoute('blog_index');
    }

blog/index.html.twig に追記

          <td>操 作</td>
        </tr>
        </thead>
        <tbody>
          {# posts配列をループして、投稿記事の情報を表示 #}
          {% for post in posts %}
            <tr>
              <td><a href="{{ path('blog_show', {id: post.id}) }}">
                    {{ post.id }}</a></td>
              <td>{{ post.title }}</td>
              <td>{{ post.createdAt|date('Y/m/d H:i') }}</td>
              <td>{{ post.updatedAt|date('Y/m/d H:i') }}</td>
              <td><a class="btn btn-danger" 
                href="{{ path('blog_delete', {'id':post.id}) }}">削除</a></td>

14.編集機能

コントローラーにeditActionを追加します。

    /**
     * @Route("/blog/new", name="blog_new")
     */
    public function newAction(Request $request)
    {
        // ・・・

        return $this->render('blog/new.html.twig', [
            'post' => $post,
            'form' => $form->createView(),
        ]);
    }  

    /**
     * @Route("/blog/{id}/edit",
     *          name="blog_edit", 
     *          requirements={"id"="\d+"})
     */
    public function editAction(Request $request, $id)
    {
        $em = $this->getDoctrine()->getManager();
        $post = $em->getRepository(Post::class)->find($id);
        if (!$post) {
            throw $this->createNotFoundException(
                'No post found for id '.$id
            );
        }

        $form = $this->createFormBuilder($post)
            ->add('title')
            ->add('content')
            ->getForm();

        $form->handleRequest($request);
        if ($form->isSubmitted() && $form->isValid()) {
            // フォームから送信されてきた値と一緒に更新日時も更新して保存
            $post->setUpdatedAt(new \DateTime());
            $em->flush();

            return $this->redirectToRoute('blog_index');
        }

        // 新規作成するときと同じテンプレートを利用
        return $this->render('blog/new.html.twig', [
            'post' => $post,
            'form' => $form->createView(),
        ]);
    }

/blog/new.html.twigの修正

{% block content %}
  {{ form_start(form) }}
    {{ form_widget(form) }}
    <a class="btn btn-light" href="{{ path('blog_index') }}">一覧に戻る</a>
    <button type="submit" class="btn btn-primary">{{ post.id ? '編集' : '作成' }}</button>
  {{ form_end(form) }}
{% endblock content %}

/blog/index.html.twigの追加

              <td>
                <a class="btn btn-info" 
                  href="{{ path('blog_edit', {'id': post.id}) }}">編集</a>
                <a class="btn btn-danger" 
                  href="{{ path('blog_delete', {'id':post.id}) }}">削除</a>
              </td>

まだいろいろなことがあるが今回はここまでです

第2シリーズはあるかな?