Collective

We’re a worker-owned agency that designs, builds and supports websites for organisations we believe in.

One way to deal with the Drupal 8 beta to beta upgrade cycle

As anyone that has tried and failed will know, Drupal 8 beta to beta upgrades can explode your development site. One method we've successfully employed is to use an install profile and rebuild the site each time we need to upgrade. For this to work we also need to re-import content each time the site is rebuilt otherwise any content added is lost. This post is about the method we employed to import content on site install.

After reading a blog post where the writer was using a method of importing from CSV it occured to me that instead of trying to maintain a CSV offline it would be a lot easier to maintain the CSV online in Google Spreadsheets and use Google's API to import the content dynamically. This would then allow the content editors and the developers to collaborate on the spreadsheet together without having to email it around. Here is an example spreadsheet that we'll use for demonstration purposes.

Google provides a library to deal with OAuth authentication to their APIs so the first thing we need to do is install it. We also need a third party library to ease working with Google Spreadsheets. The easiest way to install these dependencies is to use composer. Change directory into your profile and create a composer.json file with the libraries we need:

{
  "require": {
    "google/apiclient": "1.0.*@beta",
    "asimlqt/php-google-spreadsheet-client": "2.3.*"
  }
}

Install the libraries with:

composer install

 

Once they've installed themselves in your profile in the vendor directory we can start writing some code. The easiest way to make use of the libraries is to use the autoload file composer creates for us. In your profile file (eg myprofile.profile) we'll create a function to deal with OAuth through Google's API. Write the following function:

/**
 * Authenticates with Google API.
 */
function _get_google_api_token() {
  require 'vendor/autoload.php';
}

 

We're going to need to set ourselves up a new service account with Google so follow the steps under the heading "Creating a service account" on the developer documentation. You'll also want to follow the steps on the same page under "Delegating domain-wide authority to the service account". Once you've done that you should save your p12 file in the top level of your profile. You should also make a note of your 'client email'. Next we're going to add some code to our _get_google_api_token() function:

/**
 * Authenticates with Google API.
 */
function _get_google_api_token() {
  require 'vendor/autoload.php';

  // Add your own client email below.
  $client_email = '58823120483339-eezadadasd2dt7b1kcchke0kp1fqeg62@developer.gserviceaccount.com';
  // Change the following file name string to that of your p12 key.
  $private_key = file_get_contents(__DIR__ . '/your_servicename_key.p12');
  $scopes = array('https://spreadsheets.google.com/feeds');
  // Put the email address in of the google user to impersonate.
  $user_to_impersonate = 'you@youremail.com';

  $credentials = new Google_Auth_AssertionCredentials(
    $client_email,
    $scopes,
    $private_key,
    'notasecret',                                 // Default P12 password
    'http://oauth.net/grant_type/jwt/1.0/bearer', // Default grant type
    $user_to_impersonate
  );
}

 

Make sure you add your $client_email and the correct file name for your p12 key. This gets us to the point where we can create a new instance of the Google_Client object and add our credentials and then make an OAuth call to hopefully retrieve a token. To do this add the following code to our function:

$client = new Google_Client();
$client->setAssertionCredentials($credentials);

if ($client->getAuth()->isAccessTokenExpired()) {
  $client->getAuth()->refreshTokenWithAssertion();
}
$obj_token = json_decode($client->getAccessToken());
$accessToken = $obj_token->access_token;

return $accessToken;

What we should get if we were to call that function is an access token which will allow us to make API calls to the spreadsheet.

Create a new function called _get_google_worksheet with the following code:

/**
 * Gets a Google Spreadsheet worksheet.
 *
 * @param string $accessToken
 *   The access token used in the Google API authentication.
 * @param string $spreadsheetName
 *   The name of the spreadsheet to access.
 * @param string $worksheetName
 *   The name of the worksheet within the spreadsheet to access.
 *
 * @return \Google\Spreadsheet\ListFeed
 */
function _get_google_worksheet($accessToken, $spreadsheetName, $worksheetName) {
  require 'vendor/autoload.php';

  $serviceRequest = new DefaultServiceRequest($accessToken);
  ServiceRequestFactory::setInstance($serviceRequest);

  $spreadsheetService = new Google\Spreadsheet\SpreadsheetService();
  $spreadsheetFeed = $spreadsheetService->getSpreadsheets();

  $spreadsheet = $spreadsheetFeed->getByTitle($spreadsheetName);
  $worksheetFeed = $spreadsheet->getWorksheets();
  $worksheet = $worksheetFeed->getByTitle($worksheetName);

  return $worksheet->getListFeed();
};

When called, this function will return a ListFeed object which allows us to iterate over the rows in our spreadsheet. You will need to pass it the access token from the previous function, a spreadsheet name and a worksheet name from within that spreadsheet. In our sample spreadsheet we want to import standard page content so lets create a new function for importing standard pages:

/**
 * Imports standard page content from the Google Spreadsheet.
 */
function import_content_standard_page() {
  $token = _get_google_api_token();
  $listFeed = _get_google_worksheet($token, 'Sample spreadsheet', 'Standard page');
}

If import_content_standard_page() were to be executed, we would get our authentication token and then pass that into our _get_google_worksheet function along with the name of the spreadsheet and the worksheet within in. The $listFeed variable would then be a ListFeed object which we can use to iterate over our worksheet rows and consume the data. Lets add some more lines to the function so that we start creating nodes:

/**
 * Imports standard page content from the Google Spreadsheet.
 */
function import_content_standard_page() {
  $token = _get_google_api_token();
  $listFeed = _get_google_worksheet($token, 'Sample spreadsheet', 'Standard page');
  /** @var \Drupal\node\Entity\Node[] $pages */

  $index = 0;
  $pages = array();

  // Iterate over the worksheet rows created nodes.
  foreach ($listFeed->getEntries() as $entry) {
    $values = $entry->getValues();
    $index++;

    $node = Node::create(
      array(
        'type' => 'page',
        'langcode' => 'en',
        'title' => $values['title'],
        'body' => array('value' => $values['body'], 'format' => 'basic_html'),
        'sticky' => 0,
      )
    );

    $node->save();
    $pages[$values['id']] = $node;
  }
}

We also want to be able to save a custom path alias for each item so lets add in that code:

/** @var \Drupal\Core\Path\AliasStorageInterface $path_aliases */
    $path_aliases = \Drupal::service('path.alias_storage');
    $path_aliases->save(
      'node/' . $pages[$values['id']]->id(),
      $values['path'],
      (LanguageInterface::LANGCODE_NOT_SPECIFIED)
    );

We can also extend this to add menu items for your nodes by adding the following code:

// If there is no menu item continue to the next row.
    if (!isset($values['menu'])) {
      continue;
    }

    $menu = entity_create('menu_link_content', array(
      'link' => ['uri' => 'entity:node/' . $node->id()],
      'langcode' => $node->getUntranslated()->language()->getId(),
    ));
    $menu->enabled->value = 1;
    $menu->title->value = trim($values['linktitle']);
    $menu->menu_name->value = $values['menu'];

    // See if there is a parent with the given title.
    if (!empty($values['linkparent'])) {
      // Load any menu items which have the linkparent as their title.
      // Not ideal but works for now.
      $menu_parent = entity_load_multiple_by_properties('menu_link_content', array('title' => $values['linkparent']));
      // Assume its the first one found.
      $menu_parent = reset($menu_parent);
      // Fetch the pluginId which is basically the UUID of the link item.
      $menu->parent->value = $menu_parent->getPluginId();
    }

    $menu->weight->value = !empty($values['linkweight']) ? $values['linkweight'] : 0;
    $menu->save();

And there we have it, the working code which when added to your profiles hook_install function will import a standard page node per row of your spreadsheet. This is the final function with all the parts from above:

/**
 * Imports standard page content from the Google Spreadsheet.
 */
function import_content_standard_page() {
  $token = gcp_get_google_api_token();
  $listFeed = _get_google_worksheet($token, 'Sample spreadsheet', 'Standard page');

  /** @var \Drupal\node\Entity\Node[] $pages */

  $index = 0;
  $pages = array();

  // Iterate over the worksheet rows created nodes.
  foreach ($listFeed->getEntries() as $entry) {
    $values = $entry->getValues();
    $index++;

    $node = Node::create(
      array(
        'type' => 'page',
        'langcode' => 'en',
        'title' => $values['title'],
        'body' => array('value' => $values['body'], 'format' => 'basic_html'),
        'sticky' => 0,
      )
    );

    $node->save();
    $pages[$values['id']] = $node;
    /** @var \Drupal\Core\Path\AliasStorageInterface $path_aliases */
    $path_aliases = \Drupal::service('path.alias_storage');
    $path_aliases->save(
      'node/' . $pages[$values['id']]->id(),
      $values['path'],
      (LanguageInterface::LANGCODE_NOT_SPECIFIED)
    );

    // If there is no menu item continue to the next row.
    if (!isset($values['menu'])) {
      continue;
    }

    $menu = entity_create('menu_link_content', array(
      'link' => ['uri' => 'entity:node/' . $node->id()],
      'langcode' => $node->getUntranslated()->language()->getId(),
    ));
    $menu->enabled->value = 1;
    $menu->title->value = trim($values['linktitle']);
    $menu->menu_name->value = $values['menu'];

    // See if there is a parent with the given title.
    if (!empty($values['linkparent'])) {
      // Load any menu items which have the linkparent as their title.
      // Not ideal but works for now.
      $menu_parent = entity_load_multiple_by_properties('menu_link_content', array('title' => $values['linkparent']));
      // Assume its the first one found.
      $menu_parent = reset($menu_parent);
      // Fetch the pluginId which is basically the UUID of the link item.
      $menu->parent->value = $menu_parent->getPluginId();
    }

    $menu->weight->value = !empty($values['linkweight']) ? $values['linkweight'] : 0;
    $menu->save();

  }

}

 

Meet our Guest Author
  • An image of Tanc looking into the abyss
    Tanc
    Tanc was a member of Agile Collective from 2013 to 2022. He's an experienced Drupal developer now working for himself. He remains part of the Agile Collective family, occasionally popping out of the woodwork to join a social or solve a gnarly technical problem.
Back to blog