個人的には編集時に、プレビューを見ながらフォーム記入をする方が好きなのですが、確認画面が欲しい、というニーズはよくありますよね。
ちょっとやってみたので、メモとして書いておきます。
フォームの確認画面なのですが、特に画像のアップロードが入ると結構厄介なんですよね。つまり、ファイルをどこに置くの?ってことになるので。と言うのも、確認画面の時点でアップロードが完了していないといけないんですよね。もし、そこで「やーめた」って思ったら、ファイルがゴミとして残ってしまうんですわ。また、「やっぱ他の画像にしよー」と思って戻られても、同様にファイルがゴミとして残ってしまうんですわ。ここは結構悩ましいところでいくつか方法があるのかな、と思います。私がパッと思いついた方法は、一時ディレクトリに保存しておいて、確認ができたら、ファイルを本番ディレクトリに移動って感じかな、と。
また、確認画面ってデータの保持ってどうするの?っていう問題があります。セッションに持っておいたり、hiddenで渡したり、一時的にデータベースに保存したり、とかいったことがよくされる方法ですかね。私はめんどくさがりやさんだったので、hiddenで渡しました。一時的にデータベースに保存した方が画像の保存のことも考えると楽かもしれません。
というわけで、ここでやろうとしていることを整理すると、次のようになります。
登録フォームがあり、画像もアップロードができるようにフォームをレンダーする
バリデーションに引っかかったら、登録フォームに戻る
バリデーションに通ったら、確認画面に行く
確認画面では、各値は hidden で渡す
確認画面のフォームで本登録をする
確認画面のフォームで、修正ボタンが押されたら、値をセットして、フォームに戻る
本登録されたら、hidden に入っているデータをデータベースに登録し、画像も正しい場所に移動する
さて、ここで厄介そうなのは、 hidden のフォームってどうするの、ってことかな、と。おそらく方法としては、新しく FormType を作る方法と、twig のフォーム周りの widget を修正して hidden で出す、ってくらいかな、と。私は後者でやってみました。
confirm.html.twig
{% form_theme form _self %}
{% block field_widget %}
{% if type is not defined or type != 'hidden' %}{{ value }}{% endif %}
{% endblock field_widget %}
{% block textarea_widget %}
{% set type = 'hidden' %}
{{ value }}
{% endblock textarea_widget %}
{% block checkbox_widget %}
{% spaceless %}
{% endspaceless %}
{% endblock checkbox_widget %}
{% block radio_widget %}
{% spaceless %}
{% endspaceless %}
{% endblock radio_widget %}
{% block choice_widget %}
{% spaceless %}
{% if expanded %}
{% for child in form %}
{% if child.vars.checked %}
{{ form_label(child) }}
{{ form_widget(child) }}
{% endif %}
{% endfor %}
{% else %}
{% for choice, label in choices %}
{% if _form_is_choice_selected(form, choice) %}
{{label|trans}}
{% endif %}
{% endfor %}
{% endif %}
{% endspaceless %}
{% endblock choice_widget %}
とかやって、hidden で出力するように変更してあげます。choice の辺りが結構面倒でした。あとは、普通に扱ってあげれば大丈夫です。画像に関しては、確認画面では表示させてあげます。
{{ form_label(form.file) }}
{% if entity.path is not null %}
{% endif %}
{{ form_widget(form.path, {'value': entity.path}) }}
{{ form_widget(form.file) }}
{{ form_widget(form.file_delete, {'value': null}) }}
...snip...
{{ form_rest(form) }}
修正する
登録
...snip...
確認画面では、file を使用しませんし、file_delete も使用しません。ただ、path を FormType に加えて、一時保存先へのパスを入れておいてます。
「修正する」ボタンを付けておきました。back が入っていれば、データを保持しつつフォームを表示するようにしています。
DocumentType.php
namespace Ganchiku\TestBundle\Form;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilder;
class DocumentType extends AbstractType
{
public function buildForm(FormBuilder $builder, array $options)
{
$builder
->add('title', null, array(
'label' => 'タイトル',
'required' => true,
))
->add('file', null, array(
'label' => '画像ファイル',
'required' => false,
))
->add('file_delete', 'checkbox', array(
'label' => '画像ファイル削除',
'required' => false,
))
->add('path', 'hidden', array(
'required' => false,
))
;
public function getName()
{
return 'ganchiku_testbundle_documenttype';
}
}
前回のポストの延長です。こんな感じで、path も追加しておいてあげます。hidden フィールドで。あとは Entity の方でゴニョゴニョします。Action でも少しだけセットしてあげたりしています。
Document.php
namespace Ganchiku\TestBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;
use Symfony\Component\HttpFoundation\File\UploadedFile;
use Symfony\Component\HttpFoundation\File\File;
/**
* Ganchiku\TestBundle\Entity\Document
* @ORM\Table(name="document")
* @ORM\HasLifecycleCallbacks
*/
class Document
{
/**
* @Assert\File(maxSize="6000000")
*/
public $file;
public $file_delete = false;
public $file_tmp = false;
public $file_confirmed = false;
public $path_old = null;
/**
* @var integer $id
*
* @ORM\Column(name="id", type="integer")
* @ORM\Id
* @ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* @var string $title
*
* @ORM\Column(name="title", type="string", length=255)
*/
private $title;
/**
* @var string $path
*
* @ORM\Column(name="path", type="string", length="255", nullable=true)
*/
private $path;
/**
* Get id
*
* @return integer
*/
public function getId()
{
return $this->id;
}
/**
* Set title
*
* @param string $title
*/
public function setTitle($title)
{
$this->title = $title;
}
/**
* Get title
*
* @return string
*/
public function getTitle()
{
return $this->title;
}
/**
* Set path
*
* @param string $path
*/
public function setPath($path)
{
$this->path = $path;
}
/**
* Get path
*
* @return string
*/
public function getPath()
{
return $this->path;
}
public function getAbsolutePath()
{
return null === $this->path ? null : $this->getUploadRootDir().'/' . $this->path;
}
public function getWebPath()
{
return null === $this->path ? null : $this->getUploadDir().'/' . $this->path;
}
/**
* @ORM\PrePersist()
* @ORM\PreUpdate()
*/
public function preUpload()
{
if (null !== $this->path and $this->file_delete) {
$this->path_old = $this->getAbsolutePath();
$this->path = null;
}
if (null !== $this->file) {
$this->path = md5_file($this->file->getPathname()) . '.' . $this->file->guessExtension();
}
}
/**
* @ORM\PostPersist()
* @ORM\PostUpdate()
*/
public function upload()
{
if ($this->path_old) {
unlink($this->path_old);
return;
}
if ($this->file_confirmed) {
$tmp_file = new File($this->getUploadRootDir(true) . '/' . $this->path);
$tmp_file->move($this->getUploadRootDir(), $this->path);
return;
}
if (null === $this->file) {
return;
}
$this->file->move($this->getUploadRootDir(), $this->path);
unset($this->file);
}
/**
* @ORM\PostRemove()
*/
public function removeUpload()
{
if ($file = $this->getAbsolutePath()) {
unlink($file);
}
}
protected function getUploadRootDir($file_tmp = false)
{
return __DIR__.'/../../../../web' . $this->getUploadDir($file_tmp);
}
protected function getUploadDir($file_tmp = false)
{
if ($this->file_tmp or $file_tmp) {
return '/uploads/tmp';
}
return '/uploads/documents';
}
}
なんか public な変数が増えて少し嫌な感じですが、まぁ、許容範囲でしょう。ちょっとだけ説明しておきます。
$file は、実際のアップロードファイルが保存されます。 SplFileInfo の孫クラスの Symfony\Component\HttpFoundation\File\UploadedFile インスタンスが入ることになります。アップロードファイルがあれば。なければ、放置です。
$file_delete は、今回はあまり関係がありません。前回のポストを参照してください。ファイル削除のフラグを見ているだけです。
$file_tmp は、一時ファイルを使用するかどうかを見るフラグです。このフラグは、isser にした方がスマートかも。コントローラーの方で、confirmAction 内でセットしてあげます。getUploadDir() の保存先を変更するのに使用しています。
$file_confirmed は、一時ファイルから本番ディレクトリに移動する際に使用します。コントローラーの方で、createAction 内でセットしてあげます。upload()内の動作を変更しています。
$path_old は、今回はあまり関係がありません。前回のポストを参照してください。ファイルの削除の際のカラムの変更のために使用しています。
confirmAction はこんな感じになりました。
/**
* Displays a form confirmation to create a new Document entity.
*
* @Route("/confirm", name="document_confirm")
* @Secure(roles="ROLE_USER")
* @Method("post|get")
* @Template()
*/
public function confirmAction(Request $request)
{
if ($request->getMethod() == "GET") {
return $this->redirect($this->generateUrl('document_new'));
}
$context = $this->get('security.context');
$user = $context->getToken()->getUser();
$entity = new Document();
$entity->file_tmp = true;
$request = $this->getRequest();
$form = $this->createForm(new DocumentType(), $entity);
$form->bindRequest($request);
if ($form->isValid()) {
$entity->preUpload();
$entity->upload();
// todo 本来は、 redirect すること
// ここでは confirm ページを表示させる
return array(
'entity' => $entity,
'form' => $form->createView()
);
}
$response = $this->render('GanchikuTestBundle:Document:new.html.twig', array(
'entity' => $entity,
'form' => $form->createView()
));
return $response;
}
実際に保存をするわけではないので、preUpload()やupload()は呼び出されないので、アップロードのために直接書いておいてあげます。また、その前に file_tmp に true をセットしてファイルの保存先を変更してあげています。Method で GET を受け付けているのは、個人的な趣向です。redirect 先が confirm とかに来られると 405 エラーを出すので、嫌だなー、と。リダイレクトしちゃった方が実際はいいんじゃないかなぁ、と。 todo はしておいた方がいいですが、とりあえず。。。
/**
* Creates a new Document entity.
*
* @Route("/create", name="document_create")
* @Secure(roles="ROLE_USER")
* @Method("post")
* @Template("GanchikuTestBundle:Document:new.html.twig")
*/
public function createAction()
{
$context = $this->get('security.context');
$user = $context->getToken()->getUser();
$entity = new Document();
$entity->setUser($user);
$entity->file_confirmed = true;
$request = $this->getRequest();
$form = $this->createForm(new DocumentType(), $entity);
$form->bindRequest($request);
if ($request->request->get('back', null) != 'back') {
if ($form->isValid()) {
$em = $this->getDoctrine()->getEntityManager();
$em->persist($entity);
$em->flush();
return $this->redirect($this->generateUrl('document_show', array('id' => $entity->getId())));
}
}
return array(
'entity' => $entity,
'form' => $form->createView()
);
}
ここで実際に保存(永続化)します。ただ、その前に file_confirmed に true をセットして、ファイルを移動してあげましょう。backは、修正する際に使用しており、フォームをデータを持ったまま表示させています。
なんか抜け落ちているような感じがしないでもないですが、こんな感じですかね。
あとは、一時ファイルを cron 等で定期的にお掃除してあげればいいかな、と。