Magentoのエクステンションを作ってみよう〜中級編2〜

前回はデータベース上に作成した独自のテーブルに登録されているデータを読み出して表示する処理を作成しました。今回はその続きとして、管理画面側でデータの登録や編集、削除ができるようにしてみましょう。

管理画面を実装する場合に必要になること

最初に、管理画面を実装する場合に必要になることを整理しておきましょう。管理画面を作ろうとすると、最低でも以下の項目の実装が必要になります。

  • メニューの作成
  • 管理者権限の定義
  • 一覧画面と一括操作の作成
  • 新規作成画面と編集画面の作成
  • 保存と削除機能の作成

Magentoの管理画面はかなりの割合がフレームワークで用意されたコンポーネントを利用すれば作成できるようになっています(もちろん自前で凝ったコンポーネントを実装することもできます)。ただし、管理画面のほうがフロントエンドよりも覚えることが多いので、慣れるまでは少し大変です。

メニューの作成と管理者権限の定義

最初に以下の2つの定義を済ませてしまいましょう。

  • メニューの作成
  • 管理者権限の定義

これらはXMLファイルで行います。それぞれ専用のXSD(XMLスキーマ定義)があり、以下のディレクトリに配置することでMagentoフレームワークが自動的に読み込みます。

ファイル名 場所
メニュー menu.xml <エクステンションディレクトリ>/etc/adminhtml/menu.xml
管理者権限 acl.xml <エクステンションディレクトリ>/etc/acl.xml

では、XMLを定義してみましょう。

menu.xmlの定義

前述の場所にmenu.xmlを作成し、以下のように記述します。

<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Backend:etc/menu.xsd">
    <menu>
        <add id="My_News::manager" translate="title" title="Manage News"
             module="My_News" sortOrder="99" resource="My_News::manage" parent="Magento_Backend::content"
             action="mynews/index/index"
        />
    </menu>
</config>
​

今回の例では、CMS機能のメニューに項目を追加します。新しい最上位メニュー項目を追加する場合は、アイコンも用意しなければならないので、今回は省略します。

acl.xmlの定義

続いてacl.xmlを定義しましょう。acl.xmlを定義することで、管理者グループごとに権限を付与できるようになります。acl.xmlは前述の表で示した場所に作成します。ファイルを作成したら、下記のように記述してください。

<?xml version="1.0"?>

<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Acl/etc/acl.xsd">
    <acl>
        <resources>
            <resource id="Magento_Backend::admin">
                <resource id="Magento_Backend::content">
                    <resource id="My_News::manage" title="News list" translate="title" sortOrder="99"/>
                </resource>
            </resource>
        </resources>
    </acl>
</config>

menu.xmlとacl.xmlの関係

この2つのXMLを定義するときは、リソース名に注意してください。menu.xmlのresource属性の値がacl.xmlのresourceタグのidとして使用されます。この2つのファイル間で定義に誤りがあると、権限の設定が期待通りに行われない可能性があります。

定義の反映と動作チェック

ここまで定義ができたら、一旦作成したXMLをサーバーに反映します。反映が済んだらMagentoのキャッシュをクリアして、メニューが定義したとおりに管理画面で表示されているかを確認しましょう。次図のように表示されていればOKです。

一覧画面の作成

次は一覧画面と、関連する諸機能を作成していきましょう。一覧画面には以下のような機能があり、一覧表示以外の処理は別途実装しなければなりません。

  • 対象データの一覧表示
  • (定義してある場合)選択したデータの一括処理
  • 編集画面への導線

今回は前回定義したデータベースのテーブルから読み出したデータを一覧表示させます。

一覧画面の定義

一覧画面を作る場合、いくつか方法があるのですが、ここではUI Component定義を使って作成する方法をご紹介します。UI ComponentはMagento独自の仕組みで、特に管理画面を定義する際に多用します。Magento1系で管理画面の実装をしたことがある方はその発展形だと思っていただければ多少わかりやすいと思います。

UI Componentを定義する場合、以下の手順で実装を行います。

  1. <エクステンションディレクトリ>/view/adminhtml/ui_component/コンポーネント名.xml に定義を作成
  2. <エクステンションディレクトリ>/etc/di.xmlを作成し、定義を作成
  3. di.xmlで定義したクラスやインターフェイスを作成
  4. <エクステンションディレクトリ>/view/adminhtml/layout/画面名.xml に実際に使用する画面レイアウト定義を作成
  5. <エクステンションディレクトリ>/etc/adminhtml/routes.xml を作成し、フロントエンドと同様にControllerが認識されるようにする
  6. <エクステンションディレクトリ>/Controller/Adminhtml/機能名/アクション名.php に処理を担当するControllerクラスを作成

単純に1つのテーブルを見るだけの場合は上記の手順で構いません。複数のテーブルをJOINするような場合はもう少し工夫が必要ですが、今回は対象外にします。

UI Component用XMLを定義しよう

それではUI Component用のXMLを定義してみましょう。前述の手順の1番目の場所に「my_news_grid.xml」を作成し、以下のように記述してください。

<?xml version="1.0" encoding="UTF-8"?>

<listing xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Ui/etc/ui_configuration.xsd">
    <argument name="data" xsi:type="array">
        <item name="js_config" xsi:type="array">
            <item name="provider" xsi:type="string">my_news_grid.my_news_grid_data_source</item>
            <item name="deps" xsi:type="string">my_news_grid.my_news_grid_data_source</item>
        </item>
        <item name="spinner" xsi:type="string">my_news_grid_columns</item>
        <item name="buttons" xsi:type="array">
            <item name="add" xsi:type="array">
                <item name="name" xsi:type="string">add</item>
                <item name="label" xsi:type="string" translate="true">Add New</item>
                <item name="class" xsi:type="string">primary</item>
                <item name="url" xsi:type="string">*/*/newaction</item>
            </item>
        </item>
    </argument>
    <dataSource name="my_news_grid_data_source">
        <argument name="dataProvider" xsi:type="configurableObject">
            <argument name="class" xsi:type="string">Magento\Framework\View\Element\UiComponent\DataProvider\DataProvider</argument>
            <!-- here we pass dataprovider name which i will define in di.xml file of module in next step -->
            <argument name="name" xsi:type="string">my_news_grid_data_source</argument>
            <argument name="primaryFieldName" xsi:type="string">news_id</argument>
            <argument name="requestFieldName" xsi:type="string">news_id</argument>
            <argument name="data" xsi:type="array">
                <item name="config" xsi:type="array">
                    <item name="update_url" xsi:type="url" path="mui/index/render"/>
                    <item name="component" xsi:type="string">Magento_Ui/js/grid/provider</item>
                    <item name="storageConfig" xsi:type="array">
                        <item name="indexField" xsi:type="string">news_id</item>
                    </item>
                </item>
            </argument>
        </argument>
        <argument name="data" xsi:type="array">
            <item name="js_config" xsi:type="array">
                <item name="component" xsi:type="string">Magento_Ui/js/grid/provider</item>
            </item>
        </argument>
    </dataSource>
    <container name="listing_top">
        <argument name="data" xsi:type="array">
            <item name="config" xsi:type="array">
                <item name="template" xsi:type="string">ui/grid/toolbar</item>
                <item name="stickyTmpl" xsi:type="string">ui/grid/sticky/toolbar</item>
            </item>
        </argument>
        <component name="columns_controls">
            <argument name="data" xsi:type="array">
                <item name="config" xsi:type="array">
                    <item name="columnsData" xsi:type="array">
                        <item name="provider" xsi:type="string">my_news_grid.my_news_grid.my_news_grid_columns</item>
                    </item>
                    <item name="component" xsi:type="string">Magento_Ui/js/grid/controls/columns</item>
                    <item name="displayArea" xsi:type="string">dataGridActions</item>
                </item>
            </argument>
        </component>
        <filters name="listing_filters">
            <argument name="data" xsi:type="array">
                <item name="config" xsi:type="array">
                    <item name="columnsProvider" xsi:type="string">my_news_grid.my_news_grid.my_news_grid_columns</item>
                    <item name="storageConfig" xsi:type="array">
                        <item name="provider" xsi:type="string">my_news_grid.my_news_grid.listing_top.bookmarks</item>
                        <item name="namespace" xsi:type="string">current.filters</item>
                    </item>
                    <item name="templates" xsi:type="array">
                        <item name="filters" xsi:type="array">
                            <item name="select" xsi:type="array">
                                <item name="component" xsi:type="string">Magento_Ui/js/form/element/ui-select</item>
                                <item name="template" xsi:type="string">ui/grid/filters/elements/ui-select</item>
                            </item>
                        </item>
                    </item>
                    <item name="childDefaults" xsi:type="array">
                        <item name="provider" xsi:type="string">my_news_grid.my_news_grid.listing_top.listing_filters</item>
                        <item name="imports" xsi:type="array">
                            <item name="visible" xsi:type="string">my_news_grid.my_news_grid.my_news_grid_columns.${ $.index }:visible</item>
                        </item>
                    </item>
                </item>
                <item name="observers" xsi:type="array">
                    <item name="column" xsi:type="string">column</item>
                </item>
            </argument>
        </filters>
        <massaction name="listing_massaction">
            <argument name="data" xsi:type="array">
                <item name="config" xsi:type="array">
                    <item name="selectProvider" xsi:type="string">my_news_grid.my_news_grid.my_news_grid_columns.ids</item>
                    <item name="component" xsi:type="string">Magento_Ui/js/grid/tree-massactions</item>
                    <item name="indexField" xsi:type="string">id</item>
                </item>
            </argument>
            <!-- Mass actions which you want to add in your grid-->
            <action name="delete">
                <argument name="data" xsi:type="array">
                    <item name="config" xsi:type="array">
                        <item name="type" xsi:type="string">delete</item>
                        <item name="label" xsi:type="string" translate="true">Delete</item>
                        <item name="url" xsi:type="url" path="mynews/index/massDelete"/>
                        <item name="confirm" xsi:type="array">
                            <item name="title" xsi:type="string" translate="true">Delete</item>
                            <item name="message" xsi:type="string" translate="true">Are you sure you want to delete the selected item(s)?</item>
                        </item>
                    </item>
                </argument>
            </action>
        </massaction>
        <paging name="listing_paging">
            <argument name="data" xsi:type="array">
                <item name="config" xsi:type="array">
                    <item name="storageConfig" xsi:type="array">
                        <item name="provider" xsi:type="string">my_news_grid.my_news_grid.listing_top.bookmarks</item>
                        <item name="namespace" xsi:type="string">current.paging</item>
                    </item>
                    <item name="selectProvider" xsi:type="string">my_news_grid.my_news_grid.my_news_grid_columns.ids</item>
                </item>
            </argument>
        </paging>
    </container>
    <!-- from here we'll add columns of grid list -->
    <columns name="my_news_grid_columns">
        <selectionsColumn name="ids">
            <argument name="data" xsi:type="array">
                <item name="config" xsi:type="array">
                    <item name="indexField" xsi:type="string">news_id</item>
                    <item name="sorting" xsi:type="string">desc</item>
                    <item name="sortOrder" xsi:type="number">0</item>
                </item>
            </argument>
        </selectionsColumn>
        <column name="title">
            <argument name="data" xsi:type="array">
                <item name="config" xsi:type="array">
                    <item name="filter" xsi:type="string">text</item>
                    <item name="label" xsi:type="string" translate="true">Title</item>
                    <item name="sortOrder" xsi:type="number">20</item>
                </item>
            </argument>
        </column>
        <column name="created_at" class="Magento\Ui\Component\Listing\Columns\Date" >
            <argument name="data" xsi:type="array">
                <item name="config" xsi:type="array">
                    <item name="filter" xsi:type="string">dateRange</item>
                    <item name="component" xsi:type="string">Magento_Ui/js/grid/columns/date</item>
                    <item name="dataType" xsi:type="string">date</item>
                    <item name="label" xsi:type="string" translate="true">Created At</item>
                    <item name="sortOrder" xsi:type="number">90</item>
                </item>
            </argument>
        </column>
        <column name="updated_at" class="Magento\Ui\Component\Listing\Columns\Date" >
            <argument name="data" xsi:type="array">
                <item name="config" xsi:type="array">
                    <item name="filter" xsi:type="string">dateRange</item>
                    <item name="component" xsi:type="string">Magento_Ui/js/grid/columns/date</item>
                    <item name="dataType" xsi:type="string">date</item>
                    <item name="label" xsi:type="string" translate="true">Updated At</item>
                    <item name="sortOrder" xsi:type="number">100</item>
                </item>
            </argument>
        </column>
        <actionsColumn name="actions" class="My\News\Ui\Component\Listing\Grid\Column\Action">
            <argument name="data" xsi:type="array">
                <item name="config" xsi:type="array">
                    <item name="resizeEnabled" xsi:type="boolean">false</item>
                    <item name="resizeDefaultWidth" xsi:type="string">110</item>
                    <item name="indexField" xsi:type="string">id</item>
                    <item name="sortOrder" xsi:type="number">110</item>
                </item>
            </argument>
        </actionsColumn>
    </columns>
</listing>

これで一覧を表示するためのコンポーネント定義ができました。

di.xmlでコンポーネントの型定義を宣言する

di.xmlはMagento2系の挙動やカスタマイズ方法を理解する上でとても重要なファイルです。詳しく解説し始めるとそれだけで記事1回分くらい必要になりますので、とりあえず今回は下記の内容を記述しておいてください。

<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">
    <preference for="My\News\Api\NewsRepositoryInterface" type="My\News\Model\NewsRepository"/>
    <preference for="My\News\Api\Data\NewsInterface" type="My\News\Model\News" />

    <virtualType name="My\News\Model\ResourceModel\Grid\Collection" type="Magento\Framework\View\Element\UiComponent\DataProvider\SearchResult">
        <arguments>
            <argument name="mainTable" xsi:type="string">my_news</argument>
            <argument name="resourceModel" xsi:type="string">My\News\Model\ResourceModel\News</argument>
        </arguments>
    </virtualType>

    <type name="Magento\Framework\View\Element\UiComponent\DataProvider\CollectionFactory">
        <arguments>
            <argument name="collections" xsi:type="array">
                <item name="my_news_grid_data_source" xsi:type="string">My\News\Model\ResourceModel\Grid\Collection</item>
            </argument>
        </arguments>
    </type>
</config>

di.xmlで定義したクラスやインターフェイスを作成する

3番目の手順では、先程定義したdi.xmlに記述したクラスやインターフェイスを作成します。

  • My\News\Api\NewsRepositoryInterface
  • My\News\Api\Data\NewsInterface
  • My\News\Model\NewsRepository

インターフェイスを定義しなくても実装はできます。ただ、Web APIを介して今作っている機能を利用したい場合には、インターフェイスを実装しておく必要があります。後々APIで利用することを考えて、インターフェイスを定義しておきましょう。

最初はNewsRepositoryInterfaceです。<エクステンションディレクトリ>/Api/NewsRepositoryInterface.phpを作成し、以下の内容を記述します。

<?php
namespace My\News\Api;

use Magento\Framework\Api\SearchCriteriaInterface;


interface NewsRepositoryInterface
{
    /**
     * Save news.
     *
     * @param \My\News\Api\Data\NewsInterface $news
     * @return \My\News\Api\Data\NewsInterface
     * @throws \Magento\Framework\Exception\LocalizedException
     */
    public function save(Data\NewsInterface $news);

    /**
     * Retrieve news.
     *
     * @param int $blockId
     * @return \My\News\Api\Data\NewsInterface
     * @throws \Magento\Framework\Exception\LocalizedException
     */
    public function getById($newsId);

    /**
     * Retrieve news matching the specified criteria.
     *
     * @param \Magento\Framework\Api\SearchCriteriaInterface $searchCriteria
     * @return \Magento\Framework\Api\Search\SearchResultInterface
     * @throws \Magento\Framework\Exception\LocalizedException
     */
    public function getList(\Magento\Framework\Api\SearchCriteriaInterface $searchCriteria);

    /**
     * Delete news.
     *
     * @param \My\News\Api\Data\NewsInterface $news
     * @return bool true on success
     * @throws \Magento\Framework\Exception\LocalizedException
     */
    public function delete(Data\NewsInterface $news);

    /**
     * Delete news by ID.
     *
     * @param int $newsId
     * @return bool true on success
     * @throws \Magento\Framework\Exception\NoSuchEntityException
     * @throws \Magento\Framework\Exception\LocalizedException
     */
    public function deleteById($newsId);
}

次はこのインターフェイスの実装になる、NewsRepositoryを作成します。<エクステンションディレクトリ>/Model/NewsRepository.phpを作成し、以下の内容を記述します。

<?php
namespace My\News\Model;

use \My\News\Api\Data;
use \My\News\Api\NewsRepositoryInterface;
use \My\News\Model\ResourceModel\News as ResourceNews;
use \My\News\Model\ResourceModel\News\CollectionFactory as ResourceCollectionFactory;
use \Magento\Framework\Api\SearchCriteriaInterface;
use \Magento\Framework\Api\Search\SearchResultInterface;
use \Magento\Framework\Api\Search\SearchResultInterfaceFactory;
use Magento\Framework\Exception\CouldNotDeleteException;
use Magento\Framework\Exception\CouldNotSaveException;
use Magento\Framework\Exception\NoSuchEntityException;
use Magento\Framework\Api\SearchCriteria\CollectionProcessorInterface;


class NewsRepository implements NewsRepositoryInterface
{
    /**
     * @var NewsFactory
     */
    private $newsFactory;
    /**
     * @var ResourceNews
     */
    private $resource;
    /**
     * @var ResourceCollectionFactory
     */
    private $resourceCollectionFactory;
    /**
     * @var SearchResultInterfaceFactory
     */
    private $searchResultFactory;
    /**
     * @var CollectionProcessorInterface
     */
    private $collectionProcessor;

    /**
     * NewsRepository constructor.
     * @param NewsFactory $newsFactory
     * @param ResourceNews $resource
     * @param ResourceCollectionFactory $resourceCollectionFactory
     * @param SearchResultInterfaceFactory $searchResultFactory
     * @param CollectionProcessorInterface $collectionProcessor
     */
    public function __construct(
        NewsFactory $newsFactory,
        ResourceNews $resource,
        ResourceCollectionFactory $resourceCollectionFactory,
        SearchResultInterfaceFactory $searchResultFactory,
        CollectionProcessorInterface $collectionProcessor
    )
    {
        $this->newsFactory = $newsFactory;
        $this->resource = $resource;
        $this->resourceCollectionFactory = $resourceCollectionFactory;
        $this->searchResultFactory = $searchResultFactory;
        $this->collectionProcessor = $collectionProcessor;
    }


    /**
     * @param Data\NewsInterface $news
     * @return Data\NewsInterface
     * @throws CouldNotSaveException
     */
    public function save(Data\NewsInterface $news)
    {
        try {
            $this->resource->save($news);
        } catch (\Exception $exception) {
            throw new CouldNotSaveException(__($exception->getMessage()));
        }
        return $news;
    }

    /**
     * @param $newsId
     * @return Data\NewsInterface
     * @throws NoSuchEntityException
     */
    public function getById($newsId)
    {
        $news = $this->newsFactory->create();
        $this->resource->load($news, $newsId);
        if (!$news->getId()) {
            throw new NoSuchEntityException(__('News with id "%1" does not exist.', $newsId));
        }
        return $news;
    }

    /**
     * @param SearchCriteriaInterface $searchCriteria
     * @return SearchResultInterface
     */
    public function getList(SearchCriteriaInterface $searchCriteria)
    {
        /** @var \Magento\Cms\Model\ResourceModel\Block\Collection $collection */
        $collection = $this->resourceCollectionFactory->create();

        $this->collectionProcessor->process($searchCriteria, $collection);

        /** @var SearchResultInterface $searchResults */
        $searchResults = $this->searchResultFactory->create();
        $searchResults->setSearchCriteria($searchCriteria);
        $searchResults->setItems($collection->getItems());
        $searchResults->setTotalCount($collection->getSize());
        return $searchResults;
    }

    /**
     * @param Data\NewsInterface $news
     * @return bool
     * @throws CouldNotDeleteException
     */
    public function delete(Data\NewsInterface $news)
    {
        try {
            $this->resource->delete($news);
        } catch (\Exception $exception) {
            throw new CouldNotDeleteException(__($exception->getMessage()));
        }
        return true;
    }

    /**
     * @param int $newsId
     * @return bool
     * @throws CouldNotDeleteException
     * @throws NoSuchEntityException
     */
    public function deleteById($newsId)
    {
        return $this->delete($this->getById($newsId));
    }

}

そしてNewsInterfaceを定義します。<エクステンションディレクトリ>/Api/Data/NewsInterface.phpを作成し、以下の内容を記述します。

<?php
namespace My\News\Api\Data;

interface NewsInterface
{
    /**
     * Constants for keys of data array. Identical to the name of the getter in snake case.
     */
    const NEWS_ID = 'news_id';

    /**
     * @return mixed
     */
    public function getNewsId();

    /**
     * @param $newsId
     * @return mixed
     */
    public function setNewsId($newsId);

    /**
     * @return mixed
     */
    public function getId();

    /**
     * @param $id
     * @return mixed
     */
    public function setId($id);

    /**
     * @return mixed
     */
    public function getTitle();

    /**
     * @param $title
     * @return mixed
     */
    public function setTitle($title);

    /**
     * @return mixed
     */
    public function getContent();

    /**
     * @param $content
     * @return mixed
     */
    public function setContent($content);

    /**
     * @return mixed
     */
    public function getCreatedAt();

    /**
     * @param $createdAt
     * @return mixed
     */
    public function setCreatedAt($createdAt);

    /**
     * @return mixed
     */
    public function getUpdatedAt();

    /**
     * @param $updatedAt
     * @return mixed
     */
    public function setUpdatedAt($updatedAt);
}

仕上げに<エクステンションディレクトリ>/Model/News.phpを開いて、いま定義したNewsInterfaceを実装します。

<?php
namespace My\News\Model;

use \Magento\Framework\Model\AbstractModel;
use \My\News\Api\Data\NewsInterface;

class News extends AbstractModel implements NewsInterface
{
    /**
     * cache tag
     */
    const CACHE_TAG = 'my_news';

    /**
     * @var string
     */
    protected $_cacheTag = 'my_news';

    /**
     * @var string
     */
    protected $_eventPrefix = 'my_news';

    /**
     *
     */
    protected function _construct()
    {
        $this->_init(
            'My\News\Model\ResourceModel\News'
        );
    }

    /**
     * @return mixed
     */
    public function getNewsId()
    {
        return $this->getData('news_id');
    }

    /**
     * @param $newsId
     * @return $this|mixed
     */
    public function setNewsId($newsId)
    {
        $this->setData('news_id', $newsId);
        return $this;
    }

    /**
     * @return mixed
     */
    public function getTitle()
    {
        return $this->getData('title');
    }

    /**
     * @param $title
     * @return News
     */
    public function setTitle($title)
    {
        return $this->setData('title', $title);
    }

    /**
     * @return mixed
     */
    public function getContent()
    {
        return $this->getData('content');
    }

    /**
     * @param $content
     * @return News
     */
    public function setContent($content)
    {
        return $this->setData('content', $content);
    }

    /**
     * @return mixed
     */
    public function getCreatedAt()
    {
        return $this->getData('created_at');
    }

    /**
     * @param $createAt
     * @return News
     */
    public function setCreatedAt($createdAt)
    {
        return $this->setData('created_at', $createdAt);
    }

    /**
     * @return mixed
     */
    public function getUpdatedAt()
    {
        return $this->getData('updated_at');
    }

    /**
     * @param $updatedAt
     * @return mixed|News
     */
    public function setUpdatedAt($updatedAt)
    {
        return $this->setData('updated_at', $updatedAt);
    }
}

前回作成したものより少し記述が増えていますが、ほとんどの部分はそのまま使えます。

レイアウトXMLを定義しよう

次はこのコンポーネントを利用する画面定義を行いましょう。管理画面もフロントエンドと同じく、レイアウトXMLで画面の定義を行います。手順の4番目に挙げた場所に、「news_index_index.xml」を作成し、以下のように記述してください。

<?xml version="1.0"?>

<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd">
    <body>
        <referenceContainer name="content">
            <uiComponent name="my_news_grid"/>
        </referenceContainer>
    </body>
</page>

routes.xmlを作成してControllerが認識されるようにする

レイアウトXMLができたら、Controllerを作成する前に、routes.xmlを作成します。ここは前回取り上げたフロントエンドと同じで、routes.xmlで定義をしておかないと、いくらControllerを作成してもMagentoが処理を振り分けてくれません。手順の5番目に挙げた場所にroutes.xmlを以下の内容で作成しておいてください。

<?xml version="1.0"?>

<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:App/etc/routes.xsd">
    <router id="admin">
        <route id="adminhtml">
            <module name="My_News" before="Magento_Backend" />
        </route>

        <route id="mynews" frontName="mynews">
            <module name="My_News" />
        </route>
    </router>
</config>

Controllerを定義して、リクエストが受け付けられるようにする

最後にControllerクラスを定義します。手順の6番目です。機能名とアクション名は、レイアウトXMLのファイル名につけた名称と揃えてください。今回の場合だと、

<エクステンションディレクトリ>/Controller/Adminhtml/Index/Index.php

となります。ファイルを上記の場所に作成したら、以下のように記述しておいてください。

<?php
namespace My\News\Controller\Adminhtml\Index;

use \Magento\Backend\App\Action\Context;
use \Magento\Framework\View\Result\PageFactory;

class Index extends \Magento\Backend\App\Action
{
    /**
     * @var \Magento\Framework\View\Result\PageFactory
     */
    protected $resultPageFactory;

    /**
     * @param \Magento\Backend\App\Action\Context        $context
     * @param \Magento\Framework\View\Result\PageFactory $resultPageFactory
     */
    public function __construct(
        Context $context,
        PageFactory $resultPageFactory
    )
    {
        parent::__construct($context);
        $this->resultPageFactory = $resultPageFactory;
    }

    /**
     * Grid List page.
     *
     * @return \Magento\Backend\Model\View\Result\Page
     */
    public function execute()
    {
        /** @var \Magento\Backend\Model\View\Result\Page $resultPage */
        $resultPage = $this->resultPageFactory->create();
        $resultPage->setActiveMenu('My_News::manage');
        $resultPage->getConfig()->getTitle()->prepend(__('Manage News'));

        return $resultPage;
    }

    /**
     * Check Grid List Permission.
     *
     * @return bool
     */
    protected function _isAllowed()
    {
        return $this->_authorization->isAllowed('My_News::manage');
    }
}

動作確認

ここまでできたらファイルを一式サーバーにアップロードし、Magentoのキャッシュをクリアします。menu.xmlで定義したメニューから一覧画面にアクセスし、前回テーブルに登録したデータが次図のように表示されていれば成功です。

まだ続きます

今回は文量が長くなってしまったので、一旦ここまでにしたいと思います。新規作成・編集画面と保存・削除処理の実装には更に多くのクラスや定義が必要になるので、もう少しややこしくなります。