Magento1.x エクステンション入門(3)


このエントリは[Magento Advent Calendar](http://www.adventar.org/calendars/931)の21日目です。

＊諸般の事情により、本来より2日遅れでの公開となりました。
予定より大幅に遅れてしまったことを深くお詫び申し上げます。


# 概要
[前回の記事](http://qiita.com/iskou9821/items/693b36e5fb687db89594)で、独自に作成したエクステンションを使い、Magento内に新しい画面を作成しました。

今回はそれに引き続き、Magento内に独自のデータを定義して、読み出し・保存する方法について書きたいと思います。

前提として、前回の記事で作成したプラグインの内容を引き継ぎ、「Thridext」とリネームしたものが手元にあるものとしています。

# Magentoに独自のデータを持たせる方法
Magentoで独自のデータをデータベースに保持する方法は、大きく分けて2つあります。

 1. 既存データの定義を拡張する
 2. 新しいテーブルを作成する

今回の記事で詳しくご紹介するのは後者の「新しいテーブルを作成する」になりますが、前者の方法に関しても、先に軽く触れておきたいと思います。

## 既存データの定義拡張
既存データに関しては、項目拡張を前提に設計されているものと、そうでないものがあります。

具体的には、Magentoの独自フレームワークである「EAV(Entity Attribute Value)」を利用して作られているデータに関しては、比較的簡単に拡張を行うことが可能です。

EAVを利用して作られたデータは、「属性の定義情報」と「属性の値」がそれぞれ別テーブルに保存されます。
例えば属性の定義はEAV_ATTRIBUTEというテーブルに、属性の値はEAV_ENTITY_INTやEAV_ENTITY_TEXTといったテーブルに入るといった形です。

EAVを利用して作られているデータとしては

 * 顧客情報
 * 商品カテゴリー情報
 * 商品情報
 * 注文情報

などが挙げられます。

とりわけ、商品情報に関しては管理画面より項目拡張が可能な仕組みとなっているため、そちらを利用するのが一番簡単です。

管理画面上で、「カタログ」→「属性」→「属性管理」がそれになります。
![スクリーンショット 2015-12-23 22.17.23.png](https://qiita-image-store.s3.amazonaws.com/0/50449/658b8c02-ec27-8385-4c31-1c57de972074.png "スクリーンショット 2015-12-23 22.17.23.png")


商品カテゴリや顧客情報の項目を拡張する場合、新規項目の定義データをデータベースに入れるためのコーディングを行うこととなります。

＊それらを管理画面から実行できるようにするエクステンションも存在します。

注文情報に関してはEAVを利用しているものの、データベース上でのデータの持ち方が少々異なっているため(テーブル名もSALES_FLAT_ORDERとなっている)、商品カテゴリーや顧客情報と同じ方法ではうまく拡張を行うことができません。

この部分に関して、今回は詳しく触れませんが、いずれ機会があれば整理を行いたいと思います。

## 新しいテーブルを作成する
本記事で本題となるのがこちらの方法です。

既存の項目の拡張でなく、完全に新規のテーブルを作成し、MagentoのAPIを通してそれらの検索や保存を行える状態とします。

## 大まかな手順

＊管理画面より「システム」→「キャッシュ管理」を開き、すべてのキャッシュを無効にしてから作業を開始してください。

少し長いですので、2つに分けて説明を行います。

### テーブルを作成する

 * etc/config.xmlに「Model」の定義を追加する。
 * etc/config.xmlに「ResourceModel」の定義を追加する。
 * etc/config.xmlに「setup」の定義を追加する。
 * Modelディレクトリに「Mage_Core_Model_Abstract」を継承したクラスを作成する。
 * Model/Resourceディレクトリに「Mage_Core_Model_Resource_Db_Abstract」を継承したクラスを作成する。
 * Model/Resource/[モデル名] ディレクトリに「Mage_Core_Model_Resource_Db_Collection_Abstract」を継承したクラスを作成する。
 * Magentoの画面をリロードし、テーブルが作成されたことを確認する。

### 作成したテーブルを利用する

 * 独自に作成した画面に、読み出したデータを表示するためのコードを書く。
 * 独自に作成した画面に、データを保存するためのコードを書く。
 * 動作確認をする。

# テーブルを作成する

## 作成・編集するファイル
今回作成または編集するファイルは以下のとおりです。
なお今回作成するモデルには「ExtSampleModel」という名前が、エクステンションには「Advent2015_Thridext」という名前が付いているものとします。

 * app/code/community/Advent2015/Thirdext/etc/config.xml
 * app/code/community/Advent2015/Thirdext/Model/ExtSampleModel.php
 * app/code/community/Advent2015/Thirdext/Model/Resource/ExtSampleModel.php
 * app/code/community/Advent2015/Thirdext/Model/Resource/ExtSampleModel/Collection.php
 * app/code/community/Advent2015/Thirdext/sql/thirdext_setup/mysql4-install-0.0.1.php

データベースに対して読み込み・書き込みを行うコードを実現するのに必要なファイルは以上となります。

＊今回もサンプルコードは[GitHubに上がっています](https://github.com/iskou9821/MagentoAdventCalendar2015)ので、そちらも参考にして頂ければ幸いです。

## ファイル内容

### etc/config.xml
```xml:app/code/community/Advent2015/Thridext/etc/config.xml
<?xml version="1.0"?>
<config>
    <modules>
        <Advent2015_Thirdext>
            <version>0.0.1</version>
        </Advent2015_Thirdext>
    </modules>
    
    <global>
        <!-- ブロック(Viewに対応するクラス)定義 -->
        <blocks>
            <Advent2015_Thirdext>
                <class>Advent2015_Thirdext_Block</class>
            </Advent2015_Thirdext>
        </blocks>
        
        <!-- Helperの定義 -->
        <helpers>
            <Advent2015_Thirdext>
                <class>Advent2015_Thirdext_Helper</class>
            </Advent2015_Thirdext>
        </helpers>
        
        <!--  モデルの定義 -->
        <models>
            <!--  モデルのクラス名のプレフィクスなどに関する定義 -->
            <Advent2015_Thirdext>
                <class>Advent2015_Thirdext_Model</class>
                <resourceModel>Advent2015_Thirdext_resource</resourceModel>
            </Advent2015_Thirdext>

            <!--  データベースに関わる定義 -->
            <Advent2015_Thirdext_resource>
                <class>Advent2015_Thirdext_Model_Resource</class>
                <deprecatedNode>thirdext_mysql4</deprecatedNode>
                <entities>
                    <extSampleModel>
                        <table>advent2015_ext_sample</table>
                    </extSampleModel>
                </entities>
            </Advent2015_Thirdext_resource>
        </models>
        
        <!--セットアップ(テーブル作成のSQL実行など)に関わる定義 -->
        <resources>
            <thirdext_setup>
                <setup>
                    <module>Advent2015_Thirdext</module>
                    <class>Mage_Eav_Model_Entity_Setup</class>
                </setup>
                <connection>
                    <use>core_setup</use>
                </connection>
            </thirdext_setup>
        </resources>
    </global>
    
    <!-- ルーティングに関わる定義 -->
    <frontend>
        <layout>
            <updates>
                <thirdext>
                    <file>third_ext.xml</file>
                </thirdext>
            </updates>
        </layout>
        <routers>
            <thirdext>
                <use>standard</use>
                <args>
                    <module>Advent2015_Thirdext</module>
                    <frontName>ext-db-test</frontName>
                </args>
            </thirdext>
        </routers>
    </frontend>
</config>
```

config.xmlで重要となるのは、コメントで「モデルの定義」と書かれている部分です。

こちらに、このエクステンションで定義されるモデルクラスの命名規則や、データ保存先となるテーブル名などが入ることとなります。

### Model/ExtSampleModel.php

```php:app/code/community/Advent2015/Thridext/Model/ExtSampleModel.php
<?php
class Advent2015_Thirdext_Model_ExtSampleModel extends Mage_Core_Model_Abstract {
    protected function _construct() {
        $this->_init('Advent2015_Thirdext/extSampleModel');
    }
}
```

こちらは内容がほぼ固定となります。
ただし、ファイル名、クラス名、$this->init() に書かれている識別子の同期が取れていないとうまく動作しませんので、typoなどに注意が必要です。

 * config.xmlのglobal > models > Advent2015_Thirdext > class に書かれている内容が「Advent2015_Thirdext_Model」であるため、クラス名は「Advent2015_Thirdext_Model」で始めます。
 * config.xmlのglobal > models > Advent2015_Thirdext_resource > entities以下に書かれている「extSampleModel」に関する定義であるため、「ExtSampleModel」というクラス名をプレフィクスの後に付けます。
 * 「ExtSampleModel」というサフィックスをクラス名に付けたので、ファイル名も「ExtSampleModel」とします。
 * 同様の理由から、$this->_initの中にも「Advent2015_Thirdext/extSampleModel」(モジュール名/モデル名)とします。

### Model/Resource/ExtSampleModel.php

```php:app/code/community/Advent2015/Thridext/Model/Resource/ExtSampleModel.php
<?php
class Advent2015_Thirdext_Model_Resource_ExtSampleModel extends Mage_Core_Model_Resource_Db_Abstract {
    protected function _construct() {
        $this->_init('Advent2015_Thirdext/extSampleModel', 'model_id');
    }
}
```

こちらはモデルとデータベースとの関連付け(保存・読み出し)に関わるクラスとなります。
「model_id」と書かれている部分は、このオブジェクトと関連付けられたテーブルのIDとなるカラム名です。
こちらは任意の値を設定できますが、後で説明するテーブル作成用のスクリプトの内容と合っていなければなりません。

なお、Magentoのモデルオブジェクト自体には、プロパティ定義の情報を持ちません。
それらは、テーブルを作成する際のSQLスクリプトで指定するのみです。

### Model/Resource/ExtSampleModel/Collection.php

```php:app/code/community/Advent2015/Thridext/Model/Resource/ExtSampleModel/Collection.php
<?php
class Advent2015_Thirdext_Model_Resource_ExtSampleModel_Collection extends Mage_Core_Model_Resource_Db_Collection_Abstract {
    protected function _construct() {
        $this->_init('Advent2015_Thirdext/extSampleModel');
    }
}
```

こちらはデータベースからの読み出しを行う際、クエリーを柔軟に組み立てるのに使われるクラスです。

内容はほぼ固定ですが前述のクラスと同様、typoには注意が必要です。

### sql/thirdext_setup/mysql4-install-0.0.1.php

```php:app/code/community/Advent2015/Thirdext/sql/thirdext_setup/mysql4-install-0.0.1.php
<?php
$this->startSetup();

//テーブル名を取得
$tableName = $this->getTable('Advent2015_Thirdext/extSampleModel');

//データベースとのコネクションを取得
$conn = $this->getConnection();

//テーブルが存在しなければ作成
if ($conn->isTableExists($tableName) != true) {
    //新規テーブルの定義を作成
    $table = $conn
        ->newTable($tableName)
        ->addColumn('model_id', Varien_Db_Ddl_Table::TYPE_INTEGER, null, array(
            'identity'  => true,
            'unsigned'  => true,
            'nullable'  => false,
            'primary'   => true,
        ), 'Model Id')
        ->addColumn('ext_text', Varien_Db_Ddl_Table::TYPE_VARCHAR, 256, array(
            'nullable'  => true,
        ), 'text data')
        ->addColumn('ext_num', Varien_Db_Ddl_Table::TYPE_INTEGER, null, array(
            'nullable'  => true,
        ), 'num data');
        
    //テーブル作成を実行
    $conn->createTable($table);
}

$this->endSetup();
```

こちらが、データ保存先となるテーブルを作成するための、スクリプトを生成するクラスとなります。

基本的にはこのような形で、MagentoのAPIを利用してテーブルを作成していくこととなります。

これは、本記事の初めで述べた「EAV」を利用する場合も同様となります。

## テーブル作成を実行する
ここまで作成が完了したら、一旦、サーバへのアップロードを行い、WebブラウザからMagentoのサイトにアクセスします。

すると、新しいプラグイン及び実行すべきスクリプトの検出が行われ、テーブルの作成が行われます。

うまくいけば、「core_resource」テーブルへのデータ追加と、「advent2015_ext_sample」テーブルの作成が行われるはずです。

core_resourceテーブル
![スクリーンショット 2015-12-23 21.29.37.png](https://qiita-image-store.s3.amazonaws.com/0/50449/1f89e558-aa76-b266-44d4-60d3f4b0c241.png "スクリーンショット 2015-12-23 21.29.37.png")
↑「thirdext_setup」と書かれた行が追加される。


advent2015_ext_sampleテーブル
![スクリーンショット 2015-12-23 21.29.19.png](https://qiita-image-store.s3.amazonaws.com/0/50449/9b28f59e-76ae-a69b-716e-46c631f3ea64.png "スクリーンショット 2015-12-23 21.29.19.png")
↑新規にテーブルが作成される。

core_resourceテーブルにデータが追加されているにも関わらずテーブルが作成されない場合、mysql4-install-0.0.1.phpの実行に何らかの問題があった、もしくは、ファイル名間違いなどの理由でスクリプトの実行自体が行われていない可能性があります。

そのような場合は、core_resourceテーブルから「thirdext_setup」に関する行を削除し、再度Webブラウザからアクセスを行ってください。

＊Magentoはcore_resourceテーブルの内容を元に実行すべきスクリプトの判定を行っているため、データが残ったままだと、mysql4-install-0.0.1.phpを更新しても、スクリプトは実行されません。

# 作成したテーブルを利用する
それでは、作成したモデルクラスを実際に画面で利用してみましょう。

前回作成したような独自のテンプレートファイルを持った画面に、データベースに保存された情報の一覧と、新規にデータを追加する入力欄を作成してみます。

## 作成・編集するファイル
画面に関連するファイルは以下のとおりです。

 * app/code/community/Advent2015/Thirdext/Block/IndexForm.php
 * app/code/community/Advent2015/Thirdext/controllers/IndexController.php
 * app/code/design/frontend/base/default/layout/third_ext.xml
 * app/code/design/frontend/base/default/template/thirdext/index.phtml

## ファイル内容

### Block/IndexForm.php

```php:app/code/community/Advent2015/Thirdext/Block/IndexForm.php
<?php
class Advent2015_Thirdext_Block_IndexForm extends Mage_Core_Block_Template {
    public function getSampleModels() {
        return Mage::getResourceModel('Advent2015_Thirdext/extSampleModel_collection')
                        ->addOrder('model_id', 'DESC');
    }
    
    public function getActionUrl() {
        return Mage::getBaseUrl() . 'ext-db-test/index/add';
    }
}

```
こちらはViewに対応するクラスですが、データベースからモデルの読み出しを行うためのメソッドと、新規追加データのPOST先となるURLの生成に関わるメソッドを置いています。

ここでは触れていませんが、Mage::getResourceModelには、この他にも取得データをフィルタする機能や、ページングのために取得件数を絞ったり、開始位置を指定するなどの機能も持っています。

### controllers/IndexController.php

```php:app/code/community/Advent2015/Thirdext/controllers/IndexController.php
<?php
class Advent2015_Thirdext_IndexController extends Mage_Core_Controller_Front_Action {
    
    public function indexAction() {
        $this->loadLayout();
        $this->renderLayout();
    }
    
    public function addAction() {
        //POSTされたパラメータから配列を作成
        $data = array(
            'ext_text' => $this->getRequest()->getPost('ext_text'),
            'ext_num' => $this->getRequest()->getPost('ext_num')
        );
        
        //保存先となるモデルオブジェクトを生成し、データをセット
        $model = Mage::getModel('Advent2015_Thirdext/extSampleModel');
        $model->setData($data);
        
        //データベースに保存
        $model->save();
        
        //元のページにリダイレクト
        return $this->_redirect('ext-db-test');
    }
}
```
こちらはMagentoにリクエストが渡ってきた時の処理の定義となります。
indexActionは画面描画の際に利用される処理、addActioはデータの追加の際に利用される処理となります。

indexActionは、config.xmlで定義された「/ext-db-test」というURLにアクセスした際に呼び出される処理、addActioは「ext-db-test/index/add」にアクセスした際に呼び出される処理となります。

＊メソッド名がそのままURLと関連を持ちます。

### layout/third_ext.xml

```xml:app/code/design/frontend/base/default/layout/third_ext.xml
<?xml version="1.0"?>
<layout version="0.0.1">
    <thirdext_index_index translate="label">
        <label>Test Form</label>

        <!-- HTMLの<head>部分に対する定義 -->
        <reference name="head">
            <action method="setTitle" translate="title" module="Advent2015_Thirdext"><title>Test Form</title></action>
        </reference>

        <!-- 全体の画面レイアウトに関する定義 -->
        <reference name="root">
            <action method="setTemplate"><template>page/1column.phtml</template></action>
            <action method="setHeaderTitle" translate="title" module="Advent2015_Thirdext"><title>Test Form</title></action>
        </reference>

        <!-- 表示するコンテンツに関する定義 -->
        <reference name="content">
            <block type="Advent2015_Thirdext/IndexForm" template="thirdext/index.phtml"/>
        </reference>
    </thirdext_index_index>
</layout>
```
画面のレイアウトに関する定義となります。
今回、アクションの定義してあるURLは「/ext-db-test」と「ext-db-test/index/add」の二つになりますが、画面描画が必要となるのは「/ext-db-test」のみとなりますので、そちらのみ定義をしています。

### thirdext/index.phtml

```php:app/code/design/frontend/base/default/template/thirdext/index.phtml
<?php
    $models = $this->getSampleModels();
?>

<style type="text/css">
    table.list, table.list tr, table.list tr th, table.list tr td {
        border-style:solid
    }
    
    table.list tr th {
        background-color: #3399cc;
        text-align: center;
        min-width: 120px
    }
    
    table.list tr td {
        padding-left :10px;
        padding-right :10px;
    }
</style>

<div>サンプルデータ一覧</div>
<table class="list">
    <tr>
        <th>id</th>
        <th>text</th>
        <th>num</th>
    </tr>
<?php foreach($models as $model): ?>
    <tr>
        <td><?php echo $model->getId(); ?></td>
        <td><?php echo $model->getExtText(); ?></td>
        <td><?php echo $model->getExtNum(); ?></td>
    </tr>
<?php endforeach; ?>
</table>

<div style="padding-top:30px;">データ新規追加</div>

<form method="post" action='<?php echo $this->getActionUrl(); ?>'>
    <table>
        <tr>
            <th>テキスト</th>
            <td><input type="text" name="ext_text"></td>
        </tr>
        <tr>
            <th>数値</th>
            <td><input type="nunber" name="ext_num"></td>
        </tr>
        <tr>
            <td colspan="2" style="text-align:center">
                <button type="submit">データを送信</button>
            </td>
        </tr>
    </table>
</form>
```

今回の場合、このテンプレートでの$thisは「Advent2015_Thirdext_Block_IndexForm」となります。

また、モデルクラスには$model->getExtText(), $model->getExtNum()といったメソッドを明示的に定義してはいませんが、Magentoのモデルクラスでは「get」「set」で始まるメソッドは(特に定義を行っていない限り)暗黙的にあるものとして処理されます。

この場合、内部的に持っている「_data」という名前の連想配列に対するアクセスとなります。

例えば、以下のような形です。

$model->_data → 内部的に持っている連想配列本体を取得。

$model->getData() → _dataに対するgetter。

$model->setData() → _dataに対するsetter。

$model->getExtText() → 前述の_dataから、「extText」というキーに対応する値を取得。

$model->setExtText('aaa') →前述の_dataに、「extText」というキーを持った値を設定。

これらをサーバにアップロードし、「/ext-db-text」というURLにアクセスすると、以下のような画面となるはずです。
![スクリーンショット 2015-12-23 22.19.07.png](https://qiita-image-store.s3.amazonaws.com/0/50449/2e7f1311-b2bc-b032-256c-ec569e46d8b7.png "スクリーンショット 2015-12-23 22.19.07.png")

「データ送信」をクリックすると、「テキスト」「数値」に入力した値がデータベースに保存され、「サンプルデータ一覧」に表示されます。


# まとめ
ここまでで、Magentoに新しいデータモデルの作成、および、それらを画面に表示する方法について説明しました。

[サンプルコード](https://github.com/iskou9821/MagentoAdventCalendar2015)についても、参照を頂ければと思います。

12/22は、HirokazuNishさんの[Magento2を用いた開発で求められる技術要素](http://www.adventar.org/calendars/931#list-2015-12-22)となります。
どうぞよろしくお願いします。