Posts Tagged ‘PHP’

[CakePHP3]2つのファイルアップロードのうちどちらか一方を必須にしたい

複数のファイルアップロードフィールドを配置し、それら全てではなく、そのうち一つが入力されることを保証するバリデーションを行いたいというケースがある。

https://book.cakephp.org/3.0/ja/core-libraries/validation.html#id9
ここにあるように条件付きバリデーションで実装しようとしてnotEmpty()メソッドに on オプションをつけてみたが、$this->Form->file()でレンダリングされた時点で全てのファイルアップロードフィールドに required 属性が付与されてしまって全てが必須になってしまう。

こういう時は時間があればバリデーションクラスを実装する方が美しいが、余裕がなければ標準から外れてしまうが思い切ってバリデータの使用は諦めて、コントローラーで実装してしまった方が早い。

その場合、エラーメッセージはEntityクラスのsetError()メソッドを使って、

1
2
3
4
5
6
7
8
9
10
11
12
$foo = $this->Foos->patchEntity($foo, $this->request->getData());
if ($foo->getErrors()) {
    // 入力チェックエラー
}
else if (!isset($this->request->getData('file1')['name'])
        && !isset($this->request->getData('file2')['name'])) {
    // 独自のバリデーションエラー
    $foo->setError('file1', 'ここにエラーメッセージを書く');
}
else {
    // 保存処理など
}

のような形でセットしてやれば、画面の方で他のバリデーションエラーと同様に表示ができる。

—-

[CakePHP3]ファイルを持つエンティティにpatchEntityメソッドでファイルアップロードフィールドをコピーしたい

Webサイトであれば、アップロードしたファイルをストレージに保存し、ファイルのパスまたはURLをデータベースで管理するといったケースはよく行われるだろう。

とあるBlogエンティティに画像のファイルパスを複数持っているとする。例えばこんな感じ。

  • blog_title
  • blog_image_path_1
  • blog_image_path_2

これを愚直にこのようなフォームにしてしまうとする。

1
2
3
4
5
<?= $this->Form->create($blog, ['enctype' => 'multipart/form-data']) ?>
  <?= $this->Form->text('blog_title') ?>
  <?= $this->Form->file('blog_image_path_1') ?>
  <?= $this->Form->file('blog_image_path_2') ?>
<?= $this->Form->end() ?>

このフォームのリクエストを受け取って

1
2
3
$blog = $this->Blogs->get($id); // DBから取得
$blog = $this->Blogs->patchEntity($blog, $this->request->getData()); // 画面の入力を反映
$this->Blogs->save($blog); // DBに保存

としてしまうのは、2つのファイルが両方とも必須項目であればともかく、片方だけ変更するといったケースでは、指定しなかった方のファイルパスがブランクになってしまって都合が悪い。

そういう場合は、イレギュラーな対応かもしれないが、直接コピーすることをせずに、エンティティにはない項目を一時的に使用して、最終的にエンティティに格納するようにする。そもそもファイルアップロードフィールドには初期値は設定できないのだ。

1
2
3
4
5
<?= $this->Form->create($blog, ['enctype' => 'multipart/form-data']) ?>
  <?= $this->Form->text('blog_title') ?>
  <?= $this->Form->file('blog_image_file_1') ?>
  <?= $this->Form->file('blog_image_file_2') ?>
<?= $this->Form->end() ?>
1
2
3
4
5
6
7
8
9
10
11
$blog = $this->Blogs->get($id);
$blog = $this->Blogs->patchEntity($blog, $this->request->getData());
// 1つ目のファイルが指定されていたら反映
if (isset($this->request->getData('blog_image_file_1')['name'])) {
    $blog->blog_image_path_1 = $this->request->getData('blog_image_file_1')['name'];
}
// 2つ目のファイルが指定されていたら反映
if (isset($this->request->getData('blog_image_file_2')['name'])) {
    $blog->blog_image_path_2 = $this->request->getData('blog_image_file_2')['name'];
}
$this->Blogs->save($blog);

尚、入力チェックや、基本ディレクトリ名の合成などは省略しているので、各々の状況に合わせてアレンジして欲しい。

ファイルアップロードフォームから渡ってくる項目の詳細については以下を参照のこと。
https://secure.php.net/manual/ja/features.file-upload.post-method.php

—-

Symfony2のアノテーション@Route、@Method – ルーティング設定

基本的なルートの設定

1
2
3
_welcome:
    path
:     /
    defaults
: { _controller: TipsBlogBundle:Main:index }

これは以下と等価。

1
2
3
4
5
6
7
8
9
class MainController extends Controller
{
    /**
     * @Route("/")
     */

    public function indexAction()
    {
    }
}

プレースホルダー付きのルート

1
2
3
blog_show:
    path
:     /blog/{slug}
    defaults
: { _controller: TipsBlogBundle:Blog:show }

これは以下と等価。

1
2
3
4
5
6
7
8
9
class BlogController extends Controller
{
    /**
     * @Route("/blog/{slug}", name="blog_show")
     */

    public function showAction()
    {
    }
}

または

1
2
3
4
5
6
7
8
9
10
11
12
/**
 * @Route("/blog")
 */

class BlogController extends Controller
{
    /**
     * @Route("/{slug}", name="blog_show")
     */

    public function showAction($slug)
    {
    }
}

プレースホルダーの必須/任意設定

1
2
3
blog:
    path
:     /blog/{page}
    defaults
: { _controller: TipsBlogBundle:Blog:index, page: 1 }

これは以下と等価。

1
2
3
4
5
6
    /**
     * @Route("/blog/{page}", name="blog", defaults={"page" = 1})
     */

    public function indexAction($page)
    {
    }

ルートの条件

1
2
3
4
5
blog:
    path
:     /blog/{page}
    defaults
: { _controller: TipsBlogBundle:Blog:index, page: 1 }
    requirements
:
        page
: \d+

これは以下と等価。

1
2
3
4
5
6
    /**
     * @Route("/blog/{page}", name="blog", defaults={"page" = 1}, requirements={"page" = "\d+"})
     */

    public function indexAction($page)
    {
    }

別のパターンでは、

1
2
3
4
5
homepage:
    path
:     /{culture}
    defaults
: { _controller: TipsBlogBundle:Main:index, culture: en }
    requirements
:
        culture
: en|ja

これは以下と等価。

1
2
3
4
5
6
    /**
     * @Route("/{culture}", name="homepage", requirements={"culture" = "en|ja"})
     */

    public function indexAction($culture)
    {
    }

HTTP メソッド の制約をつける

同一のURLでもHTTPメソッドによって呼び出すメソッドやコントローラを使い分けることが可能。

1
2
3
4
5
6
7
8
9
contact:
    path
:    /contact
    defaults
: { _controller: AcmeDemoBundle:Main:contact }
    methods
: [GET]

contact_process
:
    path
:    /contact
    defaults
: { _controller: AcmeDemoBundle:Main:contactProcess }
    methods
: [POST]

これはこう書ける。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class MainController extends Controller
{
    /**
     * @Route("/contact")
     * @Method({"GET"})
     */

    public function contactAction()
    {
    }

    /**
     * @Route("/contact")
     * @Method({"POST"})
     */

    public function contactProcessAction()
    {
    }
}

Windows版EclipseのSymfonyプラグインで任意のバージョンを使う

 私は以前からPHPの開発にも統合開発環境ソフトのEclipseを利用してきた。それでも5年程前のものはあまり使い勝手が良いとは言い切れなかった面があるが、WTPそしてPDTの進化に伴って今では私にとって必要不可欠なツールとなっている。
 そしてPHPのWebアプリケーションフレームワークであるSymfony2。バージョン1系に比べたら格段に取っ付き易くなった。そしてさらに開発効率を向上させるのがEclipse用のSymfonyプラグインだ。Eclipseで新規Symfonyプロジェクトを作成すると、基本的なWebアプリケーションのアーキテクチャは既に出来上がっている状態で開発をスタートできる。
 ところがこのプラグイン、Symfonyのバージョンは2.0.17と2.1.2しか用意されていない。一応カスタムレイアウトと称して、Symfonyのサイトからダウンロードした任意のバージョンのアーカイブファイルを利用してプロジェクトのセットアップができるようになっている。なのにWindowsで作業をした場合、プラグインの不具合のためかエラーとなってしまい先に進まない。今回はそれを解消する力技を紹介したい。

 尚、Eclipseのバージョンは3.7(Indigo)、Symfonyプラグインは2013年4月10日現在マーケットプレイスで入手できる最新版を用いた。Symfonyのバージョンは2013年4月10日現在公式サイトでダウンロードできるバージョン2.2.1を試した。

 まず普通に設定を試みる。Eclipseのメニューから ウィンドウ>設定 を選択し、設定画面を開く。設定画面のメニューから Symfony>Distributions を選択し、 Custom Symfony distributions 欄の「新規」ボタンを押下する。ファイルダイアログが開いたらアーカイブ(例えば Symfony_Standard_Vendors_2.2.1.tgz )を選ぶのだが、何故かファイルのあるフォルダを開いてもファイルが見えない。それでもファイル名を直接入力することで登録は可能だ。
 次に今登録したアーカイブを使ってSymfonyプロジェクトを作ってみよう。Eclipseメニューの ファイル>新規>プロジェクト… を選ぶ。新規プロジェクトダイアログが開いたら Symfony>Symfony Project を選んで「次へ」ボタンを押下する。 New Symfony project ダイアログが表示されるのでプロジェクト・レイアウトの欄のリストから Custom project layout を選ぼう。すると隣のアーカイブのリストの表示が不自然ではないだろうか。アーカイブを1つしか登録していなくてもリストは2項目表示され、しかも1つ目はドライブレターのみだ。試しに「プロジェクト名」欄に入力してもエラーが表示されて先には進めないはずだ。
 これは推測だが、おそらくWindowsのパスに含まれるコロン「:」をUnix系でいうパス区切り文字として処理してしまっているのではないだろうか。その証拠に、リストのアーカイブ名のパスの最後にセミコロン「;」がくっついている。セミコロンはWindowsにおけるパス区切り文字だ。(作者はもしかしたらMacで開発しているのかもしれない。)

 しばらく悩んだのだが、これを解決するには直接設定ファイルを書き換えるしかないと思った。この設定ファイルは以下の場所にある。

1
{ワークスペースのパス}\.metadata\.plugins\org.eclipse.core.runtime\.settings\com.dubture.symfony.ui.prefs

このファイルをテキストエディタで開き、

1
Symfony\ distributions=C\:\\path\\to\\archive\\Symfony_Standard_Vendors_2.2.1.tgz;

となっているところを次のように書き換える。但し、ドライブレターを省略するため、Eclipseをインストールしているドライブである必要がある。

1
Symfony\ distributions=/path/to/archive/Symfony_Standard_Vendors_2.2.1.tgz\:

書き換えたら保存してEclipseを再起動してみよう。そして先ほどと同じ様に新規プロジェクトを作成してみて欲しい。今度はエラーもなく、最新のバージョンでSymfonyプロジェクトをセットアップできたはずだ。

 余談だが、これをやったら設定画面からは設定し直すことはできない。尤も、設定し直しても全く意味ないが。そのうち作者が気付いてこのような手段をとる必要が無くなるだろう。

Symfony公式サイト
Symfony Eclipse Plugin公式サイト

アーカイブ