 README.txt                                    |  10 -
 config/schema/fastly.schema.yml               |  10 +
 fastly.admin.inc                              | 433 --------------------------
 fastly.api.inc                                | 209 -------------
 fastly.info                                   |   7 -
 fastly.info.yml                               |   6 +
 fastly.install                                |  27 --
 fastly.links.menu.yml                         |   5 +
 fastly.module                                 | 183 -----------
 fastly.permissions.yml                        |   4 +
 fastly.routing.yml                            |   7 +
 fastly.services.yml                           |  20 ++
 src/Api.php                                   | 145 +++++++++
 src/CacheTagsInvalidator.php                  |  51 +++
 src/EventSubscriber/SurrogateKeyGenerator.php |  86 +++++
 src/Form/FastlySettingsForm.php               | 141 +++++++++
 16 files changed, 475 insertions(+), 869 deletions(-)

diff --git a/README.txt b/README.txt
index 6750534..9c926e0 100644
--- a/README.txt
+++ b/README.txt
@@ -79,16 +79,6 @@ if (isset($_SERVER['HTTP_FASTLY_SSL']) && $_SERVER['HTTP_FASTLY_SSL']) {
   $_SERVER['HTTPS'] = 'on';
 }
 
-Expire Module Integration
--------------------------
-The Faslty module has integration with the Cache Expiration module
-(https://www.drupal.org/project/expire).
-
-You can enable this by visiting admin/config/system/expire.  You should see
-Fastly in the list of modules that support external expiration.
-Make sure you select "External expiration", and also ensure you untick "Include
-base URL in expires".
-
 Custom VCL
 ----------
 Fastly gives you the ability to upload and use your own VCL file.
diff --git a/config/schema/fastly.schema.yml b/config/schema/fastly.schema.yml
new file mode 100644
index 0000000..2b6bb13
--- /dev/null
+++ b/config/schema/fastly.schema.yml
@@ -0,0 +1,10 @@
+fastly.settings:
+  type: config_object
+  label: 'Fastly Settings'
+  mapping:
+    api_key:
+      type: string
+      label: 'Fastly API key'
+    service_id:
+      type: string
+      label: 'Fastly Service ID'
diff --git a/fastly.admin.inc b/fastly.admin.inc
deleted file mode 100644
index 6daa9a2..0000000
--- a/fastly.admin.inc
+++ /dev/null
@@ -1,433 +0,0 @@
-<?php
-
-/**
- * @file
- * Administrative forms for Fastly module.
- */
-
-/**
- * Settings form.
- */
-function fastly_setup_form($form_state) {
-  $api = fastly_get_api();
-  $service_id = variable_get('fastly_service_id', '');
-  $api_key = variable_get('fastly_api_key', '');
-
-  $form['fastly_api_key'] = array(
-    '#type' => 'textfield',
-    '#title' => t('Fastly API Key'),
-    '#default_value' => $api_key,
-    '#required' => TRUE,
-    '#description' => t('You can find it on your account settings page. If you dont have an account, please go to <a href="/?q=admin/config/services/fastly/register">registration page</a>'),
-  );
-
-  if ($api_key) {
-    $services = array();
-
-    foreach ($api->getServices() as $service) {
-      $services[$service->id] = $service->name;
-    }
-
-    ksort($services);
-
-    $form['fastly_service_id'] = array(
-      '#type' => 'radios',
-      '#title' => t('Service'),
-      '#options' => $services,
-      '#default_value' => $service_id,
-      '#required' => TRUE,
-      '#description' => t('A Service represents the configuration for your website to be served through Fastly.'),
-    );
-
-    $form['actions']['new_service'] = array(
-      '#markup' => l(t('New service'), 'admin/config/services/fastly/new',
-        array('attributes' => array('class' => 'button'))),
-      '#weight' => 10,
-    );
-  }
-
-  $default_ttl = $api->getSetting('general.default_ttl');
-
-  $form['fastly_ttl'] = array(
-    '#type' => 'textfield',
-    '#title' => t('Default TTL'),
-    '#default_value' => $default_ttl ? $default_ttl : '',
-    '#description' => t('The default time to live for cached content in seconds.'),
-  );
-
-  $description = t("Specify pages by using their paths. Enter one path per line. The '*' character is a wildcard. Example paths are %blog for the blog page and %blog-wildcard for every personal blog. %front is the front page.",
-    array(
-      '%blog' => 'blog',
-      '%blog-wildcard' => 'blog/*',
-      '%front' => '<front>',
-    ));
-
-  $form['fastly_non_cached'] = array(
-    '#type' => 'textarea',
-    '#title' => t('Non-cached pages'),
-    '#default_value' => variable_get('fastly_non_cached', ''),
-    '#description' => $description,
-  );
-
-  $form['#submit'][] = 'fastly_setup_form_submit';
-
-  return system_settings_form($form);
-}
-
-/**
- * Register form.
- */
-function fastly_register_form($form_state) {
-  if (variable_get('fastly_used_registration')) {
-    drupal_set_message(t('You are already registered. You can <a href="https://app.fastly.com/#password_reset">reset your password</a> if you don\'t remember it.'), 'warning');
-  }
-  else {
-    global $user;
-
-    $form['owner_first_name'] = array(
-      '#type' => 'textfield',
-      '#title' => t("First Name"),
-      '#description' => t("The customer account owner's first name. Ex: John."),
-      '#required' => TRUE,
-    );
-
-    $form['owner_last_name'] = array(
-      '#type' => 'textfield',
-      '#title' => t("Last Name"),
-      '#description' => t("The customer account owner's last name. Ex: Smith."),
-      '#required' => TRUE,
-    );
-
-    $form['owner_login'] = array(
-      '#type' => 'textfield',
-      '#title' => t("Email"),
-      '#default_value' => $user->mail,
-      '#description' => t("The owner's email to be used as login. Ex: john@somebusiness.com."),
-      '#required' => TRUE,
-    );
-
-    $form['owner_password'] = array(
-      '#type' => 'password',
-      '#title' => t("Password"),
-      '#default_value' => '',
-      '#required' => TRUE,
-    );
-
-    $form['confirm_password'] = array(
-      '#type' => 'password',
-      '#title' => t("Confirm Password"),
-      '#default_value' => '',
-      '#description' => t("The customer account owner's password. Please enter 4 or more characters. Ex: P@ssW0rd!1"),
-      '#required' => TRUE,
-    );
-
-    $form['account_name'] = array(
-      '#type' => 'textfield',
-      '#title' => t('Company Name'),
-      '#default_value' => variable_get('site_name', ''),
-      '#description' => t('The customer account name. Ex: Some Business, LLC.'),
-      '#required' => TRUE,
-    );
-
-    $form['origin_ip'] = array(
-      '#type' => 'textfield',
-      '#title' => t('Origin Server IP'),
-      '#default_value' => $_SERVER['SERVER_ADDR'],
-      '#required' => TRUE,
-    );
-
-    $form['port'] = array(
-      '#type' => 'textfield',
-      '#title' => t('Origin Port'),
-      '#default_value' => '80',
-      '#required' => TRUE,
-    );
-
-    $form['domain_name'] = array(
-      '#type' => 'textfield',
-      '#title' => t('Domain Name'),
-      '#default_value' => $_SERVER['HTTP_HOST'],
-      '#required' => TRUE,
-    );
-
-    $form['actions']['submit'] = array(
-      '#type' => 'submit',
-      '#value' => t('Sign Up'),
-    );
-
-    $form['actions']['already_registered'] = array(
-      '#markup' => t('Already registered?') . l(t('Click here to enter your account information and get started!'), 'admin/config/services/fastly/config'),
-    );
-
-    $form['policy'] = array(
-      '#markup' => '<p>' . t('By clicking "Sign Up" you are agreeing to the <a target="_blank" href="https://www.fastly.com/terms">Terms of Use</a> and <a target="_blank" href="https://www.fastly.com/privacy">Privacy Policy</a>.') . '</p>',
-    );
-
-    return $form;
-  }
-}
-
-/**
- * Purge form.
- */
-function fastly_purge_form($form_state) {
-  if (variable_get('fastly_service_id', '')  && variable_get('fastly_api_key', '')) {
-    $form['purge_url'] = array(
-      '#type' => 'fieldset',
-      '#title' => t('Purge by URL'),
-      '#description' => t('Paste one or more URLs to purge. Each in new line.'),
-    );
-
-    $form['purge_url']['urls_list'] = array(
-      '#type' => 'textarea',
-    );
-
-    $form['purge_url']['submit'] = array(
-      '#type' => 'button',
-      '#value' => t('Purge'),
-      '#id' => 'urls',
-      '#name' => 'urls',
-      '#executes_submit_callback' => 1,
-    );
-
-    $form['purge_key'] = array(
-      '#type' => 'fieldset',
-      '#title' => t('Purge by key'),
-      '#description' => t('Paste one or more keys to purge. Each in new line.'),
-    );
-
-    $form['purge_key']['keys_list'] = array(
-      '#type' => 'textarea',
-    );
-
-    $form['purge_key']['submit'] = array(
-      '#type' => 'button',
-      '#value' => t('Purge'),
-      '#id' => 'keys',
-      '#name' => 'keys',
-      '#executes_submit_callback' => 1,
-    );
-
-    $form['purge_all'] = array(
-      '#type' => 'fieldset',
-      '#title' => t('Purge all'),
-      '#description' => t('Purge whole service. You might not use this function too often.'),
-    );
-
-    $form['purge_all']['submit'] = array(
-      '#type' => 'button',
-      '#value' => t('Purge'),
-      '#id' => 'all',
-      '#name' => 'all',
-      '#executes_submit_callback' => 1,
-    );
-
-    return $form;
-  }
-  else {
-    drupal_set_message(t('You need to set up your API key and service ID to use this form.'), 'warning');
-  }
-}
-
-/**
- * New service form.
- */
-function fastly_new_service_form() {
-  $form['name'] = array(
-    '#type' => 'textfield',
-    '#title' => t('Service Name'),
-    '#default_value' => variable_get('site_name', ''),
-    '#required' => TRUE,
-  );
-
-  $form['origin_ip'] = array(
-    '#type' => 'textfield',
-    '#title' => t('Origin Server IP'),
-    '#default_value' => $_SERVER['SERVER_ADDR'],
-    '#required' => TRUE,
-  );
-
-  $form['port'] = array(
-    '#type' => 'textfield',
-    '#title' => t('Origin Port'),
-    '#default_value' => '80',
-    '#required' => TRUE,
-  );
-
-  $form['domain_name'] = array(
-    '#type' => 'textfield',
-    '#title' => t('Domain Name'),
-    '#default_value' => $_SERVER['HTTP_HOST'],
-    '#required' => TRUE,
-  );
-
-  $form['submit'] = array(
-    '#type' => 'submit',
-    '#value' => t('Create'),
-  );
-
-  return $form;
-}
-
-/**
- * Implements hook_form_validate().
- */
-function fastly_register_form_validate($form, &$form_state) {
-  if ($form_state['values']['owner_password'] && strlen($form_state['values']['owner_password']) < 4) {
-    form_set_error('owner_password', t('The password must contain 4 or more characters.'));
-  }
-
-  if (!valid_email_address($form_state['values']['owner_login'])) {
-    form_set_error('owner_login', t('The email address is invalid.'));
-  }
-
-  if ($form_state['values']['owner_password'] !== $form_state['values']['confirm_password']) {
-    form_set_error('confirm_password', t('The passwords do not match.'));
-  }
-
-  fastly_help_message();
-}
-
-/**
- * Implements hook_form_validate().
- */
-function fastly_setup_form_validate($form, &$form_state) {
-  $api = fastly_get_api($form_state['values']['fastly_api_key']);
-
-  if (!$api->validate()) {
-    form_set_error('', t('Invalid API key.'));
-  }
-
-  fastly_help_message();
-}
-
-/**
- * Implements hook_form_submit().
- */
-function fastly_setup_form_submit($form, &$form_state) {
-  $api = fastly_get_api();
-  $api->updateSettings(array('general.default_ttl' => $form_state['values']['fastly_ttl']));
-}
-
-/**
- * Implements hook_form_submit().
- */
-function fastly_register_form_submit($form, &$form_state) {
-  $data = array(
-    'name' => $form_state['values']['account_name'],
-    'owner' => array(
-      'name' => $form_state['values']['owner_first_name'] . ' ' . $form_state['values']['owner_last_name'],
-      'login' => $form_state['values']['owner_login'],
-      'password' => $form_state['values']['owner_password'],
-    ),
-    'domain' => $form_state['values']['domain_name'],
-    'port' => $form_state['values']['port'],
-    'address' => $form_state['values']['origin_ip'],
-    'ipv4' => $form_state['values']['origin_ip'],
-    'version' => 1,
-    'id' => 'syslog',
-  );
-
-  $api = fastly_get_api();
-
-  $account = $api->signup($data);
-
-  if (isset($account->msg)) {
-    drupal_set_message($account->msg, 'error');
-  }
-  else {
-    variable_set('fastly_used_registration', 1);
-    variable_set('fastly_api_key', $account->api_key);
-    variable_set('fastly_service_id', $account->service_id);
-
-    drupal_set_message(t('Registration successful! You will receive a confirmation email very soon. Please check your inbox and verify your account by clicking the received link.'));
-
-    drupal_goto('admin/config/services/fastly/purge');
-  }
-}
-
-/**
- * Implements hook_form_submit().
- */
-function fastly_new_service_form_submit($form, &$form_state) {
-  $data = array(
-    'name' => $form_state['values']['name'],
-    'domain' => $form_state['values']['domain_name'],
-    'port' => $form_state['values']['port'],
-    'address' => $form_state['values']['domain_name'],
-    'ipv4' => $form_state['values']['origin_ip'],
-    'version' => 1,
-    'id' => 'syslog',
-  );
-
-  $api = fastly_get_api();
-
-  $service = $api->createService($data);
-
-  if (isset($service->msg)) {
-    drupal_set_message($service->msg, 'error');
-  }
-  else {
-    variable_set('fastly_service_id', $service->id);
-
-    drupal_set_message(t('A new service successfuly created and set to default.'));
-
-    drupal_goto('admin/config/services/fastly');
-  }
-}
-
-/**
- * Implements hook_form_submit().
- */
-function fastly_purge_form_submit($form, &$form_state) {
-  $method = $form_state['triggering_element']['#name'];
-  $api = fastly_get_api();
-
-  switch ($method) {
-    case 'all':
-      $api->purgeAll();
-      break;
-
-    case 'urls':
-      $values = trim($form_state['values']['urls_list']);
-
-      if (empty($values)) {
-        $error = (bool) drupal_set_message(t('Please input the URLs to purge.'), 'error');
-      }
-      else {
-        foreach (explode("\n", $values) as $line) {
-          $api->purgePath(trim($line));
-        }
-      }
-      break;
-
-    case 'keys':
-      $values = trim($form_state['values']['keys_list']);
-
-      if (empty($values)) {
-        $error = (bool) drupal_set_message(t('Please input the keys to purge.'), 'error');
-      }
-      else {
-        foreach (explode("\n", $values) as $line) {
-          $api->purgeKey(trim($line));
-        }
-      }
-      break;
-  }
-
-  if (empty($error)) {
-    drupal_set_message(t('Cache successfuly purged.'));
-  }
-}
-
-/**
- * A reusable snippet.
- *
- * Checks for error messages
- * and sets up an extra message with a link to Zendesk.
- */
-function fastly_help_message() {
-  if (drupal_get_messages('error', FALSE)) {
-    drupal_set_message(t('<a href="https://fastly.zendesk.com">Click here</a> if you need help.'), 'warning');
-  }
-}
diff --git a/fastly.api.inc b/fastly.api.inc
deleted file mode 100644
index 27247d4..0000000
--- a/fastly.api.inc
+++ /dev/null
@@ -1,209 +0,0 @@
-<?php
-/**
- * @file
- * Contains Faslt class that handles API calls to the Fastly service.
- */
-
-/**
- * Fastly API for Drupal.
- */
-class Fastly {
-
-  /**
-   * Construct function for the Fastly class.
-   */
-  public function __construct($api_key, $service_id) {
-    $this->api_key = $api_key;
-    $this->service_id = $service_id;
-    $this->host = 'https://api.fastly.com/';
-    // $this->host = 'http://stg.fastly.com/';
-  }
-
-  /**
-   * Registers a new customer.
-   *
-   * @param array $data
-   *   Data to post to Fastly for the signup request.
-   *
-   * @return array
-   *   Data returned from Fastly.s
-   */
-  public function signup($data) {
-    $headers['Content-Type'] = 'application/x-www-form-urlencoded';
-
-    $result = $this->query('plugin/drupal/signup', $data, 'POST', $headers);
-
-    return json_decode($result->data);
-  }
-
-  /**
-   * Used to validate API key and service ID.
-   *
-   * @return bool
-   *   FALSE if any corrupt data is passed.
-   */
-  public function validate() {
-    return $this->query('current_customer')->status_message == 'OK';
-  }
-
-  /**
-   * Gets a list of services for the current customer.
-   */
-  public function getServices() {
-    $result = $this->query('service');
-
-    return json_decode($result->data);
-  }
-
-  /**
-   * Creates a default service for our website once we signed up.
-   *
-   * @param array $data
-   *   An array of data to create the service with.
-   *
-   * @return object
-   *   Data returned from the Fastly request.
-   */
-  public function createService($data) {
-    $service = json_decode($this->query('service', $data, 'POST')->data);
-
-    if (isset($service->id)) {
-      $data['service'] = $service->id;
-
-      $this->query('service/' . $service->id . '/version/1/domain', array('name' => $data['domain']), 'POST');
-
-      unset($data['domain']);
-      unset($data['address']);
-
-      $this->query('service/' . $service->id . '/version/1/backend', $data, 'POST');
-      $this->query('service/' . $service->id . '/version/1/syslog', $data, 'POST');
-      $this->query('service/' . $service->id . '/version/1/activate', array(), 'PUT');
-    }
-
-    return $service;
-  }
-
-  /**
-   * Gets the settings for a version.
-   */
-  public function getSettings() {
-    if ($active_version = $this->getActiveVersion()) {
-      $result = $this->query('service/' . $this->service_id . '/version/' . $active_version . '/settings');
-      return json_decode($result->data);
-    }
-    return NULL;
-  }
-
-  /**
-   * Gets the settings for a version.
-   */
-  public function getSetting($setting_id) {
-    $settings = $this->getSettings();
-
-    if ($settings && isset($settings->{$setting_id})) {
-      return $settings->{$setting_id};
-    }
-  }
-
-  /**
-   * Updates the settings for a version.
-   *
-   * @param array $data
-   *   An array of settings to update.
-   */
-  public function updateSettings($data) {
-    if ($this->service_id) {
-      $active_version = $this->getActiveVersion();
-
-      $new_version = json_decode($this->query('service/' . $this->service_id . '/version/' . $active_version . '/clone', array(), 'PUT')->data);
-
-      $headers['Content-Type'] = 'application/x-www-form-urlencoded';
-
-      $this->query('service/' . $this->service_id . '/version/' . $new_version->number . '/settings', $data, 'PUT', $headers);
-      $this->query('service/' . $this->service_id . '/version/' . $new_version->number . '/activate', array(), 'PUT');
-    }
-  }
-
-  /**
-   * Purge whole service.
-   */
-  public function purgeAll() {
-    $this->query('service/' . $this->service_id . '/purge_all', array(), 'POST');
-  }
-
-  /**
-   * Purge cache by path.
-   */
-  public function purgePath($path) {
-    global $base_url;
-    $path = str_replace($base_url, '', $path);
-    $this->purgeQuery($path);
-    $this->purgeQuery(drupal_get_path_alias($path));
-  }
-
-  /**
-   * Performs an actual purge request for the given path.
-   */
-  protected function purgeQuery($path) {
-    drupal_http_request(url($path, array('absolute' => TRUE)), array(
-      'headers' => array(
-        'Host' => $_SERVER['HTTP_HOST'],
-      ),
-      'method' => 'PURGE',
-    ));
-  }
-
-  /**
-   * Purge cache by key.
-   */
-  public function purgeKey($key) {
-    $this->query('service/' . $this->service_id . '/purge/' . $key, array(), 'POST');
-  }
-
-  /**
-   * Gets active version number for the current service.
-   */
-  protected function getActiveVersion() {
-    $service = json_decode($this->query('service/' . $this->service_id)->data);
-
-    if (is_object($service) && isset($service->versions)) {
-      foreach ($service->versions as $version) {
-        if ($version->active) {
-          return $version->number;
-        }
-      }
-    }
-    return NULL;
-  }
-
-  /**
-   * Performs http queries to Fastly API server.
-   *
-   * @param string $uri
-   *   The uri to use for the request, appended to the host.
-   * @param array $data
-   *   (optional) Data to send with the request.
-   * @param string $method
-   *   (optional) The method to use for the request, defaults to GET.
-   * @param array $headers
-   *   (optional) An array of headers to send with the request.
-   *
-   * @return object
-   *   From drupal_http_request().
-   */
-  protected function query($uri, $data = array(), $method = 'GET', $headers = array()) {
-    $url = $this->host . $uri;
-
-    $options['headers'] = $headers;
-    $options['method'] = $method;
-    $options['data'] = http_build_query($data);
-
-    if ($this->api_key) {
-      $options['headers']['Fastly-Key'] = $this->api_key;
-    }
-
-    $result = drupal_http_request($url, $options);
-
-    return $result;
-  }
-}
diff --git a/fastly.info b/fastly.info
deleted file mode 100644
index 8316937..0000000
--- a/fastly.info
+++ /dev/null
@@ -1,7 +0,0 @@
-name = Fastly
-description = Integration with the Fastly service
-package = Reports
-core = 7.x
-configure = admin/config/services/fastly
-files[] = fastly.api.inc
-dependencies[] = expire
diff --git a/fastly.info.yml b/fastly.info.yml
new file mode 100644
index 0000000..996db2d
--- /dev/null
+++ b/fastly.info.yml
@@ -0,0 +1,6 @@
+name: Fastly
+description: 'Integration with the Fastly service.'
+package: Reports
+core: 8.x
+configure: fastly.settings
+type: module
diff --git a/fastly.install b/fastly.install
deleted file mode 100644
index 70a7041..0000000
--- a/fastly.install
+++ /dev/null
@@ -1,27 +0,0 @@
-<?php
-
-/**
- * @file
- * Fastly install
- */
-
-/**
- * Implements hook_uninstall().
- */
-function fastly_uninstall() {
-  variable_del('fastly_used_registration');
-  variable_del('fastly_api_key');
-  variable_del('fastly_service_id');
-  variable_del('fastly_non_cached');
-}
-
-/**
- * Delete old variable.
- */
-function fastly_update_7200() {
-
-  // This variable was used for "Log API requests" option.
-  variable_del('fastly_log_enabled');
-
-  return t('fastly_log_enabled variable has been deleted.');
-}
diff --git a/fastly.links.menu.yml b/fastly.links.menu.yml
new file mode 100644
index 0000000..560ded0
--- /dev/null
+++ b/fastly.links.menu.yml
@@ -0,0 +1,5 @@
+fastly.settings:
+  title: 'Fastly'
+  parent: system.admin_config_services
+  description: 'Configure the Fastly integration.'
+  route_name: fastly.settings
diff --git a/fastly.module b/fastly.module
deleted file mode 100644
index d833216..0000000
--- a/fastly.module
+++ /dev/null
@@ -1,183 +0,0 @@
-<?php
-
-/**
- * @file
- * Fastly module.
- */
-
-/**
- * Implements hook_exit().
- *
- * Add cache-control headers to tell Fastly to cache content.
- * We mimic the cache-control headers sent by Drupal core, by avoiding sending
- * cache headers if the result is an error or if in maintenance mode.
- */
-function fastly_exit() {
-
-  // Avoid caching 404's and error pages.
-  // To do this check the status header.
-  $status = drupal_get_http_header("status");
-  $is_error = preg_match('/^(4|5)/', $status);
-  
-  // For anonymous users unicode.inc and path.inc will not have been loaded.
-  include_once DRUPAL_ROOT . '/includes/unicode.inc';
-  include_once DRUPAL_ROOT . '/' . variable_get('path_inc', 'includes/path.inc');
-  
-  $path = drupal_strtolower(drupal_get_path_alias($_GET['q']));
-  $no_store = drupal_strtolower(variable_get('fastly_non_cached', ''));
-
-  if (drupal_page_is_cacheable() &&
-      !$is_error &&
-      !variable_get('maintenance_mode', 0) &&
-      !drupal_match_path($path, $no_store)) {
-    drupal_add_http_header('Surrogate-Control', 'max-age=' . variable_get('fastly_ttl', '86400'));
-  }
-  else {
-    drupal_add_http_header('Surrogate-Control', 'no-store');
-  }
-
-  drupal_add_http_header('Vary', 'Cookie,fastly-ssl');
-
-  // Add Surrogate-Key headers based on path segments.
-  // E.g. if the current path is product/some-category/product-name
-  // we should end up with the following Surrogate-Keys:
-  // product product/some-category product/some-category/product-name
-  $path_segments = explode('/', drupal_get_path_alias());
-
-  $surrogate_keys = array();
-  $full_url = '';
-  foreach ($path_segments as $segment) {
-    if (empty($surrogate_keys)) {
-      $full_url = $segment;
-    }
-    else {
-      $full_url .= '/' . $segment;
-    }
-    $surrogate_keys[] = $full_url;
-  }
-  drupal_add_http_header('Surrogate-Key', implode(' ', $surrogate_keys));
-}
-
-/**
- * Return permissions for the Fastly module.
- */
-function fastly_permission() {
-  $perms = array(
-    'administer fastly' => array(
-      'title' => t('Administer Fastly'),
-      'description' => t('Allows users to administer Fastly.'),
-      'restrict access' => TRUE,
-    ),
-  );
-
-  return $perms;
-}
-
-/**
- * Implements hook_menu().
- */
-function fastly_menu() {
-  $items = array();
-
-  $items['admin/config/services/fastly'] = array(
-    'title' => 'Fastly configuration',
-    'description' => 'Fastly configuration',
-    'page callback' => 'fastly_select_page',
-    'access arguments' => array('administer fastly'),
-    'type' => MENU_NORMAL_ITEM,
-  );
-
-  $items['admin/config/services/fastly/new'] = array(
-    'title' => 'Create a service',
-    'description' => 'Create a service',
-    'page callback' => 'drupal_get_form',
-    'page arguments' => array('fastly_new_service_form'),
-    'access arguments' => array('administer fastly'),
-    'file' => 'fastly.admin.inc',
-    'type' => MENU_NORMAL_ITEM,
-  );
-
-  $items['admin/config/services/fastly/config'] = array(
-    'title' => 'Configuration',
-    'description' => 'Fastly configuration',
-    'page callback' => 'drupal_get_form',
-    'page arguments' => array('fastly_setup_form'),
-    'access arguments' => array('administer fastly'),
-    'file' => 'fastly.admin.inc',
-    'type' => MENU_LOCAL_TASK,
-  );
-
-  $items['admin/config/services/fastly/register'] = array(
-    'title' => 'Registration',
-    'description' => 'Fastly registration',
-    'page callback' => 'drupal_get_form',
-    'page arguments' => array('fastly_register_form'),
-    'access arguments' => array('administer fastly'),
-    'file' => 'fastly.admin.inc',
-    'type' => MENU_LOCAL_TASK,
-  );
-
-  $items['admin/config/services/fastly/purge'] = array(
-    'title' => 'Purge cache',
-    'description' => 'Fastly purge cache',
-    'page callback' => 'drupal_get_form',
-    'page arguments' => array('fastly_purge_form'),
-    'access arguments' => array('administer fastly'),
-    'file' => 'fastly.admin.inc',
-    'type' => MENU_LOCAL_TASK,
-    'weight' => -10,
-  );
-
-  return $items;
-}
-
-/**
- * Menu callback. Redirect the user to the right page.
- */
-function fastly_select_page() {
-  if (variable_get('fastly_api_key', FALSE)) {
-    $path = 'admin/config/services/fastly/config';
-  }
-  else {
-    $path = 'admin/config/services/fastly/register';
-  }
-  drupal_goto($path);
-}
-
-/**
- * Returns the API object.
- *
- * The key and service id can be overriden for validation reasons.
- */
-function fastly_get_api($api_key = '', $service_id = '') {
-  if (empty($api_key)) {
-    $api_key = variable_get('fastly_api_key', '');
-  }
-
-  if (empty($service_id)) {
-    $service_id = variable_get('fastly_service_id', '');
-  }
-
-  return new Fastly($api_key, $service_id);
-}
-/**
- * Implements hook_expire_cache().
- *
- * Provides integration with the Cache Expiration (expire) module.
- */
-function fastly_expire_cache($urls, $wildcards, $object_type, $object) {
-  $api = fastly_get_api();
-  foreach ($urls as $url) {
-    $api->purgePath($url);
-  }
-
-  // For wildcards, we use the Surrogate-Key purging functionality.
-  // Surrogate-Key headers are set in the response based on the
-  // url path segments.
-  // @See fastly_exit().
-  foreach ($wildcards as $path => $wildcard) {
-    if ($wildcard) {
-      $api->purgeKey($path);
-    }
-  }
-}
diff --git a/fastly.permissions.yml b/fastly.permissions.yml
new file mode 100644
index 0000000..e49118b
--- /dev/null
+++ b/fastly.permissions.yml
@@ -0,0 +1,4 @@
+administer fastly:
+  title: 'Administer Fastly'
+  description: 'Allows users to administer Fastly'
+  restrict access: true
diff --git a/fastly.routing.yml b/fastly.routing.yml
new file mode 100644
index 0000000..e8552d8
--- /dev/null
+++ b/fastly.routing.yml
@@ -0,0 +1,7 @@
+fastly.settings:
+  path: '/admin/config/services/fastly'
+  defaults:
+    _form: '\Drupal\fastly\Form\FastlySettingsForm'
+    _title: 'Fastly'
+  requirements:
+    _permission: 'administer fastly'
diff --git a/fastly.services.yml b/fastly.services.yml
new file mode 100644
index 0000000..a010b04
--- /dev/null
+++ b/fastly.services.yml
@@ -0,0 +1,20 @@
+parameters:
+  fastly.host: 'https://api.fastly.com/'
+
+services:
+  fastly.api:
+    class: Drupal\fastly\Api
+    arguments: ['@config.factory', '%fastly.host%', '@http_client', '@logger.channel.fastly']
+  fastly.cache_tags.invalidator:
+    class: Drupal\fastly\CacheTagsInvalidator
+    arguments: ['@fastly.api']
+    tags:
+      - { name: cache_tags_invalidator}
+  fastly.cache_tags.surrogate_key_generator:
+    class: Drupal\fastly\EventSubscriber\SurrogateKeyGenerator
+    arguments: ['@logger.channel.fastly']
+    tags:
+      - { name: event_subscriber }
+  logger.channel.fastly:
+    parent: logger.channel_base
+    arguments: ['fastly']
diff --git a/src/Api.php b/src/Api.php
new file mode 100644
index 0000000..23a9a15
--- /dev/null
+++ b/src/Api.php
@@ -0,0 +1,145 @@
+<?php
+
+/**
+ * @file
+ * Handles API calls to the Fastly service.
+ */
+
+namespace Drupal\Fastly;
+
+use Drupal\Core\Config\ConfigFactoryInterface;
+use GuzzleHttp\ClientInterface;
+use GuzzleHttp\Exception\RequestException;
+use Psr\Log\LoggerInterface;
+
+/**
+ * Fastly API for Drupal.
+ */
+class Api {
+
+  /**
+   * The Fastly logger channel.
+   *
+   * @var \Psr\Log\LoggerInterface
+   */
+  protected $logger;
+
+  /**
+   * Constructs a \Drupal\fastly\Api object.
+   *
+   * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
+   *   The config.
+   * @param string $host
+   *   The host to use to talk to the Fastly API.
+   * @param \GuzzleHttp\ClientInterface $http_client
+   *   The HTTP client.
+   * @param \Psr\Log\LoggerInterface $logger
+   *   The Fastly logger channel.
+   */
+  public function __construct(ConfigFactoryInterface $config_factory, $host, ClientInterface $http_client, LoggerInterface $logger) {
+    $config = $config_factory->get('fastly.settings');
+
+    $this->apiKey = $config->get('api_key');
+    $this->serviceId = $config->get('service_id');
+
+    $this->host = $host;
+    $this->httpClient = $http_client;
+    $this->logger = $logger;
+  }
+
+  /**
+   * Used to validate API key and service ID.
+   *
+   * @return bool
+   *   FALSE if any corrupt data is passed.
+   */
+  public function validate() {
+    return $this->query('current_customer')->status_message == 'OK';
+  }
+
+  /**
+   * Gets a list of services for the current customer.
+   */
+  public function getServices() {
+    $response = $this->query('service');
+    return $response->json();
+  }
+
+  /**
+   * Purge whole service.
+   */
+  public function purgeAll() {
+    $this->query('service/' . $this->service_id . '/purge_all', array(), 'POST');
+  }
+
+  /**
+   * Purge cache by path.
+   */
+  public function purgePath($path) {
+    global $base_url;
+    $path = str_replace($base_url, '', $path);
+    $this->purgeQuery($path);
+    $this->purgeQuery(drupal_get_path_alias($path));
+  }
+
+  /**
+   * Performs an actual purge request for the given path.
+   */
+  protected function purgeQuery($path) {
+    drupal_http_request(url($path, array('absolute' => TRUE)), array(
+      'headers' => array(
+        'Host' => $_SERVER['HTTP_HOST'],
+      ),
+      'method' => 'PURGE',
+    ));
+  }
+
+  /**
+   * Purge cache by key.
+   *
+   * @param string $key
+   *   A Surrogate Key value; in the case of Drupal: a cache tag.
+   */
+  public function purgeKey($key) {
+    try {
+      $response = $this->query('service/' . $this->serviceId . '/purge/' . $key, [], 'POST');
+
+      $result = $response->json();
+      if ($result['status'] === 'ok') {
+        $this->logger->info('Successfully purged the key %key. Purge ID: %id.', ['%key' => $key, '%id' => $result['id']]);
+      }
+      else {
+        $this->logger->critical('Unable to purge the key %key was purged from Fastly. Response status: %status. Purge ID: %id.', ['%key' => $key, '%status' => $result['status'], '%id' => $result['id']]);
+      }
+    }
+    catch (RequestException $e) {
+//      $this->logger->critical($e->getMessage());
+    }
+  }
+
+  /**
+   * Performs http queries to Fastly API server.
+   *
+   * @param string $uri
+   *   The uri to use for the request, appended to the host.
+   * @param array $data
+   *   (optional) Data to send with the request.
+   * @param string $method
+   *   (optional) The method to use for the request, defaults to GET.
+   * @param array $headers
+   *   (optional) An array of headers to send with the request.
+   *
+   * @return \GuzzleHttp\Message\ResponseInterface
+   *
+   * @throws \GuzzleHttp\Exception\RequestException
+   */
+  protected function query($uri, $data = array(), $method = 'GET', $headers = array()) {
+    $request = $this->httpClient->createRequest($method, $this->host . $uri, $data);
+    $request->addHeaders($headers);
+    if ($this->apiKey) {
+      $request->addHeader('Fastly-Key', $this->apiKey);
+    }
+
+    return $this->httpClient->send($request);
+  }
+}
diff --git a/src/CacheTagsInvalidator.php b/src/CacheTagsInvalidator.php
new file mode 100644
index 0000000..32fc8e7
--- /dev/null
+++ b/src/CacheTagsInvalidator.php
@@ -0,0 +1,51 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\fastly\CacheTagsInvalidator.
+ */
+
+namespace Drupal\fastly;
+
+use Drupal\Core\Cache\CacheTagsInvalidatorInterface;
+use Drupal\fastly\EventSubscriber\SurrogateKeyGenerator;
+
+/**
+ * Cache tags invalidator implementation that invalidates Fastly.
+ */
+class CacheTagsInvalidator implements CacheTagsInvalidatorInterface {
+
+  /**
+   * The Fastly API.
+   *
+   * @var \Drupal\Fastly\Api
+   */
+  protected $fastlyApi;
+
+  /**
+   * Constructs a CacheTagsInvalidator object.
+   *
+   * @param \Drupal\Fastly\Api $fastly_api
+   *   The Fastly API.
+   */
+  public function __construct(Api $fastly_api) {
+    $this->fastlyApi = $fastly_api;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function invalidateTags(array $tags) {
+    foreach ($tags as $tag) {
+      $this->fastlyApi->purgeKey($tag);
+    }
+
+    // Also invalidate the cache tags as hashes, to automatically also work for
+    // responses that exceed the 16 KB header limit.
+    $hashes = SurrogateKeyGenerator::cacheTagsToHashes($tags);
+    foreach ($hashes as $hash) {
+      $this->fastlyApi->purgeKey($hash);
+    }
+  }
+
+}
diff --git a/src/EventSubscriber/SurrogateKeyGenerator.php b/src/EventSubscriber/SurrogateKeyGenerator.php
new file mode 100644
index 0000000..d62d061
--- /dev/null
+++ b/src/EventSubscriber/SurrogateKeyGenerator.php
@@ -0,0 +1,86 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Fastly\EventSubscriber\CacheTagsHeaderLimitDetector.
+ */
+
+namespace Drupal\fastly\EventSubscriber;
+
+use Psr\Log\LoggerInterface;
+use Symfony\Component\HttpKernel\Event\FilterResponseEvent;
+use Symfony\Component\HttpKernel\KernelEvents;
+use Symfony\Component\EventDispatcher\EventSubscriberInterface;
+
+class SurrogateKeyGenerator implements EventSubscriberInterface {
+
+  /**
+   * The Fastly logger channel.
+   *
+   * @var \Psr\Log\LoggerInterface
+   */
+  protected $logger;
+
+  /**
+   * Constructs a new CacheTagsHeaderLimitDetector object.
+   *
+   * @param \Psr\Log\LoggerInterface $logger
+   *   The Fastly logger channel.
+   */
+  public function __construct(LoggerInterface $logger) {
+    $this->logger = $logger;
+  }
+
+  /**
+   * Logs an emergency event when the X-Drupal-Cache-Tags header exceeds 16 KB.
+   *
+   * @param \Symfony\Component\HttpKernel\Event\FilterResponseEvent $event
+   *   The event to process.
+   */
+  public function onRespond(FilterResponseEvent $event) {
+    if (!$event->isMasterRequest()) {
+      return;
+    }
+
+    $response = $event->getResponse();
+
+    $surrogate_key_header_value = $response->headers->get('X-Drupal-Cache-Tags');
+    if (strlen($surrogate_key_header_value) > 16384) {
+      $this->logger->notice('X-Drupal-Cache-Tags header size exceeded the 16 KB limit that Fastly supports; replaced the cache tags with hashed equivalents.');
+      $cache_tags = explode(' ', $surrogate_key_header_value);
+      $hashes = static::cacheTagsToHashes($cache_tags);
+      $surrogate_key_header_value = implode(' ', $hashes);
+    }
+
+    $response->headers->set('Surrogate-Key', $surrogate_key_header_value);
+  }
+
+  /**
+   * Maps cache tags to hashes.
+   *
+   * Used when the Surrogate-Key/X-Drupal-Cache-Tags header size otherwise
+   * exceeds 16 KB.
+   *
+   * @param string[] $cache_tags
+   *   The cache tags in the header.
+   *
+   * @return string[]
+   *   The hashes to use instead in the header.
+   */
+  public static function cacheTagsToHashes(array $cache_tags) {
+    $hashes = [];
+    foreach ($cache_tags as $cache_tag) {
+      $hashes[] = substr(md5($cache_tag), 0, 3);
+    }
+    return $hashes;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function getSubscribedEvents() {
+    $events[KernelEvents::RESPONSE][] = ['onRespond'];
+    return $events;
+  }
+
+}
diff --git a/src/Form/FastlySettingsForm.php b/src/Form/FastlySettingsForm.php
new file mode 100644
index 0000000..29e1631
--- /dev/null
+++ b/src/Form/FastlySettingsForm.php
@@ -0,0 +1,141 @@
+<?php
+/**
+ * @file
+ * This is the GlobalRedirect admin include which provides an interface to global redirect to change some of the default settings
+ * Contains \Drupal\globalredirect\Form\GlobalredirectSettingsForm.
+ */
+
+namespace Drupal\fastly\Form;
+
+use Drupal\Core\Config\ConfigFactoryInterface;
+use Drupal\Core\Form\ConfigFormBase;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Fastly\Api;
+use GuzzleHttp\Exception\RequestException;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * Defines a form to configure module settings.
+ */
+class FastlySettingsForm extends ConfigFormBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getFormID() {
+    return 'fastly_settings';
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function getEditableConfigNames() {
+    return ['fastly.settings'];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildForm(array $form, FormStateInterface $form_state) {
+    $config = $this->config('fastly.settings');
+
+    $api_key = count($form_state->getValues()) ? $form_state->getValue('api_key') : $config->get('api_key');
+    $form['api_key'] = array(
+      '#type' => 'textfield',
+      '#title' => $this->t('API key'),
+      '#default_value' => $api_key,
+      '#required' => TRUE,
+      // Update the listed services whenever the API key is modified.
+      '#ajax' => array(
+        'callback' => '::updateServices',
+        'wrapper' => 'edit-service-wrapper',
+      ),
+    );
+
+    $service_options = $this->getServiceOptions($api_key);
+    $form['service_id'] = array(
+      '#type' => 'select',
+      '#title' => $this->t('Service'),
+      '#options' => $service_options,
+      '#default_value' => $config->get('service_id'),
+      '#required' => TRUE,
+      '#description' => t('A Service represents the configuration for your website to be served through Fastly.'),
+      // Hide while no API key is set.
+      '#states' => [
+        'invisible' => [
+          'input[name="api_key"]' => ['empty' => TRUE],
+        ],
+      ],
+      '#prefix' => '<div id="edit-service-wrapper">',
+      '#suffix' => '</div>',
+    );
+
+    return parent::buildForm($form, $form_state);
+  }
+
+  /**
+   * Handles changing the API key.
+   */
+  public function updateServices($form, FormStateInterface $form_state) {
+    return $form['service_id'];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function validateForm(array &$form, FormStateInterface $form_state) {
+    if (!$this->isValidApiKey($form_state->getValue('api_key'))) {
+      $form_state->setErrorByName('api_key', $this->t('Invalid API key.'));
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function submitForm(array &$form, FormStateInterface $form_state) {
+    $this->config('fastly.settings')
+      ->set('api_key', $form_state->getValue('api_key'))
+      ->set('service_id', $form_state->getValue('service_id'))
+      ->save();
+
+    parent::submitForm($form, $form_state);
+  }
+
+  protected function getServiceOptions($api_key) {
+    if (!$this->isValidApiKey($api_key)) {
+      return [];
+    }
+
+    $request = \Drupal::httpClient()->createRequest('GET', 'https://api.fastly.com/'. 'service');
+    $request->addHeader('Fastly-Key', $api_key);
+    $response = \Drupal::httpClient()->send($request);
+    $services = $response->json();
+
+    $service_options = [];
+    foreach ($services as $service) {
+      $service_options[$service['id']] = $service['name'];
+    }
+    ksort($service_options);
+    return $service_options;
+  }
+
+  protected function isValidApiKey($api_key) {
+    if (empty($api_key)) {
+      return FALSE;
+    }
+
+    $request = \Drupal::httpClient()->createRequest('GET', 'https://api.fastly.com/'. 'current_customer');
+    $request->addHeader('Fastly-Key', $api_key);
+    try {
+      $response = \Drupal::httpClient()->send($request);
+      if ($response->getStatusCode() === 200) {
+        return TRUE;
+      }
+      return FALSE;
+    }
+    catch (RequestException $e) {
+      return FALSE;
+    }
+  }
+
+}
