tyoshikawa1106のブログ

- Force.com Developer Blog -

SFDC:Community Cloud開発向け - Apex TriggerとConnect APIで試行錯誤した話

Community Cloudで顧客とChatterでやりとりする際に相手の投稿が通知されない問題に遭遇することがあると思います。例えば自分のユーザプロファイルに直接投稿してもらったり、メンションを指定してくれれば通知メールは届きますが、B2Cの顧客にそんなことをお願いするのは難しいと思います。


そこでApexトリガで対応するのはどうかと下記のトリガ処理をつくってみました。(サンプル処理なので本番向けではありません。)

trigger FeedItemTrigger on FeedItem (after insert) {

    private FeedItemTriggerHandler handler = new FeedItemTriggerHandler();

    if (Trigger.isAfter) {
        if (Trigger.isInsert) {
            // コミュニティユーザがサポートフィードに投稿したことを担当者に通知
            handler.notificationCustomerChatterPost(Trigger.new);
        }
    }
}
public with sharing class FeedItemTriggerHandler {

    /**
     * コンストラクタ
     */
    public FeedItemTriggerHandler() {
        
    }

    /**
     * コミュニティユーザがサポートフィードに投稿したことを担当者に通知
     */
    public void notificationCustomerChatterPost(List<FeedItem> feedItems) {
        // サポートフィードへの投稿を通知対象として取得
        List<FeedItem> targetFeedItems = new List<FeedItem>();
        for (FeedItem f : feedItems) {
            // オブジェクト判定
            if (String.isNotEmpty(f.ParentId) && f.ParentId.getSObjectType().getDescribe().getName() == 'Support__c') {
                targetFeedItems.add(f);
            }
        }
        // Chatterに投稿
        for (FeedItem f : targetFeedItems) {
            // new connect api
            ConnectApi.MentionSegmentInput mentionSegmentInput = new ConnectApi.MentionSegmentInput();    
            ConnectApi.FeedItemInput feedItemInput = new ConnectApi.FeedItemInput();
            ConnectApi.MessageBodyInput messageBodyInput = new ConnectApi.MessageBodyInput();
            ConnectApi.TextSegmentInput textSegmentInput = new ConnectApi.TextSegmentInput();
            ConnectApi.FeedElementCapabilitiesInput feedElementCapabilitiesInput = new ConnectApi.FeedElementCapabilitiesInput();
            // new List
            messageBodyInput.messageSegments = new List<ConnectApi.MessageSegmentInput>();
            // Post Message
            String post = '【システム通知】新しいメッセージが投稿されました。';
            // textSegment set
            textSegmentInput.text = post;
            messageBodyInput.messageSegments.add(textSegmentInput);
            
            // feedItem set
            feedItemInput.body = messageBodyInput;
            feedItemInput.feedElementType = ConnectApi.FeedElementType.FeedItem;

            // Use a group ID for the subject ID.
            feedItemInput.subjectId = '<TARGET_USER_ID>';

            // capabilities
            feedElementCapabilitiesInput.link = new ConnectApi.LinkCapabilityInput();
            feedElementCapabilitiesInput.link.url = 'https://<YOUR_DOMAIN>.salesforce.com/' + f.parentId;
            feedElementCapabilitiesInput.link.urlName = 'メッセージリンク';
            feedItemInput.capabilities = feedElementCapabilitiesInput;

            // Mention a group.
            mentionSegmentInput.id = 'TARGET_USER_ID';
            messageBodyInput.messageSegments.add(mentionSegmentInput);

            // Chatter Post
            ConnectApi.FeedElement feedElement = ConnectApi.ChatterFeeds.postFeedElement(Network.getNetworkId(), feedItemInput);
        }
    }
}

顧客がコミュニティから投稿すると・・・
f:id:tyoshikawa1106:20170305212000p:plain


Apexトリガの処理が実行され、所有者ユーザのプロファイルにメンション付きで通知投稿が行われます。それにより次のようなメール通知が届きます。
f:id:tyoshikawa1106:20170305212147p:plain


Salesforceにログインしてメール内のリンクをクリックすると対象の投稿ページに移動できます。
f:id:tyoshikawa1106:20170305212306p:plain


これで顧客の投稿時に通知メールを送信できるのではと考えました。

やってみてわかったこと

1. FeedItemトリガだけではコメント投稿時に処理が実行されない。

FeedItemオブジェクトはChatterの投稿用オブジェクトなのでコメント投稿時には実行されません。コメント投稿時には標準の通知機能が働くので大きな問題ではないですが、正しくやるにはコメントオブジェクトにもトリガを用意した方が安全かもしれません。

2. 社内用Chatterページにはコミュニティ側の投稿は表示されない

コミュニティ内で社内ユーザにメンション付きで投稿しても社内ユーザのプロファイルページには表示されませんでした。ここも大きな問題にはならないと思いますが地味に落とし穴です。
f:id:tyoshikawa1106:20170305212746p:plain

3. 社内のChatterグループはコミュニティ側ではアクセスできない。

例えばコミュニティユーザの連絡通知投稿をひとつにまとめるためにChatterグループを用意しても、コミュニティ側ではアクセスできません。コミュニティ側でChatterグループを作成すればアクセスは可能ですが、コミュニティに移動しないと見れませんし本来の用途としてはあまり意味のないものになってしまいました。
f:id:tyoshikawa1106:20170305213054j:plain

4. Community Login UserライセンスでもConnect APIは利用可能

そもそもライセンス的に実装可能なのか不安だったのですが、これは問題ありませんでした。APIの利用を許可の設定はプロファイルで指定できます。Community Login Userライセンスが問題なければ他のライセンスでもほぼ問題ないはずです。ただしConnect APIには利用時の制約があるので注意して下さい。

Salesforce Developers

5. 通知のための処理にApexトリガは向いていない..と思う

最終的に通知メールを送信するためにApexトリガを使うのは向いていないと思います。投稿時にトリガで通知用の別投稿を行うということがそもそも効率的ではないと思います。投稿機能自体を自作してしまい、投稿時にかならずメンションを付ける処理にした方が、投稿/コメント関係なく通知メールが届きますし、通知メールから直接返信も可能になります。


このトリガをつかった方法で一番の障壁は顧客のプロフィールページにApexトリガから投稿した内容が表示されてしまうことです。
f:id:tyoshikawa1106:20170305214802p:plain


これも致命的な問題では無いと思うのですがあまり綺麗ではないと思います。


こんな感じでいろいろ試行錯誤してみたのですが、ApexトリガでConnectAPIをつかって通知するのはイマイチな方法だと感じました。投稿機能が自作する方が綺麗なシステムになると思います。

Connect API (Chatter in Apex) のサンプル (Spring'16バージョン)

少し前のバージョン用ですがまだ利用できると思います。

Demo Video1

Demo Video2

Demo Video3

SFDC:Spring'17 - 新しい一括処理ジョブページ

Spring'17 へのアップデートで新しい一括処理ジョブページが利用できるようになりました。

f:id:tyoshikawa1106:20170228215218p:plain

Apex 一括処理ジョブの状況監視


設定のApexジョブのページに行くとリンクが追加されています。
f:id:tyoshikawa1106:20170228215406p:plain


こちらが新しいジョブページです。
f:id:tyoshikawa1106:20170228215505p:plain


特定の一括処理クラスで [詳細情報] をクリックすると、次の情報を含む一括処理クラスの親ジョブが表示されるそうです。

  • 状況
  • 実行日と完了日
  • 各一括処理の経過時間
  • 一括処理された数
  • 一括処理に失敗した数


実行時に表示された内容です。
f:id:tyoshikawa1106:20170228215710p:plain


詳細リンクをクリックしたときの内容はこんな感じでした。
f:id:tyoshikawa1106:20170228215757p:plain


新しいジョブページはClassicとExperienceの両方で利用可能とのことです。

SFDC:Spring'17 - ApexテストのモックフレームワークStub APIを試してみました

Spring'17で正式リリースされたStub APIを試してみました。Apexテストで例外テストができる仕組みを用意できたりするみたいです。

f:id:tyoshikawa1106:20170228224932p:plain

Apex スタブ API を正式リリース


リリースノートだけではイマイチ使い方がわかりませんでしたが、Apex開発者ガイドでサンプルコードが紹介されていました。
f:id:tyoshikawa1106:20170228225143p:plain

Build a Mocking Framework with the Stub API

使い方概要

まずテスト対象のクラスとして下記処理を用意します。

public class DateFormatter {    
    // Method to test    
    public String getFormattedDate(DateHelper helper) {
        return 'Today\'s date is ' + helper.getTodaysDate();
    }
}


次のヘルパークラスに処理部分をもたせます。

public class DateHelper {   
    // Method to stub    
    public String getTodaysDate() {
        return Date.today().format();
    }
}


上記処理は以下のような書き方で呼び出すことができます。

DateFormatter df = new DateFormatter();
DateHelper dh = new DateHelper();
String dateStr = df.getFormattedDate(dh);


このように独立させた処理をスタブAPIをつかって効率よくテストできるみたいです。

試験のために、私たちは隔離したいです getFormattedDate()この方法は、フォーマットが正常に動作していることを確認します。の戻り値getTodaysDate()この方法は、通常、日によって異なります。ただし、この場合には、我々は、フォーマットに私達のテストを分離するために一定の、予測可能な値を返すようにしたいです。むしろこの方法が一定の値を返すクラスの「偽」のバージョンを書くよりも、私たちは、クラスのスタブバージョンを作成します。スタブオブジェクトは、実行時に動的に作成され、私たちはその方法の「スタブ」動作を指定することができます。

Apexクラスのスタブバージョンを使用するには:

  • 実装することで、スタブクラスの動作を定義します System.StubProvider インタフェース。
  • 使用してスタブオブジェクトをインスタンス化します System.Test.createStub() 方法。
  • テストクラス内からスタブオブジェクトの関連するメソッドを呼び出します。


StubProviderインターフェイスの実装します。implements System.StubProvider と宣言したテストクラスになります。

@isTest
public class MockProvider implements System.StubProvider {
    
    public Object handleMethodCall(Object stubbedObject, String stubbedMethodName, 
        Type returnType, List<Type> listOfParamTypes, List<String> listOfParamNames, 
        List<Object> listOfArgs) {
        
        // The following debug statements show an example of logging 
        // the invocation of a mocked method.
       
        // You can use the method name and return type to determine which method was called.
        System.debug('Name of stubbed method: ' + stubbedMethodName);
        System.debug('Return type of stubbed method: ' + returnType.getName());
        
        // You can also use the parameter names and types to determine which method 
        // was called.
        for (integer i =0; i < listOfParamNames.size(); i++) {
            System.debug('parameter name: ' + listOfParamNames.get(i));
            System.debug('  parameter type: ' + listOfParamTypes.get(i).getName());
        }
        
        // This shows the actual parameter values passed into the stubbed method at runtime.
        System.debug('number of parameters passed into the mocked call: ' + 
            listOfArgs.size());
        System.debug('parameter(s) sent into the mocked call: ' + listOfArgs);
        
        // This is a very simple mock provider that returns a hard-coded value 
        // based on the return type of the invoked.
        if (returnType.getName() == 'String')
            return '8/8/2016';
        else 
            return null;
    }
}


クラスのスタブバージョンをインスタンス化します。

public class MockUtil {
    private MockUtil(){}

    public static MockProvider getInstance() {
        return new MockProvider();
    }
    
     public static Object createMock(Type typeToMock) {
        // Invoke the stub API and pass it our mock provider to create a 
        // mock class of typeToMock.
        return Test.createStub(typeToMock, MockUtil.getInstance());
    }
}


これでテストクラスからスタブAPIを利用する準備ができました。次のようなテストの書き方ができます。

@isTest 
public class DateFormatterTest {   
    @isTest 
    public static void testGetFormattedDate() {
        // Create a mock version of the DateHelper class.
        DateHelper mockDH = (DateHelper)MockUtil.createMock(DateHelper.class);
        DateFormatter df = new DateFormatter();
        
        // Use the mocked object in the test.
        System.assertEquals('Today\'s date is 8/8/2016', df.getFormattedDate(mockDH));
    }
}


無事開発者ガイドのサンプルコードのテストを実行することができました。
f:id:tyoshikawa1106:20170228230941p:plain


開発者ガイドのサンプルでは任意のシステム日付をセットしてテスト実行できることを確認できます。DateHelperクラスの処理でDate.todayでシステム日付を取得しています。MockProviderクラスの30行目でテストデータ用に任意の値で疑似システム日付を準備していました。DateFormatterTestクラスの12行目でスタブAPIをつかって用意した疑似システム日付がただしく利用できていることをassert処理で確認しています。


StubProviderインターフェイスの実装は少しややこしそうですが、疑似システム日付を用意できるのはすごく便利そうです。また、疑似Exceptionなども実装することができればテストがやりやすくなると思います。

SFDC:Developer Edition組織の有効期限と通知メールについて

Salesforceには開発者が自由に学習 / 動作検証を行うためのDeveloper Edition組織が用意されています。

f:id:tyoshikawa1106:20170219144924p:plain

Salesforce Developers


Developer Edtion組織ですが、365日間で一度もログインされていない組織はクローズされる仕組みとなっています。普段使っていない組織でも意外とそのような状況になった組織はなかったのですが、先日1つのDE組織が対象になりました。


期日が近づいたときは次のように「ATTENTION: Inactive Salesforce Developer orgs will be Locked on **」という件名で通知メールが届く仕組みとなっていました。
f:id:tyoshikawa1106:20170219145926p:plain


メールには『365日間一度もログインしていない組織があるのでもうすぐクローズされます。クローズ後に再度ログインする必要がある場合はサポートに問い合わせください。』という感じの内容が記載されていました。


このように事前にきちんと通知メールを送ってくれるようになっていました。また期日の2ヶ月前に連絡をしてくれています。不要な組織の場合はこのまま放置してクローズしてもらって大丈夫だと思います。必要な組織の場合はきちんとログインして有効期限を伸ばしておけばいいみたいです。


無料のDeveloper Editionでもこのようにきちんとサポートしてくれていました。

SFDC:Lightning Design Systemのアセットを参照できる$Assetを試してみました

Lightning Design Systemのアセットを参照できる$Assetを試してみました。$AssetはSpring'17から利用できるグローバル変数です。apex:sldsタグと一緒に利用できます。

f:id:tyoshikawa1106:20170213071600p:plain

$Asset グローバル変数を使用した Lightning Design System アセットの参照


使い方はこんな感じです。

<apex:page>
    <apex:slds />
    <span class="slds-icon_container slds-icon--small slds-icon-standard-account" title="Contact Avatar">
        <img src="{!URLFOR($Asset.SLDS, 'assets/images/avatar1.jpg')}" alt="Contact Avatar" />
    </span>
</apex:page>


次のサンプルコードを用意してみました。

<apex:page docType="html-5.0" sidebar="false" tabStyle="Account">
    <apex:slds rendered="true" />
    <div class="slds">
        <div class="slds-box">
            <apex:form id="form">
                <apex:input type="text" styleClass="slds-input" html-placeholder="Account Name" />
                <div class="slds-m-top--small">
                    <apex:commandButton value="Go!" styleClass="slds-button slds-button--brand" />
                </div>
                <div class="slds-m-top--small">
                    <span class="slds-icon_container slds-icon--large slds-icon-standard-account" title="Contact Avatar">
                        <img src="{!URLFOR($Asset.SLDS, 'assets/images/avatar1.jpg')}" alt="Contact Avatar" />
                    </span>
                </div>
            </apex:form>
        </div>
    </div>
</apex:page>

無事にアイコンを表示することができました。
f:id:tyoshikawa1106:20170213071756p:plain


assetsフォルダに簡単にアクセスできて便利そうです。

追記

この機能についてちょっと教えてもらいました。SVGが表示できないバグがあるみたいです。ただこのバグは修正対応が始まっていて近いうちに直るとのことです。
http://salesforce.stackexchange.com/questions/159406/visualforce-asset-violating-cors

SFDC:API39.0と開発者コンソールの例外クラス作成サポートについて

Spring'17へのバージョンアップによりAPI39.0から開発者コンソールで例外クラスを作成できるようになりました。

f:id:tyoshikawa1106:20170212210556p:plain

開発者コンソールでの例外クラスの作成


Apex開発をするときは基本SublimeText×MavensMateだったので、開発者コンソールからは例外クラスを作れないという事自体あまり気にしたことがなかったのですが、Spring'17からは作成できるようになりました。


実際にDev環境で試してみた所、エラーなく作成することができました。
f:id:tyoshikawa1106:20170212210459p:plain


ちょっと驚いたのですが、MyExceptionというクラス名で作成したところ、自動で例外クラスとして生成されました。末尾がExceptionの場合は例外クラスとして生成されるんだと思います。

SFDC:Spring'17 - FlexiPageリソースの廃止対応

FlexiPageリソースは API バージョン 39.0 で廃止されたみたいです。注意点として以前のすべての API バージョンからも削除されると記載があります。

f:id:tyoshikawa1106:20170212205530p:plain

削除されたリソース: FlexiPage


このFlexiPageをつかったことは無かったのですが、Salesforce1用のページを作成できる機能だったと思います。SOAP APIのdescribeFlexiPages() と DescribeFlexiPageResultも一緒に利用できなくなりました。

Spring'17 - SOAP API


過去バージョンも含め FlexiPageのすべてが廃止されたと思ったのですが、メタデータ API および Tooling APIでは引き続き取得できるみたいです。使ったことがないので今後も問題なく作成したり利用したりできるのかちょっと不明ですが、こういったことがリリースノートに記載されていました。