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;
  }

}

Главная | Обратная связь

drupal hosting | друпал хостинг | it patrol .inc