転職したらスマレジだった件

マジレス大歓迎です。

諸事情で Zend Framework を理解する 2022 - ④登録機能の作成

f:id:ryamate:20220330004846p:plain

INDEX

こんにちは!

スマレジ・テックファームのWebエンジニアやまてと申します。

smaregiconf.connpass.com

来週は自社のこのイベントに参加!(視聴者として…)

研修時のメンターの方がお一人いらっしゃるけど、こういうお話は聞いたことないし気になります。

さて、前回の記事(③一覧画面の作成)で、Webサービスのトップページとなる一覧画面を作成しました。

INDEX - 『諸事情で Zend Framework を理解する 2022』連載

本記事では以下の順序で開発を進めています。

① Docker での開発環境構築

チュートリアルを進める上で必要な開発環境の構築方法をアウトプットしました。

② Zend Framework のインストール

Composer で Zend Framework(zendframework/skeleton-application) をインストールし、Welcome画面を表示しました。

③ 一覧画面の作成(前回)

Webサービスのトップページとなる一覧画面を作成しました。

④ 登録機能の作成(今回)

データの登録機能を作成します。

⑤ 編集・削除機能の作成

登録済みのデータを編集・削除する機能を作成します。

⑥ バリデーションメッセージの日本語化

パッケージを用いて、英語のバリデーションメッセージを日本語化します。

④登録機能の作成

今回は、商品データを Products テーブルに登録する、登録機能を作成します。

Zend Framework公式ドキュメントのチュートリアルを参考に作成しました。(ところどころアレンジしています)

本記事完了時点のソースコード

本記事完了時点のソースコードGitHub上に公開しています。

github.com

以下は変更差分です。

github.com

商品管理ページの構成

ページ 説明
商品一覧 商品の一覧を表示。商品の新規登録や編集、削除のためのリンクボタン表示。
商品登録⭐️ 新規商品を登録するためのフォーム。
商品編集 商品を編集するためのフォーム。
商品削除 商品を削除することを確認し、削除するページ。

実装内容

1. パッケージ zend-form のインストール

フォーム画面を簡単に作成するためのパッケージ zend-form をインストールします。

https://github.com/zendframework/zend-form/

docker-compose exec app composer require zendframework/zend-form

コマンド実行結果

# r_yamate @ mbp in ~/Documents/code/zend-framework-crud-sample on git:feature-create-add o [22:58:16]
$ docker-compose exec app composer require zendframework/zend-form
Warning from https://repo.packagist.org: Support for Composer 1 is deprecated and some packages will not be available. You should upgrade to Composer 2. See https://blog.packagist.com/deprecating-composer-1-support/
Info from https://repo.packagist.org: #StandWithUkraine
Using version ^2.14 for zendframework/zend-form
./composer.json has been updated
Loading composer repositories with package information
Warning from https://repo.packagist.org: Support for Composer 1 is deprecated and some packages will not be available. You should upgrade to Composer 2. See https://blog.packagist.com/deprecating-composer-1-support/
Info from https://repo.packagist.org: #StandWithUkraine
Updating dependencies (including require-dev)
Package operations: 4 installs, 0 updates, 0 removals
  - Installing zendframework/zend-hydrator (3.0.2): Downloading (100%)

  Please select which config file you wish to inject 'Zend\Hydrator' into:
  [0] Do not inject
  [1] config/modules.config.php
  [2] config/development.config.php.dist
  Make your selection (default is 1):1

  Remember this option for other packages of the same type? (Y/n)Y
    Installing Zend\Hydrator from package zendframework/zend-hydrator
  - Installing zendframework/zend-filter (2.9.2): Downloading (100%)
    Installing Zend\Filter from package zendframework/zend-filter
  - Installing zendframework/zend-inputfilter (2.10.1): Downloading (100%)
    Installing Zend\InputFilter from package zendframework/zend-inputfilter
  - Installing zendframework/zend-form (2.14.3): Downloading (100%)
    Installing Zend\Form from package zendframework/zend-form
zendframework/zend-hydrator suggests installing zendframework/zend-serializer (^2.9, to use the SerializableStrategy)
zendframework/zend-filter suggests installing psr/http-factory-implementation (psr/http-factory-implementation, for creating file upload instances when consuming PSR-7 in file upload filters)
zendframework/zend-filter suggests installing zendframework/zend-crypt (Zend\Crypt component, for encryption filters)
zendframework/zend-filter suggests installing zendframework/zend-i18n (Zend\I18n component for filters depending on i18n functionality)
zendframework/zend-inputfilter suggests installing psr/http-message-implementation (PSR-7 is required if you wish to validate PSR-7 UploadedFileInterface payloads)
zendframework/zend-form suggests installing zendframework/zend-captcha (^2.7.1, required for using CAPTCHA form elements)
zendframework/zend-form suggests installing zendframework/zend-code (^2.6 || ^3.0, required to use zend-form annotations support)
zendframework/zend-form suggests installing zendframework/zend-i18n (^2.6, required when using zend-form view helpers)
zendframework/zend-form suggests installing zendframework/zendservice-recaptcha (in order to use the ReCaptcha form element)
Package container-interop/container-interop is abandoned, you should avoid using it. Use psr/container instead.
Package zendframework/zend-component-installer is abandoned, you should avoid using it. Use laminas/laminas-component-installer instead.
Package zendframework/zend-config is abandoned, you should avoid using it. Use laminas/laminas-config instead.
Package zendframework/zend-db is abandoned, you should avoid using it. Use laminas/laminas-db instead.
Package zendframework/zend-escaper is abandoned, you should avoid using it. Use laminas/laminas-escaper instead.
Package zendframework/zend-eventmanager is abandoned, you should avoid using it. Use laminas/laminas-eventmanager instead.
Package zendframework/zend-http is abandoned, you should avoid using it. Use laminas/laminas-http instead.
Package zendframework/zend-json is abandoned, you should avoid using it. Use laminas/laminas-json instead.
Package zendframework/zend-loader is abandoned, you should avoid using it. Use laminas/laminas-loader instead.
Package zendframework/zend-modulemanager is abandoned, you should avoid using it. Use laminas/laminas-modulemanager instead.
Package zendframework/zend-mvc is abandoned, you should avoid using it. Use laminas/laminas-mvc instead.
Package zendframework/zend-router is abandoned, you should avoid using it. Use laminas/laminas-router instead.
Package zendframework/zend-servicemanager is abandoned, you should avoid using it. Use laminas/laminas-servicemanager instead.
Package zendframework/zend-stdlib is abandoned, you should avoid using it. Use laminas/laminas-stdlib instead.
Package zendframework/zend-uri is abandoned, you should avoid using it. Use laminas/laminas-uri instead.
Package zendframework/zend-validator is abandoned, you should avoid using it. Use laminas/laminas-validator instead.
Package zendframework/zend-view is abandoned, you should avoid using it. Use laminas/laminas-view instead.
Package zfcampus/zf-development-mode is abandoned, you should avoid using it. Use laminas/laminas-development-mode instead.
Package zendframework/zend-hydrator is abandoned, you should avoid using it. Use laminas/laminas-hydrator instead.
Package zendframework/zend-filter is abandoned, you should avoid using it. Use laminas/laminas-filter instead.
Package zendframework/zend-inputfilter is abandoned, you should avoid using it. Use laminas/laminas-inputfilter instead.
Package zendframework/zend-form is abandoned, you should avoid using it. Use laminas/laminas-form instead.
Writing lock file
Generating autoload files

途中、

Please select which config file you wish to inject 'Zend\Hydrator' into:
(どの設定ファイルに 'Zend\Hydrator' を注入したいかを選択してください。)

という質問をされて、 [1] config/modules.config.php を選択すると、'Zend\Hydrator' 使用についての設定を追記してくれます。

Remember this option for other packages of the same type? (Y/n)
(このオプションを他の同じタイプのパッケージにも適用しますか? (Y/n))

とも聞かれるので、Yにします。

アプリケーションへの zend-form 追加

アプリケーションにパッケージを追加するために設定します。(上の質問で [1] config/modules.config.php 選択していたら確認のみです)

確認 or 編集:config/modules.config.php

<?php

// 略
return [
+    'Zend\Form',
+    'Zend\InputFilter',
+    'Zend\Filter',
+    'Zend\Hydrator',
    'Zend\Db',
    'Zend\Router',
    'Zend\Validator',
    'Application',
    'Product',
];

関連するパッケージ('Zend\InputFilter', 'Zend\Filter', 'Zend\Hydrator')も併せて追加されています。

2. パッケージ zend-i18n のインストール

zend-i18n は翻訳関連のパッケージです。

https://github.com/zendframework/zend-i18n

zend-i18n がないと、 zend-form 使用時にエラーが出るため、インストールします。

エラー内容の参考

An error occurred
An error occurred during execution; please try again later.
Additional information:
Error
File:
/var/www/zf-sample/vendor/zendframework/zend-form/src/View/Helper/AbstractHelper.php:21
Message:
Class 'Zend\I18n\View\Helper\AbstractTranslatorHelper' not found
Stack trace:
#0 /var/www/zf-sample/vendor/composer/ClassLoader.php(444): include()
#1 /var/www/zf-sample/vendor/composer/ClassLoader.php(322): Composer\Autoload\includeFile('/var/www/zf-sam...')
#2 [internal function]: Composer\Autoload\ClassLoader->loadClass('Zend\\Form\\View\\...')
#3 /var/www/zf-sample/vendor/zendframework/zend-form/src/View/Helper/Form.php(19): spl_autoload_call('Zend\\Form\\View\\...')
#4 /var/www/zf-sample/vendor/composer/ClassLoader.php(444): include('/var/www/zf-sam...')
#5 /var/www/zf-sample/vendor/composer/ClassLoader.php(322): Composer\Autoload\includeFile('/var/www/zf-sam...')
#6 [internal function]: Composer\Autoload\ClassLoader->loadClass('Zend\\Form\\View\\...')
#7 /var/www/zf-sample/vendor/zendframework/zend-servicemanager/src/Factory/InvokableFactory.php(30): spl_autoload_call('Zend\\Form\\View\\...')
#8 /var/www/zf-sample/vendor/zendframework/zend-servicemanager/src/ServiceManager.php(764): Zend\ServiceManager\Factory\InvokableFactory->__invoke(Object(Zend\ServiceManager\ServiceManager), 'Zend\\Form\\View\\...', NULL)
#9 /var/www/zf-sample/vendor/zendframework/zend-servicemanager/src/ServiceManager.php(200): Zend\ServiceManager\ServiceManager->doCreate('Zend\\Form\\View\\...')
#10 /var/www/zf-sample/vendor/zendframework/zend-servicemanager/src/AbstractPluginManager.php(152): Zend\ServiceManager\ServiceManager->get('Zend\\Form\\View\\...')
#11 /var/www/zf-sample/vendor/zendframework/zend-view/src/Renderer/PhpRenderer.php(376): Zend\ServiceManager\AbstractPluginManager->get('form', NULL)
#12 /var/www/zf-sample/vendor/zendframework/zend-view/src/Renderer/PhpRenderer.php(394): Zend\View\Renderer\PhpRenderer->plugin('form')
#13 /var/www/zf-sample/module/Product/view/product/product/add.phtml(23): Zend\View\Renderer\PhpRenderer->__call('form', Array)
#14 /var/www/zf-sample/vendor/zendframework/zend-view/src/Renderer/PhpRenderer.php(506): include('/var/www/zf-sam...')
#15 /var/www/zf-sample/vendor/zendframework/zend-view/src/View.php(207): Zend\View\Renderer\PhpRenderer->render(NULL)
#16 /var/www/zf-sample/vendor/zendframework/zend-view/src/View.php(236): Zend\View\View->render(Object(Zend\View\Model\ViewModel))
#17 /var/www/zf-sample/vendor/zendframework/zend-view/src/View.php(200): Zend\View\View->renderChildren(Object(Zend\View\Model\ViewModel))
#18 /var/www/zf-sample/vendor/zendframework/zend-mvc/src/View/Http/DefaultRenderingStrategy.php(105): Zend\View\View->render(Object(Zend\View\Model\ViewModel))
#19 /var/www/zf-sample/vendor/zendframework/zend-eventmanager/src/EventManager.php(322): Zend\Mvc\View\Http\DefaultRenderingStrategy->render(Object(Zend\Mvc\MvcEvent))
#20 /var/www/zf-sample/vendor/zendframework/zend-eventmanager/src/EventManager.php(171): Zend\EventManager\EventManager->triggerListeners(Object(Zend\Mvc\MvcEvent))
#21 /var/www/zf-sample/vendor/zendframework/zend-mvc/src/Application.php(367): Zend\EventManager\EventManager->triggerEvent(Object(Zend\Mvc\MvcEvent))
#22 /var/www/zf-sample/vendor/zendframework/zend-mvc/src/Application.php(348): Zend\Mvc\Application->completeRequest(Object(Zend\Mvc\MvcEvent))
#23 /var/www/zf-sample/public/index.php(40): Zend\Mvc\Application->run()
#24 {main}
© 2005 - 2022 by Zend Technologies Ltd. All rights reserved.

パッケージ zend-i18n をインストールします。

docker-compose exec app composer require zendframework/zend-i18n

コマンド実行結果

# r_yamate @ mbp in ~/Documents/code/zend-framework-crud-sample on git:feature-create-add x [23:39:58]
$ docker-compose exec app composer require zendframework/zend-i18n
Warning from https://repo.packagist.org: Support for Composer 1 is deprecated and some packages will not be available. You should upgrade to Composer 2. See https://blog.packagist.com/deprecating-composer-1-support/
Info from https://repo.packagist.org: #StandWithUkraine
Using version ^2.10 for zendframework/zend-i18n
./composer.json has been updated
Loading composer repositories with package information
Warning from https://repo.packagist.org: Support for Composer 1 is deprecated and some packages will not be available. You should upgrade to Composer 2. See https://blog.packagist.com/deprecating-composer-1-support/
Info from https://repo.packagist.org: #StandWithUkraine
Updating dependencies (including require-dev)
Package operations: 1 install, 0 updates, 0 removals
  - Installing zendframework/zend-i18n (2.10.1): Downloading (100%)

  Please select which config file you wish to inject 'Zend\I18n' into:
  [0] Do not inject
  [1] config/modules.config.php
  [2] config/development.config.php.dist
  Make your selection (default is 1):1

  Remember this option for other packages of the same type? (Y/n)Y
    Installing Zend\I18n from package zendframework/zend-i18n
zendframework/zend-i18n suggests installing zendframework/zend-cache (Zend\Cache component)
zendframework/zend-i18n suggests installing zendframework/zend-i18n-resources (Translation resources)
Package container-interop/container-interop is abandoned, you should avoid using it. Use psr/container instead.
Package zendframework/zend-component-installer is abandoned, you should avoid using it. Use laminas/laminas-component-installer instead.
Package zendframework/zend-config is abandoned, you should avoid using it. Use laminas/laminas-config instead.
Package zendframework/zend-db is abandoned, you should avoid using it. Use laminas/laminas-db instead.
Package zendframework/zend-escaper is abandoned, you should avoid using it. Use laminas/laminas-escaper instead.
Package zendframework/zend-eventmanager is abandoned, you should avoid using it. Use laminas/laminas-eventmanager instead.
Package zendframework/zend-filter is abandoned, you should avoid using it. Use laminas/laminas-filter instead.
Package zendframework/zend-form is abandoned, you should avoid using it. Use laminas/laminas-form instead.
Package zendframework/zend-http is abandoned, you should avoid using it. Use laminas/laminas-http instead.
Package zendframework/zend-hydrator is abandoned, you should avoid using it. Use laminas/laminas-hydrator instead.
Package zendframework/zend-inputfilter is abandoned, you should avoid using it. Use laminas/laminas-inputfilter instead.
Package zendframework/zend-json is abandoned, you should avoid using it. Use laminas/laminas-json instead.
Package zendframework/zend-loader is abandoned, you should avoid using it. Use laminas/laminas-loader instead.
Package zendframework/zend-modulemanager is abandoned, you should avoid using it. Use laminas/laminas-modulemanager instead.
Package zendframework/zend-mvc is abandoned, you should avoid using it. Use laminas/laminas-mvc instead.
Package zendframework/zend-router is abandoned, you should avoid using it. Use laminas/laminas-router instead.
Package zendframework/zend-servicemanager is abandoned, you should avoid using it. Use laminas/laminas-servicemanager instead.
Package zendframework/zend-stdlib is abandoned, you should avoid using it. Use laminas/laminas-stdlib instead.
Package zendframework/zend-uri is abandoned, you should avoid using it. Use laminas/laminas-uri instead.
Package zendframework/zend-validator is abandoned, you should avoid using it. Use laminas/laminas-validator instead.
Package zendframework/zend-view is abandoned, you should avoid using it. Use laminas/laminas-view instead.
Package zfcampus/zf-development-mode is abandoned, you should avoid using it. Use laminas/laminas-development-mode instead.
Package zendframework/zend-i18n is abandoned, you should avoid using it. Use laminas/laminas-i18n instead.
Writing lock file
Generating autoload files

同じく途中、

Please select which config file you wish to inject 'Zend\Hydrator' into:
(どの設定ファイルに 'Zend\Hydrator' を注入したいかを選択してください。)

という質問をされて、 [1] config/modules.config.php を選択すると、'Zend\Hydrator' 使用についての設定を追記してくれます。

Remember this option for other packages of the same type? (Y/n)
(このオプションを他の同じタイプのパッケージにも適用しますか? (Y/n))

とも聞かれるので、Yにします。

アプリケーションへの zend-i18n 追加

アプリケーションにパッケージを追加するために設定します。(上の質問で [1] config/modules.config.php 選択していたら確認のみです)

確認 or 編集:config/modules.config.php

<?php

// 略
return [
+    'Zend\I18n',
    'Zend\Form',
    'Zend\InputFilter',
    'Zend\Filter',
    'Zend\Hydrator',
    'Zend\Db',
    'Zend\Router',
    'Zend\Validator',
    'Application',
    'Product',
];

3. 登録機能の作成

パッケージ zend-form を利用して、フォーム機能の追加

作成・編集:module/Product/src/Form/ProductForm.php

<?php

namespace Product\Form;

use Zend\Form\Form;

/**
 * 商品フォームクラス。
 * 商品の新規作成・編集に使用する。
 */
class ProductForm extends Form
{
    /**
     * コンストラクタ。
     *
     * 親のコンストラクタを呼び出す際にフォームの名前を設定する。コンストラクタに指定された名前は無視する。
     * フォーム要素を設定する。各項目について、表示されるラベルを含むさまざまな属性やオプションを設定する。
     * - name 属性は テーブルのフィールド名に合わせている。
     *
     * @param mixed $name 指定されているが、使用されない。
     */
    public function __construct($name = null)
    {
        parent::__construct('product');

        $this->add([
            'name' => 'id',
            'type' => 'hidden',
        ]);
        $this->add([
            'name' => 'item_name',
            'type' => 'text',
            'options' => [
                'label' => '商品名',
            ],
        ]);
        $this->add([
            'name' => 'price',
            'type' => 'text',
            'options' => [
                'label' => '商品単価',
            ],
        ]);
        $this->add([
            'name' => 'submit',
            'type' => 'submit',
            'attributes' => [
                'value' => 'Go',
                'id'    => 'submitbutton',
            ],
        ]);
    }
}

パッケージ zend-inputfilter でバリデーションの追加

編集:module/Product/src/Model/Product.php

<?php

namespace Product\Model;

use DomainException;
use Zend\Filter\StringTrim;
use Zend\Filter\StripTags;
use Zend\Filter\ToInt;
use Zend\InputFilter\InputFilter;
use Zend\InputFilter\InputFilterAwareInterface;
use Zend\InputFilter\InputFilterInterface;
use Zend\Validator\StringLength;
use Zend\Validator\Between;

/**
 * 商品モデルクラス。
 * zend-db の TableGateway クラスで動作させるために、 exchangeArray() メソッドを実装する。
 */
class Product implements InputFilterAwareInterface
{
    /** @var null|int $id */
    public ?int $id;
    /** @var null|string $itemName */
    public ?string $itemName;
    /** @var null|int $price */
    public ?int $price;
    /** @var null|string $image */
    public ?string $image;

    /** @var $inputFilter */
    private $inputFilter;

    /**
     * 指定した配列からデータをエンティティのプロパティにコピーする。
     * (zend-db の TableGateway クラスで動作させるため)
     *
     * @param array $data
     * @return void
     */
    public function exchangeArray(array $data)
    {
        $this->id = isset($data['id']) ? $data['id'] : null;
        $this->itemName = isset($data['item_name']) ? $data['item_name'] : null;
        $this->price = isset($data['price']) ? $data['price'] : null;
        $this->image = isset($data['image']) ? $data['image'] : null;
    }

    /**
     * InputFilterAwareInterfaceでは、setInputFilter()とgetInputFilter()という2つのメソッドが定義されており、
     * getInputFilter()のみを実装すればよいので、setInputFilter()からは例外を投げる。
     *
     * @param \Zend\InputFilter\InputFilterInterface $inputFilter
     * @return void
     * @throws DomainException
     */
    public function setInputFilter(InputFilterInterface $inputFilter)
    {
        throw new DomainException(sprintf(
            '%s does not allow injection of an alternate input filter',
            __CLASS__
        ));
    }

    /**
     * InputFilter のインスタンスを作成。フィルタリングやバリデーションを行うプロパティごとに設定。
     *
     * @return InputFilterInterface $inputFilter
     */
    public function getInputFilter()
    {
        if ($this->inputFilter) {
            return $this->inputFilter;
        }

        $inputFilter = new InputFilter();

        $inputFilter->add([
            'name' => 'id',
            'required' => true, // 必須項目として設定
            'filters' => [
                ['name' => ToInt::class], // int フィルタを追加し、整数値のみを使用
            ],
        ]);

        $inputFilter->add([
            'name' => 'item_name',
            'required' => true, // 必須項目として設定
            // StripTagsとStringTrimという2つのフィルタを追加して、不要なHTMLや不要な空白を削除。
            'filters' => [
                ['name' => StripTags::class],
                ['name' => StringTrim::class],
            ],
            // データベースに格納できる文字数以上の文字をユーザーが入力しないように StringLength バリデーターを追加。
            'validators' => [
                [
                    'name' => StringLength::class,
                    'options' => [
                        'encoding' => 'UTF-8',
                        'min' => 1,
                        'max' => 85,
                    ],
                ],
            ],
        ]);

        $inputFilter->add([
            'name' => 'price',
            'required' => true, // 必須項目として設定
            'filters' => [
                // int フィルタを追加し、整数値のみを使用
                ['name' => ToInt::class],
                // StripTagsとStringTrimという2つのフィルタを追加して、不要なHTMLや不要な空白を削除。
                ['name' => StripTags::class],
                ['name' => StringTrim::class],
            ],
            'validators' => [
                [
                    // 指定した値が他の2つの値の間にあるかどうか
                    'name' => Between::class,
                    'options' => [
                        'min' => 1,
                        'max' => 100000000, // 8桁までOK
                    ],
                ],
            ],
        ]);

        $this->inputFilter = $inputFilter;
        return $this->inputFilter;
    }
}

編集:module/Product/src/Controller/ProductController.php

<?php

namespace Product\Controller;

use Product\Form\ProductForm;
use Product\Model\Product;
use Product\Model\ProductTable;
use Zend\Mvc\Controller\AbstractActionController;
use Zend\View\Model\ViewModel;

/**
 * 商品コントローラークラス。
 */
class ProductController extends AbstractActionController
{
    /** @var \Product\Model\ProductTable $table */
    private ProductTable $table;

    /**
     * コンストラクタ。
     * ProductController は ProductTable に依存する。
     *
     * @param \Product\Model\ProductTable $table productsテーブル
     */
    public function __construct(ProductTable $table)
    {
        $this->table = $table;
    }

    /**
     * 商品一覧を表示する。
     * 商品一覧を表示するために、モデルから商品を取得し、ビューに渡す。
     *
     * @return Zend\View\Model\ViewModel 商品一覧
     */
    public function indexAction()
    {
        // ビューに変数を設定するために、 ViewModel のインスタンスを返す。
        return new ViewModel([
            // コンストラクタの最初のパラメータは、表現したいデータを含む配列。自動的にビュースクリプトに渡される。
            'products' => $this->table->fetchAll(),
        ]);
    }

    /**
     * 商品を新規登録する。
     * フォームからデータを取得し、新しい商品の行を保存し、商品一覧にリダイレクトする。
     *
     * @return Response|array
     */
    public function addAction()
    {
        // ProductFormインスタンスを作成し、送信ボタンのラベルを "新規登録" に設定する。
        $form = new ProductForm();
        $form->get('submit')->setValue('新規登録');

        $request = $this->getRequest();

        // POSTでない場合、フォームデータは送信されていないのでフォームを表示する(zend-mvcでは、ビューモデルの代わりにデータの配列を返せる)。
        if (!$request->isPost()) {
            return ['form' => $form];
        }

        // フォームの投稿があるため、Productインスタンスを作成し、入力フィルタをフォームに渡し、リクエストインスタンスからフォームに送信されたデータを渡す。
        $product = new Product();
        $form->setInputFilter($product->getInputFilter());
        $form->setData($request->getPost());
        // バリデーション失敗の場合、フォームを再表示する(バリデーション失敗の内容を、ビューレイヤーに伝える)。
        if (!$form->isValid()) {
            return ['form' => $form];
        }

        // フォームからデータを取得し、新しい商品の行を保存し、Redirectコントローラプラグインを使って商品一覧にリダイレクトする。
        $product->exchangeArray($form->getData());
        $this->table->createProduct($product);
        return $this->redirect()->toRoute('product');
    }
}

新しい商品データのテーブルへの登録(モデルの処理)

編集:module/Product/src/Model/ProductTable.php

<?php

namespace Product\Model;

use Zend\Db\TableGateway\TableGatewayInterface;

/**
 * 商品のデータベース・テーブルに対する操作を実行するクラス。
 */
class ProductTable
{
    /** @var \Zend\Db\TableGateway\TableGatewayInterface $tableGateway */
    private TableGatewayInterface $tableGateway;

    /**
     * コンストラクタ。
     * 渡された TableGateway インスタンスを $tableGateway に設定する。
     * TableGateway インスタンスを使用して、データベースのテーブルから行を検索、挿入、更新、削除する。
     * (これにより、テスト時にモックインスタンスを含む別の実装を簡単に提供することができる)
     *
     * @param \Zend\Db\TableGateway\TableGatewayInterface $tableGateway
     */
    public function __construct(TableGatewayInterface $tableGateway)
    {
        $this->tableGateway = $tableGateway;
    }

    /**
     * データベースからすべての商品のカラムをResultSetとして取得する。
     *
     * @return Zend\Db\ResultSet\ResultSet データベースから取得したすべての商品のカラム
     */
    public function fetchAll()
    {
        return $this->tableGateway->select();
    }

        // 以下のメソッド追記
    /**
     * データベースに新しい行を作成する。
     *
     * @param Product $product 新しく作成する商品のカラム
     * @return void
     */
    public function createProduct(Product $product)
    {
        $data = [
            'item_name' => $product->itemName,
            'price' => $product->price,
            'image' => $product->image,
        ];

        $this->tableGateway->insert($data);
    }
}

チュートリアルのメソッドの分け方は、「取得する」メソッド、「登録と更新する」メソッド、「削除する」メソッドでしたが、シンプルにするため登録と更新を切り分け、使いまわしが効くようにアレンジしています。次回で更新と削除を追加して、下記のメソッドをCRUD 処理においては用意します。

  • 「取得する」メソッド
  • 「登録する」メソッド
  • 「更新する」メソッド
  • 「削除する」メソッド

新規登録用のフォーム画面(ビュースクリプト)の作成

作成・編集:module/Product/view/product/product/index.phtml

<?php

// ページのタイトルと、ブラウザのタイトルバーに表示される headTitle() ビューヘルパーを使用して
//  <head> セクションのタイトルを設定する。
$title = '商品一覧';
$this->headTitle($title);
?>
<h1><?= $this->escapeHtml($title) ?></h1>

<!-- 新しい商品を追加するためのリンクを作成する。 -->
<p>
    <!-- url() ビューヘルパーは zend-mvc と zend-view によって提供され、必要なリンクを作成するために使用される。
url() の最初のパラメータは URL の作成に使用するルート名で、 2番目のパラメータはルートのプレースホルダに代入する変数の配列。
この場合、2つのプレースホルダ変数: action と id を受け入れるように設定されたアルバムルートを使用する。 -->
    <a href="<?= $this->url('product', ['action' => 'add']) ?>">新規登録</a>
</p>

<table class="table">
    <tr>
        <th>ID</th>
        <th>商品名</th>
        <th>商品単価</th>
    </tr>
    <!-- コントローラアクションから割り当てた$productsを繰り返し処理 -->
    <?php foreach ($products as $product) : ?>
        <tr>
            <!-- 各アルバムのタイトルとアーティストを表示するテーブルを作成 -->
            <!-- ビュースクリプトに渡す変数と内部で作成した変数を区別するために、 $this->{variable name} を使用してアクセスする -->
            <td><?= $this->escapeHtml($product->id) ?></td>
            <td><?= $this->escapeHtml($product->itemName) ?></td>
            <td><?= $this->escapeHtml(number_format($product->price)) ?></td>
        </tr>
    <?php endforeach; ?>
</table>

<!-- クロスサイトスクリプティング(XSS)対策のために、escapeHtml()ビューヘルパーを常に使用 -->

作成・編集:module/Product/view/product/product/add.phtml

<?php
$title = '商品の新規登録';
$this->headTitle($title);
?>
<h1><?= $this->escapeHtml($title) ?></h1>
<?php
// 要素にデフォルトの CSS クラスとプレースホルダーテキストを提供する。
$itemName = $form->get('item_name');
$itemName->setAttribute('class', 'form-control');
$itemName->setAttribute('placeholder', '商品名');

$price = $form->get('price');
$price->setAttribute('class', 'form-control');
$price->setAttribute('placeholder', '商品単価');

// 送信ボタン用の CSS クラスを提供する。
$submit = $form->get('submit');
$submit->setAttribute('class', 'btn btn-primary');

$form->setAttribute('action', $this->url('product', ['action' => 'add']));
$form->prepare();

echo $this->form()->openTag($form);
?>
<!-- フォームグループの div で要素をラップし、その中でラベル、要素、エラーを別々にレンダリングする。 -->
<div class="form-group">
    <?= $this->formLabel($itemName) ?>
    <?= $this->formElement($itemName) ?>
    <span class="text-danger"><?= $this->formElementErrors()->render($itemName, ['class' => 'help-block']) ?></span>
</div>

<div class="form-group">
    <?= $this->formLabel($price) ?>
    <?= $this->formElement($price) ?>
    <span class="text-danger"><?= $this->formElementErrors()->render($price, ['class' => 'help-block']) ?></span>
</div>

<?php
echo $this->formSubmit($submit);
echo $this->formHidden($form->get('id'));
echo $this->form()->closeTag();

最後に画面を確認

下記URLにアクセスします。

http://localhost/

f:id:ryamate:20220326000103p:plain

http://localhost/product/add

f:id:ryamate:20220326000134p:plain

卵かけご飯を登録してみます。

f:id:ryamate:20220326000507p:plain

登録できました!

次回、「諸事情で Zend Framework を理解する 2022 - ⑤編集・削除機能の作成」投稿予定です。