marketo_ma-8.x-2.x-dev/src/Service/MarketoMaApiClient.php
src/Service/MarketoMaApiClient.php
<?php
namespace Drupal\marketo_ma\Service;
use Drupal\marketo_ma\ActivityType;
use Drupal\marketo_ma\Exception\ProcessingException;
use Drupal\marketo_ma\Exception\SkippedException;
use Drupal\marketo_ma\Exception\SkippedReason;
use Drupal\marketo_ma\Lead;
use Drupal\marketo_ma_webform\Event\SubmissionFailure;
use NecLimDul\MarketoRest\Hacks\Model\FormResponse as FormResponseHack;
use NecLimDul\MarketoRest\Lead\Api\ActivitiesApi;
use NecLimDul\MarketoRest\Lead\Api\LeadsApi;
use NecLimDul\MarketoRest\Lead\ApiException;
use NecLimDul\MarketoRest\Lead\Model\Form;
use NecLimDul\MarketoRest\Lead\Model\FormResponse;
use NecLimDul\MarketoRest\Lead\Model\Lead as ApiLead;
use NecLimDul\MarketoRest\Lead\Model\LeadFormFields;
use NecLimDul\MarketoRest\Lead\Model\PushLeadToMarketoRequest;
use NecLimDul\MarketoRest\Lead\Model\Reason;
use NecLimDul\MarketoRest\Lead\Model\SubmitFormRequest;
use NecLimDul\MarketoRest\Lead\Model\VisitorData;
use Psr\Log\LoggerInterface;
/**
* This is a wrapper for the default API client library.
*
* If needed this can be switched out by another module that supplies an
* alternate API client library.
*
* @todo Integrate with assets api to provide better integrations?
*/
class MarketoMaApiClient implements MarketoMaApiClientInterface {
/**
* Marketo Leads API service.
*
* @var \NecLimDul\MarketoRest\Lead\Api\LeadsApi
*/
protected $leadsApi;
/**
* A logger instance.
*
* @var \Psr\Log\LoggerInterface
*/
protected $logger;
/**
* Marketo Activities API service.
*
* @var \NecLimDul\MarketoRest\Hacks\API\ActivitiesApi
*/
protected $activitiesApi;
/**
* Creates the Marketo API client wrapper service.
*
* @param \NecLimDul\MarketoRest\Lead\Api\LeadsApi $leadsApi
* Leads API service.
* @param \NecLimDul\MarketoRest\Hacks\API\ActivitiesApi $activitiesApi
* Activities API service.
* @param \Psr\Log\LoggerInterface $logger
* A logger instance.
*/
public function __construct(LeadsApi $leadsApi, ActivitiesApi $activitiesApi, LoggerInterface $logger) {
$this->leadsApi = $leadsApi;
$this->activitiesApi = $activitiesApi;
$this->logger = $logger;
}
/**
* {@inheritdoc}
*/
public function getFields() {
$results = [];
// describeUsingGET6 is the newer version but it doesn't include the
// soap name which posses problems for integrating munchkin features.
$result = $this->leadsApi->describeUsingGET2();
if ($result->getSuccess()) {
$fields_result = $result->getResult();
foreach ($fields_result as $lead_attribute) {
$rest = $lead_attribute->getRest();
$soap = $lead_attribute->getSoap();
if (!empty($rest) && $rest->getName()) {
$results[$rest->getName()] = [
'dataType' => $lead_attribute->getDataType(),
'displayName' => $lead_attribute->getDisplayName(),
'length' => $lead_attribute->getLength(),
// @todo readonly properties.
'rest' => ['name' => $rest->getName()],
'soap' => ['name' => !empty($soap) ? $soap->getName() : NULL],
];
}
}
}
else {
$this->logger->error('Problem loading fields.');
}
return $results;
}
/**
* {@inheritdoc}
*/
public function getActivityTypes() {
$types = [];
$result = $this->activitiesApi->getAllActivityTypesUsingGET();
if ($result->getSuccess()) {
foreach ($result->getResult() as $type) {
/** @var \NecLimDul\MarketoRest\Lead\Model\ActivityTypeAttribute|null $primary_attribute */
$primary_attribute = $type->getPrimaryAttribute();
$types[$type->getId()] = new ActivityType([
'id' => $type->getId(),
'name' => $type->getName(),
'description' => $type->getDescription(),
'primaryAttribute' => ['name' => $primary_attribute ? $primary_attribute->getName() : NULL],
]);
}
}
else {
$this->logger->error('Problem loading activity types.');
}
return $types;
}
/**
* {@inheritdoc}
*/
public function canConnect() {
// @todo check some actual config? Call something?
// return !!$this->leadsApi->getConfig();
return TRUE;
}
/**
* {@inheritdoc}
*/
public function getLeadById($id) {
$result = $this->leadsApi->getLeadByIdUsingGET($id);
if (!$result->getSuccess()) {
// @todo log error?
}
return $this->mapRestToLead($result->getResult()[0]);
}
/**
* {@inheritdoc}
*/
public function getLeadByEmail($email) {
$result = $this->leadsApi->getLeadsByFilterUsingGET('email', [$email]);
if ($result->getSuccess()) {
$leads_result = $result->getResult();
return !empty($leads_result[0]) ? $this->mapRestToLead(reset($leads_result)) : NULL;
}
$this->logger->error('Problem loading lead');
return NULL;
}
/**
* {@inheritdoc}
*/
public function getLeadActivity(Lead $lead, $activity_type_ids = NULL) {
// A paging token is required by the activities.json call.
// @todo make the window configurable.
$paging_token = $this->activitiesApi->getActivitiesPagingTokenUsingGET(new \DateTime('-3 months'))->getNextPageToken();
$activities = $this->activitiesApi->getLeadActivitiesUsingGET(
$paging_token,
$activity_type_ids,
NULL,
NULL,
[(int) $lead->id()]
);
if ($activities->getSuccess()) {
// Empty results are returns as a NULL result instead of an empty result
// object. This breaks deserialization in a pretty bad way so here's the
// workaround.
if ($activities->hasResult()) {
$activities->getResult();
// @todo Map native response.
return $activities->getResult();
}
return [];
}
else {
$this->logger->error(json_encode($activities->getErrors()));
}
}
/**
* {@inheritdoc}
*/
public function syncLead(Lead $lead, $key = 'email'): ?int {
$request = new PushLeadToMarketoRequest([
'input' => [$this->mapLeadToRest($lead)],
'program_name' => $lead->getProgramName(),
'lookup_field' => $key,
]);
$response = $this->leadsApi->pushToMarketoUsingPOST($request);
if ($response->getSuccess()) {
$id = $response->getResult()[0]->getId();
if ($lead->getCookie()) {
$this->leadsApi->associateLeadUsingPOST($id, $lead->getCookie());
}
return $id;
}
// @todo log error?
return NULL;
}
/**
* {@inheritdoc}
*/
public function deleteLead($leads) {
$response = $this->leadsApi->deleteLeadsUsingPOST(
NULL,
(array) $leads
);
if ($response->getSuccess()) {
// @todo these aren't really leads? only ids it looks like.
return array_map(
function ($lead) {
return $this->mapRestToLead($lead);
},
$response->getResult()
);
}
// @todo log error?
}
/**
* {@inheritdoc}
*/
public function addLeadsToList($listId, array $leads, array $options = []) {
$this->getLeadsIds($leads);
// @todo finish conversion.
// https://developers.marketo.com/rest-api/endpoint-reference/lead-database-endpoint-reference/#!/Static_Lists/addLeadsToListUsingPOST
}
/**
* {@inheritdoc}
*/
public function addLeadToListByEmail($listId, $email, array $options = []) {
$this->syncLead(new Lead(['email' => $email]));
$this->addLeadsToList($listId, [$this->getLeadByEmail($email)], $options);
}
/**
* {@inheritdoc}
*/
public function submitForm(string $form_id, array $fields, ?string $cookie = NULL, array $extra = []) {
$result = $this->doSubmit($form_id, $fields, $cookie, $extra);
$status = $result->getStatus();
if ($status == FormResponse::STATUS_SKIPPED) {
$missing_fields = [];
foreach ($result->getReasons() as $reason) {
// 1021 means we tried to modify the company name on a linked account.
// We can try and submit again without the company field.
if ($reason->getCode() == 1021) {
// Unset fields linked to a company object causing failures.
unset($fields['company'], $fields['industry']);
try {
return $this->submitForm($form_id, $fields, $cookie, $extra);
}
catch (ProcessingException $e) {
$this->submitFormGiveUp($form_id, $fields, $e);
}
}
if ($reason->getCode() == 1006) {
// @todo parse name to simplify error processing.
$missing_fields[] = $reason->getMessage();
}
}
if ($missing_fields) {
$this->logger->error('Your form has missing fields :fields', [
':fields' => implode("\n", $missing_fields),
]);
}
// We couldn't fix things so throw a skipped exception so calling code can
// maybe figure it out or log it.
throw new SkippedException(
array_map(
function (Reason $reason) {
return new SkippedReason($reason->getCode(), $reason->getMessage());
},
$result->getReasons()
), 'Skipped processing form'
);
}
elseif ($status == FormResponseHack::STATUS_WARNING) {
foreach ($result->getReasons() as $reason) {
$this->logger->warning($reason->getMessage());
}
}
return $result->getId();
}
/**
* Wrap the actual form submission to simplify failure/retry logic.
*
* @param string $form_id
* Marketo Form ID.
* @param array $fields
* Form fields.
* @param string|null $cookie
* A cookie to connect with the submission.
* @param array $extra
* Extra parameters to be used for the implementation.
*
* @throws \Drupal\marketo_ma\Exception\ProcessingException
* If there was a protocol level error this can throw a process exception.
*
* @return \NecLimDul\MarketoRest\Lead\Model\FormResponse|null
* A form response. ProcessException on failure.
*/
protected function doSubmit(string $form_id, array $fields, ?string $cookie = NULL, array $extra = []): ?FormResponse {
$form_fields = new LeadFormFields();
if (isset($fields['email'])) {
$form_fields->setEmail($fields['email']);
}
$form_fields->setAdditionalProperties($fields);
$form = new Form([
'lead_form_fields' => $form_fields,
'cookie' => $cookie,
]);
if (!empty($extra)) {
$form->setVisitorData(new VisitorData([
'page_url' => $extra['referer'] ?? NULL,
'query_string' => $extra['query'] ?? NULL,
'lead_client_ip_address' => $extra['ip_address'] ?? NULL,
'user_agent_string' => $extra['user_agent'] ?? NULL,
]));
}
try {
$response = $this->leadsApi->submitFormUsingPOST(new SubmitFormRequest([
'form_id' => $form_id,
'input' => [$form],
]));
}
catch (ApiException $e) {
throw new ProcessingException($e->getMessage(), (int) $e->getCode(), $e);
}
if (!$response->getSuccess()) {
// Process errors and report them.
foreach ($response->getErrors() as $error) {
throw new ProcessingException($error->getMessage(), (int) $error->getCode());
}
// Skipped or something? Provide some sort of notice.
throw new ProcessingException('Unknown error submitting form to marketo.');
}
return $response->getResult()[0];
}
/**
* Dispatch an event so customizations can handle failures we can't handle.
*
* @param string $form_id
* Marketo Form ID.
* @param array<string, string> $data
* Field data to submit to the marketo form.
* @param \Drupal\marketo_ma\Exception\ProcessingException $exception
* The triggering exception.
*/
protected function submitFormGiveUp(string $form_id, array $data, ProcessingException $exception): void {
/**
* @todo import correctly.
* @phpstan-ignore-next-line
*/
\Drupal::service('event_dispatcher')->dispatch(new SubmissionFailure($form_id, $data, $exception));
}
/**
* Helper method to transform a list of Leads to a coma separated string.
*
* @param \Drupal\marketo_ma\Lead[] $leads
* An array containing \Drupal\marketo_ma\Lead objects.
*
* @return string
* A coma separated list of ids of the given Lead objects.
*
* @throws \Exception
* In case the given array contains an element which is not a
* \Drupal\marketo_ma\Lead object.
*/
protected function getLeadsIds(array $leads) {
$leads_raw = '';
$total_leads = count($leads);
for ($i = 0; $i < $total_leads; $i++) {
if (!is_a($leads[$i], Lead::class)) {
throw new \Exception('Only lead objects can be passed to the MarketoMaApiClient::addLeadsToList() method.');
}
$leads_raw .= $leads[$i]->id();
if ($i < count($leads) - 1) {
$leads_raw .= ',';
}
}
return $leads_raw;
}
/**
* Helper method ot map a service lead object to a module lead object.
*
* @param \NecLimDul\MarketoRest\Lead\Model\Lead $api_lead
* A service lead object.
*
* @return \Drupal\marketo_ma\Lead|null
* A populated module lead object.
*
* @todo does this really match the expected mapping? Probably not...
*/
protected function mapRestToLead(ApiLead $api_lead): ?Lead {
// This is how user defined fields are transferred under the openapi
// specification.
$props = $api_lead->getAdditionalProperties();
return new Lead(
[
'id' => $api_lead->getId(),
] + $props
);
}
/**
* Helper method to map a module lead object to a service lead object.
*
* @param \Drupal\marketo_ma\Lead $lead
* The module lead object.
*
* @return \NecLimDul\MarketoRest\Lead\Model\Lead
* A populated service lead object.
*/
protected function mapLeadToRest(Lead $lead): ApiLead {
$api_lead = new ApiLead([$lead->id()]);
$data = $lead->data();
unset($data['id']);
$api_lead->setAdditionalProperties($data);
return $api_lead;
}
}
