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

マジレス大歓迎です。

諸事情で Zend Framework を理解する 2022 - ⑤編集・削除機能の作成

f:id:ryamate:20220415070557p:plain

INDEX

こんにちは!

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

独学エンジニアで学習された方が、スマレジにいつか入社されるといいなー。

dokugaku-engineer.com

corp.smaregi.jp


さて、前回の記事(④登録機能の作成)で、商品データを Products テーブルに登録する、登録機能を作成しました。

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

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

① Docker での開発環境構築

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

② Zend Framework のインストール

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

③ 一覧画面の作成

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

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

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

⑤ 編集・削除機能の作成(今回)

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

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

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

⑤編集・削除機能の作成

今回は、登録済みの商品データを編集・削除する機能を作成します。

これで、読込、登録、編集、削除のCRUD機能が揃います。

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

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

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

github.com

以下は変更差分です。

github.com

商品管理ページの構成

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

実装内容

1. 編集機能の作成

商品データを編集する機能を作成します。

コントローラの編集( editAction() メソッドの追記)

フォームからデータを取得し、対象の商品データの行を編集し、商品一覧にリダイレクトするメソッドを追記します。

編集: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
{
        // 略

    /**
     * 商品を編集する。
     * フォームからデータを取得し、対象の商品の行を編集し、商品一覧にリダイレクトする。
     *
     * @return Response|array
     * @throws Exception\DomainException
     */
    public function editAction()
    {
        // マッチしたルートからパラメータを取得(module/Product/config/module.config.php 内に作成したルートから id を取得)する。
        $id = (int) $this->params()->fromRoute('id', 0);

        // id が 0 なら、編集フォームにリダイレクトする。
        if (0 === $id) {
            return $this->redirect()->toRoute('product', ['action' => 'add']);
        }

        // 指定されたidの商品を取得する。商品が見つからない場合は例外(リダイレクトされる)。
        try {
            $product = $this->table->getProduct($id);
        } catch (\Exception $e) {
            return $this->redirect()->toRoute('product', ['action' => 'index']);
        }

        $form = new ProductForm();
        $form->bind($product); // bind()メソッドは、モデルをフォームにアタッチする。
        $form->get('submit')->setAttribute('value', '編集');

        $request = $this->getRequest();
        $viewData = ['id' => $id, 'form' => $form];

        if (!$request->isPost()) {
            return $viewData;
        }

        $form->setInputFilter($product->getInputFilter());
        $form->setData($request->getPost());

        // バリデーション失敗の場合、フォームを再表示する(バリデーション失敗の内容を、ビューレイヤーに伝える)。
        if (!$form->isValid()) {
            return $viewData;
        }

        // フォームからデータを取得し、対象の商品の行を編集する。
        $this->table->editProduct($product);

        // 商品一覧にリダイレクトする。
        return $this->redirect()->toRoute('product', ['action' => 'index']);
    }
}

モデルの編集①(モデルをフォームにアタッチ)

Product ファイルを編集します。

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

<?php

// 略

/**
 * 商品モデルクラス。
 * zend-db の TableGateway クラスで動作させるために、 exchangeArray() メソッドを実装する。
 */
class Product implements InputFilterAwareInterface
{
        // 略

    /**
     * エンティティのプロパティを配列としてコピーして取得する。
     *
     * モデルをフォームにアタッチし、以下2つに使われる。
     *  - フォームを表示するときの各要素の初期値をモデルから抽出、
     *  - isValid()でバリデーションに成功すると、フォームのデータをモデルに戻す
     *
     * @return array エンティティのプロパティを配列にコピーしたもの
     */
    public function getArrayCopy()
    {
        return [
            'id' => $this->id,
            'item_name' => $this->itemName,
            'price' => $this->price,
            'image' => $this->image,
        ];
    }

        // 略

}

上記のPHPDocにも記述していますが、モデルをフォームにアタッチして、使用するのは「フォームを表示するときの各要素の初期値をモデルから抽出」「isValid()でバリデーションに成功すると、フォームのデータをモデルに戻す」時です。

モデルの編集②(商品データのテーブルからの取得、編集)

products テーブルからのデータ取得、テーブルへの編集処理をする ProductTable ファイルを編集します。

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

<?php

namespace Product\Model;

use Zend\Db\TableGateway\TableGatewayInterface;

/**
 * 商品のデータベース・テーブルに対する操作を実行するクラス。
 */
class ProductTable
{
        // 略

    /**
     * 一行を Product オブジェクトとして取得する。
     *
     * @param string $id 商品ID
     * @return
     */
    public function getProduct(string $id)
    {
        return $this->tableGateway->select(['id' => $id])->current();
    }

    /**
     * データベースの既に存在する行を更新する。
     *
     * @param Product $product 既に存在する商品のカラム
     * @return void
     */
    public function editProduct(Product $product)
    {
        $data = [
            'item_name' => $product->itemName,
            'price' => $product->price,
            'image' => $product->image,
        ];

        $product = $this->getProduct($product->id);
        if (isset($product)) {
            $this->tableGateway->update($data, ['id' => $product->id]);
        }
    }
}

ビューの編集(編集用のフォーム画面の作成)

商品一覧画面にフォーム画面へ遷移する「編集」ボタンを設置します。「削除」ボタンも併せて設置しておきます。

編集:module/Product/view/product/product/index.phtml

<!-- 略 -->

<table class="table">
    <tr>
        <th>ID</th>
        <th>商品名</th>
        <th>商品単価</th>
        <th>&nbsp;</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>
            <!-- レコードの編集と削除を可能にするためのリンクを提供 -->
            <td>
                <!-- url() ビューヘルパーの使用 -->
                <a href="<?= $this->url('product', ['action' => 'edit', 'id' => $product->id]) ?>">編集</a>
                <a href="<?= $this->url('product', ['action' => 'delete', 'id' => $product->id]) ?>">削除</a>
            </td>
        </tr>
    <?php endforeach; ?>
</table>

続いて、商品データの編集するためのフォーム画面(ビュースクリプト)を作成します。

作成・編集:module/Product/view/product/product/edit.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' => 'edit',
    'id'     => $id,
]));
$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:20220415071508p:plain

「日替わりランチC」を編集してみます。

http://localhost/product/edit/3

f:id:ryamate:20220415071515p:plain

商品名、商品単価を変更して、編集ボタンを押します。

f:id:ryamate:20220415071522p:plain

編集されました!

f:id:ryamate:20220415071529p:plain

2. 削除機能の作成

商品データを削除する機能を作成します。

コントローラの編集( deleteAction() メソッドの追記)

編集: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
{
        // 略

    /**
     * 商品を削除する。
     * ユーザーが削除をクリックしたときに確認フォームを表示し、「はい」をクリックしたら削除を実行する。
     *
     * @return Response|array
     * @throws Exception\DomainException
     */
    public function deleteAction()
    {
        // マッチしたルートからパラメータを取得(module/Product/config/module.config.php 内に作成したルートから id を取得)
        $id = (int) $this->params()->fromRoute('id', 0);
        if (!$id) {
            return $this->redirect()->toRoute('product');
        }

        $request = $this->getRequest();

        // isPost() をチェックして、確認ページを表示するか、アルバムを削除するかを決定する。
        if ($request->isPost()) {
            $del = $request->getPost('del', 'いいえ');

            if ($del === 'はい') {
                $id = (int) $request->getPost('id');
                $this->table->deleteProduct($id);
            }

            // 商品一覧へのリダイレクト
            return $this->redirect()->toRoute('product');
        }

        // リクエストが POST でない場合は、正しいデータベースレコードを取得し、id と共にビューに割り当てる。
        return [
            'id'    => $id,
            'product' => $this->table->getProduct($id),
        ];
    }
}

モデルの編集(商品データのテーブルからの削除)

products テーブルからのデータ取得、テーブルへの編集処理をする ProductTable ファイルを編集します。

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

<?php

namespace Product\Model;

use Zend\Db\TableGateway\TableGatewayInterface;

/**
 * 商品のデータベース・テーブルに対する操作を実行するクラス。
 */
class ProductTable
{
        // 略

   /**
     * 渡された $id の行を完全に削除する。
     *
     * @param string $id 商品ID
     * @return void
     */
    public function deleteProduct(string $id)
    {
        $this->tableGateway->delete(['id' => (int) $id]);
    }
}

ビューの編集(削除用のフォーム画面の作成)

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

<?php
$title = '商品の削除';
$url   = $this->url('product', ['action' => 'delete', 'id' => $id]);

$this->headTitle($title);
?>
<h1><?= $this->escapeHtml($title) ?></h1>

<p>
    「商品名:<?= $this->escapeHtml($product->itemName) ?>」を削除してよろしいですか?
</p>

<form action="<?= $url ?>" method="post">
    <div class="form-group">
        <input type="hidden" name="id" value="<?= (int) $product->id ?>" />
        <input type="submit" class="btn btn-danger" name="del" value="はい" />
        <input type="submit" class="btn btn-success" name="del" value="いいえ" />
    </div>
</form>

機能の確認(削除してみる)

http://localhost/

f:id:ryamate:20220415071529p:plain

「日替わりランチB」を削除してみます。

http://localhost/product/delete/2

f:id:ryamate:20220415071637p:plain

「はい」を押します。

※ ちなみに「いいえ」を押したら、商品一覧にリダイレクトしました。

f:id:ryamate:20220415071643p:plain

「日替わりランチB」が削除されました!


スペシャルランチ2000円がどんなランチか想像してお腹が空いてきたところで、今回は完了です!

次回、「諸事情で Zend Framework を理解する 2022 - ⑥バリデーションメッセージの日本語化」投稿予定です。