こんにちは。
最近、暖かくもなってきたし登山に復帰しようかと思ってる武田です。

前回に引き続き、Symfony2管理画面構築バンドル[Sonata Admin]を触っていこうと思います。

前の物を拡張する形で以下の機能を追加していきます。

  • 項目「タグ」の追加
  • 画像のアップロード
  • バリデーション
  • 日本語化

項目「タグ」の追加

これだけだとブログとしてはちょっと寂しいですね。
タグ(任意項目)の2項目を追加したいと思います。

Entityの更新

DBの設定を調整するためEntityを調整します。

以下を追記
vi src/AppBundle/Entity/BlogPost.php

...[略]
    /**
     * @var string
     * 
     * @ORM\Column(name="tag", type="string", length=255, nullable=true) 
     */
     // 今回は未入力を許可してるので [nullable=true]を設定
    private $tag;

    /**
     * Set tag
     *
     * @param string $tag
     * @return BlogPost
     */
    public function setTag($tag)
    {
        $this->tag = $tag;

        return $this;
    }

    /**
     * Get tag
     *
     * @return string 
     */
    public function getTag()
    {
        return $this->tag;
    }
...[略]

EntityをDBに反映

chema:updateを実施

[adjust@localhost adminSample]$  php app/console    doctrine:schema:update --force
Updating database schema...
Database schema updated successfully! "1" queries were executed

blog_postテーブルを確認して、追加した項目があることを確認

mysql> desc blog_post;
+-------------+--------------+------+-----+---------+----------------+
| Field       | Type         | Null | Key | Default | Extra          |
+-------------+--------------+------+-----+---------+----------------+
| id          | int(11)      | NO   | PRI | NULL    | auto_increment |
| title       | varchar(255) | NO   |     | NULL    |                |
| body        | longtext     | NO   |     | NULL    |                |
| draft       | tinyint(1)   | NO   |     | NULL    |                |
| category_id | int(11)      | YES  | MUL | NULL    |                |
| tag         | varchar(255) | YES  |     | NULL    |                |
+-------------+--------------+------+-----+---------+----------------+

ADMIN CLASSへ項目の追加

タグの項目を追加します。

以下を追記
vi src/AppBundle/Admin/BlogPostAdmin.php

...[略]
        $formMapper
            ->add('title', TextType::class)
            ->add('body',  TextareaType::class)
            ->add('category', EntityType::class, array(
                'class' => 'AppBundle\Entity\Category',
                'property' => 'name',
            ))
            ->add('tag', TextType::class) // [追加]
        ;
...[略]

登録画面を見てみると項目が追加されてますね。
問題なければきちんと登録できるかと思います

画像のアップロード

やっぱり画像くらい登録したいですよね。
アップロード機能を作りましょう

ADMIN CLASSへ項目の追加

とりあえず動き云々の前に画面上に項目だけでも増やしましょう。
vi src/AppBundle/Admin/BlogPostAdmin.php

...[略]
use Symfony\Component\Form\Extension\Core\Type\FileType;  // [追加]
{
    // 登録更新で利用する入力フォームの定義
    protected function configureFormFields(FormMapper $formMapper)
    {
        $fileOptions = ['mapped' => false];
        // [追加 START]
        // create / updateかの判定に使用するため今の記事情報を取得
        $object = $this->getSubject(); 
        if ($this->id($object)) {
            // 登録済みの場合は画像もあるのでそれを組み立てて表示
            $fileOptions['help'] = sprintf("<img src='%s%s.jpg'/>", $this->getRequest()->getUriForPath('/'), $object->getId());
        }
        // [追加 END]
        $formMapper
            ->add('title', TextType::class)
            ->add('body',  TextareaType::class)
            ->add('category', EntityType::class, array(
                'class' => 'AppBundle\Entity\Category',
                'property' => 'name',
            ))
            ->add('tag', TextType::class)
            ->add('file', FileType::class, $fileOptions) // [追加]
        ;
    }

...[略]

ファイルアップロードのボタンが追加されたかと思います。
あわせて編集画面では登録済みの画像を表示するようにhelpオプションに画像表示のソースを引き渡してます。

画像の保存処理

アップロードした画像をきっちり保存しないと意味ないですね。
想定の位置へ保存するような処理を加えます。

sonata admin では 登録/更新/削除時などに読み出せるフックがすでに備わってます。
それを使いましょう。(公式)

今回は登録後(postPersist)/更新後(postUpdate)のフックを利用してその際に画像を所定の位置へ保存させます。

vi src/AppBundle/Admin/BlogPostAdmin.php

...[略]
    public function postPersist($object)
    {
      $this->saveFile($object); 
    }

    public function postUpdate($object)
    {
      $this->saveFile($object); 
    }

    // 画像を保存させる
    private function saveFile($object)
    {
      // symfony2ではよく使うcontainerを読み出す
      $container = $this->getConfigurationPool()->getContainer();
      // 画像保存のディレクトリを作成(今回はweb直下)
      $path = $container->getParameter('kernel.root_dir').'/../web/';
      // 画像名はDBの自動採番IDにしましょう
      $imageName = sprintf("%s.jpg",  $object->getId());
      // getForm()にファイルとしてデータがあるのでそれを一旦変数へ
      $file = $this->getForm()['file']->getData();
      // 上で定義したディレクトリ/画像名で保存
      $file->move($path, $imageName);
    }
...[略]

画像が登録できるか試しましょう。

ちゃんと画像が保存できましたね。

画像の削除

これだと登録/更新時は保存されますが削除の際に画像だけが残ってしまいます。
そこもフックを使って消すようにしましょう。

vi src/AppBundle/Admin/BlogPostAdmin.php

...[略]
    // 今回はpreを利用 postだとIDが消えちゃうので。。。
    public function preRemove($object)
    {
      $this->deleteFile($object); 
    }

    private function deleteFile($object)
    {
      $container = $this->getConfigurationPool()->getContainer();
      $filePath = sprintf("%s%s.jpg",  $container->getParameter('kernel.root_dir').'/../web/',  $object->getId());
      if(file_exists($filePath))
      {
        return unlink( $filePath );
      }
    }
...[略]

バリデーション

けっこう形になってきましたね。
ただ、現状だと値の確認をしてないので不安があります。
バリデーションを入れてみましょう。

カテゴリ管理画面

まずは項目の少ないカテゴリ管理画面から実施

nameに対して最大文字数を40とします。
Symfonyのバリデーション機能を読み出してそれを適応するように設定します。

ちなみに自前でバリデーションを定義すればそれを読み出す事も可能です。

vi src/AppBundle/Admin/CategoryAdmin.php

...[略]
use Symfony\Component\Validator\Constraints\Length;    // [追加]

class CategoryAdmin extends AbstractAdmin
{
    // 登録,更新で利用する入力フォームの定義
    protected function configureFormFields(FormMapper $formMapper)
    {
        $formMapper->add(
          'name',
          TextType::class,
          // [追加 START]
          [ 
            'constraints' => [ new Length(['max' => 40]) ]
          ]
          // [追加 END]
        );
    }
...[略]

動かしてみてみると、定義どおりのバリデーションが動作してるようですね

ブログ記事管理画面

引き続き、ブログ記事もやってきます。
以下のようなバリデーションを加えます。

Title 
  文字数40文字

Body 
  文字数1000文字

Category 
  なし
Tag
  英数字のみ

File 
  jpgファイルのみを許可

vi src/AppBundle/Admin/BlogPostAdmin.php

...[略]

use Symfony\Component\Validator\Constraints\NotBlank; // [追加]
use Symfony\Component\Validator\Constraints\Length;   // [追加]
use Symfony\Component\Validator\Constraints\File;     // [追加]
use Symfony\Component\Validator\Constraints\Regex;    // [追加]


class BlogPostAdmin extends AbstractAdmin
{
    // 登録更新で利用する入力フォームの定義
    protected function configureFormFields(FormMapper $formMapper)
    {
        // [追加 START]
        $fileOptions = [
         'constraints' => [ new File(['mimeTypes' => ['image/jpg'] ]) ],
         'mapped' => false
        ];
        $object = $this->getSubject();
        if ($this->id($object)) {
            $fileOptions['help'] = sprintf("<img src='%s%s.jpg'/>", $this->getRequest()->getUriForPath('/'), $object->getId());
        }
        $formMapper
            ->add(
              'title',
              TextType::class,
              [ 'constraints' => [ new Length(['max' => 40]) ] ]
            )
            ->add(
              'body',
              TextareaType::class,
              [
                'constraints' => [ new Length(['max' => 1000]) ]
              ]
            )
            ->add('category',EntityType::class, [
                'class' => 'AppBundle\Entity\Category',
                'property' => 'name'
            ])
            ->add(
              'tag',
              TextType::class,
              [
                'constraints' => [
                  new Regex(['pattern' => '/^[a-zA-Z0-9]+$/'])
                ]
              ]
            )
            ->add('file', FileType::class, $fileOptions)
        ;
        // [追加 END]
    }

...[略]

こちらもきっちりバリデーションが動いてますね!

日本語化

概ね機能としては完成しました!
ただ見た目が英語のままなのでちょっととっつきにくいですね。
日本語化しましょう。

サイトの言語を日本語に設定

vi app/config/config.yml

...[略]
parameters:
    locale: ja 
...[略]

これでsonata adminは最初から日本語に対応してくれます。

画面をみてもらうとエラーメッセージや共通パーツなどは日本語化されていることがわかります。

当たり前ですが自分で定義して項目やメニュー名などは翻訳する言葉がないのでそのままです。
こちらも対応しましょう。

翻訳ファイルを作成
vi app/Resources/translations/translations.ja.yml

Category         : カテゴリ
Category List    : ブログ記事一覧
Category Create  : ブログ記事作成
Name             : 名前
Blog post        : ブログ記事
Blog Post List   : ブログ記事一覧
Blog Post Create : ブログ記事作成
Title            : タイトル
Body             : 本文
Tag              : タグ
File             : 画像ファイル

翻訳ファイルを適用するように調整
vi app/config/services.yml

services:
    admin.category:
        class: AppBundle\Admin\CategoryAdmin
        arguments: [~, AppBundle\Entity\Category, ~]
        tags:
            - { name: sonata.admin, manager_type: orm, label: Category }
        # [追加]
        calls:
          - [ setTranslationDomain, [translations]]
    admin.blog_post:
        class: AppBundle\Admin\BlogPostAdmin
        arguments: [~, AppBundle\Entity\BlogPost, ~]
        tags:
            - { name: sonata.admin, manager_type: orm, label: Blog post }
        # [追加]
        calls:
          - [ setTranslationDomain, [translations]]

これで定義した項目に日本語が適応されました。

まとめ

sonata admin bundle いかがでしょうか?
型にはハマったCRUDを大量生産といったシチュエーションならかなり便利です。
symforny2で管理画面が必要な開発の場合、検討してもいいんじゃないかと思いますよ!

参考