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

マジレス大歓迎です。

諸事情で Zend Framework を理解する 2022 - ③一覧画面の作成

f:id:ryamate:20220330003447p:plain

INDEX

こんにちは!

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

ついに、初めての実務が始まりました。

ということで、頑張ります。

前回の記事(②Zend Framework のインストール)で、Zend Framework のWelcome画面を表示できました。

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

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

① Docker での開発環境構築

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

② Zend Framework のインストール(前回)

Composer で Zend Framework(zendframework/skeleton-application) をインストールしました。

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

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

④ 登録機能の作成

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

⑤ 編集・削除機能の作成

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

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

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

③一覧画面の作成

今回は、MySQLより取得した値を表示する、Webサービスのトップページとなる「商品一覧画面」を作成します。

f:id:ryamate:20220325230532p:plain

Zend Framework公式ドキュメントのチュートリアルの下記ページを参考に作成しました。

チュートリアルは取り扱うデータが音楽CDですが、私はスマレジ社員なので、自社プロダクトを意識して、取り扱うデータは商品(商品名、価格)にしています。

smaregi.jp

waiter.smaregi.jp

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

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

github.com

以下は変更差分です。

github.com

商品管理ページの構成

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

productsテーブル設計

フィールド名 名前 データ型 オプションなど
id ID INTEGER PRIMARY KEY, AUTO INCREMENT
item_name 商品名 VARCHAR(256) NOT NULL
price 商品単価 INTEGER NOT NULL
image 商品画像パス VARCHAR(256)
delete_flag 論理削除 BOOLEAN DEFAULT FALSE, NOT NULL
created_at 作成日 DATETIME DEFAULT CURRENT_TIMESTAMP, NOT NULL
updated_at 更新日 DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, NOT NULL

ディレクトリ構成

Zend Framework のパッケージ zend-mvc はモジュールシステムを使用して、各モジュールごとにコードを整理します。

doc_root/
    module/
        Application/
        Product/
            config/
            src/
                Controller/
                Form/
                Model/
            view/
                product/
                    product/

Moduleディレクトリ以下に、ProductディレクトリをApplicationディレクトリと同一の階層で作成し、Productに関するMVC構成をその中で構築します。(Zend Framework はModule単位でMVC構成を構築します)

実装内容

下記の実装内容とGitHubソースコードの差分をご覧いただければと思います。

1. モジュール

Product モジュールの作成・編集

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

<?php

namespace Product;

use Zend\ModuleManager\Feature\ConfigProviderInterface;

/**
 * 商品モジュールクラス。
 * ModuleManager はProduct\Moduleクラスを探し、自動的に getConfig() を呼び出す。
 */
class Module implements ConfigProviderInterface
{
    /**
     * ModuleManagerが、自動的に getConfig() を呼び出す。
     *
     */
    public function getConfig()
    {
        return include __DIR__ . '/../config/module.config.php';
    }
}

Zend Framework が提供する ModuleManager は、モジュール( Product\\Module クラス)をロードして、設定を呼び出します。

Composer のオートローディング機能の設定

編集:composer.json

"autoload": {
    "psr-4": {
        "Application\\": "module/Application/src/",
        "Product\\": "module/Product/src/"
    }
},

autoload セクションに、名前空間とファイルのパスを記述します。

Composer のオートロード・ルール更新

Composer のオートロード・ルールを更新します。

docker-compose exec app composer dump-autoload
# r_yamate @ mbp in ~/Documents/code/zend-framework-crud-sample on git:feature-create-index x [23:28:01]
$ docker-compose exec app composer dump-autoload
Generating autoload files
Generated autoload files

モジュールコンフィグの作成

コントローラ、ビューについての設定を記述します。

作成:module/Product/config/module.config.php

<?php

namespace Product;

use Zend\ServiceManager\Factory\InvokableFactory;

return [
'controllers' => [ // モジュールが提供する全てのコントローラのリスト
        'factories' => [ // 完全修飾名のクラス名で参照し、 zend-servicemanager の InvokableFactory を使用してインスタンスを作成
            Controller\ProductController::class => InvokableFactory::class,
        ],
    ],

        // TemplatePathStack の設定にビューディレクトリを追加。
    'view_manager' => [
        'template_path_stack' => [
            // これにより、view/ ディレクトリに保存されている Album モジュールのビュースクリプトを見つける。
            'product' => __DIR__ . '/../view',
        ],
    ],];

作成したモジュールについてのアプリケーションへの通知

新しいモジュールが存在することを ModuleManager に伝えます。

編集:config/modules.config.php

<?php

// 略

return [
    'Zend\Router',
    'Zend\Validator',
    'Application',
+    'Product',
];

2. ルーティングとコントローラ

ルーティングの設定

編集:module/Product/config/module.config.php

<?php

namespace Product;

use Zend\Router\Http\Segment;
use Zend\ServiceManager\Factory\InvokableFactory;

return [
    'controllers' => [
        'factories' => [
            Controller\AlbumController::class => InvokableFactory::class,
        ],
    ],

        'router' => [
        'routes' => [
            'album' => [
                'type' => Segment::class,
                'options' => [
                    'route' => '/product[/:action[/:id]]', // ルート。`/album` で始まるすべての URL にマッチ
                    'constraints' => [ // 制約
                        'action' => '[a-zA-Z][a-zA-Z0-9_-]*', // 文字で始まり、その後の文字は英数字、アンダースコア、ハイフンのみに制限
                        'id' => '[0-9]+', // 数字に限定
                    ],
                    'defaults' => [
                        'controller' => Controller\AlbumController::class,
                        'action' => 'index',
                    ],
                ],
            ],
        ],
    ],

    'view_manager' => [
        'template_path_stack' => [
            'product' => __DIR__ . '/../view',
        ],
    ],
];

URL と特定のアクションのマッピングは、モジュールの module.config.php ファイルに定義されているルートを使って行われます。

コントローラの作成

作成・編集:module/Product/src/Controller/ProductController.php

<?php

namespace Product\Controller;

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

class ProductController extends AbstractActionController
{
    private $table;

    /**
     * ProductController は ProductTable に依存する。
     *
     * @param \Product\Model\ProductTable $table
     */
    public function __construct(ProductTable $table)
    {
        $this->table = $table;
    }

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

indexAction メソッド(商品一覧を表示するため、モデルから商品を取得し、ビューに渡す)を記述します。

3. データベースとモデル

productsテーブルの確認

確認:docker/mysql/initdb.d/01_products.sql

SET CHARACTER_SET_CLIENT = utf8;
SET CHARACTER_SET_CONNECTION = utf8;

CREATE TABLE products (
    id                    INTEGER AUTO_INCREMENT PRIMARY KEY,
    item_name             VARCHAR(256) NOT NULL,
    price                 INTEGER NOT NULL,
    image                 VARCHAR(256),
    delete_flag           BOOLEAN NOT NULL DEFAULT FALSE,
    created_at            DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
    updated_at            DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);

INSERT INTO
    products (item_name, price, image)
VALUES
    ('日替わりランチA', 600, null),
    ('日替わりランチB', 800, null),
    ('日替わりランチC', 1000, null);

MySQLproductsテーブル 及び 登録データ について、 Dockerコンテナ(dbコンテナ) を起動した際に自動生成させるようにしています。

※ image は不使用で進めます。

zend-dbのインストール

Composer を使用して、データベース操作に関する zendframework/zend-db をインストールします。

Setting Up A Database Adapter - Tutorials - Zend Framework Docs

docker-compose exec app composer require zendframework/zend-db:"^2.8.1"

コマンド実行結果

# r_yamate @ mbp in ~/Documents/code/zend-framework-crud-sample on git:feature-create-index x [8:34:50]
$ docker-compose exec app composer require zendframework/zend-db:"^2.8.1"
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
The "http://repo.packagist.org/p/provider-2017%244a643c6d65a7098901fe03a750d0d5b49308d5264539fc36b4c1886e2e533ad8.json" file could not be downloaded: failed to open stream: Address not available
http://repo.packagist.org could not be fully loaded, package information was loaded from the local cache and may be out of date
./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-db (2.11.0): Downloading (100%)

  Please select which config file you wish to inject 'Zend\Db' 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\Db from package zendframework/zend-db
zendframework/zend-db suggests installing zendframework/zend-hydrator (Zend\Hydrator component for using HydratingResultSets)
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-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-db is abandoned, you should avoid using it. Use laminas/laminas-db instead.
Writing lock file
Generating autoload files

途中、

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

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

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

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

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

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

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

<?php

// 略

return [
+    'Zend\Db',
    'Zend\Router',
    'Zend\Validator',
    'Application',
    'Product',
];

モデルファイルの作成

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

<?php

namespace Product\Model;

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

    /**
     * 指定した配列からデータをエンティティのプロパティにコピーする。
     * (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;
    }
}

パッケージ zend-dbTableGateway クラスで動作させるために、 Product.phpexchangeArray() メソッドを実装します。

issetを用いた箇所については、チュートリアルでは!empty()が使用されています。!empty()を用いると、$data[{key}]の値が(例として)intの0やstringの'0'、空文字''などの場合に$this->{key}=nullになります。PHP: empty - Manual には、

empty() は本質的に !isset($var) || $var == false と同じことを簡潔に記述しているだけです。

とあります。「パラメータが入力されているか」の判定は必要ですが、「入力されたパラメータの値が何か」の判定は不要なので、isset()に置き換えています。

作成・編集:module/Product/src/Model/ProductTable.php

(ここでは、商品一覧表示のみのコードにします。新規作成、編集、削除に関連するメソッドの記述は次の記事で追加します)

<?php

namespace Product\Model;

use RuntimeException;
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();
    }
}

パッケージ zend-dbTableGateway クラスで動作させるために、 Product.phpexchangeArray() メソッドを実装します。

ProductTable.php にて、TableGateway インスタンスを使用して、DB(テーブル)に対する操作を実行します。

ServiceManager を使用してテーブルゲートウェイを設定し、ProductTable にインジェクトする

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

<?php

namespace Product;

use Zend\Db\Adapter\AdapterInterface;
use Zend\Db\ResultSet\ResultSet;
use Zend\Db\TableGateway\TableGateway;
use Zend\ModuleManager\Feature\ConfigProviderInterface;

/**
 * 商品モジュールクラス。
 * ModuleManager はProduct\Moduleクラスを探し、自動的に getConfig() を呼び出す。
 */
class Module implements ConfigProviderInterface
{
    /**
     * ModuleManagerが、自動的に getConfig() を呼び出す。
     */
    public function getConfig()
    {
        return include __DIR__ . '/../config/module.config.php';
    }

    /**
     * ServiceManager に渡す前に ModuleManager によってすべてマージされたfactoriesの配列を返す。
     *
     * @return array ModuleManager によってすべてマージされたfactoriesの配列
     */
    public function getServiceConfig()
    {
        return [
            'factories' => [
                // ServiceManager を使用して Product\Model\ProductTableGateway サービスを作成し、そのコンストラクタに渡す。
                Model\ProductTable::class => function ($container) {
                    $tableGateway = $container->get(Model\ProductTableGateway::class);
                    return new Model\ProductTable($tableGateway);
                },
                // ProductTableGateway サービスが Zend\Db\Adapter\AdapterInterface の実装(これも ServiceManager から)を取得し、
                // それを使用して TableGateway オブジェクトを作成することによって作成されることを ServiceManager に伝える。
                // TableGateway クラスは、結果セットとエンティティの作成にプロトタイプ・パターンを使用する。
                // (必要なときにインスタンスを作成するのではなく、 以前にインスタンス化されたオブジェクトをクローンする)
                Model\ProductTableGateway::class => function ($container) {
                    $dbAdapter = $container->get(AdapterInterface::class);
                    $resultSetPrototype = new ResultSet();
                    $resultSetPrototype->setArrayObjectPrototype(new Model\Product());
                    return new TableGateway('products', $dbAdapter, null, $resultSetPrototype);
                },
            ],
        ];
    }

    /**
     * Product コントローラのファクトリ
     * (Productコントローラが ProductTable に依存するようになったため必要)
     *
     * @return array
     */
    public function getControllerConfig()
    {
        return [
            'factories' => [
                Controller\ProductController::class => function ($container) {
                    return new Controller\ProductController(
                        $container->get(Model\ProductTable::class)
                    );
                },
            ],
        ];
    }
}

常に同じ ProductTableインスタンスを使用するために、ServiceManager を使用して、インスタンスの作成方法を定義します。Moduleクラスで getServiceConfig() というメソッドを作成し、これを ModuleManager が自動的に呼び出して ServiceManager に適用します。

編集:config/autoload/global.php

<?php

//

return [
    // PDO を使って MySQL データベースに接続
    'db' => [
        'driver' => 'Pdo_Mysql',
        'dsn' => 'mysql:dbname=self_order;host=db;charset=utf8',
        'username' => 'db_user',
        'password' => 'secret',
    ],
];

global.php に、データベースの設定情報を追加します(PDO を使って MySQL データベースに接続)。

モデルをコントローラにインジェクトする

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

<?php

namespace Product\Controller;

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(),
        ]);
    }
}

コントローラのファクトリの作成

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

<?php

namespace Product;

use Zend\Db\Adapter\AdapterInterface;
use Zend\Db\ResultSet\ResultSet;
use Zend\Db\TableGateway\TableGateway;
use Zend\ModuleManager\Feature\ConfigProviderInterface;

/**
 * 商品モジュールクラス。
 * ModuleManager はProduct\Moduleクラスを探し、自動的に getConfig() を呼び出す。
 */
class Module implements ConfigProviderInterface
{
    /**
     * ModuleManagerが、自動的に getConfig() を呼び出す。
     */
    public function getConfig()
    {
        return include __DIR__ . '/../config/module.config.php';
    }

    /**
     * ServiceManager に渡す前に ModuleManager によってすべてマージされたfactoriesの配列を返す。
     *
     * @return array ModuleManager によってすべてマージされたfactoriesの配列
     */
    public function getServiceConfig()
    {
        return [
            'factories' => [
                // ServiceManager を使用して Product\Model\ProductTableGateway サービスを作成し、そのコンストラクタに渡す。
                Model\ProductTable::class => function ($container) {
                    $tableGateway = $container->get(Model\ProductTableGateway::class);
                    return new Model\ProductTable($tableGateway);
                },
                // ProductTableGateway サービスが Zend\Db\Adapter\AdapterInterface の実装(これも ServiceManager から)を取得し、
                // それを使用して TableGateway オブジェクトを作成することによって作成されることを ServiceManager に伝える。
                // TableGateway クラスは、結果セットとエンティティの作成にプロトタイプ・パターンを使用する。
                // (必要なときにインスタンスを作成するのではなく、 以前にインスタンス化されたオブジェクトをクローンする)
                Model\ProductTableGateway::class => function ($container) {
                    $dbAdapter = $container->get(AdapterInterface::class);
                    $resultSetPrototype = new ResultSet();
                    $resultSetPrototype->setArrayObjectPrototype(new Model\Product());
                    return new TableGateway('products', $dbAdapter, null, $resultSetPrototype);
                },
            ],
        ];
    }

    /**
     * Product コントローラのファクトリ
     * (Productコントローラが ProductTable に依存するようになったため必要)
     *
     * @return array
     */
    public function getControllerConfig()
    {
        return [
            'factories' => [
                Controller\ProductController::class => function ($container) {
                    return new Controller\ProductController(
                        $container->get(Model\ProductTable::class)
                    );
                },
            ],
        ];
    }
}

編集:module/Product/config/module.config.php

<?php

namespace Product;

use Zend\Router\Http\Segment; // URLパターン(ルート)にプレースホルダーを指定し、マッチしたルートの名前付きパラメーターにマッピングさせる

return [
    'router' => [
        'routes' => [
            'product' => [
                'type'    => Segment::class,
                'options' => [
                    'route' => '/product[/:action[/:id]]', // ルート。/product で始まるすべての URL にマッチ
                    'constraints' => [ // 制約
                        'action' => '[a-zA-Z][a-zA-Z0-9_-]*', // 文字で始まり、その後の文字は英数字、アンダースコア、ハイフンのみに制限
                        'id'     => '[0-9]+', // 数字に限定
                    ],
                    'defaults' => [
                        'controller' => Controller\ProductController::class,
                        'action'     => 'index',
                    ],
                ],
            ],
        ],
    ],

    // TemplatePathStack の設定にビューディレクトリを追加。これにより、view/ ディレクトリに保存されている Product モジュールのビュースクリプトを見つける。
    'view_manager' => [
        'template_path_stack' => [
            'product' => __DIR__ . '/../view',
        ],
    ],
];

コントローラが ProductTable に依存するようになったので、コントローラのファクトリを作成します。

4. ビュー

ビュースクリプトファイルの作成

商品一覧についての記述をします。

作成:module/Product/view/product/product/index.phtml

<?php

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

<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()ビューヘルパーを常に使用 -->

ちなみに、「.phtml」はPHPのバージョン3より前に使用されていた拡張子とのことです…

5. トップページに一覧画面が表示されるよう変更

これまで Zend Framework のWelcome画面を表示していたトップページについて、商品データの一覧画面が表示されるよう変更します。

編集:module/Application/config/module.config.php

<?php

// 略

namespace Application;

+ use Product\Controller\ProductController;
use Zend\Router\Http\Literal;
use Zend\Router\Http\Segment;
use Zend\ServiceManager\Factory\InvokableFactory;

return [
    'router' => [
        'routes' => [
            'home' => [
                'type'    => Literal::class,
                'options' => [
                    'route'    => '/',
                    'defaults' => [
-                        'controller' => Controller\IndexController::class,
+                        'controller' => ProductController::class,
                        'action'     => 'index',
                    ],
                ],
            ],
// 略
];

最後に画面を確認

Dockerコンテナを起動した状態で、下記URLにアクセスします。

http://localhost/

f:id:ryamate:20220325230532p:plain

次回、「諸事情で Zend Framework を理解する 2022 - ④登録機能の作成」投稿予定です。