[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

—-

Quick Password Maker バージョン1.1 公開

パスワードをより強固に、より安全に!

 これは新しいパスワードを勝手に考えてくれるアプリです。

 昨今、パスワードの使い回しによってアカウントに不正にアクセスされてしまう事故が多く発生しています。ここまで世の中にITサービスが増えてくると、それぞれで異なるパスワードを設定することが必要になってきます。しかし、そのためには、自分の頭で考えたのではバリエーションにも限界がありますし、強度にも不安が残ります。そこでこのアプリを使えば、より強固でより安全なパスワードを代わりに考えてくれます。

スクリーンショット

スクリーンショット

◆ 特徴

  • 文字種として英大文字、英小文字、数字の使用・不使用をそれぞれ指定できます。
  • 記号は13種類の中から使用・不使用を個別に指定できます。
  • 全ての文字種を必ず1回以上使うようにできます。
    (記号はいくつ指定されても1種類と数えます。)
  • 同じ文字を2回以上使わないようにできます。
    (文字数と文字種の設定の兼ね合いで出来ない場合もあります。)

◆ 使い方

  1. パスワードに使う文字種を指定します。
  2. パスワードの長さを指定します。
  3. 「パスワードを生成」ボタンを押します。
  4. 必要に応じてコピーして利用します。

◆ 動作環境

iPhone, iPod Touch(iOS 6.0以上)

◆ 利用条件

  • 本ソフトウェアはフリーウェアとして公開しています。ご利用に当たり料金は発生いたしません。
  • 著作権はクロスラボラトリーにあります。

◆ 免責事項

  • 本ソフトウェアは無保証です。ご利用によって生じたいかなる損害に対しても作者は責任を負わないものとします。ご自身の責任においてご利用ください。

◆ オマケ

  • パスワード生成後に端末をシェイクすると生成したパスワードを消去します。

◆ 更新履歴

  • v1.0 (2013-06-22)
    • 公開しました。
  • v1.1 (2016-06-16)
    • iOS 9 に対応しました。

Quick Password Maker ver1.1 Released!

Get Your Password Stronger and Safer!

This app automatically creates a new password.

Recently, many accidents that are unauthorized access to accounts by the recycling of password have occurred. With the increase in IT service, it becomes necessary to set a different password in each. However, to perform it, when a person thinks with one’s head, there is a limit in both the variation and the strength. If you use this application, the application makes stronger and safer password, instead of you.

Screenshot of Quick Password Maker

Screenshot

Features

  • You can specify each use and non-use uppercase, lowercase, numbers as character types.
  • You can specify individual use / non-use from 13 types as a symbol.
  • You can specify to use one or more times always the character types of all.
    Even if how many symbols are specified, symbol is counted as one kind.
  • You can specify not to use more than once the same character.
    In some cases, you can not specify it in consideration of character types and password length.

Usage

  1. Specify the character types to use for password.
  2. Specify the length of the password.
  3. Press the button “Generate Password”.
  4. Copy and use as needed.

Requirements

iPhone, iPod Touch(iOS 6.0 or later)

License & Copyright

  • This software is FREEWARE. There is NO CHARGE Upon use.
  • CROSS LABORATORY holds the copyright.

Disclaimer

  • This software is NO GUARANTEE. We shall not take responsibility for any damage that occurred because of the use. Please use it in the responsibility of own.

Misc

  • After generate passowrd, Shake your device to erase the password.

History

  • v1.0 (2013-06-22)
    • Released.
  • v1.1 (2016-06-16)
    • Adapted to iOS 9.

Xls/xlsx Names Eraser バージョン1.0.0版公開

ダウンロード

◆ これは何か?

  • 拡張子が xls または xlsx であるMicrosoft® Excel®ブック形式のファイルに定義されている不要な「名前」を削除するためのアプリケーションです。ブックファイルに定義された全ての「名前」を削除することで、結果としてファイルサイズを抑制することができます。削除したくない名前を設定することもできます。
  • Microsoft® Excel® ブック形式のファイルを情報資産として担当者から担当者へと、特にコンテンツは引き継がなくても書式だけ引き継がれるということもあります。その過程で、意図すると意図せざるとにかかわらず名前を定義することがありますが、それがある時は有用であったにせよ、たまたま外部リソースを参照しているなどしてたために別の環境では使えず、よく分かっていない人が元の名前を削除せずに別の名前を再定義して、そうやって再利用を繰り返しているうちにファイルサイズが肥大化してしまうということがあります。
    書式を使いたいなと思うファイルをコピーして、書式だけ残してコンテンツをクリアし、新しいドキュメントとして作成し始めたとします。大したコンテンツを追加していないのに、まっさらな状態から作成した時と比べてやけにファイルサイズが大きいとか、編集時の操作がもたつくといったことに気付いた場合、よくよく調べてみると無用な名前定義が数十個も登録されていたというようなケースがありました。更にはそれらの名前定義が非表示になっており、見える名前だけ削除しても状況があまり改善しないということもあります。
  • 主に既存ドキュメントの書式を流用して別のドキュメントを作成するような管理者や担当者の方に使っていただくことを想定しています。これまではこうしたメンテナンスは行われておらず、あるいはメンテナンスを行ったとしても個別に手作業で不要な名前を削除していたと思いますが、これからはそうした手作業の必要が無くなります。私自身もそうした手作業を、忙しいさなかに行ってうんざりした経験からこのアプリケーションを開発しました。

◆ 動作環境

.NET Framework 4.0 が動作する環境。

  1. お使いのコンピュータにまだ .NET Framework 4.0 を導入されていない場合は以下のURLからダウンロードしてインストールを行う必要があります。
    https://www.microsoft.com/ja-jp/net/netfx4/download.aspx
  2. Microsoft® Excel® 2007 以降がインストールされている環境。

◆ 使い方

  • Microsoft® Excel® ブック形式のファイルまたはそれらを含むフォルダをマウス等でドラッグ&ドロップします。フォルダの中にはブック形式以外のファイルが含まれていても構いません。
  • 削除したくない名前を「削除しない名前」欄に追加します。
  • 実行ボタンを押すと確認ダイアログが表示され、OKボタンを押すと処理を開始します。
  • 尚、隠しファイルや読取専用ファイルは処理されません。
  • 一度実行後、続けて作業を行いたい場合は「全てクリア」ボタンを押してリストをクリアしてから、再度ファイルまたはフォルダをドロップして作業を行うことができます。

◆ 利用条件

  • 本ソフトウェアはフリーウェアとして公開しています。ご利用に当たり料金は発生いたしません。
  • 著作権はクロスラボラトリーにあります。

◆ 免責事項

  • 本ソフトウェアは無保証です。ご利用によって生じたいかなる損害に対しても作者は責任を負わないものとします。ご自身の責任においてご利用ください。
  • 特に大切な情報資産を取り扱う場合には、バックアップを取得するなど万全の態勢を敷いた上でご利用いただくようにお願いいたします。

更新履歴

  • v0.0.1b (2015-07-02)
    β(ベータ)版公開
  • v0.0.2b (2016-02-29)
    ファイルを開くパスワードを指定できるように変更
  • v0.0.3b (2016-03-02)
    パスワードを渡せていなかったバグを解消
  • v1.0.0 (2016-03-07)
    タイムスタンプを維持するオプションを追加
    設定を保存できるように変更

NSFetchedResultsControllerとUISearchDisplayControllerを使ったUITableView検索機能の実装

NSFetchedResultsControllerとUISearchDisplayControllerを使ったUITableViewの検索機能を実現するというトピックはネット上でも幾つか見つかるのだが、複数のクラスが組み合わさって動作するためになかなか思うような動きをするコードを書くことができなかった。

UITableView(ここではMaster-Detail Appとする)にUISearchDisplayControllerを組み合わせ、UISearchBarに検索文字列を入力するとすぐに絞り込まれる(ようにコードを書く)のだが、その時表示される絞り込まれた表はUITableViewではなくUISearchResultTableViewのインスタンスである。だから、現在見えている表がどのクラスのインスタンスなのかを意識しなければ思うようなコードを書けないということを痛感した。

表のデータソースにCore Dataを使用している場合、NSFetchedResultsControllerとUISearchDisplayControllerの両クラスの連携が鍵になる。

よく見かけるサンプルコードは次のようなものである。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
- (void)controllerWillChangeContent:(NSFetchedResultsController *)controller
{
    [self.tableView beginUpdates];
}

- (void)controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject
       atIndexPath:(NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type
      newIndexPath:(NSIndexPath *)newIndexPath
{
    UITableView *tableView = self.tableView;
  // (省略)
}

- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller
{
    [self.tableView endUpdates];
}

だが、検索して絞り込んだ表から選択して更に編集画面でDBを更新した場合、このcontrollerDidChangeContentメソッドでアベンドしてしまう。

1
2
 Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Invalid update: invalid number of rows in section 0.
(以下省略)

こんなメッセージだ。言いたいことは分かるがどう対応して良いか分からない。

1
[self.tableView endUpdates];

の代わりに

1
[self.tableView reloadData];

とやれば、更新時にアベンドはしなくなるが、検索をキャンセルした場合に表示されるオリジナルの表のレンダリングがおかしくなってしまう。(あと、削除できなくなったり詳細画面に遷移できなくなったり。)散々悩んだ挙句、冒頭の書いたことに思い当たった。そう、分かってしまえば何てことはない。NSFetchedResultsControllerの操作も、どのTableViewに対して行うかを判断すれば良かったのだ。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
- (void)controllerWillChangeContent:(NSFetchedResultsController *)controller
{
    if (self.searchDisplayController.isActive) {
        [self.searchDisplayController.searchResultsTableView beginUpdates];
    }
    else {
        [self.tableView beginUpdates];
    }
}

- (void)controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject
       atIndexPath:(NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type
      newIndexPath:(NSIndexPath *)newIndexPath
{
    UITableView *tableView = self.searchDisplayController.isActive ? self.searchDisplayController.searchResultsTableView : self.tableView;
   
    // (省略)
}

- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller
{
    if (self.searchDisplayController.isActive) {
        [self.searchDisplayController.searchResultsTableView endUpdates];
    }
    else {
        [self.tableView endUpdates];
    }
}

UITableViewCellの再利用について

iOS 5.x までは
– (UITableViewCell*)tableView:(UITableView*)tableView cellForRowAtIndexPath:(NSIndexPath*)indexPath
メソッド内で

1
2
3
4
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:TABLE_CELL_NAME];
if (cell == nil) {
    cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:TABLE_CELL_NAME];
}

のようなコードを書く必要があったが、iOS 6.0 からは – (void)viewDidLoad メソッドに

1
[self.tableView registerClass:[UITableViewCell class] forCellReuseIdentifier:TABLE_CELL_NAME];

というセットアップをすれば

1
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:TABLE_CELL_NAME forIndexPath:indexPath];

と書くだけで良いというのがネットで得られる情報なのだが、UISearchDisplayController を使って検索機能を実装しようとすると検索文字列を入力した途端に落ちてしまう。検索結果は UITableView が使われるのではなく、UISearchResultTableView のインスタンスが(しかも都度)生成されるので、適切な場所でセットアップをしてあげる必要がある。ここではupdateFilteredContentForNameメソッドで検索をした直後に記述したら(5行目)うまくいった。

1
2
3
4
5
6
7
- (BOOL)searchDisplayController:(UISearchDisplayController *)controller
shouldReloadTableForSearchString:(NSString *)searchString
{
    [self updateFilteredContentForName:searchString];
    [self.searchDisplayController.searchResultsTableView registerClass:[UITableViewCell class] forCellReuseIdentifier:TABLE_CELL_NAME];
    return YES;
}

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()
    {
    }
}

NSDateComponentsからNSDateを取得するとnilが返ってくる

日付値に対して加減演算を行い、その結果を日付値として取得したくてNSDateComponentsクラスを使用した。以下のようなコードを書いたら、 result は常に nil が入って来る。

1
2
3
4
5
NSDate* dt = [NSDate date];
NSCalendar* cal = [[NSCalendar alloc] initWithCalendarIdentifier:NSGregorianCalendar];
NSDateComponents* dc = [cal components:NSYearCalendarUnit fromDate:dt];
// dcに対する操作
NSDate* result = [dc date];

そこでNSCalendarオブジェクトを使って以下のように変更したら期待通りの値が取得できた。

1
2
3
4
5
NSDate* dt = [NSDate date];
NSCalendar* cal = [[NSCalendar alloc] initWithCalendarIdentifier:NSGregorianCalendar];
NSDateComponents* dc = [cal components:NSYearCalendarUnit fromDate:dt];
// dcに対する操作
NSDate* result = [cal dateFromComponents:dc];

List File Attributes バージョン1.1.0版公開

ダウンロード

◆ これは何か?

  • ファイルのパス名、サイズ、更新日時などの属性をリスト化するアプリケーションです。
  • とはいっても、ただリスト化するだけでは価値が無くて、それを属性項目ごとにクリップボードへコピーして、他のアプリケーションで利用することを想定しています。
  • 個人利用ではほとんど使う機会はないと思いますが、法人などではファイルを資産として管理する場合、ファイルの一覧を作成することがあると思います。しかし対象のファイル数が多いと、これがなかなか面倒くさい作業で、そういった作業の負荷を軽減するためにこのアプリケーションを作成しました。

◆ 動作環境

.NET Framework 4.0 が動作する環境。

  1. お使いのコンピュータにまだ .NET Framework 4.0 を導入されていない場合は以下のURLからダウンロードしてインストールを行う必要があります。
    https://www.microsoft.com/ja-jp/net/netfx4/download.aspx

◆ 使い方

  • 情報を取得したいファイルまたはそれらを含むフォルダをマウス等でドラッグ&ドロップします。
  • パスに関しては区切り文字の設定や、何文字目からコピーするかを指定します。
  • コピーしたい項目(名前、パス、サイズ、更新日時、作成日時)をプルダウンリストから選び、コピーボタンを押下するとクリップボードにその項目がコピーされます。
  • 一度実行後、続けて作業を行いたい場合は「全てクリア」ボタンを押してリストをクリアしてから、再度ファイルまたはフォルダをドロップして作業を行うことができます。

◆ 利用条件

  • 本ソフトウェアはフリーウェアとして公開しています。ご利用に当たり料金は発生いたしません。
  • 著作権はクロスラボラトリーにあります。

◆ 免責事項

  • 本ソフトウェアは無保証です。ご利用によって生じたいかなる損害に対しても作者は責任を負わないものとします。ご自身の責任においてご利用ください。

更新履歴

  • v1.0.0 (2013-07-07)
    公開
  • v1.1.0 (2013-07-10)
    パスを相対パスと絶対パスに分けて表示・コピーするように修正
アーカイブ