Skip to content

Instantly share code, notes, and snippets.

@afawcett
Created June 20, 2014 14:05
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save afawcett/2545650dc851eb0552e6 to your computer and use it in GitHub Desktop.
Save afawcett/2545650dc851eb0552e6 to your computer and use it in GitHub Desktop.
Preview of FLS Support in Domain Layer (see comments below)
public with sharing class Opportunities extends fflib_SObjectDomain
{
public Opportunities(List<Opportunity> sObjectList)
{
// Domain classes are initialised with lists to enforce bulkification throughout
super(sObjectList);
}
public override void onApplyDefaults()
{
// Apply defaults to Opportunities
for(Opportunity opportunity : (List<Opportunity>) Records)
{
opportunity.DiscountType__c = OpportunitySettings__c.getInstance().DiscountType__c;
}
}
public override void onValidate()
{
// Validate Opportunities
for(Opportunity opp : (List<Opportunity>) Records)
{
if(opp.Type!=null && opp.Type.startsWith('Existing') && opp.AccountId == null)
{
opp.AccountId.addError( error('You must provide an Account for Opportunities for existing Customers.', opp, Opportunity.AccountId) );
}
}
}
public override void onValidate(Map<Id,SObject> existingRecords)
{
// Validate changes to Opportunities
for(Opportunity opp : (List<Opportunity>) Records)
{
Opportunity existingOpp = (Opportunity) existingRecords.get(opp.Id);
if(opp.Type != existingOpp.Type)
{
opp.Type.addError( error('You cannot change the Opportunity type once it has been created.', opp, Opportunity.Type) );
}
}
}
public override void onAfterInsert()
{
// Unit of Work scope for this event
fflib_SObjectUnitOfWork uow =
new fflib_SObjectUnitOfWork(new Schema.SObjectType[] { Account.SObjectType });
// Update last Opportunity activity on the related Accounts (via the Accounts Domain class)
Accounts accounts = new Accounts(
new AccountsSelector().selectByOpportunity(Records));
accounts.updateOpportunityActivity(uow);
// Commit the work
uow.commitWork();
}
public override void onAfterDelete()
{
for(Opportunity opp : (List<Opportunity>) Records)
{
if(opp.StageName!=null && opp.StageName.startsWith('Won'))
{
opp.StageName.addError( error('You cannot delete Won Opportunities.', opp, Opportunity.StageName) );
}
}
}
public void applyDiscount(Decimal discountPercentage, fflib_SObjectUnitOfWork uow)
{
// Custom FLS checking
checkFieldIsUpdateable(Opportunity.StageName);
// Calculate discount factor
Decimal factor = calculateDiscountFactor(discountPercentage);
// Opportunity lines to apply discount to
List<OpportunityLineItem> linesToApplyDiscount = new List<OpportunityLineItem>();
// Apply discount
for(Opportunity opportunity : (List<Opportunity>) Records)
{
// Appply to the Opporunity Amount?
if(opportunity.OpportunityLineItems.size()==0)
{
// Adjust the Amount on the Opportunity if no lines
opportunity.Amount = opportunity.Amount * factor;
uow.registerDirty(opportunity);
}
else
{
// Collect lines to apply discount to
linesToApplyDiscount.addAll(opportunity.OpportunityLineItems);
}
}
// Apply discount to lines
OpportunityLineItems lineItems = new OpportunityLineItems(linesToApplyDiscount);
lineItems.applyDiscount(this, discountPercentage, uow);
}
public static Decimal calculateDiscountFactor(Decimal discountPercentage)
{
// Calculate discount factor
Decimal discountProportion = discountPercentage==null ? 0 : discountPercentage / 100;
Decimal factor = 1 - discountProportion;
return factor;
}
public class Constructor implements fflib_SObjectDomain.IConstructable, fflib_SObjectDomain.IConfigure
{
public fflib_SObjectDomain construct(List<SObject> sObjectList)
{
return new Opportunities(sObjectList);
}
public fflib_SObjectDomain.Configuration configure()
{
return new fflib_SObjectDomain.Configuration().
setEnforceCRUD(true).
setEnforceTriggerFLS(true).
onTriggerEnforceFLSRead(Opportunity.Type).
onTriggerEnforceFLSRead(Opportunity.StageName).
onTriggerEnforceFLSReadWrite(Opportunity.DiscountType__c);
}
}
}
@afawcett
Copy link
Author

That's a good question, i've seen lots of examples of triggers being forced to do to much business logic (utilising a pattern i call 'control fields') and getting into a big mess with recursive calls, data loaders etc.

So without wishing to broaden this into a wider debate of trigger vs service logic. The answer in my view is, we do need logic in the triggers to protect the data integrity and provide some feature function around default fields and manipulating other records (subject to how users interact with the app). Where one draws the line is hard to prescribe though (certainly callouts and batch apex are troublesome).

It also somewhat depends on how your users want to interact with your app via CRUD Native UI and/or via VF/Custom UI, really you have to consider both tbh. One thing is for certain, we have to think carefully before blocking direct object access or not writing approprite trigger code as it may erode platform features like Workflow, Flow and not to mention Salesforce1 Mobile.

In respect to your other quesiton, also a good one. If the developer configures triggerEnforceFLS without giving any fields, i'm tempted to throw an assertion at runtime, to say, something along the lines of 'why ask me to enforce something you've not told me about'. We could of course implicitly enable it the first time onTriggerEnforceFLSRead or onTriggerEnforceFLSReadWrite is called?. It's also present to allow disabling.

New question....

  1. What do you expect the scope of the configuration of the domain class to be, per instance or per execution context? How would you want to configure it in either case?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment