MVC(Laravel)ロジックを追加する場所


137

CRUD操作を実行したり、特定の方法で関係を変更したりするたびに、別のことも実行したいとします。たとえば、誰かが投稿を公開するときはいつでも、分析のためにテーブルに何かを保存したいと思います。おそらく最良の例ではありませんが、一般的にはこの「グループ化された」機能がたくさんあります。

通常、私はこのタイプのロジックがコントローラーに組み込まれるのを見ます。多くの場所でこの機能を再現するまでは、それで十分です。パーシャルの作成、APIの作成、ダミーコンテンツの生成を開始すると、DRYを維持することが問題になります。

これを管理するために私が見た方法は、イベント、リポジトリ、ライブラリ、およびモデルへの追加です。それぞれについての私の理解は次のとおりです。

サービス:これは、ほとんどの人がおそらくこのコードを配置する場所です。サービスに関する私の主な問題は、特定の機能を見つけるのが難しい場合があり、人々がEloquentの使用に集中すると、それらが忘れられるように感じることです。メソッドを呼び出す必要があることをどのように知ることができますかpublishPost()ライブラリでます$post->is_published = 1か?

私がこれがうまく機能している唯一の条件は、サービスのみを使用している(そしてコントローラーからEloquentに何らかの方法でアクセスできないことが理想的です)場合です。

最終的には、リクエストが一般的にモデル構造に従っている場合、余分な不要なファイルがたくさん作成されるだけのようです。

リポジトリ:私が理解しているところから、これは基本的にはサービスのようですが、ORMを切り替えることができるインターフェースがあるので、私は必要としません。

イベント:モデルイベントは常にEloquentメソッドで呼び出されるため、通常どおりにコントローラーを記述できるため、これはある意味で最もエレガントなシステムだと思います。私はこれらが乱雑になるのを見ることができます、そして誰かが重要なカップリングのためにイベントを使用している大規模なプロジェクトの例を持っているなら、それを見てみたいです。

モデル:従来は、CRUDを実行し、重要な結合も処理するクラスがありました。CRUD +のすべての機能を知っていたので、これは実際に簡単になりました。

シンプルですが、MVCアーキテクチャでは、これは通常私が行っているとは言えません。ある意味では、検索するのが少し簡単で、追跡するファイルが少ないので、サービスよりもこちらを好みます。少し混乱するかもしれません。この方法の欠点と、ほとんどの人がそうしない理由を聞きたいのですが。

各方法の長所/短所は何ですか?何か不足していますか?


3
質問を最小限に抑えることができますか?
アルファ

3
また、これを確認することもできます
アルファ

1
「$ post-> is_published = 1だけを実行できるときに、ライブラリ内のメソッドpublishPost()を呼び出す必要があることをどのように知ることができますか?」ドキュメンテーション?
ceejayoz

雄弁とORMSの美しさの1つは、多くのドキュメントなしでそれらを使用する方が簡単ですか?
サブリナレゲット

1
これを投稿してくれてありがとう。私は同じ問題に苦労しており、あなたの投稿と回答は非常に役に立ちました。最終的に私は、Laravelが、Ruby-on-Railsの速くて汚いWebサイトを超えたあらゆるものに対して、優れたアーキテクチャを提供しないことを決定しました。どこでもトレート、どこでもクラス機能と大量の自動魔法のゴミを見つけるのは困難です。 ORMは機能していません。使用している場合は、おそらくNoSQLを使用しているはずです。
Alex Barker

回答:


171

あなたが提示するすべてのパターン/アーキテクチャは、あなたが従う限り非常に便利だと思います SOLIDの原則に。

ロジック追加する場所については、単一責任の原則を参照することが重要だと思います。また、私の答えは、あなたが中/大規模のプロジェクトに取り組んでいることを考慮しています。それがページに何かを投げるプロジェクトである場合は、この答えを忘れてすべてをコントローラーまたはモデルに追加してください。

短い答えは次のとおりです。それは、あなたにとって意味のある場所です(サービスについて)

長い答え:

コントローラーコントローラーの責任は何ですか?もちろん、すべてのロジックをコントローラーに入れることができますが、それはコントローラーの責任ですか?そうは思いません。

私にとって、コントローラーはリクエストを受け取ってデータを返す必要があり、これは検証を配置したり、dbメソッドを呼び出したりする場所ではありません。

モデル:これは、ユーザーが投稿を登録または投票数を更新したときにウェルカムメールを送信するなどのロジックを追加するのに適した場所ですか?コードの別の場所から同じメールを送信する必要がある場合はどうなりますか?静的メソッドを作成しますか?そのメールに別のモデルの情報が必要な場合はどうなりますか?

モデルはエンティティを表す必要があると思います。Laravelでは、私だけのようなものを追加するモデルクラスを使用しfillableguardedtableとの関係を(これは私がリポジトリパターンを使用しているためで、それ以外のモデルも持っているだろうsaveupdatefind、などの方法を)。

リポジトリ(Repository Pattern):最初はこれに戸惑いました。そして、あなたのように、私は「まあ、私はMySQLを使用している」と思った。

しかし、私はリポジトリパターンを使用することの長所と短所のバランスをとっており、今ではそれを使用しています。私はと思い、今、この瞬間に、私はMySQLを使用する必要があります。しかし、3年後にMongoDBのようなものに変更する必要がある場合、ほとんどの作業は完了しています。1つの追加インターフェースと1つの$app->bind(«interface», «repository»)

イベント(オブザーバーパターン):イベントは、任意のクラスでいつでもスローできるものに役立ちます。たとえば、ユーザーに通知を送信することを考えてください。必要に応じて、イベントを発生させて、アプリケーションの任意のクラスで通知を送信します。次に、UserNotificationEventsユーザー通知のすべての発生したイベントを処理するようなクラスを持つことができます。

サービス:これまでは、コントローラーまたはモデルにロジックを追加する選択肢がありました。私にとって、Services内にロジックを追加することはすべて理にかなっています。正直なところ、サービスはクラスのファンシーな名前です。そして、あなたはあなたのアプリケーションの中であなたにとって理にかなっただけ多くのクラスを持つことができます。

この例を見てみましょう:少し前に、Googleフォームのようなものを開発しました。私は開始CustomFormServiceとなってしまったCustomFormServiceCustomFormRenderCustomFieldServiceCustomFieldRenderCustomAnswerServiceCustomAnswerRender。どうして?それは私には理にかなっているので。チームで作業する場合は、チームにとって意味のある場所にロジックを配置する必要があります。

サービスvsコントローラー/モデルを使用する利点は、単一のコントローラーまたは単一のモデルによる制約を受けないことです。アプリケーションの設計とニーズに基づいて、必要な数のサービスを作成できます。さらに、アプリケーションの任意のクラス内でサービスを呼び出す利点を追加します。

これは長くなりますが、私がアプリケーションをどのように構成したかをお見せしたいと思います。

app/
    controllers/
    MyCompany/
        Composers/
        Exceptions/
        Models/
        Observers/
        Sanitizers/
        ServiceProviders/
        Services/
        Validators/
    views
    (...)

各フォルダを特定の機能に使用します。たとえば、ValidatorsディレクトリBaseValidatorには$rulesおよびに基づいて検証の処理を担当するクラスが含まれています$messages特定のバリデータ(各モデルに対して通常1)です。このコードをサービス内に配置することも簡単にできますが、サービス内でのみ使用する場合でも、このための特定のフォルダーを用意することは理にかなっています(現時点では)。

次の記事を読むことをお勧めします。

Dayle Rees(CodeBrightの作者)による型崩し:ニーズに合わせていくつか変更を加えましたが、ここですべてをまとめました。

Chris Gooseyによるリポジトリとサービスを使用してLaravelでコードを分離する:この投稿では、サービスとリポジトリパターンとは何か、およびそれらがどのように組み合わされるかについて説明しています。

Laracastには、リポジトリを簡素化し単一の責任を負うこともできます。


3
素晴らしい説明。これが私が現在立っているところです。現在のプロジェクトでは、ビジネスロジックをモデルに組み込んでいますが、実際には非常にうまく機能しています。SOLIDを少し修正する必要があることは確かですが、まだ問題は解決していません。高速で、多少汚れていますが、これまでのところ、プロジェクトは非常に乾燥しているため、非常に保守可能です。彼らが仕事を成し遂げるので、私は現時点では間違いなく彼らにこだわっていますが、将来のプロジェクトではおそらく、リポジトリがなったように思える標準的なものを何でも使うでしょう。
サブリナレゲット2014

2
あなたに意味のある方法を見つけてよかったです。今日の前提に注意してください。私は3年以上プロジェクトに取り組み、5000行以上のコードを含むコントローラーとモデルになりました。プロジェクトで頑張ってください。
ルイス・クルーズ

ちょっと汚いですが、モデルが巨大になるのを避けるために特性を使用することを考えていました。そう
すれば

この記事は、サービスを使用することが理にかなっている場合によく説明します。あなたのフォームの例では、サービスを使用することは理にかなっていますが、彼はそれをどのように使用するかを説明しています。justinweiss.com/articles/where-do-you-put-your-code
サブリナレゲット

説明が本当に好きです。私が持っている1つの質問があります:コントローラーに検証を配置しないと述べたので、検証を行うのに最適な場所はどこだと思いますか?多くの人がそれを拡張されたRequestクラス(そして現在私たちがしていること)に入れることを提案していますが、httpリクエストだけでなく、artisanコマンドなどでも検証したい場合は、本当に良い場所でしょうか?
kingshark

24

自分の質問に対する回答を投稿したかったのです。私はこれについて何日も話すことができましたが、確実に理解できるように、これをすばやく投稿してみます。

最終的に、Laravelが提供する既存の構造を利用することになりました。つまり、ファイルを主にモデル、ビュー、コントローラーとして保持しました。また、実際にはモデルではない再利用可能なコンポーネント用のLibrariesフォルダーもあります。

サービス/ライブラリでモデルをラップしませんでした。提供されたすべての理由から、サービスを使用するメリットを100%納得させることができませんでした。私が間違っているかもしれませんが、私が見る限り、モデルで作業するときに作成し、切り替える必要のある余分なほぼ空のファイルがたくさんあり、雄弁な使用の利点を本当に減らします(特に、モデルの取得に関して) 、例えば、ページネーション、スコープなどを使用する)。

ビジネスロジックをモデルに入れ、コントローラーから直接雄弁にアクセスします。ビジネスロジックがバイパスされないようにするために、いくつかのアプローチを使用しています。

  • アクセサーとミューテーター: Laravelには優れたアクセサーとミューテーターがあります。投稿がドラフトから公開に移動するたびにアクションを実行したい場合は、関数setIsPublishedAttributeを作成し、そこにロジックを含めることでこれを呼び出すことができます
  • 作成/更新などのオーバーライド:モデルのEloquentメソッドをいつでもオーバーライドして、カスタム機能を含めることができます。これにより、任意のCRUD操作で機能を呼び出すことができます。編集:新しいLaravelバージョンでのcreateのオーバーライドにはバグがあると思います(したがって、ブートに登録されているイベントを使用します)
  • 検証:同じように検証をフックします。たとえば、CRUD関数をオーバーライドして検証を実行し、必要に応じてアクセサー/ミューテーターも実行します。詳細については、Esensiまたはdwightwatson / validatingを参照してください。
  • マジックメソッド:モデルの__getおよび__setメソッドを使用して、必要に応じて機能にフックします
  • Eloquentの拡張:すべての更新/作成で実行したいアクションがある場合は、Eloquentを拡張して複数のモデルに適用することもできます。
  • イベント:これは単純明快で、これを行う場所についても一般的に合意されています。イベントの最大の欠点は、例外を追跡するのが難しいことです(Laravelの新しいイベントシステムの新しいケースではない可能性があります)。また、イベントが呼び出されたときではなく、何を実行するかによってイベントをグループ化することも好きです。たとえば、メールを送信するイベントをリッスンするMailSenderサブスクライバーを用意します。
  • Pivot / BelongsToManyイベントの追加:最も長く苦労したことの1つは、belongsToMany関係の変更に動作を関連付ける方法でした。たとえば、ユーザーがグループに参加するたびにアクションを実行します。これでカスタムライブラリの調整はほぼ完了です。まだ公開していませんが、機能しています!すぐにリンクを投稿しようとします。編集私はすべてのピボットを通常のモデルにしてしまい、私の人生はとても簡単になりました...

モデルの使用に関する人々の懸念に対処する:

  • 組織:はい、モデルにロジックを追加すると長くなる可能性がありますが、一般に、モデルの75%はまだかなり小さいことがわかりました。より大きなファイルを整理することを選択した場合、トレイトを使用してそれを行うことができます(たとえば、必要に応じてPostScopes、PostAccessors、PostValidationなどのいくつかのファイルを含むモデルのフォルダーを作成します)。私はこれが必ずしもそのための特徴であるとは限らないことを知っていますが、このシステムは問題なく機能します。

追記:モデルをサービスで包むのは、多くのツールを備えたスイスのアーミーナイフを持ち、その周りに基本的に同じことを行う別のナイフを構築するようなものだと思いますか?ええ、時々あなたはブレードをテープで留めるか、2つのブレードが一緒に使用されることを確認したいかもしれません...しかし、通常それを行う他の方法があります...

サービスを使用する場合:この記事は、サービスを使用する場合のすばらしい例を明確に示しています(ヒント:それほど頻繁ではありません)。基本的に、オブジェクトがライフサイクルの奇妙な部分で複数のモデルまたはモデルを使用する場合、それは理にかなっていると言います。http://www.justinweiss.com/articles/where-do-you-put-your-code/


2
興味深い、有効な考え。しかし、私は興味があります-データベースに関連付けられているEloquentに関連付けられているモデルに関連付けられている場合、ビジネスロジックをどのように単体テストしますか?
JustAMartin 2015

code.tutsplus.com/tutorials/…または、さらに細かく分解したい場合は、私が言ったようなイベントを使用できます
Sabrina Leggett 2015

1
@JustAMartinは、単体テストでデータベースを使用できないことを確信していますか?それをしない理由は何ですか?多くの人は、単体テストでデータベースを使用しても大丈夫であることに同意します。(Martin Fowler、martinfowler.com / bliki / UnitTest.htmlを含む:「外部リソースにdoubleを使用することは絶対的なルールとして扱いません。リソースとの通信が安定していて十分高速である場合は、しない理由はありません。ユニットテストでそれを行う」)
Alex P.

@ AlexP11223はい、それは理にかなっています。SQLiteをテストデータベースとして統合しようとしましたが、SQLiteにはいくつかの重大な制限があり、Laravelの移行とカスタムクエリ(存在する場合)で対処する必要がありますが、一般的にはうまくいきました。もちろん、これらは厳密に単体テストではなく機能テストですが、その方がより効率的です。それでも、(完全な単体テストとして)モデルを完全に分離してテストする場合は、かなりの量の追加コード(モックなど)が必要になることがあります。
JustAMartin 2016

22

コントローラとモデル間のロジックを作成するために使用するのは、サービスレイヤーを作成することです。基本的に、これは私のアプリ内のすべてのアクションのフローです。

  1. コントローラーはユーザーの要求されたアクションと送信されたパラメーターを取得し、すべてをサービスクラスに委任します。
  2. サービスクラスは、操作に関連するすべてのロジックを実行します:入力検証、イベントログ、データベース操作など...
  3. モデルは、フィールド、データ変換、および属性検証の定義の情報を保持します。

これが私のやり方です:

これは、何かを作成するコントローラーのメソッドです。

public function processCreateCongregation()
{
    // Get input data.
    $congregation                 = new Congregation;
    $congregation->name           = Input::get('name');
    $congregation->address        = Input::get('address');
    $congregation->pm_day_of_week = Input::get('pm_day_of_week');
    $pmHours                      = Input::get('pm_datetime_hours');
    $pmMinutes                    = Input::get('pm_datetime_minutes');
    $congregation->pm_datetime    = Carbon::createFromTime($pmHours, $pmMinutes, 0);

    // Delegates actual operation to service.
    try
    {
        CongregationService::createCongregation($congregation);
        $this->success(trans('messages.congregationCreated'));
        return Redirect::route('congregations.list');
    }
    catch (ValidationException $e)
    {
        // Catch validation errors thrown by service operation.
        return Redirect::route('congregations.create')
            ->withInput(Input::all())
            ->withErrors($e->getValidator());
    }
    catch (Exception $e)
    {
        // Catch any unexpected exception.
        return $this->unexpected($e);
    }
}

これは、操作に関連するロジックを実行するサービスクラスです。

public static function createCongregation(Congregation $congregation)
{
    // Log the operation.
    Log::info('Create congregation.', compact('congregation'));

    // Validate data.
    $validator = $congregation->getValidator();

    if ($validator->fails())
    {
        throw new ValidationException($validator);
    }

    // Save to the database.
    $congregation->created_by = Auth::user()->id;
    $congregation->updated_by = Auth::user()->id;

    $congregation->save();
}

そしてこれは私のモデルです:

class Congregation extends Eloquent
{
    protected $table = 'congregations';

    public function getValidator()
    {
        $data = array(
            'name' => $this->name,
            'address' => $this->address,
            'pm_day_of_week' => $this->pm_day_of_week,
            'pm_datetime' => $this->pm_datetime,
        );

        $rules = array(
            'name' => ['required', 'unique:congregations'],
            'address' => ['required'],
            'pm_day_of_week' => ['required', 'integer', 'between:0,6'],
            'pm_datetime' => ['required', 'regex:/([01]?[0-9]|2[0-3]):[0-5]?[0-9]:[0-5][0-9]/'],
        );

        return Validator::make($data, $rules);
    }

    public function getDates()
    {
        return array_merge_recursive(parent::getDates(), array(
            'pm_datetime',
            'cbs_datetime',
        ));
    }
}

Laravelアプリのコードを整理するために使用するこの方法の詳細については、https//github.com/rmariuzzo/Pitimiを参照してください。


サービスは、私の投稿ではライブラリと呼んでいるようです。複数のORMSを使用する必要がない場合、これはリポジトリよりも優れていると思いますが、問題は、プロジェクト全体を移行する必要があることです(イベントとは関係ありません)。結局のところ、モデル構造をミラーリングするだけなので、これらのすべての追加ファイルを取得できます。なぜそれをモデルに含めないのですか?少なくともその方法では、余分なファイルはありません。
サブリナレゲット2014年

それは興味深い質問です@SabrinaGelbart、私はモデルにデータベースエンティティを表現させ、ロジックを保持しないように教えられました。それが、サービスという名前の追加のファイルを作成した理由です。すべてのロジックと追加の操作を保持するためです。前に説明したイベントの全体的な意味はわかりませんが、サービスとLaravelのイベントを使用すると、すべてのサービスメソッドでイベントの開始と終了を発生させることができると思います。このようにして、あらゆるイベントをロジックから完全に切り離すことができます。どう思いますか?
Rubens Mariuzzo 2014年

私はモデルについても教えられました...なぜか(おそらく依存関係の問題)についての良い説明を得るのは良いでしょうか?
サブリナレゲット2014年

私はこのアプローチが好きです!私はインターネットを検索して、モデルのロジックをどのように処理する必要があるかを理解し、リポジトリを調べましたが、複雑すぎて、少し使用すると役に立たないように見えました。サービスは良い考えです。私の質問は、サービスのフォルダーをappフォルダーに作成した後、それをbootstrap / start.phpまたは起動用のどこかに含める必要がありますか?@RubensMariuzzo。アプリ全体で自動的に利用可能になりますか?したがって、CongregationService :: getCongregations();を使用できます。??
Oguzhan 2014年

1
もしあなたがしていることがすべてであるなら$congregation->save();、多分あなたはリポジトリを必要としないでしょう。ただし、データアクセスのニーズが時間の経過とともに増加する場合があります。あなたはのためのニーズを持って始めること$congregation->destroyByUser()$congregationUsers->findByName($arrayOfSelectedFields);のように。サービスをデータアクセスのニーズから切り離してみませんか。アプリの残りの部分でリポジトリから返されたオブジェクト/配列を操作し、操作/フォーマットなどを処理するだけです...リポジトリは大きくなります(ただし、それらを異なるファイルに分割すると、最終的にプロジェクトの複雑さがどこかに存在する必要があります)。
プログラハンマー2015年

12

私の意見では、Laravelにはすでにビジネスロジックを保存するための多くのオプションがあります。

短い答え:

  • laravelのRequestオブジェクトを使用して入力を自動的に検証し、データをリクエストに永続化します(モデルを作成します)。ユーザー入力のすべてが直接提供されていますのでにおける要求、私はそれがここにこれを実行することは理にかなって考えています。
  • laravelのJobオブジェクトを使用して、個々のコンポーネントを必要とするタスクを実行してから、それらをディスパッチするだけです。Jobサービスクラスも含まれていると思います。ビジネスロジックなどのタスクを実行します。

より長い答え:

必要に応じてリポジトリを使用: リポジトリをリポジトリは過大な負荷にバインドされており、ほとんどの場合、単にaccessorモデルのとして使用されます。私はそれらが間違いなくいくつかの用途を持っているように感じますが、あなたが必要とする大規模なアプリケーションを開発しているのでない限り laravelを完全に破棄できるようにその柔軟性をと、リポジトリから離れてください。後で感謝します。コードははるかに簡単になります。

PHPフレームワークを変更する可能性があるの、それともlaravelがサポートしていないデータベースタイプに変更するのか、考えてみてください。

答えが「おそらくない」の場合は、リポジトリパターンを実装しないでください。

上記に加えて、EloquentのようなすばらしいORMの上にパターンをたたかないでください。必要のない複雑さを追加するだけで、まったくメリットがありません。

サービスの使用は控えめに: サービスクラスは私にとって、特定の依存関係で特定のタスクを実行するためのビジネスロジックを格納する場所にすぎません。Laravelには「ジョブ」と呼ばれるこれらのボックスがあり、カスタムサービスクラスよりもはるかに柔軟性があります。

Laravelには、MVC論理的な問題に対する包括的なソリューションがあるように感じます。それは単なる問題または組織です。

例:

リクエスト

namespace App\Http\Requests;

use App\Post;
use App\Jobs\PostNotifier;
use App\Events\PostWasCreated;
use App\Http\Requests\Request;

class PostRequest extends Request
{
    /**
     * Determine if the user is authorized to make this request.
     *
     * @return bool
     */
    public function authorize()
    {
        return true;
    }

    /**
     * Get the validation rules that apply to the request.
     *
     * @return array
     */
    public function rules()
    {
        return [
            'title'       => 'required',
            'description' => 'required'
        ];
    }

    /**
     * Save the post.
     *
     * @param Post $post
     *
     * @return bool
     */
    public function persist(Post $post)
    {
        if (!$post->exists) {
            // If the post doesn't exist, we'll assign the
            // post as created by the current user.
            $post->user_id = auth()->id();
        }

        $post->title = $this->title;
        $post->description = $this->description;

        // Perform other tasks, maybe fire an event, dispatch a job.

        if ($post->save()) {
            // Maybe we'll fire an event here that we can catch somewhere else that
            // needs to know when a post was created.
            event(new PostWasCreated($post));

            // Maybe we'll notify some users of the new post as well.
            dispatch(new PostNotifier($post));

            return true;
        }

        return false;
    }
}

コントローラー

namespace App\Http\Controllers;

use App\Post;
use App\Http\Requests\PostRequest;

class PostController extends Controller
{

   /**
    * Creates a new post.
    *
    * @return string
    */
    public function store(PostRequest $request)
    {
        if ($request->persist(new Post())) {
            flash()->success('Successfully created new post!');
        } else {
            flash()->error('There was an issue creating a post. Please try again.');
        }

        return redirect()->back();
    }

   /**
    * Updates a post.
    *
    * @return string
    */
    public function update(PostRequest $request, $id)
    {
        $post = Post::findOrFail($id);

        if ($request->persist($post)) {
            flash()->success('Successfully updated post!');
        } else {
            flash()->error('There was an issue updating this post. Please try again.');
        }

        return redirect()->back();
    }
}

上記の例では、リクエスト入力は自動的に検証され、persistメソッドを呼び出して新しいPostを渡すだけで済みます。読みやすさと保守性は、常に複雑で不要なデザインパターンよりも優先されるべきだと思います。

投稿がすでに存在するかどうかを確認し、必要に応じて代替ロジックを実行できるため、投稿の更新にもまったく同じ永続化メソッドを利用できます。


しかし、ジョブはキューに入れられることを「想定」されていませんか?時々、キューに入れたいが常にではない場合があります。代わりにコマンドを使用しないのはなぜですか?コマンド、イベント、またはキューとして実行されるビジネスロジックを記述したい場合はどうでしょうか。
サブリナレゲット

1
ジョブをキューに入れる必要はありません。ShouldQueueLaravelが提供するジョブにインターフェースを実装することにより、それを指定します。コマンドまたはイベントでビジネスロジックを記述したい場合は、それらのイベント/コマンド内でジョブを起動するだけです。Laravelsの仕事は非常に柔軟ですが、結局のところ、それらは単なるサービスクラスです。
スティーブバウマン2017年
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.