Slim3のスケルトンプロジェクト、slim/slim-skeletonの解説

カテゴリ: SlimFramework | タグ:

PHPでSlim3フレームワークを使ったプロジェクトを作る場合、プロジェクト構成のひな型としてslim/slim-skeletonパッケージがあります。今回は、プロジェクトひな型として、slim-skeletonが役立つか評価したい人のため、プロジェクトを構成している全ファイルの解説を行います。

本記事を作成するにあたって確認したバージョン

slim/slim-skeletonのバージョンは、2017年10時点での最新版であるver3.1.4をもとに確認しています。

slim-skeletonパッケージの取得

slim/slim-skeletonパッケージは、PHPのプロジェクト管理ツールであるComposerで取得します。

このスケルトンは、パッケージではなくプロジェクトのテンプレートなので、composer requireコマンドではなくcomposer create-projectコマンドを使用します。

composer create-projectコマンドは、開発用のパッケージが必要か否かで下記のどちらかを実行します。(開発を始めるときは、通常--no-dev無しで実行することが多いです)

# 開発用のパッケージ(phpunit)も含めて取得する場合
composer create-project slim/slim-skeleton [ProjectName]
# 本番環境で使用するパッケージだけを取得する場合
composer create-project --no-dev slim/slim-skeleton [ProjectName]

[ProjectName]の部分は、プロジェクトの名称に置き換えてコマンドを実行して下さい。

composer.jsonの定義

composer.jsonでの依存パッケージは以下のとおりです。アプリの動作に必要な依存パッケージが4つと、開発時に使用するパッケージが1つ定義されています。

"require": {
    "php": ">=5.5.0",
    "slim/slim": "^3.1",
    "slim/php-view": "^2.0",
    "monolog/monolog": "^1.17"
},

"require-dev": {
    "phpunit/phpunit": ">=4.8 < 6.0"
},

Composerで取り込まれるパッケージの一覧

Composerでは、上記の依存パッケージに加えて、依存パッケージがさらに依存している孫パッケージ・ひ孫パッケージも同時に取得可能です。

composer create-projectでプロジェクトを作成すると、結果的には下記のパッケージが入ります。

--no-dev付きでcreate-projectしたとき

$ composer show

container-interop/container-interop 1.2.0  Promoting the interoperability of container objects (DIC, SL, etc.)
monolog/monolog                     1.23.0 Sends your logs to files, sockets, inboxes, databases and various web services
nikic/fast-route                    v1.2.0 Fast request router for PHP
pimple/pimple                       v3.2.2 Pimple, a simple Dependency Injection Container
psr/container                       1.0.0  Common Container Interface (PHP FIG PSR-11)
psr/http-message                    1.0.1  Common interface for HTTP messages
psr/log                             1.0.2  Common interface for logging libraries
slim/php-view                       2.2.0  Render PHP view scripts into a PSR-7 Response object.
slim/slim                           3.8.1  Slim is a PHP micro framework that helps you quickly write simple yet powerful web applications an...

psr/*とcontainer-interopはPSR関連のインターフェース定義だけなので、実質6パッケージだけです。各パッケージはそれぞれ下記の機能を提供しています。

monolog/monolog    ログ出力
nikic/fast-route   ルーティング処理
pimple/pimple      DIコンテナ
slim/php-view      Viewのテンプレート(smartyやtwigではなく、phpをviewに使うもの)
slim/slim          slimのフレームワーク本体

--no-dev無しでcreate-projectしたとき

--no-devオプションをつけずにcreate-projextすると、さらに下記のパッケージが追加で入ります。

$ composer show

doctrine/instantiator               1.1.0   A small, lightweight utility to instantiate objects in PHP without invoking the...
myclabs/deep-copy                   1.6.1   Create deep copies (clones) of your objects
phpdocumentor/reflection-common     1.0.1   Common reflection classes used by phpdocumentor to reflect the code structure
phpdocumentor/reflection-docblock   4.1.1   With this component, a library can provide support for annotations via DocBlock...
phpdocumentor/type-resolver         0.4.0
phpspec/prophecy                    v1.7.2  Highly opinionated mocking framework for PHP 5.3+
phpunit/php-code-coverage           4.0.8   Library that provides collection, processing, and rendering functionality for P...
phpunit/php-file-iterator           1.4.2   FilterIterator implementation that filters files based on a list of suffixes.
phpunit/php-text-template           1.2.1   Simple template engine.
phpunit/php-timer                   1.0.9   Utility class for timing
phpunit/php-token-stream            2.0.1   Wrapper around PHP's tokenizer extension.
phpunit/phpunit                     5.7.22  The PHP Unit Testing framework.
phpunit/phpunit-mock-objects        3.4.4   Mock Object library for PHPUnit
sebastian/code-unit-reverse-lookup  1.0.1   Looks up which function or method a line of code belongs to
sebastian/comparator                1.2.4   Provides the functionality to compare PHP values for equality
sebastian/diff                      1.4.3   Diff implementation
sebastian/environment               2.0.0   Provides functionality to handle HHVM/PHP environments
sebastian/exporter                  2.0.0   Provides the functionality to export PHP variables for visualization
sebastian/global-state              1.1.1   Snapshotting of global state
sebastian/object-enumerator         2.0.1   Traverses array structures and object graphs to enumerate all referenced objects
sebastian/recursion-context         2.0.0   Provides functionality to recursively process PHP variables
sebastian/resource-operations       1.0.0   Provides a list of PHP built-in functions that operate on resources
sebastian/version                   2.0.1   Library that helps with managing the version number of Git-hosted PHP projects
symfony/yaml                        v3.3.10 Symfony Yaml Component
webmozart/assert                    1.2.0   Assertions to validate method input/output with nice error messages.

require-devしているのはphpunitだけだったので、上記のパッケージ群はPHPUnit関係の物のみです。

ファイルの構成

パッケージを管理しているvendorディレクトリを除くと、17ファイルで構成されています。

その内、プログラム関するファイルは以下の7つで、ファイルはpublic, src, templatesディレクトリに存在しています。

public/.htaccess
public/index.php
src/dependencies.php
src/middleware.php
src/routes.php
src/settings.php
templates/index.phtml

testsディレクトリには、ユニットテスト用のファイルが2つあります。

tests/Functional/BaseTestCase.php
tests/Functional/HomepageTest.php

上記以外に、8個のファイルがあります。これらのファイルはアプリの動作に直接関係しないものです。

.gitignore            gitの定義ファイル

composer.json         composerの定義ファイル
composer.lock         composerの定義ファイル

docker-compose.yml    Dockerの定義ファイル

logs/README.md        ログディレクトリであることの説明
CONTRIBUTING.md       slim-skeletonへコントリビュート方法の説明(プルリクエストの送り方など)
phpunit.xml           UnitTest用の定義
README.md             プロジェクトの説明

その他ファイル系の確認

まずは、ドキュメント、環境設定系のファイルを確認していきます。

README.mdの内容

README.mdには、使い方の説明が記載されています。ざっくり日本語に訳すと以下のような感じです。

Slim Framework 3 Skeletonアプリケーション

このSkeletonを使用して、新しいSlim Framework 3アプリケーションを素早くにセットアップして作業を開始できます。このアプリケーションでは、PHP-Viewテンプレートを使った、最新のSlim 3フレームワークを使用しています。また、Monolog loggerも使用しています。

このSkeletonは、Composer向けに作成されています。これにより、新しいSlim Frameworkアプリケーションを簡単にセットアップできます。

アプリケーションのインストール

Slim Frameworkアプリケーションのプロジェクトを作りたい場所で、下記のコマンドを実行します。

php composer.phar create-project slim/slim-skeleton [my-app-name]

[my-app-name]の部分には、作成するアプリケーションのディレクトリ名を指定します。

さらに、プロジェクトを実行するために、追加で以下の作業が必要です。
1. virtual hostのdocument rootをpublic/に指定します。
2. logs/に書き込み権限を付与しておきます。

開発中にアプリケーションを実行する場合は、以下のコマンドで実行可能です。(phpのビルトインサーバ経由でアプリが実行されます)

php composer.phar start

また、以下のコマンドで、PHPUnitによる単体テストを実行できます。

php composer.phar test

これで全部です! 今すぐクールなアプリを作りましょう。

under the MIT License in Rob Allen(Original) and akamist.com(Translation).

docker-compose.ymlの内容

以下のように、php:7-alpineベースでphpのビルトインサーバを起動させるような設定になっています。本番サーバ用の設定ではなく、開発時の動作確認で実行するためのものです。

ymlファイルに記載されている通り、Dockerコンテナ作ってrunすると8080ポート経由でアクセスできます。また、環境変数dockerに"true"の文字列をセットしています。

version: '2'

volumes:
    logs:
        driver: local

services:
    slim:
        image: php:7-alpine
        working_dir: /var/www
        command: php -S 0.0.0.0:8080 -t public public/index.php
        environment:
            docker: "true"
        ports:
            - 8080:8080
        volumes:
            - .:/var/www
            - logs:/var/www/logs

.gitignoreの内容

vendorとlogsに書かれるログファイルをgitの管理外にしています。よくある設定なので特に解説はありません。

/vendor/
/logs/*
!/logs/README.md

phpunit.xmlの内容

testディレクトリのにUnitTestのコードを置くようになっています。テストスイートの名前がSlimSkeletonになっているので、実際のアプリケーションを作る場合は名前を変更した方が分かりやすいでしょう。

<phpunit bootstrap="vendor/autoload.php">
    <testsuites>
        <testsuite name="SlimSkeleton">
            <directory>tests</directory>
        </testsuite>
    </testsuites>
</phpunit>

このPHPUnitの実行ですが、composer.jsonを見るとscriptsにtestの定義があるので...

{
    "name": "slim/slim-skeleton",

    ...
    "scripts": {
        "start": "php -S localhost:8080 -t public public/index.php",
        "test": "phpunit"
    }
}

コマンドラインからcomposer testコマンドを実行すれば、単体テストが走ります。

$ composer test
> phpunit
PHPUnit 5.7.22 by Sebastian Bergmann and contributors.

...                                                                 3 / 3 (100%)

Time: 149 ms, Memory: 4.00MB

OK (3 tests, 7 assertions)

設定されていたテストスイート名を明示的に指定する場合は、"composer test"ではなく、vendor\bin\phpunit --testsuite SlimSkeletonと、vendor以下にあるスクリプト名を明示的に指定する必要があるようです。

$ vendor\bin\phpunit.bat --testsuite SlimSkeleton
PHPUnit 5.7.22 by Sebastian Bergmann and contributors.

...                                                                 3 / 3 (100%)

Time: 66 ms, Memory: 4.00MB

OK (3 tests, 7 assertions)

テストコードが保存されているtestsディレクトリには、2つのファイルが登録済みです。

tests/Functional/BaseTestCase.php
tests/Functional/HomepageTest.php

2つのファイルの関係ですが、BaseTestCaseクラス側は共通処理が書かれた基底クラスです。実際のUnitTestコードはHomepageTestの方に記載されています。

$ grep "^class" tests\Functional\BaseTestCase.php
class BaseTestCase extends \PHPUnit_Framework_TestCase

$ grep "^class" tests\Functional\HomepageTest.php
class HomepageTest extends BaseTestCase

HomepageTest.phpでは3つのテストケースが記載されています。それぞれ、特定のURLへアクセスして想定した文字列を含むHtmlBodyが帰ってくるかをチェックしています。このコードは、新たなテストを書くときの参考になりますね。

<?php

namespace Tests\Functional;

class HomepageTest extends BaseTestCase
{
    /**
     * Test that the index route returns a rendered response containing the text 'SlimFramework' but not a greeting
     */
    public function testGetHomepageWithoutName()
    {
        $response = $this->runApp('GET', '/');

        $this->assertEquals(200, $response->getStatusCode());
        $this->assertContains('SlimFramework', (string)$response->getBody());
        $this->assertNotContains('Hello', (string)$response->getBody());
    }

    /**
     * Test that the index route with optional name argument returns a rendered greeting
     */
    public function testGetHomepageWithGreeting()
    {
        $response = $this->runApp('GET', '/name');

        $this->assertEquals(200, $response->getStatusCode());
        $this->assertContains('Hello name!', (string)$response->getBody());
    }

    /**
     * Test that the index route won't accept a post request
     */
    public function testPostHomepageNotAllowed()
    {
        $response = $this->runApp('POST', '/', ['test']);

        $this->assertEquals(405, $response->getStatusCode());
        $this->assertContains('Method not allowed', (string)$response->getBody());
    }
}

メソッド内で呼ばれている$this->runApp()ですが、これは継承元のBaseTestCaseクラスで定義されています。内容は後述のpublic/index.phpとほぼ同じなので、ここでの説明は省略します(気になる人は確認してみてください)。

プログラムの確認

public/.htaccess

.htaccessの中身はコメントが大部分です。コメント部を除いた定義本体のみを抜粋すると、Rewrite定義が5つだけになります。

<IfModule mod_rewrite.c>
  RewriteEngine On

  RewriteCond %{REQUEST_URI}::$1 ^(/.+)/(.*)::\2$
  RewriteRule ^(.*) - [E=BASE:%1]

  RewriteCond %{REQUEST_FILENAME} !-f
  RewriteRule ^ index.php [QSA,L]
</IfModule>

全てのアクセスをindex.phpに処理させる、いわゆるFront-ControllerパターンのためのURLリライト定義です。

public/index.php

先ほど確認したpublic/.htaccessで全ての処理がindex.phpに到達する来ることが分かりました。ですので、public/index.phpがアプリケーション全体のエントリポイントとなります。

public/index.phpの中身は重要ですので、細かく見ていきます。

public/index.php

<?php
if (PHP_SAPI == 'cli-server') {
    // To help the built-in PHP dev server, check if the request was actually for
    // something which should probably be served as a static file
    $url  = parse_url($_SERVER['REQUEST_URI']);
    $file = __DIR__ . $url['path'];
    if (is_file($file)) {
        return false;
    }
}

先頭のif (PHP_SAPI == 'cli-server')は、PHPのビルトインサーバを使うときのお約束の書き方で、URLで指定されたファイルがpublic以下に存在していた場合は、該当ファイルをそのまま返す処理です。例えば、"public/asset/css/style.css"といった形で、publicディレクトリの下に、cssやjs,画像ファイルなどを置いておき、これらのファイルが指定された時に、該当ファイルの中身をそのまま返します。

PHPのビルトインサーバというのは、phpコマンドを"-S"オプション付きで実行すると,phpコマンドを簡易的なWebサーバとして実行させることができる機能の事です。ビルトインサーバを具体的にどうやって動かすかは、PHPUnitの時にも確認した、composer.jsonのscripts定義を見るとわかりますが、php -S localhost:8080 -t public public/index.phpというコマンドを実行すればよいです。(もちろんPHPUnitと同じようにcomposer startコマンドでもアプリを起動できます)

public/index.php (続き)

require __DIR__ . '/../vendor/autoload.php';

session_start();

requireで、ComposerのAutoload処理を読み込んでいます。これはcomposerによるパッケージ管理ではお約束の記述です。

次に出てくるsession_start()は、Slim3フレームワークの処理ではなくPHP標準で提供されている関数で、セッション開始の処理です。

public/index.php (続き)

// Instantiate the app
$settings = require __DIR__ . '/../src/settings.php';
$app = new \Slim\App($settings);

src/settings.phpの中身を読み込んで、その内容をSlimフレームワークのエントリポイントである、\Slim\Appクラスのコンストラクタに渡しています。

src/settings.phpの中身は後で確認しますが、ファイル内では配列をreturnしています。PHPのrequireは読み込んだファイルの中にreturnを実行した場合その内容を呼び元に返すという仕様なので、結果的にreturnされた配列が$settings変数に格納されます。

このようにして、起動の処理(index.php)と、パラメータファイル(src/settings.php)を別ファイルに分離し、起動時のsettingが変わってもindex.phpを書き換えなくて良いように工夫しています。

public/index.php (続き)

// Set up dependencies
require __DIR__ . '/../src/dependencies.php';

// Register middleware
require __DIR__ . '/../src/middleware.php';

// Register routes
require __DIR__ . '/../src/routes.php';

次に3つ続けてrequire文が続きます。3つのファイルにはそれぞれ、DIコンテナの初期化、認証やCsrf対策などの、前・後処理を行うミドルウェア、ルーティング処理が記載されています。

reuireされている3つのファイルは、アプリの内容に応じて書き換えていくファイルです。

public/index.php (続き)

// Run app
$app->run();

最後に\Slim\Appクラスのインスタンスだった$appオブジェクトの、\Slim\App::run()メソッドを呼び、アプリケーションの処理を実行します。

src/の下のファイル達

先ほど確認したpublic/index.phpでは、下記の4ファイルがrequireされていました。

  • src/settings.php
  • src/dependencies.php
  • src/middleware.php
  • src/routes.php

これら4ファイルの内容を確認していきます。

src/settings.php

まずはsrc/settings.phpを確認します。このファイルは\Slim\Appクラスのコンストラクタ引数になっていましたね。

src/settings.php

<?php
return [
    'settings' => [
        'displayErrorDetails' => true, // set to false in production
        'addContentLengthHeader' => false, // Allow the web server to send the content-length header

public/index.phpを確認したときにも説明したように、このファイル全体としては配列をreturnしています。配列の中には'settings'というキーを持つハッシュがあり、この中にSlim3フレームワークの設定を記述していきます。

displayErrorDetailsをtrueにすると、エラー発生時にSlim3フレームワークが、エラーハンドラへ詳細なエラーログを出力させるよう指示します。コメントにも記載されている通り、本番サーバへ展開する時には、この値をfalseに書き換える必要があります。

addContentLengthHeaderは、HTTP応答ヘッダのContent-Lengthの出力制御です。デフォルト値はtrueなのですがこのスケルトンでは明示的にfalseにしています。もし、New Relicのようなランタイムでの分析ツールを使っている場合はfalseにして置く必要があります。

src/settings.php (続き)

        // Renderer settings
        'renderer' => [
            'template_path' => __DIR__ . '/../templates/',
        ],

rendererでは、Viewテンプレートの保存ディレクトリを指定しています。

src/settings.php (続き)

        // Monolog settings
        'logger' => [
            'name' => 'slim-app',
            'path' => isset($_ENV['docker']) ? 'php://stdout' : __DIR__ . '/../logs/app.log',
            'level' => \Monolog\Logger::DEBUG,
        ],
    ],
];

loggerの項目では、monologのログ出力定義です。

docker環境で動作する場合はログを標準出力に出す必要があるため、環境変数"docker"が設定されている場合は、php://stdoutを出力先としています。ログ出力レベルはDEBUG以上なので、本番環境で動作する場合は変更が必要です。

src/dependencies.php

次は、DIコンテナの定義を行うdependencies.phpです。

src/dependencies.php (続き)

<?php
// DIC configuration

$container = $app->getContainer();

Slim3では、\Slim\App::getContainer()メソッドでDIコンテナであるpimpleパッケージのコンテナオブジェクトが取得できます。
public/index.phpで生成した$appインスタンスからコンテナオブジェクトを取得し、$container変数にセットします。

src/dependencies.php (続き)

// view renderer
$container['renderer'] = function ($c) {
    $settings = $c->get('settings')['renderer'];
    return new Slim\Views\PhpRenderer($settings['template_path']);
};

rendererの名称でViewレンダラであるSlim\Views\PhpRendererクラスのインスタンが取得できるようにしています。
Viewのテンプレートファイルは、先ほど\Slim\Appのコンストラクタでセットした、settingsのrendererから取得しています。

src/dependencies.php (続き)

// monolog
$container['logger'] = function ($c) {
    $settings = $c->get('settings')['logger'];
    $logger = new Monolog\Logger($settings['name']);
    $logger->pushProcessor(new Monolog\Processor\UidProcessor());
    $logger->pushHandler(new Monolog\Handler\StreamHandler($settings['path'], $settings['level']));
    return $logger;
};

loggerでは、Monolog\Loggerのインスタンスが取得できるようにします。こちらも設定情報をsettingsから取得しています。

src/middleware.php

middleware.phpはコメントだけが書かれた空のファイルです。
src/middleware.php

<?php
// Application middleware

// e.g: $app->add(new \Slim\Csrf\Guard);

コメントに記載があるように、Slimのミドルウェアを定義したい場合は、$app->add()にオブジェクトを渡すよう、定義を追加すればよいです。

src/routes.php

routes.phpは、ルーティングの定義を行うファイルです。ルーティングというのは、指定されたURLパスに対して、実行させる処理の対応付けを行う処理でのことです。

src/routes.php

<?php

use Slim\Http\Request;
use Slim\Http\Response;

// Routes

$app->get('/[{name}]', function (Request $request, Response $response, array $args) {
    // Sample log message
    $this->logger->info("Slim-Skeleton '/' route");

    // Render index view
    return $this->renderer->render($response, 'index.phtml', $args);
});

このスケルトンでは、route定義の中で直接ロジックを記載していますが、大きめのプログラムだとActionクラスを作って処理を委譲させるなど、役割を分けることなります。

URLの定義が'/[{name}]'となっているため、http://localhost/alicehttp://localhost/bob、もしくはnameが省略されたhttp://localhost/のパターンでアクセスされるとルーティング対象となり処理が行われます。

指定されたnameは、コールバック関数の第三引数である、$argsにセットされます。
ここでは、monologでログだけ出力したのち、そのままviewに処理を委譲しています。

$this->rendererは、DIコンテナでrendererを定義していたため、Slim\Views\PhpRendererクラスのインスタンスが返されます。その後、Slim\Views\PhpRenderer::render()メソッドをコールし、その結果がクライアント(ブラウザ)に返されます。

ビューテンプレートであるindex.phtmlファイルは、settings.phpで記載した/../templates/ディレクトリに存在します。

ビュー

先ほどのルート定義で、index.phtmlが指定されたので、templates/index.phtmlを確認してみます。

templates/index.phtml

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8"/>
        <title>Slim 3</title>
        <link href='//fonts.googleapis.com/css?family=Lato:300' rel='stylesheet' type='text/css'>
        <style>
            body {
                margin: 50px 0 0 0;
                padding: 0;
                width: 100%;
                font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
                text-align: center;
                color: #aaa;
                font-size: 18px;
            }

            h1 {
                color: #719e40;
                letter-spacing: -3px;
                font-family: 'Lato', sans-serif;
                font-size: 100px;
                font-weight: 200;
                margin-bottom: 0;
            }
        </style>
    </head>
    <body>
        <h1>Slim</h1>
        <div>a microframework for PHP</div>

        <?php if (isset($name)) : ?>
            <h2>Hello <?= htmlspecialchars($name); ?>!</h2>
        <?php else: ?>
            <p>Try <a href="http://www.slimframework.com">SlimFramework</a></p>
        <?php endif; ?>
    </body>
</html>

少し長いですがほとんどがcss等のhtmlデザイン定義です。プログラムに関係するのはhtml>bodyの中にある下記の5行だけなので、この部分をピックアップします。

templates/index.phtml (抜粋)

<?php if (isset($name)) : ?>
    <h2>Hello <?= htmlspecialchars($name); ?>!</h2>
<?php else: ?>
    <p>Try <a href="http://www.slimframework.com">SlimFramework</a></p>
<?php endif; ?>

ルーティングから、render($response, 'index.phtml', $args);の形で、パラメータ$argsが渡されていました。

$argsは連想配列で$args['name']の形でパラメータがセット渡されているのですが、viewではこれを$name変数として受け取れます。
値がセットされていれば、htmlspecialchars()でエスケープしたうえで、画面に表示しています。

まとめ

以上で、Slim3プロジェクトのひな型である、slim/slim-skeletonパッケージの調査は完了です。

Slim3フレームワーク自体が非常にシンプルなため、スケルトンプロジェクトも短時間で全ソースが確認できるレベルのシンプルさを保っています。

DBアクセスや、Csrf対策、認証処理など、他の大型フレームワークに用意されているようなライブラリはSlimでは用意されていません。これは欠点ではなく、むしろ開発者が自由にパッケージを選定して好みのものを利用すればよいという、自由度を提示してくれています。

今回の記事を機会に、ぜひ自分が考えたアイデアを形にしてみてください。


Amazonでおトクに買い物する方法
AmazonチャージでポイントGET


Amazonは買いもの前にAmazonギフト券をチャージしてポイントをゲットしないと損!

こちらもおススメ

3 thoughts on “Slim3のスケルトンプロジェクト、slim/slim-skeletonの解説

  1. この記事のおかげでslim3でサイトを構築する事が出来ました。
    ありがとうございます。

    slim4のskeletonは更に複雑になっており解説サイトも皆無です。
    slim4も是非ともお願い致します。

  2. 以前slim3を使用してサイトを作成する時に凄く勉強になりました!
    可能であるならばslim4のskeletonも解説頂けないでしょうか?
    何卒宜しくお願い致します!

  3. このように解説してくださってるサイトが他になく、非常に面白く、また大変役に立ち助かりました。ありがとうございます。

sugerfree へ返信する コメントをキャンセル

メールアドレスが公開されることはありません。 が付いている欄は必須項目です