Magento

Adobe製の高機能ECプラットフォーム。B2BおよびB2Cの複雑な要件に対応可能。

CMSeコマースPHPオープンソースB2BB2CGraphQLREST API
ライセンス
OSL 3.0 / Commercial
言語
PHP
料金
Community Edition無料

CMS

Magento

概要

Magentoは、Adobe製の高機能ECプラットフォームです。B2BおよびB2Cの複雑な要件に対応可能で、大規模ECサイト向けのエンタープライズソリューションです。

詳細

Magento(マジェント)は、2008年にリリースされたオープンソースのECプラットフォームで、2018年にAdobeに買収されました。PHPで開発され、Zend Frameworkをベースに構築されています。Community Edition(無料)とCommerce Cloud(有料)の2つのエディションがあります。250,000以上のサイトで利用され、市場シェアは7%です。マルチストア管理、B2B機能、高度な在庫管理、価格管理、マーケティングツール、PWA(Progressive Web App)対応など、エンタープライズレベルの機能を提供します。依存性注入(DI)、プラグイン(インターセプター)、イベント/オブザーバーパターンなど、高度な開発パターンを採用しています。GraphQLとREST APIの両方をサポートし、ヘッドレスコマースやオムニチャネル戦略に対応。最新のPWA Studioによりモバイルファーストの開発も可能です。

メリット・デメリット

メリット

  • 高機能: エンタープライズレベルの豊富な機能
  • スケーラビリティ: 大規模トラフィックに対応可能
  • B2B対応: 見積もり、承認フローなどB2B機能が充実
  • カスタマイズ性: モジュラーアーキテクチャで高い拡張性
  • PWA対応: モバイルファーストの開発が可能
  • 国際化対応: 多言語・多通貨・複数税制に対応
  • Adobe統合: Adobe Experience Cloudとの連携

デメリット

  • 高い複雑性: 学習曲線が急で習得に時間がかかる
  • リソース消費大: サーバーリソースを大量に必要とする
  • 高い学習コスト: 開発者の習得に時間とコストがかかる
  • 開発コスト: カスタマイズに高額な費用が発生
  • ホスティング要件: 高スペックなサーバーが必要

主要リンク

使い方の例

Magentoモジュールの基本構造

// app/code/Vendor/Module/registration.php
<?php
use Magento\Framework\Component\ComponentRegistrar;

ComponentRegistrar::register(
    ComponentRegistrar::MODULE,
    'Vendor_Module',
    __DIR__
);

// app/code/Vendor/Module/etc/module.xml
<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
        xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/etc/module.xsd">
    <module name="Vendor_Module" setup_version="1.0.0">
        <sequence>
            <module name="Magento_Catalog"/>
            <module name="Magento_Customer"/>
        </sequence>
    </module>
</config>

// app/code/Vendor/Module/etc/di.xml - 依存性注入設定
<?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="Vendor\Module\Api\ProductInterface" 
                type="Vendor\Module\Model\Product" />
    
    <type name="Vendor\Module\Model\Product">
        <arguments>
            <argument name="logger" xsi:type="object">Psr\Log\LoggerInterface</argument>
        </arguments>
    </type>
</config>

カスタムモデルとリポジトリパターン

// Api/Data/CustomProductInterface.php
<?php
namespace Vendor\Module\Api\Data;

interface CustomProductInterface
{
    const ENTITY_ID = 'entity_id';
    const SKU = 'sku';
    const CUSTOM_ATTRIBUTE = 'custom_attribute';
    
    /**
     * @return int|null
     */
    public function getId();
    
    /**
     * @param int $id
     * @return $this
     */
    public function setId($id);
    
    /**
     * @return string
     */
    public function getSku();
    
    /**
     * @param string $sku
     * @return $this
     */
    public function setSku($sku);
    
    /**
     * @return string|null
     */
    public function getCustomAttribute();
    
    /**
     * @param string $customAttribute
     * @return $this
     */
    public function setCustomAttribute($customAttribute);
}

// Model/CustomProduct.php
<?php
namespace Vendor\Module\Model;

use Magento\Framework\Model\AbstractModel;
use Vendor\Module\Api\Data\CustomProductInterface;

class CustomProduct extends AbstractModel implements CustomProductInterface
{
    protected function _construct()
    {
        $this->_init(\Vendor\Module\Model\ResourceModel\CustomProduct::class);
    }
    
    public function getSku()
    {
        return $this->getData(self::SKU);
    }
    
    public function setSku($sku)
    {
        return $this->setData(self::SKU, $sku);
    }
    
    public function getCustomAttribute()
    {
        return $this->getData(self::CUSTOM_ATTRIBUTE);
    }
    
    public function setCustomAttribute($customAttribute)
    {
        return $this->setData(self::CUSTOM_ATTRIBUTE, $customAttribute);
    }
}

// Api/CustomProductRepositoryInterface.php
<?php
namespace Vendor\Module\Api;

use Vendor\Module\Api\Data\CustomProductInterface;
use Magento\Framework\Api\SearchCriteriaInterface;

interface CustomProductRepositoryInterface
{
    /**
     * @param int $id
     * @return \Vendor\Module\Api\Data\CustomProductInterface
     * @throws \Magento\Framework\Exception\NoSuchEntityException
     */
    public function getById($id);
    
    /**
     * @param \Vendor\Module\Api\Data\CustomProductInterface $product
     * @return \Vendor\Module\Api\Data\CustomProductInterface
     * @throws \Magento\Framework\Exception\CouldNotSaveException
     */
    public function save(CustomProductInterface $product);
    
    /**
     * @param \Magento\Framework\Api\SearchCriteriaInterface $searchCriteria
     * @return \Vendor\Module\Api\Data\CustomProductSearchResultsInterface
     */
    public function getList(SearchCriteriaInterface $searchCriteria);
}

プラグイン(インターセプター)の実装

// etc/di.xml - プラグイン設定
<type name="Magento\Catalog\Api\ProductRepositoryInterface">
    <plugin name="vendor_module_product_save_plugin" 
            type="Vendor\Module\Plugin\ProductSavePlugin" 
            sortOrder="10" />
</type>

// Plugin/ProductSavePlugin.php
<?php
namespace Vendor\Module\Plugin;

use Magento\Catalog\Api\ProductRepositoryInterface;
use Magento\Catalog\Api\Data\ProductInterface;
use Psr\Log\LoggerInterface;

class ProductSavePlugin
{
    private $logger;
    
    public function __construct(LoggerInterface $logger)
    {
        $this->logger = $logger;
    }
    
    /**
     * Before plugin - 実行前の処理
     */
    public function beforeSave(
        ProductRepositoryInterface $subject,
        ProductInterface $product,
        $saveOptions = false
    ) {
        $this->logger->info('商品保存前: ' . $product->getSku());
        
        // 価格の自動調整(例)
        if ($product->getPrice() < 100) {
            $product->setPrice($product->getPrice() * 1.1);
        }
        
        return [$product, $saveOptions];
    }
    
    /**
     * After plugin - 実行後の処理
     */
    public function afterSave(
        ProductRepositoryInterface $subject,
        ProductInterface $result,
        ProductInterface $product,
        $saveOptions = false
    ) {
        $this->logger->info('商品保存後: ' . $result->getId());
        
        // カスタム処理(キャッシュクリア、通知など)
        $this->clearCustomCache($result->getId());
        
        return $result;
    }
    
    /**
     * Around plugin - 処理の前後をラップ
     */
    public function aroundDelete(
        ProductRepositoryInterface $subject,
        \Closure $proceed,
        ProductInterface $product
    ) {
        $this->logger->info('商品削除開始: ' . $product->getSku());
        
        // 削除前の検証
        if ($this->canDelete($product)) {
            $result = $proceed($product);
            $this->logger->info('商品削除完了');
            return $result;
        }
        
        throw new \Magento\Framework\Exception\LocalizedException(
            __('この商品は削除できません。')
        );
    }
}

GraphQL APIの実装

// etc/schema.graphqls
type Query {
    customProducts(
        filter: CustomProductFilterInput
        pageSize: Int = 20
        currentPage: Int = 1
    ): CustomProducts @resolver(class: "Vendor\\Module\\Model\\Resolver\\CustomProducts")
}

type CustomProducts {
    items: [CustomProduct]
    page_info: SearchResultPageInfo
    total_count: Int
}

type CustomProduct {
    id: Int
    sku: String
    name: String
    price: Float
    custom_attribute: String
    categories: [CategoryInterface]
}

input CustomProductFilterInput {
    sku: FilterTypeInput
    name: FilterTypeInput
    price: FilterRangeTypeInput
    custom_attribute: FilterTypeInput
}

// Model/Resolver/CustomProducts.php
<?php
namespace Vendor\Module\Model\Resolver;

use Magento\Framework\GraphQl\Config\Element\Field;
use Magento\Framework\GraphQl\Query\ResolverInterface;
use Magento\Framework\GraphQl\Schema\Type\ResolveInfo;
use Magento\Framework\GraphQl\Query\Resolver\Value;
use Magento\Framework\GraphQl\Query\Resolver\ValueFactory;

class CustomProducts implements ResolverInterface
{
    private $valueFactory;
    private $customProductRepository;
    
    public function __construct(
        ValueFactory $valueFactory,
        \Vendor\Module\Api\CustomProductRepositoryInterface $customProductRepository
    ) {
        $this->valueFactory = $valueFactory;
        $this->customProductRepository = $customProductRepository;
    }
    
    public function resolve(
        Field $field,
        $context,
        ResolveInfo $info,
        array $value = null,
        array $args = null
    ) {
        $searchCriteria = $this->buildSearchCriteria($args);
        $searchResult = $this->customProductRepository->getList($searchCriteria);
        
        $products = [];
        foreach ($searchResult->getItems() as $product) {
            $products[] = [
                'id' => $product->getId(),
                'sku' => $product->getSku(),
                'name' => $product->getName(),
                'price' => $product->getPrice(),
                'custom_attribute' => $product->getCustomAttribute()
            ];
        }
        
        return [
            'items' => $products,
            'page_info' => [
                'page_size' => $searchCriteria->getPageSize(),
                'current_page' => $searchCriteria->getCurrentPage(),
                'total_pages' => ceil($searchResult->getTotalCount() / $searchCriteria->getPageSize())
            ],
            'total_count' => $searchResult->getTotalCount()
        ];
    }
}

イベントとオブザーバーパターン

// etc/events.xml
<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
        xsi:noNamespaceSchemaLocation="urn:magento:framework:Event/etc/events.xsd">
    <event name="checkout_cart_product_add_after">
        <observer name="vendor_module_cart_add_observer" 
                  instance="Vendor\Module\Observer\CartAddObserver" />
    </event>
    
    <event name="sales_order_place_after">
        <observer name="vendor_module_order_place_observer" 
                  instance="Vendor\Module\Observer\OrderPlaceObserver" />
    </event>
</config>

// Observer/CartAddObserver.php
<?php
namespace Vendor\Module\Observer;

use Magento\Framework\Event\ObserverInterface;
use Magento\Framework\Event\Observer;
use Psr\Log\LoggerInterface;

class CartAddObserver implements ObserverInterface
{
    private $logger;
    private $messageManager;
    
    public function __construct(
        LoggerInterface $logger,
        \Magento\Framework\Message\ManagerInterface $messageManager
    ) {
        $this->logger = $logger;
        $this->messageManager = $messageManager;
    }
    
    public function execute(Observer $observer)
    {
        $item = $observer->getEvent()->getQuoteItem();
        $product = $observer->getEvent()->getProduct();
        
        // カスタムロジック(例:在庫チェック、価格調整など)
        if ($item->getQty() > 10) {
            // 大量購入割引を適用
            $price = $item->getProduct()->getPrice();
            $discountedPrice = $price * 0.9; // 10%割引
            $item->setCustomPrice($discountedPrice);
            $item->setOriginalCustomPrice($discountedPrice);
            $item->getProduct()->setIsSuperMode(true);
            
            $this->messageManager->addSuccessMessage(
                __('大量購入割引が適用されました!')
            );
        }
        
        $this->logger->info(
            'カートに商品追加: ' . $product->getSku() . ', 数量: ' . $item->getQty()
        );
    }
}

B2B機能の実装例

// B2B見積もりリクエスト
<?php
namespace Vendor\Module\Model;

use Magento\Framework\Model\AbstractModel;

class QuoteRequest extends AbstractModel
{
    protected function _construct()
    {
        $this->_init(\Vendor\Module\Model\ResourceModel\QuoteRequest::class);
    }
    
    /**
     * 見積もりリクエストの作成
     */
    public function createQuoteRequest($customerId, $items)
    {
        $this->setCustomerId($customerId);
        $this->setStatus('pending');
        $this->setCreatedAt(date('Y-m-d H:i:s'));
        
        // 商品情報の保存
        $itemsData = [];
        foreach ($items as $item) {
            $itemsData[] = [
                'product_id' => $item['product_id'],
                'qty' => $item['qty'],
                'requested_price' => $item['requested_price'] ?? null
            ];
        }
        $this->setItems(json_encode($itemsData));
        
        return $this->save();
    }
    
    /**
     * 見積もりの承認
     */
    public function approveQuote($adminUserId, $approvedPrices)
    {
        $this->setStatus('approved');
        $this->setApprovedBy($adminUserId);
        $this->setApprovedAt(date('Y-m-d H:i:s'));
        $this->setApprovedPrices(json_encode($approvedPrices));
        
        // 承認後の処理(メール送信、注文作成など)
        $this->_eventManager->dispatch('quote_request_approved', ['quote' => $this]);
        
        return $this->save();
    }
}

// Controller/Adminhtml/Quote/Approve.php
<?php
namespace Vendor\Module\Controller\Adminhtml\Quote;

use Magento\Backend\App\Action;
use Magento\Backend\App\Action\Context;

class Approve extends Action
{
    private $quoteRequestFactory;
    private $jsonHelper;
    
    public function __construct(
        Context $context,
        \Vendor\Module\Model\QuoteRequestFactory $quoteRequestFactory,
        \Magento\Framework\Json\Helper\Data $jsonHelper
    ) {
        parent::__construct($context);
        $this->quoteRequestFactory = $quoteRequestFactory;
        $this->jsonHelper = $jsonHelper;
    }
    
    public function execute()
    {
        $quoteId = $this->getRequest()->getParam('quote_id');
        $approvedPrices = $this->jsonHelper->jsonDecode(
            $this->getRequest()->getParam('approved_prices')
        );
        
        try {
            $quote = $this->quoteRequestFactory->create()->load($quoteId);
            $quote->approveQuote(
                $this->_auth->getUser()->getId(),
                $approvedPrices
            );
            
            $this->messageManager->addSuccessMessage(__('見積もりが承認されました。'));
        } catch (\Exception $e) {
            $this->messageManager->addErrorMessage(__('エラーが発生しました: %1', $e->getMessage()));
        }
        
        return $this->_redirect('*/*/index');
    }
}

これらの例は、Magentoの主要な開発パターンを示しています。モジュール開発、依存性注入、プラグインシステム、GraphQL API、イベント/オブザーバーパターン、B2B機能など、エンタープライズレベルのECサイト開発に必要な機能を網羅しています。