MarketingCloudService.php 10.3 KB
Newer Older
John Avery's avatar
John Avery committed
1
2
3
4
<?php

namespace Drupal\marketing_cloud;

5
6
use Swaggest\JsonSchema\InvalidValue;
use Swaggest\JsonSchema\Schema;
John Avery's avatar
John Avery committed
7
use GuzzleHttp\Exception\RequestException;
8
use Drupal\Core\Config\ConfigFactoryInterface;
9
use Drupal\Core\Logger\LoggerChannelFactoryInterface;
10
use Drupal\Core\Messenger\MessengerInterface;
11
12
use GuzzleHttp\Client;
use Drupal\Core\StringTranslation\StringTranslationTrait;
13
use Swaggest\JsonSchema\Exception;
John Avery's avatar
John Avery committed
14
15

/**
16
17
18
19
20
21
 * Class MarketingCloudService.
 *
 * This is the base class for all API services in this suite.
 *
 * It encapsulate the API call functionality and interfaces with
 * MarketingCloudSession.
John Avery's avatar
John Avery committed
22
23
24
25
 *
 * @package Drupal\marketing_cloud
 */
abstract class MarketingCloudService {
26
  use StringTranslationTrait;
John Avery's avatar
John Avery committed
27

28
29
30
31
32
  /**
   * Config service.
   *
   * @var \Drupal\Core\Config\ConfigFactoryInterface
   */
33
  private $configFactory;
34
35
36
37

  /**
   * Logger service.
   *
38
   * @var \Drupal\Core\Logger\LoggerChannelFactoryInterface
39
   */
40
  private $loggerFactory;
41
42
43
44
45
46

  /**
   * Rest client.
   *
   * @var \GuzzleHttp\Client
   */
47
  private $httpClient;
48
49
50
51
52
53

  /**
   * Messenger service.
   *
   * @var \Drupal\Core\Messenger\MessengerInterface
   */
54
  private $messenger;
55

John Avery's avatar
John Avery committed
56
57
  /**
   * MarketingCloudService constructor.
58
   *
59
   * @param \Drupal\Core\Config\ConfigFactoryInterface $configFactory
60
   *   Dependency injection config factory.
61
   * @param \Drupal\Core\Logger\LoggerChannelFactoryInterface $loggerFactory
62
   *   Dependency injection logger factory.
63
   * @param \GuzzleHttp\Client $httpClient
64
   *   Dependency injection REST client.
65
66
   * @param \Drupal\Core\Messenger\MessengerInterface $messenger
   *   The messenger service.
67
   */
68
  public function __construct(ConfigFactoryInterface $configFactory, LoggerChannelFactoryInterface $loggerFactory, Client $httpClient, MessengerInterface $messenger) {
69
70
    $this->configFactory = $configFactory;
    $this->loggerFactory = $loggerFactory;
71
    $this->httpClient = $httpClient;
72
    $this->messenger = $messenger;
73
74
75
  }

  /**
76
   * Wrapper function to make an API call.
77
   *
78
79
   * This takes care of data validation, authentication and use of endpoint
   * configuration.
John Avery's avatar
John Avery committed
80
   *
81
82
83
84
85
86
   * @param string $moduleName
   *   The machine name of the child class's definition in its config.
   * @param string $machineName
   *   The machine name of the api call in the config.
   * @param mixed $data
   *   The payload for the body. This can be object, array or simple type.
John Avery's avatar
John Avery committed
87
   * @param array $urlParams
88
   *   Name/value pairs for token replacement in the URI defined in the config.
John Avery's avatar
John Avery committed
89
   * @param array $params
90
91
92
   *   Name/value pairs for extra arguments to be added to the URI,
   *   e.g. ['foo' => 'var'] gives &foo=bar.
   *
93
   * @throws \Exception
94
   *
95
96
97
98
   * @return array|bool|int|mixed|string
   *   The result of the API call or FALSE on failure.
   *
   * @see restCall()
John Avery's avatar
John Avery committed
99
   */
100
  protected function apiCall($moduleName, $machineName, $data, array $urlParams = [], array $params = []) {
101
    $marketingCloudConfig = $this->configFactory->getEditable('marketing_cloud.settings');
John Avery's avatar
John Avery committed
102
103
104
    $validateJson = $marketingCloudConfig->get('validate_json');
    $doNotSend = $marketingCloudConfig->get('do_not_send');
    // Create module settings path string.
105
106
    // This assumes all sub-modules will name their settings file:
    // <module_name>.settings.yml.
John Avery's avatar
John Avery committed
107
    $subModuleSettingsPath = "$moduleName.settings";
108
    $subModuleConfig = $this->configFactory->getEditable($subModuleSettingsPath);
John Avery's avatar
John Avery committed
109

110
111
112
113
114
115
116
117
118
119
    // Ensure correct object/array types for schema, e.g. associative arrays
    // are converted to objects.
    $data = json_decode(json_encode($data));
    if ($data === NULL && json_last_error() !== JSON_ERROR_NONE) {
      $message = $this->t('Could not send %machine_name, invalid JSON data',
        ['%machine_name' => $machineName]
      );
      $this->messenger->addError($message);
      $this->loggerFactory->get(__METHOD__)->error($message);
      return FALSE;
John Avery's avatar
John Avery committed
120
121
122
123
124
    }

    // Fetch method.
    $method = $subModuleConfig->get("definitions.$machineName.method");
    if (empty($method)) {
125
126
127
128
129
130
      $message = $this->t('Could not fetch the method for %machine_name. Please check the configuration: %module_name.',
        [
          '%machine_name' => $machineName,
          '%module_name' => $moduleName,
        ]
      );
131
      $this->messenger->addError($message);
132
      $this->loggerFactory->get(__METHOD__)->error($message);
John Avery's avatar
John Avery committed
133
134
135
136
137
138
      return FALSE;
    }

    // Fetch endpoint.
    $endpoint = $subModuleConfig->get("definitions.$machineName.endpoint");
    if (empty($endpoint)) {
139
140
141
142
143
144
      $message = $this->t('Could not fetch the endpoint for %machine_name. Please check the configuration: %module_name.',
        [
          '%machine_name' => $machineName,
          '%module_name' => $moduleName,
        ]
      );
145
      $this->messenger->addError($message);
146
      $this->loggerFactory->get(__METHOD__)->error($message);
John Avery's avatar
John Avery committed
147
148
149
      return FALSE;
    }

150
    if ($validateJson) {
John Avery's avatar
John Avery committed
151
152
153
154
      // Fetch endpoint JSON schema.
      $schema = $subModuleConfig->get("definitions.$machineName.schema");

      // Decode the JSON schema for validation use.
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
      if ($schema !== "") {
        $schema = json_decode($schema);
        if (json_last_error() !== JSON_ERROR_NONE) {
          $message = $this->t('Could not decode the schema for %machine_name. Please check the configuration: %module_name.',
            [
              '%machine_name' => $machineName,
              '%module_name' => $moduleName,
            ]
          );
          $this->messenger->addError($message);
          $this->loggerFactory->get(__METHOD__)->error($message);
          return FALSE;
        }

        // Load the JSON Schema.
        try {
          $validator = Schema::import($schema);
        }
        catch (Exception $e) {
          $message = $this->t('Errors were found in the schema for %machine_name in %module_name. Please check the logs.', ['%machine_name' => $machineName, '%module_name' => $moduleName]);
          $this->messenger->addError($message);
          $message = $this->t('Error in the JSON schema for the %machine_name in %module_name schema: %error',
            [
              '%machine_name' => $machineName,
              '%module_name' => $moduleName,
              '%error' => $e->getMessage(),
            ]
          );
          $this->messenger->addError($message);
          $this->loggerFactory->get(__METHOD__)->error($message);
          return FALSE;
        }
        catch (InvalidValue $e) {
          $message = $this->t('Errors were found in the schema for %machine_name in %module_name. Please check the logs.', ['%machine_name' => $machineName, '%module_name' => $moduleName]);
          $this->messenger->addError($message);
          $message = $this->t('Error in the JSON schema for the %machine_name in %module_name schema: %error',
            [
              '%machine_name' => $machineName,
              '%module_name' => $moduleName,
              '%error' => $e->getMessage(),
            ]
          );
          $this->messenger->addError($message);
          $this->loggerFactory->get(__METHOD__)->error($message);
          return FALSE;
        }

        // Validate the JSON against the Schema.
        try {
          $validator->in($data);
        }
        catch (Exception $e) {
          $message = $this->t('Data error against the schema: %error', ['%error' => $e->getMessage()]);
          $this->messenger->addError($message);
          $this->loggerFactory->get(__METHOD__)->error($message);
          return FALSE;
        }
John Avery's avatar
John Avery committed
212
213
214
215
216
217
218
219
220
221
222
      }
    }

    // Create endpoint URL with any required params.
    foreach ($urlParams as $key => $val) {
      $endpoint = str_replace($key, $val, $endpoint);
    }
    $arr = [];
    foreach ($params as $key => $val) {
      $arr[] = "$key=$val";
    }
223
    if (count($arr) > 0) {
John Avery's avatar
John Avery committed
224
225
226
      $endpoint = $endpoint . '?' . implode('&', $arr);
    }
    $url = $marketingCloudConfig->get('base_url') . $endpoint;
227

John Avery's avatar
John Avery committed
228
229
230
    // Prepare data.
    $data = json_encode($data);

231
    // Special case for testing - do not send the api request,
232
    // but instead return the URL, method and data that would be sent.
John Avery's avatar
John Avery committed
233
    if ($doNotSend) {
234
      return ['url' => $url, 'data' => $data, 'method' => $method];
John Avery's avatar
John Avery committed
235
236
    }

237
    // Fetch the token.
John Avery's avatar
John Avery committed
238
239
    $response = $this->restCall($method, $url, $data);
    if ($response == '401 Unauthorized') {
240
      $this->loggerFactory->get(__METHOD__)->notice('Stale token, fetching a fresh token and resending');
John Avery's avatar
John Avery committed
241
242
243
244
245
246
247
248
249
      $response = $this->restCall($method, $url, $data, TRUE);
    }

    return $response;
  }

  /**
   * Utility function to send a single request to MarketingCloud.
   *
250
251
252
253
254
255
   * @param string $method
   *   GET, POST, DELETE, PUT.
   * @param string $url
   *   The endpoint URL.
   * @param string $data
   *   The JSON payload string.
John Avery's avatar
John Avery committed
256
   * @param bool $force
257
258
259
260
261
   *   TRUE = always fetch a fresh token.
   *   FALSE = use the existing token, or fetch a fresh if stale.
   *
   * @return bool|int|mixed|string
   *   Return the API call result or FALSE on failure.
John Avery's avatar
John Avery committed
262
   *
263
   * @see apiCall()
John Avery's avatar
John Avery committed
264
   */
265
  private function restCall($method, $url, $data, $force = FALSE) {
John Avery's avatar
John Avery committed
266
267
268
269
270
    // Fetch authentication token.
    $session = new MarketingCloudSession();
    $token = $session->token($force);

    if (!$token) {
271
      $message = $this->t('%method to %url failed, unable to fetch authentication token', ['%method' => $method, '%url' => $url]);
272
      $this->messenger->addError($message);
273
      $this->loggerFactory->get(__METHOD__)->error($message);
John Avery's avatar
John Avery committed
274
275
276
277
278
279
280
281
282
      return FALSE;
    }

    // Send request to endpoint.
    $response = FALSE;
    try {
      $options = [
        'headers' => [
          'Content-Type' => 'application/json',
283
          'Authorization' => "Bearer $token",
John Avery's avatar
John Avery committed
284
285
286
287
288
        ],
      ];
      if (!empty($data)) {
        $options['body'] = $data;
      }
289
      $raw = $this->httpClient->{$method}($url, $options);
290
291
292
      $response = json_decode($raw->getBody(), TRUE);
    }
    catch (RequestException $e) {
293
      $message = $this->t('%error', ['%error' => $e->getMessage()]);
294
      $this->loggerFactory->get(__METHOD__)->error(json_encode($message));
295
      // Response code may sometimes contain the reason text.
John Avery's avatar
John Avery committed
296
297
298
      $code = $e->getResponse()->getStatusCode();
      $reason = $e->getResponse()->getReasonPhrase();
      $response = (strpos($code, $reason) === FALSE) ? "$code $reason" : $code;
299
300
    }
    catch (\Exception $e) {
301
      $message = $this->t('%error', ['%error' => $e->getMessage()]);
302
      $this->loggerFactory->get(__METHOD__)->error(json_encode($message));
John Avery's avatar
John Avery committed
303
304
305
306
    }

    return $response;
  }
307

John Avery's avatar
John Avery committed
308
}