-
Notifications
You must be signed in to change notification settings - Fork 12
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add createHdr() method which allows a header to be passed to the Salesforce create API. #27
base: master
Are you sure you want to change the base?
Conversation
@DonLewisFSI would it perhaps be more clear if the name was changed? s/createHdr/createWithHeader/ ? createHdr() makes me think that it's generating a header object. @genio do you have any other concerns with this PR? It seems relatively straightforward and simplifies an increasingly-common use case. FWIW I work for Salesforce, we use WWW::Salesforce extensively in our internal tools (and have pulled this patch into our internal repo for now). |
I do not have concern with the idea itself, though I'd like to propose stealing a bit from your added new method and altering the current =head2 create
# create a new account with a hash
my $res = $sforce->create(type => 'Account', name => 'Foo');
say $res->envelope->{Body}->{createResponse}->{result}->{success};
# create a new account with a hashref
my $res = $sforce->create({type => 'Account', name => 'Foo'});
say $res->envelope->{Body}->{createResponse}->{result}->{success};
# create a new account with an extra header and a hash
my $autoassign = SOAP::Header->name('useDefaultRule' => 'true' );
my $assign_hdr = SOAP::Header->name('AssignmentRuleHeader' => \$autoassign );
my $res = $sforce->create($assign_hdr, type => 'Account', name => 'Foo');
say $res->envelope->{Body}->{createResponse}->{result}->{success};
# create a new account with an extra header and a hashref
my $autoassign = SOAP::Header->name('useDefaultRule' => 'true' );
my $assign_hdr = SOAP::Header->name('AssignmentRuleHeader' => \$autoassign );
my $res = $sforce->create($assign_hdr, {type => 'Account', name => 'Foo'});
say $res->envelope->{Body}->{createResponse}->{result}->{success};
Adds one new individual object to your organization's data. This takes as
input an optional extra L<SOAP::Header> object followed by a C<hash> or
C<hashref> representing the object you wish to add to your organization.
The hash must contain the C<type> key in order to identify the type of the
record to add.
Returns a L<SOAP::Lite> object. Success of this operation can be gleaned from
the envelope result as shown in the examples above.
=cut
sub create {
my $self = shift;
my $header = ($_[0] && CORE::ref($_[0]) && Scalar::Util::blessed($_[0]) && $_[0]->isa('SOAP::Header')) ? shift : undef;
$header->uri($SF_URI) if $header;
my $args;
if (@_ == 1 && CORE::ref($_[0])) {
my %copy = eval { %{ $_[0] } }; # try shallow copy
Carp::croak("Argument to create() could not be dereferenced as a hash") if $@;
$args = \%copy;
}
elsif (@_ % 2 == 0) {
$args = {@_};
}
else {
Carp::croak("create() got an odd number of elements");
}
Carp::croak('Expected a hash object') unless keys(%{$args});
my $client = $self->_get_client(1);
my $method = SOAP::Data
->name("create")
->prefix($SF_PREFIX)
->uri($SF_URI)
->attr({'xmlns:sfons' => $SF_SOBJECT_URI});
my $type = $args->{type};
delete $args->{type};
Carp::croak('No object type defined to create') unless $type;
my @elems;
foreach my $key (keys %{$args}) {
push @elems, SOAP::Data
->prefix('sfons')
->name($key => $args->{$key})
->type(WWW::Salesforce::Constants->type($type, $key));
}
my @headers = ($self->_get_session_header());
push(@headers, $header) if $header;
my $r = $client->call(
$method => SOAP::Data
->name('sObjects' => \SOAP::Data->value(@elems))
->attr({'xsi:type' => 'sfons:' . $type}),
@headers
);
unless ($r) {
die "could not call method $method";
}
if ( $r->fault() ) {
die( $r->faultstring() );
}
return $r;
}
|
coalescing them seems good to me, though in that case I think we'll want a good bit more unit testing of the new potential flows... |
Unfortunately, it's difficult to mock the SOAP endpoints (man, I hate SOAP). That makes it hard to unit test well. I'll be doing some testing against our SF sandbox area to be sure before anything gets merged/released. Please do the same on your end and let me know if you run into any pitfalls. |
Now that we have CI that actually tests against a dev instance I setup, and the version of the API is choosable easily in the constructor in the release I just cut out there, I'll get back to this. |
Though a "reserved" hash key may make more sense: {
-headers => [$some_headers_obj, $some_other_header],
type => 'Account',
name => 'foo bar',
} |
I'm not a huge fan of this, but it seems like the best way to implement allowing headers for the various methods that allow them in Salesforce's API. It's eww, but poor design decisions way back when have us here (blame my younger self). sub _get_method_headers {
my $self = shift;
return () unless @_;
if (@_ == 1 && CORE::ref($_[0]) eq 'ARRAY') {
return grep {$_ && CORE::ref($_) eq 'SOAP::Header'} @{$_[0]};
} elsif (@_ == 1 && $_[0] && CORE::ref($_[0]) eq 'SOAP::Header') {
return($_[0]);
}
return grep {$_ && CORE::ref($_) eq 'SOAP::Header'} @_;
}
sub _get_session_header {
my ($self) = @_;
return SOAP::Header->name('SessionHeader' =>
\SOAP::Header->name('sessionId' => $self->{'sf_sid'}))
->uri($SF_URI)->prefix($SF_PREFIX);
}
sub create {
my $self = shift;
my $args;
if (@_ == 1 && CORE::ref($_[0])) {
my %copy = eval { %{ $_[0] } }; # try shallow copy
die("Argument to create() could not be dereferenced as a hash") if $@;
$args = \%copy;
}
elsif (@_ % 2 == 0) {
$args = {@_};
}
else {
die("create() got an odd number of elements");
}
die('Expected a hash object') unless keys(%{$args});
# parse headers
my @headers = ($self->_get_session_header(), $self->_get_method_headers($args->{-headers}));
delete($args->{-headers});
...
} |
Is this available anywhere for testing? |
Nah. That ended up not behaving as hoped. There's also nothing that indicates what the headers should look like from Salesforce's documentation. So, it got left by the wayside for a while. SOAP is evil
|
The Salesforce way seems to be to make the headers a persistent attribute of the connection. Scroll down for a code example: https://developer.salesforce.com/docs/atlas.en-us.236.0.api.meta/api/sforce_api_objects_lead.htm There is a setAssignmentRuleHeader() method for assignment rules, and I suppose methods for other header types. Can't say that I'm a fan ... Actually, this is only true for the java API. The C# API just wants all the possible headers (or nulls to omit those header types) to be passed as the first args to create(). Neither seems to try to do any smart guessing about what is being passed. |
Here is some generic SOAP header documentation: https://www.tutorialspoint.com/soap/soap_header.htm |
This PHP API implementation resembles the Salesforce Java API: https://github.com/forceworkbench/forceworkbench workbench/soapclient/SforceHeaderOptions.php contains the methods that capture the optional header info. workbench/soapclient/SforceBaseClient.php contains the code that outputs the headers specific to each API call such as create(). |
Unfortunately, Salesforce's documentation doesn't actually show any rendered XML examples. They show me code examples in other languages that don't really help at all if you're not using one of those languages. They don't provide any examples of what the XML-rendered headers look like. What I can do:I'd have to use another language. I'd have to pull in the WSDLs in that language. I'd have to setup my own mock Salesforce endpoint that got through the login step. I'd have to setup a mock endpoint for the various calls that accept the various headers. I'd have to have the client in one of the other languages hit that mock service on each of those endpoints in each of the various ways to add content to those headers. I'd have to have the mock service dump out the request to be able to then see what the rendered XML looks like. Then, I'd be able to write accurate tests for the rendering from the Perl app to be able to accurately handle these SOAP headers. Less effort:Or, I'd have to re-write this entire service using anything other than SOAP::Lite so I could pull in the WSDLs and have the client from the XML::Compile::* modules build the requests themselves. Rewriting that way, I might as well forego touching SOAP all together and write a REST client. If I do either of those, I'm breaking the way this old module works as the responses from method calls are most times SOAP::Lite response objects. What have I done?!I made lots of bad decisions when originally making this module ~20 years ago and now I've got myself in a no-win situation design-wise. Not a whole lot I can do other than the lots-of-effort mocking method since I can't "fix" the module without breaking it for anyone using it. |
Hmn, I'd hoped the PHP version would have the code for rendering, but it turns out that is just passes the array of headers to SoapClient::__setSoapHeaders(). The perl version would have to pass the headers to SOAP::Header(), https://metacpan.org/dist/SOAP-Lite/view/lib/SOAP/Header.pod , for each request. I found some example XML here; https://developer.salesforce.com/blogs/developer-relations/2015/06/salesforce-soap-api-sample-wsdls The perl code already knows enough about headers to insert the SessionHeader. The perl code also knows how to pass multiple headers, since query() does it to pass both the SessionHeader and the batchSize header for the limit implementation. |
I'm still not thrilled with the implementation, but I think I dislike this attempt less than the others. thoughts? |
Salesforce create API.
The new createHdr() method allows Saleforce records to be created using one of the Salesforce specialized headers.
Example:
To create a new Lead record using the the default assignment rule: