TYPO3

ドイツ発のエンタープライズCMS。複雑な多言語・多サイト管理に強みを持つ高機能システム。

CMSオープンソースエンタープライズPHPMySQL多言語対応
ライセンス
GPL v2
言語
PHP
料金
完全無料(自己ホスト)

CMS

TYPO3

概要

TYPO3は、ドイツ発のエンタープライズ向けオープンソースCMS(コンテンツ管理システム)です。1998年に誕生し、特にヨーロッパで高い人気を誇ります。複雑な多言語・マルチサイト管理に強みを持ち、大規模な企業サイトや政府機関、大学などで広く採用されています。PHP 8.2以上とMySQLなどのデータベースで動作し、高度なセキュリティと柔軟性を提供します。

詳細

TYPO3は、エンタープライズレベルの要求に応える高機能なCMSとして設計されています。特に多言語対応と複雑なコンテンツ管理において優れた機能を提供し、ワークスペース管理、バージョニング、高度なキャッシュ機能など、大規模サイト運営に必要な機能を網羅しています。

主な特徴:

  • 高度な多言語対応: 無制限の言語サポートと翻訳ワークフロー
  • ワークスペース管理: ステージング環境とライブ環境の分離
  • フレキシブルコンテンツ: 自由度の高いコンテンツ要素の組み合わせ
  • マルチサイト管理: 一つのインストールで複数サイトを管理
  • バージョニング: コンテンツの履歴管理と復元機能
  • 高度なキャッシュシステム: パフォーマンス最適化
  • エンタープライズセキュリティ: 多要素認証、IP制限、詳細なアクセス制御
  • TYPO3 v13(2024年最新版): 画像レンダリング改善、RTL UI対応、コンテンツブロック機能

メリット・デメリット

メリット

  • 強力な多言語・多地域対応機能
  • エンタープライズレベルのセキュリティ
  • 高度なワークフロー管理
  • 柔軟なコンテンツ構造
  • 優れたスケーラビリティ
  • 長期サポート(LTS)版の提供
  • 活発な開発コミュニティ(特にヨーロッパ)
  • 大規模サイトに最適化されたアーキテクチャ

デメリット

  • 非常に高い学習曲線
  • 複雑な設定とカスタマイズ
  • 日本語リソースが限定的
  • 小規模サイトには過剰な機能
  • 初期セットアップに時間がかかる
  • 専門的な知識が必要
  • 小さなコミュニティ(特にアジア地域)

参考ページ

書き方の例

1. Hello World(基本的なセットアップ)

Composerによるインストール(TYPO3 v13)

# プロジェクトディレクトリの作成
mkdir my-typo3-project && cd my-typo3-project

# TYPO3のインストール
composer create-project typo3/cms-base-distribution:^13

# データベース設定(CLIセットアップ)
./vendor/bin/typo3 setup \
  --driver=mysqli \
  --host=localhost \
  --port=3306 \
  --dbname=typo3_db \
  --username=db_user \
  --password=db_password \
  --admin-username=admin \
  --admin-password=password123 \
  [email protected] \
  --project-name="My TYPO3 Project"

# Webサーバーの設定(public/をドキュメントルートに)
# http://localhost/typo3 でバックエンドにアクセス

2. TypoScriptによるページテンプレート

setup.typoscript(基本的なページ設定)

# ページオブジェクトの定義
page = PAGE
page {
    # メタタグ
    meta {
        viewport = width=device-width, initial-scale=1.0
        description = TYPO3エンタープライズCMSサイト
        keywords = TYPO3, CMS, エンタープライズ
    }
    
    # CSSの読み込み
    includeCSS {
        styles = EXT:my_sitepackage/Resources/Public/Css/styles.css
        bootstrap = https://cdn.jsdelivr.net/npm/bootstrap@5/dist/css/bootstrap.min.css
        bootstrap.external = 1
    }
    
    # JavaScriptの読み込み
    includeJSFooter {
        scripts = EXT:my_sitepackage/Resources/Public/Js/scripts.js
        bootstrap = https://cdn.jsdelivr.net/npm/bootstrap@5/dist/js/bootstrap.bundle.min.js
        bootstrap.external = 1
    }
    
    # Fluidテンプレートの設定
    10 = FLUIDTEMPLATE
    10 {
        templateName = Default
        templateRootPaths {
            10 = EXT:my_sitepackage/Resources/Private/Templates/Page/
        }
        partialRootPaths {
            10 = EXT:my_sitepackage/Resources/Private/Partials/Page/
        }
        layoutRootPaths {
            10 = EXT:my_sitepackage/Resources/Private/Layouts/Page/
        }
        
        # データプロセッサー
        dataProcessing {
            # メニューの生成
            10 = TYPO3\CMS\Frontend\DataProcessing\MenuProcessor
            10 {
                levels = 2
                includeSpacer = 1
                as = mainNavigation
            }
            
            # 言語メニュー
            20 = TYPO3\CMS\Frontend\DataProcessing\LanguageMenuProcessor
            20 {
                languages = auto
                as = languageMenu
            }
        }
        
        # 変数の設定
        variables {
            currentYear = TEXT
            currentYear.data = date:Y
            
            siteName = TEXT
            siteName.value = My TYPO3 Site
        }
    }
}

3. Fluidテンプレート開発

Default.html(ページテンプレート)

<f:layout name="Default" />

<f:section name="Main">
    <header class="site-header">
        <nav class="navbar navbar-expand-lg">
            <div class="container">
                <a class="navbar-brand" href="/">
                    {siteName}
                </a>
                
                <!-- メインナビゲーション -->
                <f:if condition="{mainNavigation}">
                    <ul class="navbar-nav">
                        <f:for each="{mainNavigation}" as="menuItem">
                            <li class="nav-item {f:if(condition: menuItem.active, then: 'active')}">
                                <a class="nav-link" href="{menuItem.link}" 
                                   {f:if(condition: menuItem.target, then: 'target="{menuItem.target}"')}>
                                    {menuItem.title}
                                </a>
                                
                                <!-- サブメニュー -->
                                <f:if condition="{menuItem.children}">
                                    <ul class="dropdown-menu">
                                        <f:for each="{menuItem.children}" as="subItem">
                                            <li>
                                                <a class="dropdown-item" href="{subItem.link}">
                                                    {subItem.title}
                                                </a>
                                            </li>
                                        </f:for>
                                    </ul>
                                </f:if>
                            </li>
                        </f:for>
                    </ul>
                </f:if>
                
                <!-- 言語切り替え -->
                <f:if condition="{languageMenu}">
                    <div class="language-menu">
                        <f:for each="{languageMenu}" as="language">
                            <a href="{language.link}" 
                               class="lang-link {f:if(condition: language.active, then: 'active')}">
                                {language.languageIsoCode}
                            </a>
                        </f:for>
                    </div>
                </f:if>
            </div>
        </nav>
    </header>
    
    <main class="site-main">
        <div class="container">
            <!-- コンテンツエリア -->
            <f:cObject typoscriptObjectPath="lib.dynamicContent" data="{colPos: '0'}" />
        </div>
    </main>
    
    <footer class="site-footer">
        <div class="container">
            <p>&copy; {currentYear} {siteName}. All rights reserved.</p>
        </div>
    </footer>
</f:section>

4. エクステンション開発

ext_emconf.php(エクステンション設定)

<?php
$EM_CONF[$_EXTKEY] = [
    'title' => 'My Custom Extension',
    'description' => 'カスタム機能を提供するTYPO3エクステンション',
    'category' => 'plugin',
    'author' => 'Your Name',
    'author_email' => '[email protected]',
    'state' => 'stable',
    'version' => '1.0.0',
    'constraints' => [
        'depends' => [
            'typo3' => '12.4.0-13.4.99',
            'php' => '8.2.0-8.3.99'
        ],
    ],
];

Classes/Controller/MyController.php(コントローラー)

<?php
namespace Vendor\MyExtension\Controller;

use Psr\Http\Message\ResponseInterface;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Extbase\Mvc\Controller\ActionController;
use TYPO3\CMS\Extbase\Pagination\QueryResultPaginator;
use TYPO3\CMS\Extbase\Persistence\QueryInterface;
use Vendor\MyExtension\Domain\Repository\ProductRepository;

class MyController extends ActionController
{
    protected ProductRepository $productRepository;
    
    public function injectProductRepository(ProductRepository $productRepository): void
    {
        $this->productRepository = $productRepository;
    }
    
    /**
     * 製品一覧表示
     */
    public function listAction(int $currentPage = 1): ResponseInterface
    {
        // 製品を取得
        $products = $this->productRepository->findAll();
        
        // ページネーション設定
        $itemsPerPage = $this->settings['itemsPerPage'] ?? 10;
        $paginator = GeneralUtility::makeInstance(
            QueryResultPaginator::class,
            $products,
            $currentPage,
            $itemsPerPage
        );
        
        // ビューに変数を渡す
        $this->view->assignMultiple([
            'products' => $paginator->getPaginatedItems(),
            'paginator' => $paginator,
            'pages' => range(1, $paginator->getNumberOfPages()),
            'currentPage' => $currentPage
        ]);
        
        return $this->htmlResponse();
    }
    
    /**
     * 製品詳細表示
     */
    public function showAction(int $product): ResponseInterface
    {
        $productObject = $this->productRepository->findByUid($product);
        
        if (!$productObject) {
            throw new \TYPO3\CMS\Core\Error\Http\PageNotFoundException(
                'Product not found',
                1234567890
            );
        }
        
        $this->view->assign('product', $productObject);
        
        return $this->htmlResponse();
    }
    
    /**
     * 検索機能
     */
    public function searchAction(string $searchTerm = ''): ResponseInterface
    {
        $products = [];
        
        if ($searchTerm !== '') {
            $products = $this->productRepository->findBySearchTerm($searchTerm);
        }
        
        $this->view->assignMultiple([
            'products' => $products,
            'searchTerm' => $searchTerm
        ]);
        
        return $this->htmlResponse();
    }
}

5. カスタムコンテンツ要素

Configuration/TCA/Overrides/tt_content.php(コンテンツ要素の登録)

<?php
defined('TYPO3') or die();

// カスタムコンテンツ要素の登録
\TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addTcaSelectItem(
    'tt_content',
    'CType',
    [
        'LLL:EXT:my_extension/Resources/Private/Language/locallang.xlf:content_element.product_teaser',
        'product_teaser',
        'content-products'
    ],
    'textmedia',
    'after'
);

// フィールドの追加
$tempColumns = [
    'product_limit' => [
        'label' => 'LLL:EXT:my_extension/Resources/Private/Language/locallang.xlf:product_limit',
        'config' => [
            'type' => 'input',
            'size' => 5,
            'eval' => 'int',
            'default' => 3
        ]
    ],
    'product_category' => [
        'label' => 'LLL:EXT:my_extension/Resources/Private/Language/locallang.xlf:product_category',
        'config' => [
            'type' => 'select',
            'renderType' => 'selectSingle',
            'foreign_table' => 'tx_myextension_domain_model_category',
            'foreign_table_where' => 'AND {#tx_myextension_domain_model_category}.{#pid} = ###PAGE_TSCONFIG_ID###',
            'items' => [
                ['LLL:EXT:my_extension/Resources/Private/Language/locallang.xlf:all_categories', 0]
            ],
            'default' => 0
        ]
    ]
];

\TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addTCAcolumns(
    'tt_content',
    $tempColumns
);

// パレットの設定
$GLOBALS['TCA']['tt_content']['palettes']['product_settings'] = [
    'showitem' => 'product_limit, product_category'
];

// タイプの設定
$GLOBALS['TCA']['tt_content']['types']['product_teaser'] = [
    'showitem' => '
        --div--;LLL:EXT:core/Resources/Private/Language/Form/locallang_tabs.xlf:general,
            --palette--;;general,
            --palette--;;headers,
        --div--;LLL:EXT:my_extension/Resources/Private/Language/locallang.xlf:product_settings,
            --palette--;;product_settings,
        --div--;LLL:EXT:core/Resources/Private/Language/Form/locallang_tabs.xlf:appearance,
            --palette--;;frames,
            --palette--;;appearanceLinks,
        --div--;LLL:EXT:core/Resources/Private/Language/Form/locallang_tabs.xlf:language,
            --palette--;;language,
        --div--;LLL:EXT:core/Resources/Private/Language/Form/locallang_tabs.xlf:access,
            --palette--;;hidden,
            --palette--;;access
    '
];

6. データベース操作とリポジトリ

Classes/Domain/Repository/ProductRepository.php(リポジトリクラス)

<?php
namespace Vendor\MyExtension\Domain\Repository;

use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Extbase\Persistence\QueryInterface;
use TYPO3\CMS\Extbase\Persistence\Repository;
use TYPO3\CMS\Extbase\Persistence\Generic\Typo3QuerySettings;

class ProductRepository extends Repository
{
    /**
     * デフォルトの並び順を設定
     */
    protected $defaultOrderings = [
        'sorting' => QueryInterface::ORDER_ASCENDING,
        'title' => QueryInterface::ORDER_ASCENDING
    ];
    
    /**
     * リポジトリの初期化
     */
    public function initializeObject(): void
    {
        // ストレージPIDの制限を無効化(必要に応じて)
        $querySettings = GeneralUtility::makeInstance(Typo3QuerySettings::class);
        $querySettings->setRespectStoragePage(false);
        $this->setDefaultQuerySettings($querySettings);
    }
    
    /**
     * カテゴリーで製品を検索
     */
    public function findByCategory(int $categoryUid, int $limit = 0): array
    {
        $query = $this->createQuery();
        
        $constraints = [];
        $constraints[] = $query->contains('categories', $categoryUid);
        
        $query->matching($query->logicalAnd(...$constraints));
        
        if ($limit > 0) {
            $query->setLimit($limit);
        }
        
        return $query->execute()->toArray();
    }
    
    /**
     * キーワード検索
     */
    public function findBySearchTerm(string $searchTerm): array
    {
        $query = $this->createQuery();
        
        $constraints = [];
        $constraints[] = $query->like('title', '%' . $searchTerm . '%');
        $constraints[] = $query->like('description', '%' . $searchTerm . '%');
        $constraints[] = $query->like('teaser', '%' . $searchTerm . '%');
        
        $query->matching($query->logicalOr(...$constraints));
        
        return $query->execute()->toArray();
    }
    
    /**
     * 注目製品を取得
     */
    public function findFeatured(int $limit = 5): array
    {
        $query = $this->createQuery();
        
        $query->matching(
            $query->equals('featured', true)
        );
        
        $query->setOrderings([
            'featuredSorting' => QueryInterface::ORDER_ASCENDING,
            'title' => QueryInterface::ORDER_ASCENDING
        ]);
        
        $query->setLimit($limit);
        
        return $query->execute()->toArray();
    }
    
    /**
     * 関連製品を取得
     */
    public function findRelated($product, int $limit = 4): array
    {
        $query = $this->createQuery();
        
        $constraints = [];
        
        // 同じカテゴリーの製品を検索
        foreach ($product->getCategories() as $category) {
            $constraints[] = $query->contains('categories', $category);
        }
        
        // 現在の製品を除外
        $constraints[] = $query->logicalNot(
            $query->equals('uid', $product->getUid())
        );
        
        if (count($constraints) > 1) {
            $query->matching($query->logicalAnd(...$constraints));
        }
        
        $query->setLimit($limit);
        
        return $query->execute()->toArray();
    }
}